redirectAttributes源码分析

redirectAttributes是做什么的

今天改项目的bug遇到了redirectAttributes,不知道为什么一直在jsp中无法接收redirectAttributes中的值,找了一个小时,后来发现问题超级简单,简单到怀疑人生。这问题先放一放,放在本文最后说,你问为啥,因为丢人,先来简要的介绍一下redirectAttributes。

我们在开发mvc项目时,常常会遇到重定向传参的问题,常用的方式有以下几种

重定向传参第一种方式

这里把SpringMVC再重新复习一下,从新建的项目开始操作。

创建好我们的一个demo项目,web.xml中配置servlet

<web-app>
  <display-name>Archetype Created Web Application</display-name>
  <servlet>
    <servlet-name>springmvc</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <init-param>
      <param-name>contextConfigLocation</param-name>
      <param-value>classpath:springmvc-servlet.xml</param-value>
    </init-param>
  </servlet>
  <servlet-mapping>
    <servlet-name>springmvc</servlet-name>
    <url-pattern>/</url-pattern>
  </servlet-mapping>
</web-app>

springmvc-servlet.xml的配置如下

<!-- 注解支持 -->
    <context:annotation-config></context:annotation-config>

    <!-- DispatcherServlet上下文,扫描base-package包中的类,并自动加载到spring容器中 -->
    <context:component-scan base-package="com.example.controller">
    </context:component-scan>

    <!--   启用@Component,@Controller,@Service,@Repository注解驱动 -->
    <mvc:annotation-driven/>

    <!-- 配置视图解析器 -->
    <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="prefix" value="/WEB-INF/views/"/>
        <property name="suffix" value=".jsp"/>
    </bean>

index.jsp中我们就简单写一个超链接用于跳转

<a href="${pageContext.request.contextPath}/redirect/ridired">重定向controller</a>

再编写一个用于跳转成功的jsp页面,success.jsp

<body>
重定向传值el表达式要加param。变量名
userName:${param.username}
passWord:${param.password}
</body>

接下来就是重点的controller编写

@Controller
@RequestMapping("/redirect")
public class RedirectController {

    @RequestMapping("/ridired")
    public ModelAndView redirect(HttpServletRequest request) {
        ModelAndView modelAndView = new ModelAndView();
        modelAndView.addObject("username", "admin");
        modelAndView.addObject("password", "123456");
        RedirectView redirectView = new RedirectView(request.getContextPath() + "success");
        modelAndView.setView(redirectView);

        return modelAndView;
    }
    @RequestMapping("/success")
    public String success(){
        return "success";
    }
}

这样我们的代码就编写完成了,运行tomcat,访问localhost:8080
index.jsp
点击重定向controller
success.jsp
这样就获取到我们的值了,但传值的方式时通过url进行传参,不安全

重定向传值第二种方式

直接再controller中返回字符串拼接我们要传的值

@RequestMapping("/redirect")
    public String redirect() {
        return "redirect:success?username=admin&password=123456";
    }

效果如下
success.jsp
效果是和第一种方式一样的,不安全啊!接下来就是我们的重点使用redirectAttributes进行传参

使用redirectAttributes传参

代码编写

	@RequestMapping("/redirect")
    public String redirect(RedirectAttributes redirectAttributes){
        redirectAttributes.addFlashAttribute("username","admin");
        redirectAttributes.addFlashAttribute("password","123456");
        return "redirect:success";
    }

效果如下
success.jsp
相对于上面两种方式安全多了吧,其实RedirectAttributes也可以像以上两种方式传递参数,不过调用的方法是addAttribute(),和上面的类似,暴露参数,有一些风险存在。

RedirectAttributes原理

先从源码看起,完成我们添加参数的其实就是一个ModelMap类,RedirectAttributesModelMap,这个类继承了ModelMap,实现了RedirectAttributes接口,数据的传递实际上还是通过一个ModelMap完成的。

public class RedirectAttributesModelMap extends ModelMap implements RedirectAttributes {
	private final ModelMap flashAttributes = new ModelMap();
	...//其他的方法没有列出
	@Override
	public RedirectAttributesModelMap addAttribute(Object attributeValue) {
		super.addAttribute(attributeValue);
		return this;
	}
	@Override
	public RedirectAttributes addFlashAttribute(String attributeName, Object attributeValue) {
		this.flashAttributes.addAttribute(attributeName, attributeValue);
		return this;
	}
}

