SpringMVC前後端分離互動傳參詳細教程

Java小學生丶發表於2022-03-23

溫故而知新,本文為一時興起寫出,如有錯誤還請指正

本文後臺基於SpringBoot2.5.6編寫,前端基於Vue2 + axios和微信小程式JS版分別編寫進行聯調測試,用於理解前後端分離式開發的互動流程,如果沒用過axios可以點我看之前的帖子

如果你沒有學過SpringBoot也不要緊,把他看做成SpringMVC即可,寫法完全一致(其實我不說你也發現不了)

本文主要講前後端互動流程,力求幫助新人快速入門前後端分離式開發,不會講關於環境搭建部分的內容

SpringMVC接收引數的方式

在文章開頭快速的過一遍SpringMVC接收引數的幾種方式,一定要記住這幾種方式,看不懂或不理解都沒關係,後續會結合前端程式碼過一遍,這裡就不過多解釋了,直接上程式碼

1.【正常接收引數】

/**
 * 正常接收引數
 * 注意:本Controller為了演示同時寫了多個路徑相同的GetMapping,不要直接複製,啟動會報錯
 */
@RestController
public class IndexController {

    /** 通過變數接收引數 */
    @GetMapping("/index")
    public String index(String username, String password) {
        System.out.println(username);
        System.out.println(password);
        return "index";
    }

    /** 通過實體類接收引數 */
    @GetMapping("/index")
    public String index(UserEntity userEntity) {
        System.out.println(userEntity.getUsername());
        System.out.println(userEntity.getPassword());
        return "index";
    }

    /** 通過Map集合接收引數 */
    @GetMapping("/index")
    public String index(Map<String, Object> param) {
        System.out.println(param.get("username"));
        System.out.println(param.get("password"));
        return "index";
    }

    /** 通過基於HTTP協議的Servlet請求物件中獲取引數 */
    @GetMapping("/index")
    public String index(HttpServletRequest req) {
        System.out.println(req.getParameter("username"));
        System.out.println(req.getParameter("password"));
        return "index";
    }

    /** 變數接收引數還可以使用@RequestParam完成額外操作 */
    @GetMapping("/index")
    public String index(@RequestParam(value = "username", required = true, defaultValue = "zhang") String username) {
        System.out.println(username);
        return "index";
    }

}

2.【路徑佔位接收引數】

/**
 * 路徑佔位接收引數,引數作為請求路徑的一部分,使用{}作為佔位符
 */
@RestController
public class IndexController {

    /** 路徑佔位接收引數,名稱相同 */
    @GetMapping("/user/{id}")
    public String index(@PathVariable Integer id) {
        System.out.println(id);
        return "index";
    }

    /** 路徑佔位接收引數,名稱不同 */
    @GetMapping("/user/{id}")
    public String index(@PathVariable("id") Long userId) {
        System.out.println(userId);
        return "index";
    }

}

3.【請求體接收引數】

/**
 * 如果請求引數在請求體中,需要使用@RequestBody取出請求體中的值
 */
@RestController
public class IndexController {
    
    /** 使用實體類接收引數 */
    @GetMapping("/index")
    public String index(@RequestBody UserEntity userEntity) {
        System.out.println(userEntity.getUsername());
        System.out.println(userEntity.getPassword());
        return "index";
    }
    
    /** 使用Map集合接收引數 */
    @GetMapping("/index")
    public String index(@RequestBody Map<String, Object> param) {
        System.out.println(param.get("username"));
        System.out.println(param.get("password"));
        return "index";
    }
    
    /** 變數接收引數 */
    @GetMapping("/index")
    public String index(@RequestBody String username) {
        System.out.println(username);
        return "index";
    }

}

細心的人應該留意到了,最後使用變數接收引數的時候只接收了username這一個值,並沒有接收password,作為擴充套件在這裡解釋一下,不看也可以,看了不理解也沒關係,知道這個事兒就夠了,以後接觸多了就理解了

