赞
踩
本文要解决的是重定向请求时,把数据传到目标地方。
默认情况下,所有模型属性都被视为在重定向URL中作为URI模板变量暴露。在其余的属性中,基本类型或基本类型的集合或基本类型的数组将自动附加为查询参数。
如果专门为重定向准备了一个模型实例,那么将基本类型属性作为查询参数附加可能是理想的结果。但是,在带注解的控制器中,模型可以包含为渲染目的添加的其他属性(例如,下拉字段值)。为了避免此类属性出现在URL中,@RequestMapping
方法可以声明类型为RedirectAttributes
的参数,并使用它指定可供RedirectView
使用的确切属性。如果方法确实重定向,则使用RedirectAttributes
的内容。否则,将使用model的内容。
RequestMappingHandlerAdapter
提供了一个标记叫做ignoreDefaultModelOnRedirect
,可以用来声明如果控制器方法重定向时,默认的Model是否使用。RequestMappingHandlerAdapter
提供了一个名为ignoreDefaultModelOnRedirect
的标志,您可以使用该标志指示如果控制器方法重定向,则不应使用默认模型的内容。相反,控制器方法应该声明RedirectAttributes类型的属性,如果不这样做,则不应该将任何属性传递给RedirectView。MVC命名空间和MVC Java配置都将此标志设置为false,以保持向后兼容性。但是,对于新应用程序,我们建议将其设置为true。
请注意,当前请求中的URI模板变量在扩展重定向URL时自动可用,您不需要通过模型或重定向属性显式添加它们。以下示例显示如何定义重定向:
@PostMapping("/files/{path}")
public String upload(...) {
// ...
return "redirect:files/{path}";
}
向重定向目标传递数据的另一种方法是使用flash属性。与其他重定向属性不同,flash属性保存在HTTP会话中(因此不会出现在URL中)。
Flash属性为一个请求提供了一种方法来存储打算在另一个请求中使用的属性。这是重定向时最常用的 — 例如Post-Redirect-Get
模式。flash属性
在重定向之前(通常在会话中)临时保存,以便在重定向后可用于请求,之后会被删除。
SpringMVC有两个主要的类来支持flash属性。FlashMap
类用于保存flash属性,而FlashMapManager
接口的实例用于存储、检索和管理FlashMap实例。
默认支持Flash 属性,你不需要显示进行启用。但是如果不使用,它不会导致HTTP会话创建。在每个请求上,都有一个 “input” FlashMap
和一个“output” FlashMap
。前者具有从上一个请求(如果有)传递的属性,后者具有为后续请求保存的属性。这两个FlashMap实例都可以通过RequestContextUtils
中的静态方法从SpringMVC中的任何位置访问。
public static Map<String, ?> getInputFlashMap(HttpServletRequest request) {
return (Map<String, ?>) request.getAttribute(DispatcherServlet.INPUT_FLASH_MAP_ATTRIBUTE);
}
public static final String INPUT_FLASH_MAP_ATTRIBUTE = DispatcherServlet.class.getName() + ".INPUT_FLASH_MAP";
public static FlashMap getOutputFlashMap(HttpServletRequest request) {
return (FlashMap) request.getAttribute(DispatcherServlet.OUTPUT_FLASH_MAP_ATTRIBUTE);
}
public static final String OUTPUT_FLASH_MAP_ATTRIBUTE = DispatcherServlet.class.getName() + ".OUTPUT_FLASH_MAP";
public static final String FLASH_MAP_MANAGER_ATTRIBUTE = DispatcherServlet.class.getName() + ".FLASH_MAP_MANAGER";
带注解的控制器通常不需要直接使用FlashMap。相反,@RequestMapping
注解的方法可以接受RedirectAttributes
类型的参数,并使用它为重定向场景添加flash属性。通过RedirectAttributes
添加的Flash属性会自动传播到“output”FlashMap
。类似地,在重定向之后,“input”FlashMap
中的属性会自动添加到为目标URL提供服务的控制器的Model中。
为flash 属性匹配请求
flash属性的概念存在于许多其他web框架中,并且已经证明有时会遇到并发问题。这是因为,根据定义,flash属性
将存储到下一个请求。但是,“下一个”请求可能不是预期的接收者,而是另一个异步请求(例如,轮询或资源请求),在这种情况下,flash属性被过早删除。
为了减少出现此类问题的可能性,RedirectView
会使用目标重定向URL的路径和查询参数自动“标记”FlashMap
实例。反过来,默认FlashMapManager
在查找“input”FlashMap
时将该信息与传入请求相匹配。
// DispatcherServlet#doService有如下代码来获取inputFlashMap FlashMap inputFlashMap = this.flashMapManager.retrieveAndUpdate(request, response); //具体获取inputFlashMap 方法如下 @Override @Nullable public final FlashMap retrieveAndUpdate(HttpServletRequest request, HttpServletResponse response) { //从session中获取FLASH_MAPS_SESSION_ATTRIBUTE List<FlashMap> allFlashMaps = retrieveFlashMaps(request); if (CollectionUtils.isEmpty(allFlashMaps)) { return null; } // 获取过期的FlashMap List<FlashMap> mapsToRemove = getExpiredFlashMaps(allFlashMaps); //获取与当前请求匹配的FlashMap FlashMap match = getMatchingFlashMap(allFlashMaps, request); if (match != null) { mapsToRemove.add(match); } // 从allFlashMaps移除掉mapsToRemove,然后更新session的FLASH_MAPS_SESSION_ATTRIBUTE 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); } } // 返回当前请求匹配的FlashMap return match; }
这并不能完全消除并发问题的可能性,但可以通过重定向URL中已有的信息大大减少并发问题。因此,我们建议您主要在重定向场景中使用flash属性。
常见的可以使用URL传参、uriPathVariable,如/testr?name=jane
或/testr/{jane}
。除此之外,可以考虑使用flash attributes。
一个请求在实际转发到doDispatch前,请求属性里面会有如下属性:
org.springframework.web.context.request.async.WebAsyncManager.WEB_ASYNC_MANAGER--WebAsyncManager
CharacterEncodingFilter.FILTERED--true
org.springframework.web.servlet.DispatcherServlet.CONTEXT--WebApplicationContext for namespace 'DispatcherServlet-servlet', started on Fri Dec 10 15:45:06 CST 2021
org.springframework.web.servlet.DispatcherServlet.LOCALE_RESOLVER--AcceptHeaderLocaleResolver
org.springframework.web.servlet.DispatcherServlet.OUTPUT_FLASH_MAP--FlashMap
HiddenHttpMethodFilter.FILTERED--true
org.springframework.web.servlet.DispatcherServlet.FLASH_MAP_MANAGER--SessionFlashMapManager
org.springframework.web.servlet.DispatcherServlet.THEME_RESOLVER--FixedThemeResolver
org.springframework.web.servlet.DispatcherServlet.THEME_SOURCE--WebApplicationContext for namespace 'DispatcherServlet-servlet', started on Fri Dec 10 15:45:06 CST 2021
可以看到此时有一个空的outputFlashMap,但是没有inputFlashMap。
如下所示,不通过URL传参,testRedirect请求重定向到testr。
// 重定向前的方法 @RequestMapping("/testRedirect") public String testRedirect(HttpServletRequest request, RedirectAttributes redirectAttributes){ // 普通属性 redirectAttributes.addAttribute("redirectAttributes","redirectAttributesValue"); //flash 属性 redirectAttributes.addFlashAttribute("addFlashAttribute","redirectAttributes.addFlashAttribute-value"); return "redirect:/testr"; } // 重定向后的 @RequestMapping("/testr") public String testr(HttpServletRequest request, String redirectAttributes, String addFlashAttribute,Model model){ // redirectAttributesValue System.out.println(request.getParameter("redirectAttributes")); // null System.out.println(request.getParameter("addFlashAttribute")); // {addFlashAttribute=redirectAttributes.addFlashAttribute-value} System.out.println(model); return "success"; }
重定向URL这里最终为:
http://localhost:8080/testr?redirectAttributes=redirectAttributesValue
可以看到方法testRedirect中redirectAttributes.addAttribute("redirectAttributes","redirectAttributesValue");
被作为URL的queryString参数。此时方法testr中request中获取不到FlashAttribute
,model里面可以获取到FlashAttribute
// 路径链如下
DispatcherServlet#doService--
DispatcherServlet#doDispatch--
AbstractHandlerMethodAdapter#handle--
RequestMappingHandlerAdapter#handleInternal--
RequestMappingHandlerAdapter#invokeHandlerMethod--
ServletInvocableHandlerMethod#invokeAndHandle--
RequestMappingHandlerAdapter#getModelAndView
testRedirect
目标方法处理前,DispatcherServlet#doService
中,此时inputFlashMap
为null
,outputFlashMap
为new FlashMap()
。默认情况下,这时是没有inputFlashMap的。除非已经有其他重定向请求使用了flashMap且没有过期并匹配当前请求。
RequestMappingHandlerAdapter#invokeHandlerMethod
方法中,获取到ModelAndViewContainer
实例后会从request中获取属性DispatcherServlet.INPUT_FLASH_MAP_ATTRIBUTE
也就是获取inputFlashMap
放入到model中。这里面默认放入的是defaultModel
而非redirectModel
(默认情况下redirectModelScenario
是false,ignoreDefaultModelOnRedirect
也是false。)
另外,这里默认为mavContainer设置ignoreDefaultModelOnRedirect=false
。
mavContainer.addAllAttributes(RequestContextUtils.getInputFlashMap(request));
mavContainer.setIgnoreDefaultModelOnRedirect(this.ignoreDefaultModelOnRedirect);
我们先看一下方法String testRedirect(HttpServletRequest request, RedirectAttributes redirectAttributes)
解析参数结果(RedirectAttributes
实例是一个RedirectAttributesModelMap
,该参数被RedirectAttributesMethodArgumentResolver
解析):
RedirectAttributesModelMap
主要属性如下所示,也就是说比default model多了一个private final ModelMap flashAttributes = new ModelMap();
:
public class RedirectAttributesModelMap extends ModelMap implements RedirectAttributes {
// 数据绑定器
@Nullable
private final DataBinder dataBinder;
// 存放flashAttribute
private final ModelMap flashAttributes = new ModelMap();
}
RedirectAttributesMethodArgumentResolver
解析RedirectAttributes
参数的过程如下所示:
@Override public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception { Assert.state(mavContainer != null, "RedirectAttributes argument only supported on regular handler methods"); ModelMap redirectAttributes; // 实例化RedirectAttributesModelMap if (binderFactory != null) { DataBinder dataBinder = binderFactory.createBinder(webRequest, null, DataBinder.DEFAULT_OBJECT_NAME); redirectAttributes = new RedirectAttributesModelMap(dataBinder); } else { redirectAttributes = new RedirectAttributesModelMap(); } // 将当前redirectAttributes赋予mavContainer的RedirectModel属性 mavContainer.setRedirectModel(redirectAttributes); return redirectAttributes; }
这里非常有必要说的是,如果目前方法有参数redirectAttributes
,那么在解析目标方法参数后,mavContainer
中的RedirectModel
就是redirectAttributes
实例对象!
然后目标方法则会分别向默认集合与flashAttributes中放入数据:
// 普通属性
redirectAttributes.addAttribute("redirectAttributes","redirectAttributesValue");
//flash 属性
redirectAttributes.addFlashAttribute("addFlashAttribute","redirectAttributes.addFlashAttribute-value");
返回结果是redirect:/testr
,这里使用ViewNameMethodReturnValueHandlr
处理返回结果。过程如下所示,这里会将mavContainer的RedirectModelScenario
属性设置为true。
@Override public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception { if (returnValue instanceof CharSequence) { String viewName = returnValue.toString(); mavContainer.setViewName(viewName); if (isRedirectViewName(viewName)) { // 这里设置RedirectModelScenario为true哦 mavContainer.setRedirectModelScenario(true); } } else if (returnValue != null) { // should not happen throw new UnsupportedOperationException("Unexpected return type: " + returnType.getParameterType().getName() + " in method: " + returnType.getMethod()); } }
如下所示,在RequestMappingHandlerAdapter#getModelAndView
方法中(此时已经调用目标方法并对返回结果做了处理),判断model是RedirectAttributes,则会读取flashAttributes 然后放入OutputFlashMap中。
@Nullable private ModelAndView getModelAndView(ModelAndViewContainer mavContainer, ModelFactory modelFactory, NativeWebRequest webRequest) throws Exception { modelFactory.updateModel(webRequest, mavContainer); if (mavContainer.isRequestHandled()) { return null; } ModelMap model = mavContainer.getModel(); ModelAndView mav = new ModelAndView(mavContainer.getViewName(), model, mavContainer.getStatus()); if (!mavContainer.isViewReference()) { mav.setView((View) mavContainer.getView()); } if (model instanceof RedirectAttributes) { Map<String, ?> flashAttributes = ((RedirectAttributes) model).getFlashAttributes(); HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class); if (request != null) { // request.getAttribute(DispatcherServlet.OUTPUT_FLASH_MAP_ATTRIBUTE); RequestContextUtils.getOutputFlashMap(request).putAll(flashAttributes); } } return mav; }
如下过程所示,这里会将RedirectAttributes
的flash属性放入到outputFlashMap
中,但是RedirectAttributes中还有普通属性没有处理哦。
RedirectView#renderMergedOutputModel
方法是AbstractView#render
方法的核心方法。其会获取目标URL(会将model中的属性-值作为URL的queryString Param拼接到URL后面),也就是这里会处理RedirectAttributes的普通属性。
@Override
protected void renderMergedOutputModel(Map<String, Object> model, HttpServletRequest request,
HttpServletResponse response) throws IOException {
// 这里很关键,会将RedirectAttributes中的普通属性拼接作为url 参数
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);
}
解析targetUrl 过程如下所示:
我们再继续看一下RequestContextUtils.saveOutputFlashMap(targetUrl, request, response);
过程:
public static void saveOutputFlashMap(String location, HttpServletRequest request, HttpServletResponse response) {
FlashMap flashMap = getOutputFlashMap(request);
if (CollectionUtils.isEmpty(flashMap)) {
return;
}
UriComponents uriComponents = UriComponentsBuilder.fromUriString(location).build();
flashMap.setTargetRequestPath(uriComponents.getPath());
flashMap.addTargetRequestParams(uriComponents.getQueryParams());
FlashMapManager manager = getFlashMapManager(request);
Assert.state(manager != null, "No FlashMapManager. Is this a DispatcherServlet handled request?");
// 这里又会调用FlashMapManager 的saveOutputFlashMap方法
manager.saveOutputFlashMap(flashMap, request, response);
}
FlashMapManager
的saveOutputFlashMap
方法如下所示,这里很重要的一步是会获取sessionFlashMap
-List<FlashMap> allFlashMaps
(FLASH_MAPS_SESSION_ATTRIBUTE
),然后将outputFlashMap
保存更新进去。给重定向后的请求使用。
@Override public final void saveOutputFlashMap(FlashMap flashMap, HttpServletRequest request, HttpServletResponse response) { if (CollectionUtils.isEmpty(flashMap)) { return; } String path = decodeAndNormalizePath(flashMap.getTargetRequestPath(), request); // 设置目标请求地址 flashMap.setTargetRequestPath(path); // 设置过期时间,默认是180 flashMap.startExpirationPeriod(getFlashMapTimeout()); Object mutex = getFlashMapsMutex(request); if (mutex != null) { synchronized (mutex) { // 从session中获取属性FLASH_MAPS_SESSION_ATTRIBUTE对应的值 List<FlashMap> allFlashMaps = retrieveFlashMaps(request); allFlashMaps = (allFlashMaps != null ? allFlashMaps : new CopyOnWriteArrayList<>()); // 放入flashMap allFlashMaps.add(flashMap); // 将FLASH_MAPS_SESSION_ATTRIBUTE -- allFlashMaps 放入session中 updateFlashMaps(allFlashMaps, request, response); } } else { List<FlashMap> allFlashMaps = retrieveFlashMaps(request); allFlashMaps = (allFlashMaps != null ? allFlashMaps : new ArrayList<>(1)); allFlashMaps.add(flashMap); updateFlashMaps(allFlashMaps, request, response); } }
详细过程如下图所示
核心代码如下,从session中获取到inputFlashMap(然后会从session移除掉哦)。如果inputFlashMap不为null,则放入request属性中。然后放入OUTPUT_FLASH_MAP_ATTRIBUTE
和FLASH_MAP_MANAGER_ATTRIBUTE
if (this.flashMapManager != null) {
FlashMap inputFlashMap = this.flashMapManager.retrieveAndUpdate(request, response);
// 如果inputFlashMap 不为null,则放入request中一个不可修改的inputFlashMap
if (inputFlashMap != null) {
request.setAttribute(INPUT_FLASH_MAP_ATTRIBUTE, Collections.unmodifiableMap(inputFlashMap));
}
// 放入outputFlashMap
request.setAttribute(OUTPUT_FLASH_MAP_ATTRIBUTE, new FlashMap());
// 放入flashMapManager
request.setAttribute(FLASH_MAP_MANAGER_ATTRIBUTE, this.flashMapManager);
}
这里面我们看一下retrieveAndUpdate
方法。这个方法很有意思,首先从session中获取FLASH_MAPS_SESSION_ATTRIBUTE
属性对应的值对象我们姑且称之为sessionFlashMap
。然后获取sessionFlashMap
中过期的ExpiredFlashMaps
以及与当前请求匹配的MatchingFlashMap
。
接下来要做的就是从sessionFlashMap
移除掉ExpiredFlashMaps
和MatchingFlashMap
,然后更新session的FLASH_MAPS_SESSION_ATTRIBUTE
属性值,并将MatchingFlashMap
作为方法结果返回。
@Override @Nullable public final FlashMap retrieveAndUpdate(HttpServletRequest request, HttpServletResponse response) { // 获取session中的flashMap List<FlashMap> allFlashMaps = retrieveFlashMaps(request); if (CollectionUtils.isEmpty(allFlashMaps)) { return null; } // 获取过期的flashMap List<FlashMap> mapsToRemove = getExpiredFlashMaps(allFlashMaps); // 获取匹配当前请求的flashMap FlashMap match = getMatchingFlashMap(allFlashMaps, request); if (match != null) { mapsToRemove.add(match); } // 移除掉mapsToRemove,然后将剩余的更新session中的flashMap 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); } } // 返回匹配的flashMap return match; }
在RequestMappingHandlerAdapter#invokeHandlerMethod
方法中有如下代码会从request中获取InputFlashMap
然后放入mavContainer
的defaultModel
中。
mavContainer.addAllAttributes(RequestContextUtils.getInputFlashMap(request));
那么此时再解析重定向后的方法参数,很显然将会得到如下结果:
至此,我们讲重定向前后参数传参分析完毕。
这里的model默认是指BindingAwareModelMap
,其不能实现RedirectAttributesModelMap
的功能。
如下所示,方法重定向前在model中放入modelAttr-modelAttrValue
,但是方法重定向后,是接收不到该键值对的。
如下所示,方法重定向后的model中只有addFlashAttribute属性:
{addFlashAttribute=redirectAttributes.addFlashAttribute-value}
因为在处理返回结果时,已经将redirectModelScenario
设置为true并且redirectModel
不为null,那么在getModelAndView
方法中获取Model时将会获取到redirectModel
也就是RedirectAttributesModelMap
实例。该实例的flashAttributes
数据将会放入outputFlashMap中,而该实例非flashAttributes
数据将会用来构造ModelAndView
实例。
ModelMap model = mavContainer.getModel();
ModelAndView mav = new ModelAndView(mavContainer.getViewName(), model, mavContainer.getStatus());
// 获取model的代码如下
public ModelMap getModel() {
if (useDefaultModel()) {
return this.defaultModel;
}
else {
if (this.redirectModel == null) {
this.redirectModel = new ModelMap();
}
return this.redirectModel;
}
}
为啥redirectAttributes.addAttribute("redirectAttributes","redirectAttributesValue");
同样能把属性带过去呢?因为其将作为URL的query String Params拼接到URL后面带过去。
如下所示RedirectView的属性中exposeModelAttributes 、expandUriTemplateVariables 默认为true。
private static final Pattern URI_TEMPLATE_VARIABLE_PATTERN = Pattern.compile("\\{([^/]+?)\\}"); private boolean contextRelative = false; private boolean http10Compatible = true; private boolean exposeModelAttributes = true; @Nullable private String encodingScheme; @Nullable private HttpStatus statusCode; private boolean expandUriTemplateVariables = true; private boolean propagateQueryParams = false;
这两个属性有什么作用呢?我们可以细探究一下RedirectView解析目标URL的过程
protected final String createTargetUrl(Map<String, Object> model, HttpServletRequest request) throws UnsupportedEncodingException { // Prepare target URL. StringBuilder targetUrl = new StringBuilder(); // 获取请求url String url = getUrl(); Assert.state(url != null, "'url' not set"); // 拼接contextPath if (this.contextRelative && getUrl().startsWith("/")) { // Do not apply context path to relative URLs. targetUrl.append(getContextPath(request)); } targetUrl.append(getUrl()); String enc = this.encodingScheme; if (enc == null) { enc = request.getCharacterEncoding(); } if (enc == null) { enc = WebUtils.DEFAULT_CHARACTER_ENCODING; } // 尝试用model的数据对url上的占位符进行解析替代 if (this.expandUriTemplateVariables && StringUtils.hasText(targetUrl)) { Map<String, String> variables = getCurrentRequestUriVariables(request); targetUrl = replaceUriTemplateVariables(targetUrl.toString(), model, variables, enc); } // 是否将当前请求参数拼接到URL后面,propagateQueryParams默认false if (isPropagateQueryProperties()) { appendCurrentQueryParams(targetUrl, request); } // 是否将model里面属性暴露出来--拼接到URL后面 if (this.exposeModelAttributes) { appendQueryProperties(targetUrl, model, enc); } // /testr?redirectAttributes=redirectAttributesValue return targetUrl.toString(); }
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。