SpringMVC前后端分離交互傳參詳細說明


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

本文后台基於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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM