SpringMVC 中的參數(shù)還能這么傳遞?漲姿勢(shì)了!
今天來(lái)聊一個(gè) JavaWeb 中簡(jiǎn)單的話題,但是感覺(jué)卻比較稀罕,因?yàn)檫@個(gè)技能點(diǎn),有的小伙伴們可能沒(méi)聽(tīng)過(guò)!
1.緣起
說(shuō)到 Web 請(qǐng)求參數(shù)傳遞,大家能想到哪些參數(shù)傳遞方式?
參數(shù)可以放在地址欄中,不過(guò)地址欄參數(shù)的長(zhǎng)度有限制,并且在有的場(chǎng)景下我們可能不希望參數(shù)暴漏在地址欄中。參數(shù)可以放在請(qǐng)求體中,這個(gè)沒(méi)啥好說(shuō)的。
小伙伴們?cè)囅脒@樣一個(gè)場(chǎng)景:
在一個(gè)電商項(xiàng)目中,有一個(gè)提交訂單的請(qǐng)求,這個(gè)請(qǐng)求是一個(gè) POST 請(qǐng)求,請(qǐng)求參數(shù)都在請(qǐng)求體中。當(dāng)用戶提交成功后,為了防止用戶刷新瀏覽器頁(yè)面造成訂單請(qǐng)求重復(fù)提交,我們一般會(huì)將用戶重定向到一個(gè)顯示訂單的頁(yè)面,這樣即使用戶刷新頁(yè)面,也不會(huì)造成訂單請(qǐng)求重復(fù)提交。
大概的代碼就像下面這樣:
- @Controller
- public class OrderController {
- @PostMapping("/order")
- public String order(OrderInfo orderInfo) {
- //其他處理邏輯
- return "redirect:/orderlist";
- }
- }
這段代碼我相信大家都懂吧!如果不懂可以看看松哥錄制的免費(fèi)的 SpringMVC 入門(mén)教程(硬核!松哥又整了一套免費(fèi)視頻,搞起!)。
但是這里有一個(gè)問(wèn)題:如果我想傳遞參數(shù)怎么辦?
如果是服務(wù)器端跳轉(zhuǎn),我們可以將參數(shù)放在 request 對(duì)象中,跳轉(zhuǎn)完成后還能拿到參數(shù),但是如果是客戶端跳轉(zhuǎn)我們就只能將參數(shù)放在地址欄中了,像上面這個(gè)方法的返回值我們可以寫(xiě)成:return "redirect:/orderlist?xxx=xxx";,這種傳參方式有兩個(gè)缺陷:
- 地址欄的長(zhǎng)度是有限的,也就意味著能夠放在地址欄中的參數(shù)是有限的。
- 不想將一些特殊的參數(shù)放在地址欄中。
那該怎么辦?還有辦法傳遞參數(shù)嗎?
有!這就是今天松哥要和大家介紹的 flashMap,專門(mén)用來(lái)解決重定向時(shí)參數(shù)的傳遞問(wèn)題。
2.flashMap
在重定向時(shí),如果需要傳遞參數(shù),但是又不想放在地址欄中,我們就可以通過(guò) flashMap 來(lái)傳遞參數(shù),松哥先來(lái)一個(gè)簡(jiǎn)單的例子大家看看效果:
首先我們定義一個(gè)簡(jiǎn)單的頁(yè)面,里邊就一個(gè) post 請(qǐng)求提交按鈕,如下:
- <!DOCTYPE html>
- <html lang="en">
- <head>
- <meta charset="UTF-8">
- <title>Title</title>
- </head>
- <body>
- <form action="/order">
- <input type="submit" value="提交">
- </form>
- </body>
- </html>
然后在服務(wù)端接收該請(qǐng)求,并完成重定向:
- @Controller
- public class OrderController {
- @PostMapping("/order")
- public String order(HttpServletRequest req) {
- FlashMap flashMap = (FlashMap) req.getAttribute(DispatcherServlet.OUTPUT_FLASH_MAP_ATTRIBUTE);
- flashMap.put("name", "江南一點(diǎn)雨");
- return "redirect:/orderlist";
- }
- @GetMapping("/orderlist")
- @ResponseBody
- public String orderList(Model model) {
- return (String) model.getAttribute("name");
- }
- }
首先在 order 接口中,獲取到 flashMap 屬性,然后存入需要傳遞的參數(shù),這些參數(shù)最終會(huì)被 SpringMVC 自動(dòng)放入重定向接口的 Model 中,這樣我們?cè)?orderlist 接口中,就可以獲取到該屬性了。
當(dāng)然,這是一個(gè)比較粗糙的寫(xiě)法,我們還可以通過(guò) RedirectAttributes 來(lái)簡(jiǎn)化這一步驟:
- @Controller
- public class OrderController {
- @PostMapping("/order")
- public String order(RedirectAttributes attr) {
- attr.addFlashAttribute("site", "www.javaboy.org");
- attr.addAttribute("name", "微信公眾號(hào):江南一點(diǎn)雨");
- return "redirect:/orderlist";
- }
- @GetMapping("/orderlist")
- @ResponseBody
- public String orderList(Model model) {
- return (String) model.getAttribute("site");
- }
- }
RedirectAttributes 中有兩種添加參數(shù)的方式:
- addFlashAttribute:將參數(shù)放到 flashMap 中。
- addAttribute:將參數(shù)放到 URL 地址中。
經(jīng)過(guò)前面的講解,現(xiàn)在小伙伴們應(yīng)該大致明白了 flashMap 的作用了,就是在你進(jìn)行重定向的時(shí)候,不通過(guò)地址欄傳遞參數(shù)。
很多小伙伴可能會(huì)有疑問(wèn),重定向其實(shí)就是瀏覽器發(fā)起了一個(gè)新的請(qǐng)求,這新的請(qǐng)求怎么就獲取到上一個(gè)請(qǐng)求保存的參數(shù)呢?這我們就要來(lái)看看 SpringMVC 的源碼了。
3.源碼分析
首先這里涉及到一個(gè)關(guān)鍵類(lèi)叫做 FlashMapManager,如下:
- public interface FlashMapManager {
- @Nullable
- FlashMap retrieveAndUpdate(HttpServletRequest request, HttpServletResponse response);
- void saveOutputFlashMap(FlashMap flashMap, HttpServletRequest request, HttpServletResponse response);
- }
兩個(gè)方法含義一眼就能看出來(lái):
retrieveAndUpdate:這個(gè)方法用來(lái)恢復(fù)參數(shù),并將恢復(fù)過(guò)的的參數(shù)和超時(shí)的參數(shù)從保存介質(zhì)中刪除。
saveOutputFlashMap:將參數(shù)保存保存起來(lái)。
FlashMapManager 的實(shí)現(xiàn)類(lèi)如下:
從這個(gè)繼承類(lèi)中,我們基本上就能確定默認(rèn)的保存介質(zhì)時(shí) session。具體的保存邏輯則是在 AbstractFlashMapManager 類(lèi)中。
整個(gè)參數(shù)傳遞的過(guò)程可以分為三大步:
第一步,首先我們將參數(shù)設(shè)置到 outputFlashMap 中,有兩種設(shè)置方式:我們前面的代碼 req.getAttribute(DispatcherServlet.OUTPUT_FLASH_MAP_ATTRIBUTE) 就是直接獲取 outputFlashMap 對(duì)象然后把參數(shù)放進(jìn)去;第二種方式就是通過(guò)在接口中添加 RedirectAttributes 參數(shù),然后把需要傳遞的參數(shù)放入 RedirectAttributes 中,這樣當(dāng)處理器處理完畢后,會(huì)自動(dòng)將其設(shè)置到 outputFlashMap 中,具體邏輯在 RequestMappingHandlerAdapter#getModelAndView 方法中:
- private ModelAndView getModelAndView(ModelAndViewContainer mavContainer,
- ModelFactory modelFactory, NativeWebRequest webRequest) throws Exception {
- //省略...
- if (model instanceof RedirectAttributes) {
- Map<String, ?> flashAttributes = ((RedirectAttributes) model).getFlashAttributes();
- HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
- if (request != null) {
- RequestContextUtils.getOutputFlashMap(request).putAll(flashAttributes);
- }
- }
- return mav;
- }
可以看到,如果 model 是 RedirectAttributes 的實(shí)例的話,則通過(guò) getOutputFlashMap 方法獲取到 outputFlashMap 屬性,然后相關(guān)的屬性設(shè)置進(jìn)去。
這是第一步,就是將需要傳遞的參數(shù),先保存到 flashMap 中。
第二步,重定向?qū)?yīng)的視圖是 RedirectView,在它的 renderMergedOutputModel 方法中,會(huì)調(diào)用 FlashMapManager 的 saveOutputFlashMap 方法,將 outputFlashMap 保存到 session 中,如下:
- protected void renderMergedOutputModel(Map<String, Object> model, HttpServletRequest request,
- HttpServletResponse response) throws IOException {
- String targetUrl = createTargetUrl(model, request);
- targetUrl = updateTargetUrl(targetUrl, model, request, response);
- // Save flash attributes
- RequestContextUtils.saveOutputFlashMap(targetUrl, request, response);
- // Redirect
- sendRedirect(request, response, targetUrl, this.http10Compatible);
- }
RequestContextUtils.saveOutputFlashMap 方法最終就會(huì)調(diào)用到 FlashMapManager 的 saveOutputFlashMap 方法,將 outputFlashMap 保存下來(lái)。我們來(lái)大概看一下保存邏輯:
- public final void saveOutputFlashMap(FlashMap flashMap, HttpServletRequest request, HttpServletResponse response) {
- if (CollectionUtils.isEmpty(flashMap)) {
- return;
- }
- String path = decodeAndNormalizePath(flashMap.getTargetRequestPath(), request);
- flashMap.setTargetRequestPath(path);
- flashMap.startExpirationPeriod(getFlashMapTimeout());
- Object mutex = getFlashMapsMutex(request);
- if (mutex != null) {
- synchronized (mutex) {
- List<FlashMap> allFlashMaps = retrieveFlashMaps(request);
- allFlashMaps = (allFlashMaps != null ? allFlashMaps : new CopyOnWriteArrayList<>());
- allFlashMaps.add(flashMap);
- updateFlashMaps(allFlashMaps, request, response);
- }
- }
- else {
- List<FlashMap> allFlashMaps = retrieveFlashMaps(request);
- allFlashMaps = (allFlashMaps != null ? allFlashMaps : new ArrayList<>(1));
- allFlashMaps.add(flashMap);
- updateFlashMaps(allFlashMaps, request, response);
- }
- }
其實(shí)這里的邏輯也很簡(jiǎn)單,保存之前會(huì)給 flashMap 設(shè)置兩個(gè)屬性,一個(gè)是重定向的 url 地址,另一個(gè)則是過(guò)期時(shí)間,過(guò)期時(shí)間默認(rèn) 180 秒,這兩個(gè)屬性在第三步加載 flashMap 的時(shí)候會(huì)用到。然后將 flashMap 放入集合中,并調(diào)用 updateFlashMaps 方法存入 session 中。
第三步,當(dāng)重定向請(qǐng)求到達(dá) DispatcherServlet#doService 方法后,此時(shí)會(huì)調(diào)用 FlashMapManager#retrieveAndUpdate 方法從 Session 中獲取 outputFlashMap 并設(shè)置到 Request 屬性中備用(最終會(huì)被轉(zhuǎn)化到 Model 中的屬性),相關(guān)代碼如下:
- protected void doService(HttpServletRequest request, HttpServletResponse response) throws Exception {
- //省略...
- if (this.flashMapManager != null) {
- FlashMap inputFlashMap = this.flashMapManager.retrieveAndUpdate(request, response);
- if (inputFlashMap != null) {
- request.setAttribute(INPUT_FLASH_MAP_ATTRIBUTE, Collections.unmodifiableMap(inputFlashMap));
- }
- request.setAttribute(OUTPUT_FLASH_MAP_ATTRIBUTE, new FlashMap());
- request.setAttribute(FLASH_MAP_MANAGER_ATTRIBUTE, this.flashMapManager);
- }
- //省略...
- }
注意這里獲取出來(lái)的 outputFlashMap 換了一個(gè)名字,變成了 inputFlashMap,其實(shí)是同一個(gè)東西。
我們可以大概看一下獲取的邏輯 AbstractFlashMapManager#retrieveAndUpdate:
- public final FlashMap retrieveAndUpdate(HttpServletRequest request, HttpServletResponse response) {
- List<FlashMap> allFlashMaps = retrieveFlashMaps(request);
- if (CollectionUtils.isEmpty(allFlashMaps)) {
- return null;
- }
- List<FlashMap> mapsToRemove = getExpiredFlashMaps(allFlashMaps);
- FlashMap match = getMatchingFlashMap(allFlashMaps, request);
- if (match != null) {
- mapsToRemove.add(match);
- }
- if (!mapsToRemove.isEmpty()) {
- Object mutex = getFlashMapsMutex(request);
- if (mutex != null) {
- synchronized (mutex) {
- allFlashMaps = retrieveFlashMaps(request);
- if (allFlashMaps != null) {
- allFlashMaps.removeAll(mapsToRemove);
- updateFlashMaps(allFlashMaps, request, response);
- }
- }
- }
- else {
- allFlashMaps.removeAll(mapsToRemove);
- updateFlashMaps(allFlashMaps, request, response);
- }
- }
- return match;
- }
- 首先調(diào)用 retrieveFlashMaps 方法從 session 中獲取到所有的 FlashMap。
- 調(diào)用 getExpiredFlashMaps 方法獲取所有過(guò)期的 FlashMap,F(xiàn)lashMap 默認(rèn)的過(guò)期時(shí)間是 180s。
- 獲取和當(dāng)前請(qǐng)求匹配的 getMatchingFlashMap,具體的匹配邏輯就兩點(diǎn):重定向地址要和當(dāng)前請(qǐng)求地址相同;預(yù)設(shè)參數(shù)要相同。一般來(lái)說(shuō)我們不需要配置預(yù)設(shè)參數(shù),所以這一條可以忽略。如果想要設(shè)置,則首先給 flashMap 設(shè)置,像這樣:flashMap.addTargetRequestParam("aa", "bb");,然后在重定向的地址欄也加上這個(gè)參數(shù):return "redirect:/orderlist?aa=bb"; 即可。
- 將獲取到的匹配的 FlashMap 對(duì)象放入 mapsToRemove 集合中(這個(gè)匹配到的 FlashMap 即將失效,放入集合中一會(huì)被清空)。
- 將 allFlashMaps 集合中的所有 mapsToRemove 數(shù)據(jù)清空,同時(shí)調(diào)用 updateFlashMaps 方法更新 session 中的 FlashMap。
- 最終將匹配到的 flashMap 返回。
這就是整個(gè)獲取 flashMap 的方法,整體來(lái)看還是非常 easy 的,并沒(méi)有什么難點(diǎn)。
4.小結(jié)
好啦,今天就和小伙伴們分享了一下 SpringMVC 中的 flashMap,不知道大家有沒(méi)有在工作中用到這個(gè)東西?如果剛好碰到松哥前面所說(shuō)的需求,用 FlashMap 真的還是蠻方便的。
本文轉(zhuǎn)載自微信公眾號(hào)「江南一點(diǎn)雨」,可以通過(guò)以下二維碼關(guān)注。轉(zhuǎn)載本文請(qǐng)聯(lián)系江南一點(diǎn)雨公眾號(hào)。






























