天天看點

從原理層面掌握@ModelAttribute的使用(核心原理篇)【享學Spring MVC】關注A哥

每篇一句

我們應該做一個:胸中有藍圖,腳底有計劃的人

前言

Spring MVC

提供的基于注釋的程式設計模型,極大的簡化了

web

應用的開發,我們都是受益者。比如我們在

@RestController

标注的

Controller

控制器元件上用

@RequestMapping

@ExceptionHandler

等注解來表示請求映射、異常處理等等。

使用這種注解的方式來開發控制器我認為最重要的優勢是:

  1. 靈活的方法簽名(入參随意寫)
  2. 不必繼承基類
  3. 不必實作接口

總之一句話:靈活性非常強,耦合度非常低。

在衆多的注解使用中,

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

就得登場了。

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);

這裡我貼出參考值:

從原理層面掌握@ModelAttribute的使用(核心原理篇)【享學Spring MVC】關注A哥

如下示例,它會正常列印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

,敬請關注~

相關閱讀

從原理層面掌握@SessionAttributes的使用【享學Spring MVC】

從原理層面掌握@RequestAttribute、@SessionAttribute的使用【享學Spring MVC】

從原理層面掌握@ModelAttribute的使用(使用篇)【享學Spring MVC】

關注A哥

Author A哥(YourBatman)
個人站點 www.yourbatman.cn
E-mail [email protected]
微 信 fsx641385712

活躍平台

從原理層面掌握@ModelAttribute的使用(核心原理篇)【享學Spring MVC】關注A哥
從原理層面掌握@ModelAttribute的使用(核心原理篇)【享學Spring MVC】關注A哥
從原理層面掌握@ModelAttribute的使用(核心原理篇)【享學Spring MVC】關注A哥
從原理層面掌握@ModelAttribute的使用(核心原理篇)【享學Spring MVC】關注A哥
從原理層面掌握@ModelAttribute的使用(核心原理篇)【享學Spring MVC】關注A哥
從原理層面掌握@ModelAttribute的使用(核心原理篇)【享學Spring MVC】關注A哥
從原理層面掌握@ModelAttribute的使用(核心原理篇)【享學Spring MVC】關注A哥
從原理層面掌握@ModelAttribute的使用(核心原理篇)【享學Spring MVC】關注A哥
公衆号 BAT的烏托邦(ID:BAT-utopia)
知識星球 BAT的烏托邦
每日文章推薦 每日文章推薦
從原理層面掌握@ModelAttribute的使用(核心原理篇)【享學Spring MVC】關注A哥

繼續閱讀