但用屁股想一下,重定向传参,只用这样一个类就能完成吗?显然不,从一个请求到另一个请求,肯定是把参数放到了session中了,但具体是怎么放的,请看下面的分析。我们直接从组装ModelAndView之后的过程来说

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);
			RequestContextUtils.getOutputFlashMap(request).putAll(flashAttributes);
		}
		return mav;
	}

这个方法中的参数ModelAndViewContainer中包含了我们所要传递的两个参数,通过getModel()方法获取到model,进行类型判断,判断后通过getFlashAttributes()获取到参数的Map,再把这两个参数放到一个OutputFlashMap中,返回modelAndView对象,不过这时候这个对象的model还是空的,接下来继续执行直到执行到RedirectView中的这个方法。

// RedirectView.java
protected void renderMergedOutputModel(Map<String, Object> model, HttpServletRequest request,
			HttpServletResponse response) throws IOException {

		String targetUrl = createTargetUrl(model, request);
		targetUrl = updateTargetUrl(targetUrl, model, request, response);

		FlashMap flashMap = RequestContextUtils.getOutputFlashMap(request);
		if (!CollectionUtils.isEmpty(flashMap)) {
			UriComponents uriComponents = UriComponentsBuilder.fromUriString(targetUrl).build();
			flashMap.setTargetRequestPath(uriComponents.getPath());
			flashMap.addTargetRequestParams(uriComponents.getQueryParams());
			FlashMapManager flashMapManager = RequestContextUtils.getFlashMapManager(request);
			if (flashMapManager == null) {
				throw new IllegalStateException("FlashMapManager not found despite output FlashMap having been set");
			}
			flashMapManager.saveOutputFlashMap(flashMap, request, response);
		}

		sendRedirect(request, response, targetUrl, this.http10Compatible);
	}

targetUrl就是我们要重定向的地址,flashMap就是我们刚刚存的OutputFlashMap,到这里,我们就获取到了参数,判断一下是否为空,不为空就进行填充其他属性。这里说一下FlashMap

	private String targetRequestPath;  //重定向地址

	private final MultiValueMap<String, String> targetRequestParams = new LinkedMultiValueMap<String, String>(4);

	private long expirationTime = -1;
	...
	public void startExpirationPeriod(int timeToLive) {
		this.expirationTime = System.currentTimeMillis() + timeToLive * 1000;
	}
	public boolean isExpired() {
		return (this.expirationTime != -1 && System.currentTimeMillis() > this.expirationTime);
	}
	...

targetRequestParams装参数的容器,expriationTime是过期时间,startExpirationPeriod(int timeToLive)传递一个timeToLive经过运算规则计算设置值,isExpired()判断是否过期。
通过flashMapManager.saveOutputFlashMap进行存储

public final void saveOutputFlashMap(FlashMap flashMap, HttpServletRequest request, HttpServletResponse response) {
		if (CollectionUtils.isEmpty(flashMap)) {
			return;
		}

		String path = decodeAndNormalizePath(flashMap.getTargetRequestPath(), request);
		flashMap.setTargetRequestPath(path);

		if (logger.isDebugEnabled()) {
			logger.debug("Saving FlashMap=" + flashMap);
		}
		flashMap.startExpirationPeriod(getFlashMapTimeout());

		Object mutex = getFlashMapsMutex(request);
		if (mutex != null) {
			synchronized (mutex) {
				List<FlashMap> allFlashMaps = retrieveFlashMaps(request);
				allFlashMaps = (allFlashMaps != null ? allFlashMaps : new CopyOnWriteArrayList<FlashMap>());
				allFlashMaps.add(flashMap);
				updateFlashMaps(allFlashMaps, request, response);
			}
		}
		else {
			List<FlashMap> allFlashMaps = retrieveFlashMaps(request);
			allFlashMaps = (allFlashMaps != null ? allFlashMaps : new LinkedList<FlashMap>());
			allFlashMaps.add(flashMap);
			updateFlashMaps(allFlashMaps, request, response);
		}
	}