如果請求引數放在了請求體中,只有引數列表第一個變數能接收到值,這裡需要站在Servlet的角度來看:

/** 通過基於HTTP協議的Servlet請求物件獲取請求體內容 */
@GetMapping("/index")
public String index(HttpServletRequest req) {
    ServletInputStream inputStream = req.getInputStream();
    return "index";
}

可以看到請求體內容是存到了InputStream輸入流物件中,想要知道請求體中的內容是什麼必須讀流中的資料,讀取到資料後會將值給第一個變數,而流中的資料讀取一次之後就沒了,當第二個變數讀流時發現流已經被關閉了,自然就接收不到

前後端分離式互動流程

SpringMVC回顧到此為止,只需要記住那三種方式即可,在前後端互動之前先在Controller中寫個測試介面

@RestController
public class IndexController {

    @GetMapping("/index")
    public Map<String, Object> index() {
        // 建立map集合物件,新增一些假資料並返回給前端
        HashMap<String, Object> result = new HashMap<>();
        result.put("user", "zhang");
        result.put("name", "hanzhe");
        result.put("arr", new int[]{1, 2, 3, 4, 5, 6});
        // 返回資料給前端
        return result;
    }

}

這個介面對應的是GET型別的請求,這裡直接在瀏覽器位址列訪問測試一下:

image

這裡推薦一個Chrome瀏覽器的外掛JSONView,它可以對瀏覽器顯示的JSON資料進行格式化顯示,推薦的同時也提個醒,安裝需謹慎,如果JSON資料量太大的話頁面會很卡

image

跨域請求

之前已經寫好一個GET請求的測試介面了,這裡就在前端寫程式碼訪問一下試試看

VUE請求程式碼

<template>
    <!-- 我這裡為了看著好看(心情好點),引用了ElementUI -->
    <el-button-group>
        <el-button type="primary" size="small" @click="request1">發起普通請求</el-button>
    </el-button-group>
</template>

<script>
export default {
    methods: {
        request1() {
            // 通過axios發起一個GET請求
            this.axios.get("http://localhost:8080/index").then(res => {
                // 列印介面返回的結果
                console.log("res", res);
            });
        }
    }
};
</script>

程式碼已經寫完了,接下來開啟頁面試一下能不能調通:

image

可以看到請求程式碼報錯了,檢視報錯資訊找到重點關鍵詞CORS,表示該請求屬於跨域請求

認識跨域請求

什麼是跨域請求?跨域請求主要體現在跨域兩個字上,當發起請求的客戶端和接收請求的服務端他們的【協議、域名、埠號】有任意一項不一致的情況都屬於跨域請求,拿剛剛訪問的地址舉例,VUE頁面執行在9000埠上,後臺介面執行在8080埠上,埠號沒有對上所以該請求為跨域請求

image

處理跨域請求

如果在除錯的時候仔細一點就會發現,雖然前端提示請求報錯了,但是後端還是接收到請求了,那為什麼會報錯呢?是因為後端返回資料後,瀏覽器接收到響應結果發現該請求跨域,然後給我們提示錯誤資訊,也就是說問題在瀏覽器這裡

怎樣才能讓瀏覽器允許該請求呢?我們需要在後端動點手腳,在返回結果的時候設定允許前端訪問即可

首先配置一個過濾器,配置過濾器有很多種實現的方法,我這裡是實現Filter介面

@Component
public class CorsFilter implements Filter {

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        // 將response響應轉換為基於HTTP協議的響應物件
        HttpServletResponse resp = (HttpServletResponse) servletResponse;
        // 這個方法是必須呼叫的,不做解釋
        filterChain.doFilter(servletRequest, resp);
    }

    @Override
    public void init(FilterConfig filterConfig) throws ServletException { }
    @Override
    public void destroy() { }

}

