@ModelAttribute和@InitBinder相信大家都不陌生了,但是背后的原理设计到spring的核心知识,源码之下无秘密,让咱们看看:
@InitBinder很简单,就是初始化WebDataBinder
,用于数据绑定流程
@ModelAttribute感觉用处不是太多,其实仔细一想:controller方法的参数无非就是来自三个来源:1、request 2、session 3、方法调用前由程序嵌入的值 很明显这个注解就是干第三个活的,调用方法前先调用标有这个注解的方法,把值放入Model里面,这样方法就可以通过@ModelAttribute来获取值了。其实就是一个内置的拦截器
protected ModelAndView invokeHandlerMethod(HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod) throws Exception
{
ServletWebRequest webRequest = new ServletWebRequest(request, response);
try
{
//获取WebDataBinder工厂
//@InitBinder
WebDataBinderFactory binderFactory = getDataBinderFactory(handlerMethod);
//获取模型工厂
//@ModelAttribute
ModelFactory modelFactory = getModelFactory(handlerMethod, binderFactory);
//控制器包装方法
//参数解析器,返回值解析器,数据绑定工具类,参数名称获取工具类
ServletInvocableHandlerMethod invocableMethod = createInvocableHandlerMethod(handlerMethod);
if(this.argumentResolvers != null)
{
invocableMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers);
}
if(this.returnValueHandlers != null)
{
invocableMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers);
}
invocableMethod.setDataBinderFactory(binderFactory);
invocableMethod.setParameterNameDiscoverer(this.parameterNameDiscoverer);
//表示本次请求的模型和视图的容器
ModelAndViewContainer mavContainer = new ModelAndViewContainer();
//重定向来的数据
mavContainer.addAllAttributes(RequestContextUtils.getInputFlashMap(request));
//重点方法,会调用带有@ModelAttribute注解的方法
modelFactory.initModel(webRequest, mavContainer, invocableMethod);
mavContainer.setIgnoreDefaultModelOnRedirect(this.ignoreDefaultModelOnRedirect); //调用控制器方法
invocableMethod.invokeAndHandle(webRequest, mavContainer);
return getModelAndView(mavContainer, modelFactory, webRequest);
}
finally
{
webRequest.requestCompleted();
}
}
private WebDataBinderFactory getDataBinderFactory(HandlerMethod handlerMethod) throws Exception
{
//获取控制器方法所在类的类型
Class <? > handlerType = handlerMethod.getBeanType();
//从缓存中获取本类带有@InitBinder的方法
//Map<Class<?>, Set<Method>> initBinderCache = new ConcurrentHashMap<>(64);
//用了并发容器,所以不用加锁
Set < Method > methods = this.initBinderCache.get(handlerType);
if(methods == null)
{
//第一次惩罚
//缓存没有就要获取了
methods = MethodIntrospector.selectMethods(handlerType, INIT_BINDER_METHODS);
//放到缓存
this.initBinderCache.put(handlerType, methods);
}
//初始化所有@InitBinder注解的方法数组
//看来是每次请求都会这样做
List < InvocableHandlerMethod > initBinderMethods = new ArrayList < > ();
//Map<ControllerAdviceBean, Set<Method>> initBinderAdviceCache = new LinkedHashMap<>();
//表示以ControllerAdvice类为K,@InitBinder方法集合为V的Map
this.initBinderAdviceCache.forEach((clazz, methodSet) - >
{
//获取支持当前类型的所有@ControllerAdvice类
if(clazz.isApplicableToBeanType(handlerType))
{
Object bean = clazz.resolveBean();
for(Method method: methodSet)
{
initBinderMethods.add(createInitBinderMethod(bean, method));
}
}
});
for(Method method: methods)
{ //加入当前controller带有@InitBinder的方法
Object bean = handlerMethod.getBean();
initBinderMethods.add(createInitBinderMethod(bean, method));
}
//return new ServletRequestDataBinderFactory(binderMethods, getWebBindingInitializer());
//WebBindingInitializer对象在父类DefaultDataBinderFactory中
return createDataBinderFactory(initBinderMethods);
}
//代码流程和上面的基本一样
private ModelFactory getModelFactory(HandlerMethod handlerMethod, WebDataBinderFactory binderFactory)
{
//看javadoc可以知道是处理controller的@SessionAttributes注解的
//如果当前控制器方法把数据放进Model里面,它会检查数据的K或者类型是否是注解指定的
//如果是,就把数据放到session里面
//直到调用SessionStatus.setComplete()
SessionAttributesHandler sessionAttrHandler = getSessionAttributesHandler(handlerMethod);
Class <? > handlerType = handlerMethod.getBeanType();
Set < Method > methods = this.modelAttributeCache.get(handlerType);
if(methods == null)
{
methods = MethodIntrospector.selectMethods(handlerType, MODEL_ATTRIBUTE_METHODS);
this.modelAttributeCache.put(handlerType, methods);
}
List < InvocableHandlerMethod > attrMethods = new ArrayList < > ();
// Global methods first
this.modelAttributeAdviceCache.forEach((clazz, methodSet) - >
{
if(clazz.isApplicableToBeanType(handlerType))
{
Object bean = clazz.resolveBean();
for(Method method: methodSet)
{
attrMethods.add(createModelAttributeMethod(binderFactory, bean, method));
}
}
});
for(Method method: methods)
{
Object bean = handlerMethod.getBean();
attrMethods.add(createModelAttributeMethod(binderFactory, bean, method));
}
//ModelFactory需要@ModelAttribute注解的方法集合和数据绑定工厂和session管理器
return new ModelFactory(attrMethods, binderFactory, sessionAttrHandler);
}
//Map<Class<?>, SessionAttributesHandler> sessionAttributesHandlerCache = new ConcurrentHashMap<>(64);
//看看怎样在多线程环境下使用缓存
//注意和上面的区别
private SessionAttributesHandler getSessionAttributesHandler(HandlerMethod handlerMethod)
{
Class <? > handlerType = handlerMethod.getBeanType();
SessionAttributesHandler sessionAttrHandler = this.sessionAttributesHandlerCache.get(handlerType);
if(sessionAttrHandler == null)
{
synchronized(this.sessionAttributesHandlerCache)
{
sessionAttrHandler = this.sessionAttributesHandlerCache.get(handlerType);
if(sessionAttrHandler == null)
{
sessionAttrHandler = new SessionAttributesHandler(handlerType, sessionAttributeStore);
this.sessionAttributesHandlerCache.put(handlerType, sessionAttrHandler);
}
}
}
return sessionAttrHandler;
}
public void initModel(NativeWebRequest request, ModelAndViewContainer container, HandlerMethod handlerMethod)
throws Exception
{
//从http session中获取@SessionAttributes注解以names为K的数据集合
Map < String, ?> sessionAttributes = this.sessionAttributesHandler.retrieveAttributes(request);
//将数据合并到容器
container.mergeAttributes(sessionAttributes);
//调用@ModelAttribute注解的方法
invokeModelAttributeMethods(request, container);
//获取controller方法带有@ModelAttribute注解并且是类上面的
//@SessionAttributes指定的参数集合
for(String name: findSessionAttributeArguments(handlerMethod))
{
//如果符合,希望是在Model里面存在的
//反之从session里面获取
//获取不到直接抛出异常
if(!container.containsAttribute(name))
{
Object value = this.sessionAttributesHandler.retrieveAttribute(request, name);
if(value == null)
{
throw new HttpSessionRequiredException("Expected session attribute '" + name + "'", name);
}
container.addAttribute(name, value);
}
}
}
public boolean isHandlerSessionAttribute(String attributeName, Class <? > attributeType)
{
if(this.attributeNames.contains(attributeName) || this.attributeTypes.contains(attributeType))
{
this.knownAttributeNames.add(attributeName);
return true;
}
else
{
return false;
}
}
总结:就是说controller带有@SessionAttributes,在方法执行前会先从session中获取这个注解属性names为K的session数据放到Model里面,然后调用@ModelAttribute注解的方法,最后检查方法参数带有@ModelAttribute注解并且是@SessionAttributes注解指定的数据是否在Model里面,其实仔细想一下,如果是指定的证明在第一步就从session里面获取了,也可能获取不到,这是很正常的,但是@ModelAttribute注解的方法也没有这就不好了,这我想到了一个场景:浏览器第一次调用这个controller的方法,并且这个方法有@SessionAttributes注解指定的@ModelAttribute方法参数,肯定是不可能从session从获取了,只能从@ModelAttribute注解方法里如果没有,那就没有办法完成绑定了,就是不明白为什么会抛出HttpSessionRequiredException
异常,我觉得spring官方应该把这个错误的名字改正以下,不然很容易让别人误会,就那第一次浏览器调用来说,如果这样直接抛出异常不是很好把
所以建议使用@ModelAttribute和@SessionAttributes分开避免出现异常,即@SessionAttributes不指定@ModelAttribute注解的参数