SpringMVC 中的參數還能這麼傳遞?漲姿式了!

今天來聊一個 JavaWeb 中簡單的話題,可是感受卻比較稀罕,由於這個技能點,有的小夥伴們可能沒聽過!html

1.緣起

說到 Web 請求參數傳遞,你們能想到哪些參數傳遞方式?java

參數能夠放在地址欄中,不過地址欄參數的長度有限制,而且在有的場景下咱們可能不但願參數暴漏在地址欄中。參數能夠放在請求體中,這個沒啥好說的。web

小夥伴們試想這樣一個場景:segmentfault

在一個電商項目中,有一個提交訂單的請求,這個請求是一個 POST 請求,請求參數都在請求體中。當用戶提交成功後,爲了防止用戶刷新瀏覽器頁面形成訂單請求重複提交,咱們通常會將用戶重定向到一個顯示訂單的頁面,這樣即便用戶刷新頁面,也不會形成訂單請求重複提交。瀏覽器

大概的代碼就像下面這樣:服務器

@Controller
public class OrderController {
    @PostMapping("/order")
    public String order(OrderInfo orderInfo) {
        //其餘處理邏輯
        return "redirect:/orderlist";
    }
}

這段代碼我相信你們都懂吧!若是不懂能夠看看鬆哥錄製的免費的 SpringMVC 入門教程(硬核!鬆哥又整了一套免費視頻,搞起!)。微信

可是這裏有一個問題:若是我想傳遞參數怎麼辦?session

若是是服務器端跳轉,咱們能夠將參數放在 request 對象中,跳轉完成後還能拿到參數,可是若是是客戶端跳轉咱們就只能將參數放在地址欄中了,像上面這個方法的返回值咱們能夠寫成:return "redirect:/orderlist?xxx=xxx";,這種傳參方式有兩個缺陷:app

  • 地址欄的長度是有限的,也就意味着可以放在地址欄中的參數是有限的。
  • 不想將一些特殊的參數放在地址欄中。

那該怎麼辦?還有辦法傳遞參數嗎?源碼分析

有!這就是今天鬆哥要和你們介紹的 flashMap,專門用來解決重定向時參數的傳遞問題。

2.flashMap

在重定向時,若是須要傳遞參數,可是又不想放在地址欄中,咱們就能夠經過 flashMap 來傳遞參數,鬆哥先來一個簡單的例子你們看看效果:

首先咱們定義一個簡單的頁面,裏邊就一個 post 請求提交按鈕,以下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<form action="/order" method="post">
    <input type="submit" value="提交">
</form>
</body>
</html>

而後在服務端接收該請求,並完成重定向:

@Controller
public class OrderController {
    @PostMapping("/order")
    public String order(HttpServletRequest req) {
        FlashMap flashMap = (FlashMap) req.getAttribute(DispatcherServlet.OUTPUT_FLASH_MAP_ATTRIBUTE);
        flashMap.put("name", "江南一點雨");
        return "redirect:/orderlist";
    }

    @GetMapping("/orderlist")
    @ResponseBody
    public String orderList(Model model) {
        return (String) model.getAttribute("name");
    }
}

首先在 order 接口中,獲取到 flashMap 屬性,而後存入須要傳遞的參數,這些參數最終會被 SpringMVC 自動放入重定向接口的 Model 中,這樣咱們在 orderlist 接口中,就能夠獲取到該屬性了。

固然,這是一個比較粗糙的寫法,咱們還能夠經過 RedirectAttributes 來簡化這一步驟:

@Controller
public class OrderController {
    @PostMapping("/order")
    public String order(RedirectAttributes attr) {
        attr.addFlashAttribute("site", "www.javaboy.org");
        attr.addAttribute("name", "微信公衆號:江南一點雨");
        return "redirect:/orderlist";
    }

    @GetMapping("/orderlist")
    @ResponseBody
    public String orderList(Model model) {
        return (String) model.getAttribute("site");
    }
}

RedirectAttributes 中有兩種添加參數的方式:

  • addFlashAttribute:將參數放到 flashMap 中。
  • addAttribute:將參數放到 URL 地址中。

通過前面的講解,如今小夥伴們應該大體明白了 flashMap 的做用了,就是在你進行重定向的時候,不經過地址欄傳遞參數。

不少小夥伴可能會有疑問,重定向其實就是瀏覽器發起了一個新的請求,這新的請求怎麼就獲取到上一個請求保存的參數呢?這咱們就要來看看 SpringMVC 的源碼了。