過濾器建立完成了,回來看前端提示的報錯資訊為Access-Control-Allow-Origin,意思是允許訪問的地址中並不包含當前VUE的地址,那麼我們就在響應結果時將VUE的地址追加上

@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
    // 將response響應轉換為基於HTTP協議的響應物件
    HttpServletResponse resp = (HttpServletResponse) servletResponse;
    // 在允許請求的地址列表中新增VUE的地址
    resp.addHeader("Access-Control-Allow-Origin", "http://localhost:9000");
    // 這個方法是必須呼叫的,不做解釋
    filterChain.doFilter(servletRequest, resp);
}

新增完成後重啟專案後臺就會發現請求已經成功並且拿到了返回值

image

再次進行測試,將後臺的GetMapping修改為PostMapping,修改前端請求程式碼後重新發起請求進行測試

image

可以看到POST請求還是提示跨域請求,對應的錯誤資訊則是Access-Control-Allow-Headers,也就是說請求頭中包含了不被允許的資訊,這裡圖省事兒用*萬用字元把所有請求頭都放行

@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
    // 將response響應轉換為基於HTTP協議的響應物件
    HttpServletResponse resp = (HttpServletResponse) servletResponse;
    // 後臺介面除了VUE訪問之外微信小程式也會訪問,這裡使用萬用字元替換
    resp.addHeader("Access-Control-Allow-Origin", "*");
    // 這裡圖省事也允許所有請求頭訪問
    resp.addHeader("Access-Control-Allow-Headers", "*");
    // 這個方法是必須呼叫的,不做解釋
    filterChain.doFilter(servletRequest, resp);
}

這樣處理之後,請求就可以正常訪問啦

image

傳參-路徑佔位引數

路徑佔位引數,就是將引數作為請求路徑的一部分,例如你現在正在看的這篇部落格使用的就是路徑佔位傳參

image

這種傳參方法很簡單,就不細講了,可以效仿他這種方法寫個測試案例

後臺介面的編寫

@RestController
public class IndexController {
    // 路徑中包含user和blogId兩個佔位引數
    @GetMapping("/{user}/p/{blogId}.html")
    public Map<String, Object> index(@PathVariable String user, @PathVariable Long blogId) {
        // 將接收的引數返回給前端
        HashMap<String, Object> result = new HashMap<>();
        result.put("user", user);
        result.put("blogId", blogId);

        return result;
    }
}

VUE請求程式碼

request1() {
    this.axios.get("http://localhost:8080/hanzhe/p/11223344.html", this.config).then(res => {
        console.log("res", res);
    });
}

image

小程式請求程式碼

request1() {
    wx.request({
        // url:請求的目標地址
        url: 'http://localhost:8080/hanzhe/p/223344.html',
        // success:請求成功後執行的方法
        success: res => {
            console.log(res);
        }
    })
}

image

傳參-路徑引數

這裡需要注意區分【路徑佔位傳參】和【路徑傳參】兩個概念,不要記混

什麼是路徑傳參?發起一個請求http://localhost:8080/index?a=1&b=2,在路徑?後面的都屬於路徑傳參,路徑傳參就是將引數以明文方式拼接在請求地址後面

路徑傳參使用【正常接收引數】中的例項程式碼即可接收到值

後臺介面的編寫

@RestController
public class IndexController {
    @GetMapping("/index")
    public Map<String, Object> index(String user, String name) {
        // 將接收的引數返回給前端
        HashMap<String, Object> result = new HashMap<>();
        result.put("user", user);
        result.put("name", name);

        return result;
    }
}

VUE程式碼

除了自己手動拼接請求引數之外,axios在config中提供了params屬性,也可以實現該功能

// 正常拼接
request1() {
    this.axios.get("http://localhost:8080/index?user=zhang&name=hanzhe").then(res => {
        console.log("res", res);
    });
},
// 使用config中的params屬性進行路徑傳參
request2() {
    let config = {
        params: {
            user: "zhang",
            name: "hanzhe"
        }
    }
    this.axios.get("http://localhost:8080/index", config).then(res => {
        console.log("res", res);
    });
}

