从原理层面掌握@ModelAttribute的使用(核心原理篇)【享学Spring MVC】

前言

Spring MVC提供的基于注释的编程模型,极大的简化了web应用的开发,我们都是受益者。比如我们在@RestController标注的Controller控制器组件上用@RequestMapping@ExceptionHandler等注解来表示请求映射、异常处理等等。 使用这种注解的方式来开发控制器我认为最重要的优势是:

  1. 灵活的方法签名(入参随意写)
  2. 不必继承基类
  3. 不必实现接口

总之一句话:灵活性非常强,耦合度非常低。

在众多的注解使用中,Spring MVC中有一个非常强大但几乎被忽视的一员:@ModelAttribute。关于这个注解的使用情况,我在群里/线下问了一些人,感觉很少人会使用这个注解(甚至有的不知道有这个注解),这着实让我非常的意外。我认为至少这对于"久经战场"的一个老程序员来说这是不应该的吧。

不过没关系,有幸看到此文,能够帮你弥补弥补这块的盲区。 @ModelAttribute它不是开发必须的注解(不像@RequestMapping那么重要),so即使你不知道它依旧能正常书写控制器。当然,正所谓没有最好只有更好,倘若你掌握了它,便能够帮助你更加高效的写代码,让你的代码复用性更强、代码更加简洁、可维护性更高。

这种知识点就像反射、就像内省,即使你不知道它你完全也可以工作、写业务需求。但是若你能够熟练使用,那你的可想象空间就会更大了,未来可期。虽然它不是必须,但是它是个很好的辅助~

@ModelAttribute官方解释

首先看看Spring官方的JavaDoc对它怎么说:它将方法参数/方法返回值绑定到web viewModel里面。只支持@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。 和它相关的两个类是ModelFactoryModelAttributeMethodProcessor

@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就得登场了。

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里面,供以使用

总结

本文介绍的是@ModelAttribute的核心原理,他对我们实际使用有重要的理论支撑。下面系列文章主要在原理的基础上,展示各种各样场景下的使用Demo,敬请关注~

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏Java大联盟

徒手撸一个Spring MVC框架

今天我们来仿写一个 Spring MVC 框架,用到的技术比较简单,只需要 XML 解析+反射就可以完成,不需要 JDK 动态代理。

13020
来自专栏编程一生

Spring参数的自解析--还在自己转换?你out了!

背景前段时间开发一个接口,因为调用我接口的同事脾气特别好,我也就不客气,我就直接把源代码发给他当接口定义了。

8530
来自专栏Java后端技术栈cwnait

Nginx+Tomcat搭建集群,Spring Session+Redis实现Session共享

小伙伴们好久不见!最近略忙,博客写的有点少,嗯,要加把劲。OK,今天给大家带来一个JavaWeb中常用的架构搭建,即Nginx+Tomcat搭建服务集群,然后通...

10020
来自专栏算法之名

Spring MVC的模板方法模式 顶

模板方法模式是由抽象类或接口定义好执行顺序,由子类去实现,但无论子类如何实现,他都得按照抽象类或者接口定义好的顺序去执行。实例代码请参考 设计模式整理 ,Ser...

13820
来自专栏Java研发军团

SSHM(SPRING+STRUTS+MYBATIS+HIBERNATE)书籍介绍

持久化——数据在程序实例之外留存的功能——是现代应用程序的核心。Hibernate是最流行的Java持久化工具,提供了自动且透明的对象/关系映射,使得在Java...

10720
来自专栏后端开发你必须学会的干货

SpringIoC和SpringMVC的快速入门

IoC和AOP是Spring框架的两大特性,IoC和MVC的流程密不可分,可以看作是面向对象编程的实现;而AOP特性则是面向切面编程的体现,也是前者的补充,所以...

6420
来自专栏格姗知识圈

放弃JSP吧--否则你无路可走

自从在知乎回答问题以来,以及根据最近几年给企业做技术咨询的情况,发现JSP还是一个经常被提到的问题。希望能在这篇文章里把关于JSP的问题集中说明一下。我的观点很...

22020
来自专栏明丰随笔

MVC和Webapi的区别

Mvc主要用于构建网站,在后端实现了一套完整的MVC开发框架,默认使用Razor视图引擎。

39720
来自专栏全栈开发之路

SpringMVC笔记

1、SpringMVC.xml文件 两种方式把Controller里的java文件注册上来 1)<bean> 写法:<bean name="/loanv1...

8730
来自专栏后端开发你必须学会的干货

Spring与后端模板引擎的故事

现在很多开发,都采用了前后端完全分离的模式,随着近几年前端工程化工具和MVC框架的完善,使得这种模式的维护成本逐渐降低。但是这种模式目前并不利于SEO(前后端分...

13830

扫码关注云+社区

领取腾讯云代金券

年度创作总结 领取年终奖励