在updateFlashMap(allFlashMaps, request, response),中将两个参数存储到Session中,具体代码如下

// SessionFlashMapManager.java
protected void updateFlashMaps(List<FlashMap> flashMaps, HttpServletRequest request, HttpServletResponse response) {
		WebUtils.setSessionAttribute(request, FLASH_MAPS_SESSION_ATTRIBUTE, (!flashMaps.isEmpty() ? flashMaps : null));
	}
// WebUtils.java
	public static void setSessionAttribute(HttpServletRequest request, String name, Object value) {
		Assert.notNull(request, "Request must not be null");
		if (value != null) {
			request.getSession().setAttribute(name, value);
		}
		else {
			HttpSession session = request.getSession(false);
			if (session != null) {
				session.removeAttribute(name);
			}
		}
	}

以上Session存储两个参数完毕。

接着执行上面renderMergedOutputModel()方法的最后一句sendRedirect(request, response, targetUrl, this.http10Compatible)。再回到doService方法中

protected void doService(HttpServletRequest request, HttpServletResponse response) throws Exception {
		if (logger.isDebugEnabled()) {
			String resumed = WebAsyncUtils.getAsyncManager(request).hasConcurrentResult() ? " resumed" : "";
			logger.debug("DispatcherServlet with name '" + getServletName() + "'" + resumed +
					" processing " + request.getMethod() + " request for [" + getRequestUri(request) + "]");
		}

		// Keep a snapshot of the request attributes in case of an include,
		// to be able to restore the original attributes after the include.
		Map<String, Object> attributesSnapshot = null;
		if (WebUtils.isIncludeRequest(request)) {
			attributesSnapshot = new HashMap<String, Object>();
			Enumeration<?> attrNames = request.getAttributeNames();
			while (attrNames.hasMoreElements()) {
				String attrName = (String) attrNames.nextElement();
				if (this.cleanupAfterInclude || attrName.startsWith("org.springframework.web.servlet")) {
					attributesSnapshot.put(attrName, request.getAttribute(attrName));
				}
			}
		}

		// Make framework objects available to handlers and view objects.
		request.setAttribute(WEB_APPLICATION_CONTEXT_ATTRIBUTE, getWebApplicationContext());
		request.setAttribute(LOCALE_RESOLVER_ATTRIBUTE, this.localeResolver);
		request.setAttribute(THEME_RESOLVER_ATTRIBUTE, this.themeResolver);
		request.setAttribute(THEME_SOURCE_ATTRIBUTE, getThemeSource());

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

		try {
			doDispatch(request, response);
		}
		finally {
			if (!WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) {
				// Restore the original attribute snapshot, in case of an include.
				if (attributesSnapshot != null) {
					restoreAttributesAfterInclude(request, attributesSnapshot);
				}
			}
		}
	}

FlashMap inputFlashMap = this.flashMapManager.retrieveAndUpdate(request, response);这一句获取到存储参数的FlashMap,再在request中添加进去,继续执行完剩下的步骤,就可以在页面获取值了。

上面的所有代码,都是将参数放到Session中的步骤,其实我也不是特别的了解额,哈哈,但凡事都有一个学习的过程,大致的流程就是这样,如果哪里有错误请联系我。

总结

这篇文章到这里也就快结束了,重新整理一下,就是我们在使用SpringMVC进行代码编写的过程中,可能会遇到这样的情况,重定向到另一个Controller或者页面的时候需要进行传递参数,比如登录失败跳转到失败页面,传递登录失败的信息等,这时候我们可以通过本文上述的几种方式进行传递,但是传递的信息比较重要的时候,最好是使用redirectAttributes的addFlashAttribute()进行传递,这样不仅能简化我们的请求url,还能保护我们的信息不被别人获取到。

如果你发现我的文章哪里有错误或者有什么好的想法可以联系我,我们一起学习共同进步,我的邮箱地址是lishuangCHN@gmail.com

对了我那个小错误是因为jsp我没接收这个值,一句<sys:message content="${message}"/>搞定,确实挺丢人的。不过这是写这个项目的人的疏忽,找了这么半天的我也丢人是没跑了

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值