image

小程式程式碼

// 正常拼接
request1() {
    wx.request({
        url: 'http://localhost:8080/index?user=zhang&name=hanzhe',
        success: res => {
            console.log(res);
        }
    })
},
// 將請求型別設定為GET,wx識別後會將data轉換為路徑傳參
request2() {
    wx.request({
        url: 'http://localhost:8080/index',
        method: "GET",
        data: {
            user: "zhang",
            name: "hanzhe"
        },
        success: res => {
            console.log(res);
        }
    })
}

image

傳參-表單型別引數

表單型別引數,就是通過form表單提交的引數,通常用在例如HTML、JSP頁面的form標籤上,但如果是前後端分離的話就不能使用form表單提交了,這裡可以手動建立表單物件進行傳值

需要注意,GET請求一般只用於路徑傳參,其他型別傳參需要使用POST或其他型別的請求

表單型別引數也是【正常接收引數】中的例項程式碼接收值

後臺介面的編寫

@RestController
public class IndexController {
    @PostMapping("/index")
    public Map<String, Object> index(String username, String password) {
        // 將接收的引數返回給前端
        HashMap<String, Object> result = new HashMap<>();
        result.put("username", username);
        result.put("password", password);

        return result;
    }
}

VUE程式碼

request1() {
    // 構建表單物件,向表單中追加引數
    let data = new FormData();
    data.append("username", "123");
    data.append("password", "456");
    // 發起請求
    this.axios.post("http://localhost:8080/index", data).then(res => {
        console.log("res", res);
    });
},

image

小程式程式碼

小程式刪除了FormData物件,不能發起表單型別引數的請求,如果非要寫的話可以試著使用wx.uploadFile實現,這裡就不嘗試了

傳參-請求體引數

請求體傳參,是在發起請求時將引數放在請求體中

表單型別引數需要使用上面【請求體接收引數】中的例項程式碼接收值

後臺介面的編寫

@RestController
public class IndexController {
    @PostMapping("/index")
    public Map<String, Object> index(@RequestBody UserEntity entity) {
        // 將接收的引數返回給前端
        HashMap<String, Object> result = new HashMap<>();
        result.put("username", entity.getUsername());
        result.put("password", entity.getPassword());

        return result;
    }
}

VUE程式碼

axios如果發起的為POST型別請求,預設會將引數放在請求體中,這裡直接寫即可

request1() {
    // 建立date物件儲存引數
    let data = {
        username: "哈哈哈哈",
        password: "嘿嘿嘿嘿"
    }
    // 發起請求
    this.axios.post("http://localhost:8080/index", data).then(res => {
        console.log("res", res);
    });
},

小程式程式碼

小程式程式碼也是一樣的,當發起的時POST型別的請求時,預設會把引數放在請求體中

request1() {
    // 構建表單物件,向表單中追加引數
    let data = {
        username: "哈哈哈哈哈哈",
        password: "aabbccdd"
    }
    // 發起請求
    wx.request({
        url: 'http://localhost:8080/index',
        method: "POST",
        data: data,
        success: res => {
            console.log(res.data);
        }
    })
},

image

小技巧:如何區分傳參型別

在實際開發中大概率不用寫前端程式碼,只負責編寫後臺介面,但怎樣才能知道前端請求是什麼型別引數?

關於這點可以通過瀏覽器開發者工具的【網路】皮膚可以看出來,網路皮膚開啟時會錄製網頁發起的所有請求

image

路徑佔位傳參就不解釋了,沒啥好說的,這裡介紹一下路徑傳參、表單傳參和請求體傳參

路徑傳參

編寫好路徑傳參的請求程式碼後切換到網路皮膚,點選發起請求:

image

請求體傳參

編寫好請求體傳參的請求程式碼後切換到網路皮膚,點選發起請求:

