SpringMVC 中的参数还能这么传递?涨姿势了!

今天来聊一个 JavaWeb 中简单的话题,但是感觉却比较稀罕,因为这个技能点,有的小伙伴们可能没听过!

1.缘起

说到 Web 请求参数传递,大家能想到哪些参数传递方式?

参数可以放在地址栏中,不过地址栏参数的长度有限制,并且在有的场景下我们可能不希望参数暴漏在地址栏中。参数可以放在请求体中,这个没啥好说的。

小伙伴们试想这样一个场景:

在一个电商项目中,有一个提交订单的请求,这个请求是一个 POST 请求,请求参数都在请求体中。当用户提交成功后,为了防止用户刷新浏览器页面造成订单请求重复提交,我们一般会将用户重定向到一个显示订单的页面,这样即使用户刷新页面,也不会造成订单请求重复提交。

大概的代码就像下面这样:

@Controller
public class OrderController {
    @PostMapping("/order")
    public String order(OrderInfo orderInfo) {
        //其他处理逻辑
        return "redirect:/orderlist";
    }
}

这段代码我相信大家都懂吧!如果不懂可以看看松哥录制的免费的 SpringMVC 入门教程(硬核!松哥又整了一套免费视频,搞起!)。

但是这里有一个问题:如果我想传递参数怎么办?

如果是服务器端跳转,我们可以将参数放在 request 对象中,跳转完成后还能拿到参数,但是如果是客户端跳转我们就只能将参数放在地址栏中了,像上面这个方法的返回值我们可以写成:return "redirect:/orderlist?xxx=xxx";,这种传参方式有两个缺陷:

  • 地址栏的长度是有限的,也就意味着能够放在地址栏中的参数是有限的。
  • 不想将一些特殊的参数放在地址栏中。

那该怎么办?还有办法传递参数吗?

有!这就是今天松哥要和大家介绍的 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 的实现类如下:

image

从这个继承类中,我们基本上就能确定默认的保存介质时 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,好啦,今天就和大家聊这么多~

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,948评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,371评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,490评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,521评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,627评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,842评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,997评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,741评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,203评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,534评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,673评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,339评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,955评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,770评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,000评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,394评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,562评论 2 349

推荐阅读更多精彩内容