3.源碼分析

首先這裏涉及到一個關鍵類叫作 FlashMapManager,以下:

public interface FlashMapManager {
    @Nullable
    FlashMap retrieveAndUpdate(HttpServletRequest request, HttpServletResponse response);
    void saveOutputFlashMap(FlashMap flashMap, HttpServletRequest request, HttpServletResponse response);
}

兩個方法含義一眼就能看出來:

  • retrieveAndUpdate:這個方法用來恢復參數,並將恢復過的的參數和超時的參數從保存介質中刪除。
  • saveOutputFlashMap:將參數保存保存起來。

FlashMapManager 的實現類以下:

從這個繼承類中,咱們基本上就能肯定默認的保存介質時 session。具體的保存邏輯則是在 AbstractFlashMapManager 類中。

整個參數傳遞的過程能夠分爲三大步:

第一步,首先咱們將參數設置到 outputFlashMap 中,有兩種設置方式:咱們前面的代碼 req.getAttribute(DispatcherServlet.OUTPUT_FLASH_MAP_ATTRIBUTE) 就是直接獲取 outputFlashMap 對象而後把參數放進去;第二種方式就是經過在接口中添加 RedirectAttributes 參數,而後把須要傳遞的參數放入 RedirectAttributes 中,這樣當處理器處理完畢後,會自動將其設置到 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 的實例的話,則經過 getOutputFlashMap 方法獲取到 outputFlashMap 屬性,而後相關的屬性設置進去。

這是第一步,就是將須要傳遞的參數,先保存到 flashMap 中。

第二步,重定向對應的視圖是 RedirectView,在它的 renderMergedOutputModel 方法中,會調用 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 方法最終就會調用到 FlashMapManager 的 saveOutputFlashMap 方法,將 outputFlashMap 保存下來。咱們來大概看一下保存邏輯:

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);
    }
}

其實這裏的邏輯也很簡單,保存以前會給 flashMap 設置兩個屬性,一個是重定向的 url 地址,另外一個則是過時時間,過時時間默認 180 秒,這兩個屬性在第三步加載 flashMap 的時候會用到。而後將 flashMap 放入集合中,並調用 updateFlashMaps 方法存入 session 中。

第三步,當重定向請求到達 DispatcherServlet#doService 方法後,此時會調用 FlashMapManager#retrieveAndUpdate 方法從 Session 中獲取 outputFlashMap 並設置到 Request 屬性中備用(最終會被轉化到 Model 中的屬性),相關代碼以下:

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);
    }
    //省略...
}

注意這裏獲取出來的 outputFlashMap 換了一個名字,變成了 inputFlashMap,實際上是同一個東西。

咱們能夠大概看一下獲取的邏輯 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;
}
  • 首先調用 retrieveFlashMaps 方法從 session 中獲取到全部的 FlashMap。
  • 調用 getExpiredFlashMaps 方法獲取全部過時的 FlashMap,FlashMap 默認的過時時間是 180s。
  • 獲取和當前請求匹配的 getMatchingFlashMap,具體的匹配邏輯就兩點:重定向地址要和當前請求地址相同;預設參數要相同。通常來講咱們不須要配置預設參數,因此這一條能夠忽略。若是想要設置,則首先給 flashMap 設置,像這樣:flashMap.addTargetRequestParam("aa", "bb");,而後在重定向的地址欄也加上這個參數:return "redirect:/orderlist?aa=bb"; 便可。
  • 將獲取到的匹配的 FlashMap 對象放入 mapsToRemove 集合中(這個匹配到的 FlashMap 即將失效,放入集合中一會被清空)。
  • 將 allFlashMaps 集合中的全部 mapsToRemove 數據清空,同時調用 updateFlashMaps 方法更新 session 中的 FlashMap。
  • 最終將匹配到的 flashMap 返回。

這就是整個獲取 flashMap 的方法,總體來看仍是很是 easy 的,並無什麼難點。

4.小結

好啦,今天就和小夥伴們分享了一下 SpringMVC 中的 flashMap,不知道你們有沒有在工做中用到這個東西?若是恰好碰到鬆哥前面所說的需求,用 FlashMap 真的仍是蠻方便的。若是須要下載本文案例,小夥伴們能夠在公衆號【江南一點雨】後臺回覆 20210302,好啦,今天就和你們聊這麼多~

相關文章
相關標籤/搜索