Spring Web 3.1之后引入了叫作Flash Attribute的功能,旨在解决类似POST/Redirect/GET这种请求模式中的属性传递问题,但由于从老系统到新系统的迁移过程中,用户Session都开始使用基于Redis的会话组件(HttpSession的Redis版本),在老系统的POST/Redirect/GET这种请求模式下出现了问题,因此记下一笔。
比如,用户在浏览器端填充完表单,然后提交,服务器端处理完,可以使用Forward的方式将用户转发到对应的页面,但Forward完成之前,用户有可能强制刷新页面,这样可能造成重复提交,因此可能会使用Redirect来响应用户:
这样的确可以减少服务端Redirect后的重复提交问题,但若在Redirect之前用户强制刷新,也会存在重复提交问题,其他防止重复提交的可以使用Token校验等方式,或者更好的方式是在浏览器端作一些前端优化,给予用户友好的等待提示。
但使用Redirect后,相当于用户上一次Request的请求参数被丢失了,我们希望能在Redirect后的那次Request获取上一次Request的某些参数或属性,比如提示用户某些字段非法等信息,于是Spring 3.1后,加入了Flash Attribute。Flash Attribute允许在前后两次Request之间传递一些参数或属性,原理在于Flash Attribute这种属性会临时放入Session中,当第二次Request后,又将这类属性从Session中移除,Spring Web中使用RedirectAttributes来放入Flash Attribute:
@RequestMapping(value="submitOrder", method=RequestMethod.POST)
public String submitOrder(@ModelAttribute("Order") Order order, final RedirectAttributes redirectAttributes) {
//...
redirectAttributes.addFlashAttribute("message", "Submit successfully!");
//...
return "redirect:submit_success";
}
Spring Web中使用FlashMapManager来管理Flash Attribute:
public interface FlashMapManager {
/**
* 获取之前请求中的FlashAttribute,并将一些过期的属性从Session中移除
*/
FlashMap retrieveAndUpdate(HttpServletRequest request, HttpServletResponse response);
/**
* 保存FlashAttribute,并设置过期时间,该方法会在redirect这种视图类型中调用
*/
void saveOutputFlashMap(FlashMap flashMap, HttpServletRequest request, HttpServletResponse response);
}
retrieveAndUpdate方法会在Spring Web核心组件DispatchServlet中被调用:
protected void doService(HttpServletRequest request, HttpServletResponse response) throws Exception {
// ....
FlashMap inputFlashMap = this.flashMapManager.retrieveAndUpdate(request, response);
if (inputFlashMap != null) {
// 将FlashAttribute设置在request对象中
request.setAttribute(INPUT_FLASH_MAP_ATTRIBUTE, Collections.unmodifiableMap(inputFlashMap));
}
try {
// 分发请求
doDispatch(request, response);
} finally {
// ...
}
}
saveOutputFlashMap方法会在RedirectView视图中被调用:
protected void renderMergedOutputModel(Map<String, Object> model, HttpServletRequest request,
HttpServletResponse response) throws IOException {
String targetUrl = createTargetUrl(model, request);
targetUrl = updateTargetUrl(targetUrl, model, request, response);
// 获取FlashAttribute
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");
}
// 保存FlashAttribute
flashMapManager.saveOutputFlashMap(flashMap, request, response);
}
sendRedirect(request, response, targetUrl, this.http10Compatible);
}
Spring Web默认使用SessionFlashMapManager作为FlashMap管理器,内部将Flash Attribute保存在Session中:
public class SessionFlashMapManager extends AbstractFlashMapManager {
private static final String FLASH_MAPS_SESSION_ATTRIBUTE = SessionFlashMapManager.class.getName() + ".FLASH_MAPS";
protected List<FlashMap> retrieveFlashMaps(HttpServletRequest request) {
HttpSession session = request.getSession(false);
// 从Session获取FlashMap属性
return (session != null ? (List<FlashMap>) session.getAttribute(FLASH_MAPS_SESSION_ATTRIBUTE) : null);
}
protected void updateFlashMaps(List<FlashMap> flashMaps, HttpServletRequest request, HttpServletResponse response) {
// 在Session设置FlashMap属性
request.getSession().setAttribute(FLASH_MAPS_SESSION_ATTRIBUTE, flashMaps);
}
}
老系统迁移出现的问题,就在于(List<FlashMap>)session.getAttribute(FLASH_MAPS_SESSION_ATTRIBUTE)这里强制转换为了List<FlashMap>,而HttpSession已被重写为Redis版本,并且使用JSON序列化,并不像一些Servlet容器默认使用DataOutputStream对象序列化,因而抛出了java.lang.ClassCastException异常,并一旦请求过类似使用redirectAttributes.addFlashAttribute的方法后,后续的请求都将抛java.lang.ClassCastException,因为FLASH_MAPS_SESSION_ATTRIBUTE一直保存在Session中,最后只能重写retrieveFlashMaps以解决该问题:
public class RedisSessionFlashMapManager extends SessionFlashMapManager {
private static final String FLASH_MAPS_KEY = "org.springframework.web.servlet.support.SessionFlashMapManager.FLASH_MAPS";
@Override
protected List<FlashMap> retrieveFlashMaps(HttpServletRequest request) {
HttpSession session = request.getSession(false);
return session == null ? null : renderFlashMaps(session);
}
@SuppressWarnings("unchecked")
private List<FlashMap> renderFlashMaps(HttpSession session) {
List<HashMap<String, Object>> maps = (List<HashMap<String, Object>>)session.getAttribute(FLASH_MAPS_KEY);
if (CollectionUtils.isEmpty(maps)){
return null;
}
List<FlashMap> flashMaps = Lists.newArrayListWithExpectedSize(maps.size());
FlashMap flashMap;
for (Map<String, Object> map : maps){
flashMap = new FlashMap();
for (Map.Entry<String, Object> entry : map.entrySet()){
flashMap.put(entry.getKey(), String.valueOf(entry.getValue()));
}
flashMaps.add(flashMap);
}
return flashMaps;
}
}
并且需要在Spring Web Context配置使用的FlashMapManager:
<bean id="flashMapManager" class="my.RedisSessionFlashMapManager" />
Spring Web默认认为Servlet容器使用对象序列化保存Session,这似乎不是很合理,也许应该提供一个反序列化Flash Attribute的策略,更合理的方式应该是减少这种POST/Redirect/GET这种请求模式,这种模式体验并不友好,应交由前端,如AJAX去处理。