@ModelAttribute
这个注解并不是一个必须要用到的注解,因为可以用其他方式来达到一样的效果,@ModelAttribute
只是提供一种相对简单的方式
@ModelAttribute官方解释
Spring官方的JavaDoc:它将方法参数/方法返回值
绑定到web view
的Model
里面。只支持@RequestMapping这种类型的控制器哦,因为其只存在于RequestMappingHandlerAdapter
这个Adapter。它既可以标注在方法入参上,也可以标注在方法(返回值)上。
其实官方已经说的非常的清楚了,但是我们还是要注意一点,没有@ModelAttribute
注解标识且没有其他一些参数解析相关注解标识的普通JavaBean,默认情况下,其解析器是ServletModelAttributeMethodProcessor
这个类继承自ModelAttributeMethodProcessor
,而ModelAttributeMethodProcessor
这个类,正是解析@ModelAttribute
注解标识的参数的解析器。
ModelAttributeMethodProcessor
的supportsParameter方法
@Override
public boolean supportsParameter(MethodParameter parameter) {
return (parameter.hasParameterAnnotation(ModelAttribute.class) ||
(this.annotationNotRequired && !BeanUtils.isSimpleProperty(parameter.getParameterType())));
}
可以看出,其能解析普通的JavaBean,但是
其中有个annotationNotRequired,也就是注解是否是必须的。我们来看看他的值是什么
RequestMappingHandlerAdapter
类的getDefaultArgumentResolvers
方法,从这里可以看出,其值是为true的。
综上所述,普通的没有任何解析相关注解标识的javaBean,即使是没有被@ModelAttribute
注解标识,其也会被加入到web view中。
同时观察上面图片的其中的注释Catch all
,捕获所有,这两个参数解析器就是用来兜底的可以这样理解。
那既然不加有还是要放入,要他有何用?
因为他不光可以放入controller参数中的javaBean,它还可以放入一些全局的javaBean,可以在controller中写上方法,或者在全局Controller中写上,他就能为请求初始化一个对象放入web view,就像下面这样
@ModelAttribute("person")
public Person initMode(){
Person p = new Peson();
p.setName("张三");
return p;
}
要注意,如果你的controller中有参数名为上面放入的类同名,其会先于请求参数被绑定到controller的参数上。
以下部分转载自享学Spring MVC
// @since 2.5 只能用在入参、方法上
@Target({ElementType.PARAMETER, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ModelAttribute {
@AliasFor("name")
String value() default "";
// The name of the model attribute to bind to. 注入如下默认规则
// 比如person对应的类是:mypackage.Person(类名首字母小写)
// personList对应的是:List<Person> 这些都是默认规则咯~~~ 数组、Map的省略
// 具体可以参考方法:Conventions.getVariableNameForParameter(parameter)的处理规则
@AliasFor("value")
String name() default "";
// 若是false表示禁用数据绑定。
// @since 4.3
boolean binding() default true;
}
基本原理
我们知道@ModelAttribute能标注在入参上,也可以标注在方法上。下面就从原理处深入理解,从而掌握它的使用,后面再给出多种使用场景的使用Demo。
和它相关的两个类是ModelFactory和ModelAttributeMethodProcessor
@ModelAttribute
缺省处理的是Request请求域,Spring MVC还提供了@SessionAttributes
来处理和Session域相关的模型数据
关于ModelFactory的介绍,在这里讲解@SessionAttributes的时候已经介绍一大部分了,但特意留了一部分关于@ModelAttribute的内容,在本文继续讲解
ModelFactory
ModelFactory
所在包org.springframework.web.method.annotation
,可见它和web是强关联的在一起的。作为上篇文章的补充说明,接下里只关心它对@ModelAttribute的解析部分:
// @since 3.1
public final class ModelFactory {
// 初始化Model 这个时候`@ModelAttribute`有很大作用
public void initModel(NativeWebRequest request, ModelAndViewContainer container, HandlerMethod handlerMethod) throws Exception {
// 拿到sessionAttr的属性
Map<String, ?> sessionAttributes = this.sessionAttributesHandler.retrieveAttributes(request);
// 合并进容器内
container.mergeAttributes(sessionAttributes);
// 这个方法就是调用执行标注有@ModelAttribute的方法们~~~~
invokeModelAttributeMethods(request, container);
...
}
//调用标注有注解的方法来填充Model
private void invokeModelAttributeMethods(NativeWebRequest request, ModelAndViewContainer container) throws Exception {
// modelMethods是构造函数进来的 一个个的处理吧
while (!this.modelMethods.isEmpty()) {
// getNextModelMethod:通过next其实能看出 执行是有顺序的 拿到一个可执行的InvocableHandlerMethod
InvocableHandlerMethod modelMethod = getNextModelMethod(container).getHandlerMethod();
// 拿到方法级别的标注的@ModelAttribute~~
ModelAttribute ann = modelMethod.getMethodAnnotation(ModelAttribute.class);
Assert.state(ann != null, "No ModelAttribute annotation");
if (container.containsAttribute(ann.name())) {
if (!ann.binding()) { // 若binding是false 就禁用掉此name的属性 让不支持绑定了 此方法也处理完成
container.setBindingDisabled(ann.name());
}
continue;
}
// 调用目标的handler方法,拿到返回值returnValue
Object returnValue = modelMethod.invokeForRequest(request, container);
// 方法返回值不是void才需要继续处理
if (!modelMethod.isVoid()){
// returnValueName的生成规则 上文有解释过 本处略
String returnValueName = getNameForReturnValue(returnValue, modelMethod.getReturnType());
if (!ann.binding()) { // 同样的 若禁用了绑定,此处也不会放进容器里
container.setBindingDisabled(returnValueName);
}
//在个判断是个小细节:只有容器内不存在此属性,才会放进去 因此并不会有覆盖的效果哦~~~
// 所以若出现同名的 请自己控制好顺序吧
if (!container.containsAttribute(returnValueName)) {
container.addAttribute(returnValueName, returnValue);
}
}
}
}
// 拿到下一个标注有此注解方法~~~
private ModelMethod getNextModelMethod(ModelAndViewContainer container) {
// 每次都会遍历所有的构造进来的modelMethods
for (ModelMethod modelMethod : this.modelMethods) {
// dependencies:表示该方法的所有入参中 标注有@ModelAttribute的入参们
// checkDependencies的作用是:所有的dependencies依赖们必须都是container已经存在的属性,才会进到这里来
if (modelMethod.checkDependencies(container)) {
// 找到一个 就移除一个
// 这里使用的是List的remove方法,不用担心并发修改异常??? 哈哈其实不用担心的 小伙伴能知道为什么吗??
this.modelMethods.remove(modelMethod);
return modelMethod;
}
}
// 若并不是所有的依赖属性Model里都有,那就拿第一个吧~~~~
ModelMethod modelMethod = this.modelMethods.get(0);
this.modelMethods.remove(modelMethod);
return modelMethod;
}
...
}
ModelFactory这部分做的事:执行所有的标注有@ModelAttribute注解的方法,并且是顺序执行哦。那么问题就来了,这些handlerMethods是什么时候被“找到”的呢???这个时候就来到了RequestMappingHandlerAdapter,来看看它是如何找到这些标注有此注解@ModelAttribute的处理器的~~~
RequestMappingHandlerAdapter
RequestMappingHandlerAdapter是个非常庞大的体系,本处我们只关心它对@ModelAttribute也就是对ModelFactory的创建,列出相关源码如下:
// @since 3.1
public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter implements BeanFactoryAware, InitializingBean {
// 该方法不能标注有@RequestMapping注解,只标注了@ModelAttribute才算哦~
public static final MethodFilter MODEL_ATTRIBUTE_METHODS = method ->
(!AnnotatedElementUtils.hasAnnotation(method, RequestMapping.class) && AnnotatedElementUtils.hasAnnotation(method, ModelAttribute.class));
...
// 从Advice里面分析出来的标注有@ModelAttribute的方法(它是全局的)
private final Map<ControllerAdviceBean, Set<Method>> modelAttributeAdviceCache = new LinkedHashMap<>();
@Nullable
protected ModelAndView invokeHandlerMethod(HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
WebDataBinderFactory binderFactory = getDataBinderFactory(handlerMethod);
// 每调用一次都会生成一个ModelFactory ~~~
ModelFactory modelFactory = getModelFactory(handlerMethod, binderFactory);
...
ModelAndViewContainer mavContainer = new ModelAndViewContainer();
mavContainer.addAllAttributes(RequestContextUtils.getInputFlashMap(request));
// 初始化Model
modelFactory.initModel(webRequest, mavContainer, invocableMethod);
mavContainer.setIgnoreDefaultModelOnRedirect(this.ignoreDefaultModelOnRedirect);
...
return getModelAndView(mavContainer, modelFactory, webRequest);
}
// 创建出一个ModelFactory,来管理Model
// 显然和Model相关的就会有@ModelAttribute @SessionAttributes等注解啦~
private ModelFactory getModelFactory(HandlerMethod handlerMethod, WebDataBinderFactory binderFactory) {
// 从缓存中拿到和此Handler相关的SessionAttributesHandler处理器~~处理SessionAttr
SessionAttributesHandler sessionAttrHandler = getSessionAttributesHandler(handlerMethod);
Class<?> handlerType = handlerMethod.getBeanType();
// 找到当前类(Controller)所有的标注的@ModelAttribute注解的方法
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
// 全局的有限,最先放进List最先执行~~~~
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));
}
return new ModelFactory(attrMethods, binderFactory, sessionAttrHandler);
}
// 构造InvocableHandlerMethod
private InvocableHandlerMethod createModelAttributeMethod(WebDataBinderFactory factory, Object bean, Method method) {
InvocableHandlerMethod attrMethod = new InvocableHandlerMethod(bean, method);
if (this.argumentResolvers != null) {
attrMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers);
}
attrMethod.setParameterNameDiscoverer(this.parameterNameDiscoverer);
attrMethod.setDataBinderFactory(factory);
return attrMethod;
}
}
RequestMappingHandlerAdapter这部分处理逻辑:每次请求过来它都会创建一个ModelFactory,从而收集到全局的(来自@ControllerAdvice)+ 本Controller控制器上的所有的标注有@ModelAttribute注解的方法们。
@ModelAttribute标注在单独的方法上(木有@RequestMapping注解),它可以在每个控制器方法调用之前,创建出一个ModelFactory从而管理Model数据~
ModelFactory管理着Model,提供了@ModelAttribute以及@SessionAttributes等对它的影响
同时@ModelAttribute可以标注在入参、方法(返回值)上的,标注在不同地方处理的方式是不一样的,那么接下来又一主菜ModelAttributeMethodProcessor就得登场了。
ModelAttributeMethodProcessor
从命名上看它是个Processor,所以根据经验它既能处理入参,也能处理方法的返回值:HandlerMethodArgumentResolver + HandlerMethodReturnValueHandler。解析@ModelAttribute注解标注的方法参数,并处理@ModelAttribute标注的方法返回值。
先看它对方法入参的处理(稍显复杂):
// 这个处理器用于处理入参、方法返回值~~~~
// @since 3.1
public class ModelAttributeMethodProcessor implements HandlerMethodArgumentResolver, HandlerMethodReturnValueHandler {
private static final ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();
private final boolean annotationNotRequired;
public ModelAttributeMethodProcessor(boolean annotationNotRequired) {
this.annotationNotRequired = annotationNotRequired;
}
// 入参里标注了@ModelAttribute 或者(注意这个或者) annotationNotRequired = true并且不是isSimpleProperty()
// isSimpleProperty():八大基本类型/包装类型、Enum、Number等等 Date Class等等等等
// 所以划重点:即使你没标注@ModelAttribute 单子还要不是基本类型等类型,都会进入到这里来处理
// 当然这个行为是是收到annotationNotRequired属性影响的,具体的具体而论 它既有false的时候 也有true的时候
@Override
public boolean supportsParameter(MethodParameter parameter) {
return (parameter.hasParameterAnnotation(ModelAttribute.class) ||
(this.annotationNotRequired && !BeanUtils.isSimpleProperty(parameter.getParameterType())));
}
// 说明:能进入到这里来的 证明入参里肯定是有对应注解的???
// 显然不是,上面有说 这事和属性值annotationNotRequired有关的~~~
@Override
@Nullable
public final Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
// 拿到ModelKey名称~~~(注解里有写就以注解的为准)
String name = ModelFactory.getNameForParameter(parameter);
// 拿到参数的注解本身
ModelAttribute ann = parameter.getParameterAnnotation(ModelAttribute.class);
if (ann != null) {
mavContainer.setBinding(name, ann.binding());
}
Object attribute = null;
BindingResult bindingResult = null;
// 如果model里有这个属性,那就好说,直接拿出来完事~
if (mavContainer.containsAttribute(name)) {
attribute = mavContainer.getModel().get(name);
} else { // 若不存在,也不能让是null呀
// Create attribute instance
// 这是一个复杂的创建逻辑:
// 1、如果是空构造,直接new一个实例出来
// 2、若不是空构造,支持@ConstructorProperties解析给构造赋值
// 注意:这里就支持fieldDefaultPrefix前缀、fieldMarkerPrefix分隔符等能力了 最终完成获取一个属性
// 调用BeanUtils.instantiateClass(ctor, args)来创建实例
// 注意:但若是非空构造出来,是立马会执行valid校验的,此步骤若是空构造生成的实例,此步不会进行valid的,但是下一步会哦~
try {
attribute = createAttribute(name, parameter, binderFactory, webRequest);
} catch (BindException ex) {
if (isBindExceptionRequired(parameter)) {
// No BindingResult parameter -> fail with BindException
throw ex;
}
// Otherwise, expose null/empty value and associated BindingResult
if (parameter.getParameterType() == Optional.class) {
attribute = Optional.empty();
}
bindingResult = ex.getBindingResult();
}
}
// 若是空构造创建出来的实例,这里会进行数据校验 此处使用到了((WebRequestDataBinder) binder).bind(request); bind()方法 唯一一处
if (bindingResult == null) {
// Bean property binding and validation;
// skipped in case of binding failure on construction.
WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name);
if (binder.getTarget() != null) {
// 绑定request请求数据
if (!mavContainer.isBindingDisabled(name)) {
bindRequestParameters(binder, webRequest);
}
// 执行valid校验~~~~
validateIfApplicable(binder, parameter);
//注意:此处抛出的异常是BindException
//RequestResponseBodyMethodProcessor抛出的异常是:MethodArgumentNotValidException
if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
throw new BindException(binder.getBindingResult());
}
}
// Value type adaptation, also covering java.util.Optional
if (!parameter.getParameterType().isInstance(attribute)) {
attribute = binder.convertIfNecessary(binder.getTarget(), parameter.getParameterType(), parameter);
}
bindingResult = binder.getBindingResult();
}
// Add resolved attribute and BindingResult at the end of the model
// at the end of the model 把解决好的属性放到Model的末尾~~~
// 可以即使是标注在入参上的@ModelAtrribute的属性值,最终也都是会放进Model里的~~~可怕吧
Map<String, Object> bindingResultModel = bindingResult.getModel();
mavContainer.removeAttributes(bindingResultModel);
mavContainer.addAllAttributes(bindingResultModel);
return attribute;
}
// 此方法`ServletModelAttributeMethodProcessor`子类是有复写的哦~~~~
// 使用了更强大的:ServletRequestDataBinder.bind(ServletRequest request)方法
protected void bindRequestParameters(WebDataBinder binder, NativeWebRequest request) {
((WebRequestDataBinder) binder).bind(request);
}
}
模型属性首先从Model中获取,若没有获取到,就使用默认构造函数(可能是有无参,也可能是有参)创建,然后会把ServletRequest请求的数据绑定上来, 然后进行@Valid校验(若添加有校验注解的话),最后会把属性添加到Model里面
最后加进去的代码是:mavContainer.addAllAttributes(bindingResultModel);这里我贴出参考值:
如下示例,它会正常打印person的值,而不是null(因为Model内有person了~)
请求链接是:/testModelAttr?name=wo&age=10
@GetMapping("/testModelAttr")
public void testModelAttr(@Valid Person person, ModelMap modelMap) {
Object personAttr = modelMap.get("person");
System.out.println(personAttr); //Person(name=wo, age=10)
}
注意:虽然person上没有标注@ModelAtrribute,但是modelMap.get(“person”)依然是能够获取到值的哦,至于为什么,原因上面已经分析了,可自行思考。
下例中:
@GetMapping("/testModelAttr")
public void testModelAttr(Integer age, Person person, ModelMap modelMap) {
System.out.println(age); // 直接封装的值
System.out.println("-------------------------------");
System.out.println(modelMap.get("age"));
System.out.println(modelMap.get("person"));
}
请求:/testModelAttr?name=wo&age=10
输入为:
10
-------------------------------
null
Person(name=wo, age=10)
可以看到普通类型(注意理解这个普通类型)若不标注@ModelAtrribute,它是不会自动识别为Model而放进来的哟~~~若你这么写:
@GetMapping("/testModelAttr")
public void testModelAttr(@ModelAttribute("age") Integer age, Person person, ModelMap modelMap) {
System.out.println(age); // 直接封装的值
System.out.println("-------------------------------");
System.out.println(modelMap.get("age"));
System.out.println(modelMap.get("person"));
}
打印如下:
10
------------------------------
10
Person(name=wo, age=10)
请务必注意以上case的区别,加深记忆。使用的时候可别踩坑了~
再看它对方法(返回值)的处理(很简单):
public class ModelAttributeMethodProcessor implements HandlerMethodArgumentResolver, HandlerMethodReturnValueHandler {
// 方法返回值上标注有@ModelAttribute注解(或者非简单类型) 默认都会放进Model内哦~~
@Override
public boolean supportsReturnType(MethodParameter returnType) {
return (returnType.hasMethodAnnotation(ModelAttribute.class) ||
(this.annotationNotRequired && !BeanUtils.isSimpleProperty(returnType.getParameterType())));
}
// 这个处理就非常非常的简单了,注意:null值是不放的哦~~~~
// 注意:void的话 returnValue也是null
@Override
public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {
if (returnValue != null) {
String name = ModelFactory.getNameForReturnValue(returnValue, returnType);
mavContainer.addAttribute(name, returnValue);
}
}
}
它对方法返回值的处理非常简单,只要不是null(当然不能是void)就都会放进Model里面,供以使用