前言
Spring MVC提供的基于注释的编程模型,极大的简化了web应用的开发,我们都是受益者。比如我们在@RestController标注的Controller控制器组件上用@RequestMapping、@ExceptionHandler等注解来表示请求映射、异常处理等等。
使用这种注解的方式来开发控制器我认为最重要的优势是:
- 灵活的方法签名(入参随意写)
- 不必继承基类
- 不必实现接口
总之一句话:灵活性非常强,耦合度非常低。
在众多的注解使用中,Spring MVC中有一个非常强大但几乎被忽视的一员:@ModelAttribute。关于这个注解的使用情况,我在群里/线下问了一些人,感觉很少人会使用这个注解(甚至有的不知道有这个注解),这着实让我非常的意外。我认为至少这对于"久经战场"的一个老程序员来说这是不应该的吧。
不过没关系,有幸看到此文,能够帮你弥补弥补这块的盲区。
@ModelAttribute它不是开发必须的注解(不像@RequestMapping那么重要),so即使你不知道它依旧能正常书写控制器。当然,正所谓没有最好只有更好,倘若你掌握了它,便能够帮助你更加高效的写代码,让你的代码复用性更强、代码更加简洁、可维护性更高。
这种知识点就像反射、就像内省,即使你不知道它你完全也可以工作、写业务需求。但是若你能够熟练使用,那你的可想象空间就会更大了,未来可期。虽然它不是必须,但是它是个很好的辅助~
@ModelAttribute官方解释
首先看看Spring官方的JavaDoc对它怎么说:它将方法参数/方法返回值绑定到web view的Model里面。只支持@RequestMapping这种类型的控制器哦。它既可以标注在方法入参上,也可以标注在方法(返回值)上。
但是请注意,当请求处理导致异常时,引用数据和所有其他模型内容对Web视图不可用,因为该异常随时可能引发,使Model内容不可靠。因此,标注有@Exceptionhandler的方法不提供对Model参数的访问~
// @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域相关的模型数据,详见:从原理层面掌握@SessionAttributes的使用【享学Spring MVC】
关于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就得登场了。