image

表單型別傳參

編寫好表單型別傳參的請求程式碼後切換到網路皮膚,點選發起請求:

image

封裝統一響應工具類

掌握了前後端互動的流程就可以正常開發網站了,這裡推薦後端返回一套規定好的模板資料,否則某些情況可能會比較難處理,例如這個查詢使用者列表的介面:

@RestController
public class IndexController {
    @RequestMapping("/index")
    public List<HashMap<String, String>> index() {
        // 查詢使用者列表
        List<HashMap<String, String>> userList = this.selectList();
        // 將使用者列表資料返回給前端
        return userList;
    }

    // 模擬dao層的查詢程式碼,返回一個集合列表,集合中每個元素對應一條使用者資訊
    public List<HashMap<String, String>> selectList() {
        ArrayList<HashMap<String, String>> list = new ArrayList<>();
        for (int i = 1; i <= 5; i++) {
            HashMap<String, String> map = new HashMap<>();
            map.put("id", UUID.randomUUID().toString());
            map.put("username", "遊客" + i);
            map.put("gender", i % 2 == 1 ? "男" : "女");
            list.add(map);
        }
        return list;
    }
}

image

該介面乍一看沒毛病,拿到使用者列表資料後返回給前端用於渲染,合情合理,可是如果後端業務邏輯有BUG可能會導致前端接收到的結果為空,這種情況下前端就需要判斷,如果接收到的值為空,就提示請求出錯,問題貌似已經解決,但是如果表中本來就沒有任何資料的話有應該怎麼處理

上述的就是最常見的一種比較頭疼的情況,所以針對這種情況最好指定一套標準的返回模板進行處理

制定響應工具類

根據剛剛的舉例來看,返回結果中應該有一個標識來判斷該請求是否執行成功,如果執行失敗的話還應該返回失敗原因,響應給前端的資料會被轉換為JSON資料,使用Map集合來返回最合適不過了

import java.util.HashMap;
import java.util.Map;

public class Result extends HashMap<String, Object> {

    /**
     * 私有化構造方法,不讓外界直接建立物件
     * @param status true為請求成功,false為請求失敗
     * @param msg    返回給前端的訊息
     */
    private Result(boolean status, String msg) {
        // 規定無論請求成功還是失敗,這兩個引數都必須攜帶
        super.put("status", status);
        super.put("msg", msg);
    }

    /**
     * 靜態方法,如果請求成功就呼叫ok
     */
    public static Result ok() {
        return new Result(true, "請求成功");
    }

    /**
     * 靜態方法,如果請求失敗就呼叫fail,需要提供失敗資訊
     */
    public static Result fail(String msg) {
        return new Result(false, msg);
    }

    /**
     * 規定所有返回前端的資料都放在data中
     * @param name 物件名
     * @param obj  返回的物件
     */
    public Result put(String name, Object obj) {
        // 如果集合中不包含data,就建立個Map集合新增進去
        if (!this.containsKey("data")) {
            super.put("data", new HashMap<String, Object>());
        }
        // 獲取data對應的map集合,往裡面新增資料
        Map<String, Object> data = (Map<String, Object>) this.get("data");
        data.put(name, obj);
        return this;
    }

}

image

擴充套件:ApiPost介面除錯工具

在後臺介面編寫完成後,一般情況下我們都需要進行測試,GET請求還好,瀏覽器直接就訪問呢了,如果是POST請求還要去寫前端程式碼就很煩,這裡介紹一款介面除錯工具ApiPost

你可能沒聽過ApiPost,但是你大概率聽說過Postman,他們的用法幾乎一致,且ApiPost是國人開發的免費的介面除錯工具,介面中文很友好

image

image

這裡也可以看出來,form表單傳參其實也算在了請求體裡面,只不過使用的是multipart/form-data型別的引數而已,而之前提到的請求體傳參對應的就是application/json

相關文章