User-Profile-Image
hankin
  • 5
  • Java
  • Kotlin
  • Spring
  • Web
  • SQL
  • MegaData
  • More
  • Experience
  • Enamiĝu al vi
  • 分类
    • Zuul
    • XML
    • WebSocket
    • Web Notes
    • Web
    • Vue
    • Thymeleaf
    • SQL Server
    • SQL Notes
    • SQL
    • SpringSecurity
    • SpringMVC
    • SpringJPA
    • SpringCloud
    • SpringBoot
    • Spring Notes
    • Spring
    • Servlet
    • Ribbon
    • Redis
    • RabbitMQ
    • PostgreSQL
    • OAuth2
    • NOSQL
    • Netty
    • MySQL
    • MyBatis
    • More
    • MegaData
    • Maven
    • LoadBalancer
    • Kotlin Notes
    • Kotlin
    • jQuery
    • JavaScript
    • Java Notes
    • Java
    • Hystrix
    • Git
    • Gateway
    • Freemarker
    • Feign
    • Eureka
    • Enamiĝu al vi
    • ElasticSearch
    • Docker
    • Consul
    • Ajax
    • ActiveMQ
  • 页面
    • 归档
  • 友链
    • Spring Cloud Alibaba
    • Spring Cloud
    • Nacos
    • Docker
    • ElasticSearch
    • Kotlin
    • Kotlin易百
    • KotlinWeb3
    • KotlinNhooo
    • 前端开源搜索
    • Ktorm ORM
    • Ktorm-KSP
    • Ebean ORM
    • Maven
    • 江南一点雨
    • 江南国际站
    • 设计模式
    • 熊猫大佬
    • java学习
    • kotlin函数查询
    • Istio 服务网格
    • istio
    • Ktor 异步 Web 框架
    • PostGis
    • kuangstudy
    • 源码地图
    • it教程吧
    • Arthas-JVM调优
    • Electron
    • bugstack虫洞栈
    • github大佬宝典
    • Sa-Token
    • 前端技术胖
    • bennyhuo-Kt大佬
    • Rickiyang博客
    • 李大辉大佬博客
    • KOIN
    • SQLDelight
    • Exposed-Kt-ORM
    • Javalin—Web 框架
    • http4k—HTTP包
    • 爱威尔大佬
    • 小土豆
    • 小胖哥安全框架
    • 负雪明烛刷题
    • Kotlin-FP-Arrow
    • Lua参考手册
    • 美团文章
    • 666资源站
    • Java 全栈知识体系
    • 尼恩架构师学习
Help?

Please contact us on our email for need any support

Support
    首页   ›   Spring   ›   正文
Spring

SpringMVC—ModelAndViewContainer、ModelMap、Model、ModelAndView、@ModelAttribute、@SessionAttribute等

2022-11-29 21:34:47
468  0 0

参考目录

  • ModelAndViewContainer
  • requestHandled属性
  • Model
  • RedirectAttributes
  • RedirectAttributesModelMap
  • ConcurrentModel
  • ModelMap
  • BindingAwareModelMap
  • ModelAndView
  • ModelFactory
  • @SessionAttributes
  • SessionAttributesHandler
  • ModelFactory
  • 总结
  • @ModelAttribute
  • 基本原理
  • ModelFactory
  • RequestMappingHandlerAdapter
  • ModelAttributeMethodProcessor
  • @ModelAttribute的作用
  • @ConstructorProperties讲解
  • DEMO
  • 标注在非功能方法上
  • 标注在功能方法(返回值)上
  • 标注在方法的入参上
  • 和@RequestAttribute/@SessionAttribute一起使用
  • @RequestAttribute
  • @ModelAttribute注解预存
  • HandlerInterceptor拦截器中预存
  • forward请求转发带过来
  • 原理剖析
  • RequestAttributeMethodArgumentResolver
  • 和@SessionAttributes一起使用

阅读完需:约 61 分钟

对于这几个类,总是经常的分不清楚他们的作用和关系,比较混乱,这次就好好整理一下对这几个类的内容。

ModelAndViewContainer

我把这个类放在首位,是因为相较而言它的逻辑性稍强一点,并且对于理解处理器ReturnValue返回值的处理上有很好的帮助。

ModelAndViewContainer:可以把它定义为ModelAndView上下文的容器,它承担着整个请求过程中的数据传递工作–>保存着Model和View。官方doc对它的解释是这句话:

Records model and view related decisions made by HandlerMethodArgumentResolvers and HandlerMethodReturnValueHandlers during the course of invocation of a controller method.
The setRequestHandled flag can be used to indicate the request has been handled directly and view resolution is not required.
A default Model is automatically created at instantiation. An alternate model instance may be provided via setRedirectModel for use in a redirect scenario. When setRedirectModelScenario is set to true signalling a redirect scenario, the getModel() returns the redirect model instead of the default model.
Since:3.1

翻译一下就是:

记录HandlerMethodArgumentResolver和 HandlerMethodReturnValueHandler在处理Controller的handler方法时 使用的模型model和视图view相关信息.。

当然它除了保存Model和View外,还额外提供了一些其它功能。

// @since 3.1
public class ModelAndViewContainer {
	// =================它所持有的这些属性还是蛮重要的=================
	// redirect时,是否忽略defaultModel 默认值是false:不忽略
	private boolean ignoreDefaultModelOnRedirect = false;
	// 此视图可能是个View,也可能只是个逻辑视图String
	@Nullable
	private Object view;
	// defaultModel默认的Model
	// 注意:ModelMap 只是个Map而已,但是实现类BindingAwareModelMap它却实现了org.springframework.ui.Model接口
	private final ModelMap defaultModel = new BindingAwareModelMap();
	// 重定向时使用的模型(提供set方法设置进来)
	@Nullable
	private ModelMap redirectModel;
	// 控制器是否返回重定向指令
	// 如:使用了前缀"redirect:xxx.jsp"这种,这个值就是true。然后最终是个RedirectView
	private boolean redirectModelScenario = false;
	// Http状态码
	@Nullable
	private HttpStatus status;
	
	private final Set<String> noBinding = new HashSet<>(4);
	private final Set<String> bindingDisabled = new HashSet<>(4);

	// 很容易想到,它和@SessionAttributes标记的元素有关
	private final SessionStatus sessionStatus = new SimpleSessionStatus();
	// 这个属性老重要了:标记handler是否**已经完成**请求处理
	// 在链式操作中,这个标记很重要
	private boolean requestHandled = false;
	...

	public void setViewName(@Nullable String viewName) {
		this.view = viewName;
	}
	public void setView(@Nullable Object view) {
		this.view = view;
	}
	// 是否是视图的引用
	public boolean isViewReference() {
		return (this.view instanceof String);
	}

	// 是否使用默认的Model
	private boolean useDefaultModel() {
		return (!this.redirectModelScenario || (this.redirectModel == null && !this.ignoreDefaultModelOnRedirect));
	}
	
	// 注意子方法和下面getDefaultModel()方法的区别
	public ModelMap getModel() {
		if (useDefaultModel()) { // 使用默认视图
			return this.defaultModel;
		} else {
			if (this.redirectModel == null) { // 若重定向视图为null,就new一个空的返回
				this.redirectModel = new ModelMap();
			}
			return this.redirectModel;
		}
	}
	// @since 4.1.4
	public ModelMap getDefaultModel() {
		return this.defaultModel;
	}

	// @since 4.3 可以设置响应码,最终和ModelAndView一起被View渲染时候使用
	public void setStatus(@Nullable HttpStatus status) {
		this.status = status;
	}

	// 以编程方式注册一个**不应**发生数据绑定的属性,对于随后声明的@ModelAttribute也是不能绑定的
	// 虽然方法是set 但内部是add哦  ~~~~
	public void setBindingDisabled(String attributeName) {
		this.bindingDisabled.add(attributeName);
	}
	public boolean isBindingDisabled(String name) {
		return (this.bindingDisabled.contains(name) || this.noBinding.contains(name));
	}
	// 注册是否应为相应的模型属性进行数据绑定
	public void setBinding(String attributeName, boolean enabled) {
		if (!enabled) {
			this.noBinding.add(attributeName);
		} else {
			this.noBinding.remove(attributeName);
		}
	}

	// 这个方法需要重点说一下:请求是否已在处理程序中完全处理
	// 举个例子:比如@ResponseBody标注的方法返回值,无需View继续去处理,所以就可以设置此值为true了
	// 说明:这个属性也就是可通过源生的ServletResponse、OutputStream来达到同样效果的
	public void setRequestHandled(boolean requestHandled) {
		this.requestHandled = requestHandled;
	}
	public boolean isRequestHandled() {
		return this.requestHandled;
	}

	// =========下面是Model的相关方法了==========
	// addAttribute/addAllAttributes/mergeAttributes/removeAttributes/containsAttribute
}

看过源码后,可以得出一些信息:

  • 它维护了模型model:包括defaultModle和redirectModel
  • defaultModel是默认使用的Model,redirectModel是用于传递redirect时的Model
  • 在Controller处理器入参写了Model或ModelMap类型时候,实际传入的是defaultModel。defaultModel它实际是BindingAwareModel,是个Map。而且继承了ModelMap又实现了Model接口,所以在处理器中使用Model或ModelMap时,其实都是使用同一个对象,可参考MapMethodProcessor,它最终调用的都是mavContainer.getModel()方法(这里之前的文章有介绍过)
  • 若处理器入参类型是RedirectAttributes类型,最终传入的是redirectModel。至于为何实际传入的是defaultModel?参考:RedirectAttributesMethodArgumentResolver,使用的是new RedirectAttributesModelMap(dataBinder)。
  • 维护视图view(兼容支持逻辑视图名称)
  • 维护是否redirect信息,根据这个判断HandlerAdapter使用的是defaultModel或redirectModel
  • 维护@SessionAttributes注解信息状态
  • 维护handler是否处理标记(重要)


requestHandled属性

这里属性默认值是false,请求是否已在处理程序中完全处理。

isRequestHandled()方法的使用:

这个方法的执行实际是:HandlerMethod完全调用执行完成后,就执行这个方法去拿ModelAndView了(传入了request和ModelAndViewContainer)

RequestMappingHandlerAdapter:
	@Nullable
	private ModelAndView getModelAndView(ModelAndViewContainer mavContainer ModelFactory modelFactory, NativeWebRequest webRequest) throws Exception {
		// 将列为@SessionAttributes的模型属性提升到会话
		modelFactory.updateModel(webRequest, mavContainer);
		if (mavContainer.isRequestHandled()) {
			return null;
		}

		ModelMap model = mavContainer.getModel();
		ModelAndView mav = new ModelAndView(mavContainer.getViewName(), model, mavContainer.getStatus());
		// 真正的View 可见ModelMap/视图名称、状态HttpStatus最终都交给了Veiw去渲染
		if (!mavContainer.isViewReference()) {
			mav.setView((View) mavContainer.getView());
		}
		
		// 这个步骤:是Spring MVC对重定向的支持~~~~
		// 重定向之间传值,使用的RedirectAttributes这种Model~~~~
		if (model instanceof RedirectAttributes) {
			Map<String, ?> flashAttributes = ((RedirectAttributes) model).getFlashAttributes();
			HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
			if (request != null) {
				RequestContextUtils.getOutputFlashMap(request).putAll(flashAttributes);
			}
		}
	}

可以看到如果ModelAndViewContainer已经被处理过,此处直接返回null,也就是不会再继续处理Model和View了

setRequestHandled()方法的使用:

请求是否在处理程序中被完全处理,例如@ResponseBody方法,因此不需要视图解析。当控制器方法声明ServletResponse或OutputStream类型的参数时,也可以设置此标志。 默认值为false。

感觉这里就是@ResponseBody可以直接返回json等返回值的原因。

作为设置方法,调用的地方有好多个,总结如下:

  • AsyncTaskMethodReturnValueHandler:处理返回值类型是WebAsyncTask的方法
// 若返回null,就没必要继续处理了
if (returnValue == null) {
	mavContainer.setRequestHandled(true);
	return;
}
  • CallableMethodReturnValueHandler/DeferredResultMethodReturnValueHandler/StreamingResponseBodyReturnValueHandler:处理返回值类型是Callable/DeferredResult/ListenableFuture/CompletionStage/StreamingResponseBody的方法(原理同上)
  • HttpEntityMethodProcessor:返回值类型是HttpEntity的方法
// 看一看到,这种返回值的都会标注为已处理,这样就不再需要视图(渲染)了
	@Override
	public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {
		mavContainer.setRequestHandled(true); // 第一句就是这句代码
		if (returnValue == null) {
			return;
		}
		... // 交给消息处理器去写		
		outputMessage.flush();
	}
  • 同上的原理的还有HttpHeadersReturnValueHandler/RequestResponseBodyMethodProcessor/ResponseBodyEmitterReturnValueHandler等等返回值处理器
  • ServletInvocableHandlerMethod/HandlerMethod在处理Handler方法时,有时也会标注true已处理(比如:get请求NotModified/已设置了HttpStatus状态码/isRequestHandled()==true等等case)。除了这些case,method方法执行完成后可都会显示设置false的(因为执行完handlerMethod后,还需要交给视图渲染)
  • ServletResponseMethodArgumentResolver:这唯一一个是处理入参时候的。若入参类型是ServletResponse/OutputStream/Writer,并且mavContainer != null,它就设置为true了(因为Spring MVC认为既然你自己引入了response,那你就自己做输出吧,因此使用时此处是需要特别注意的细节地方)

resolveArgument()方法:

    if (mavContainer != null) {
        mavContainer.setRequestHandled(true); // 相当于说你自己需要`ServletResponse`,那返回值就交给你自己处理吧~~~~
    }

本文最重要类:ModelAndViewContainer部分就介绍到这

Model

org.springframework.ui.Model的概念不管是在MVC设计模式上,还是在Spring MVC里都是被经常提到的:它用于控制层给前端返回所需的数据(渲染所需的数据)

//  @since 2.5.1 它是一个接口
public interface Model {
	...
	// addAttribute/addAllAttributes/mergeAttributes/containsAttribute
	...
	// Return the current set of model attributes as a Map.
	Map<String, Object> asMap();
}

它的继承树如下:

最重要的那必须是ExtendedModelMap啊,它留到介绍ModelMap的时候再详说,简单看看其余子类。

RedirectAttributes

从命名就能看出是和重定向有关的,它扩展了Model接口:

// @since 3.1
public interface RedirectAttributes extends Model {
	...
	// 它扩展的三个方法,均和flash属性有关
	RedirectAttributes addFlashAttribute(String attributeName, @Nullable Object attributeValue);
	// 这里没指定key,因为key根据Conventions#getVariableName()自动生成
	RedirectAttributes addFlashAttribute(Object attributeValue);
	// Return the attributes candidate for flash storage or an empty Map.
	Map<String, ?> getFlashAttributes();
}

RedirectAttributes例子

利用RedirectAttributes的addAttribute(String attributeName,Object attributeValue)方法

@GetMapping("/redirectAttributes")
public String redirectAttributes1(RedirectAttributes redirectAttributes){
    redirectAttributes.addAttribute("name","Tom");
    redirectAttributes.addAttribute("age","25");
    return "redirect:/redirectAttributes2";
}

@GetMapping("/redirectAttributes2")
public String redirectAttributes2(String name,String age){
    System.out.println(name+"----->"+age);    //Tom----->25
    return "login";
}

说明:输入http://localhost:8080/redirectAttributes时,页面重定向到http://localhost:8080/redirectAttributes2?name=Tom&age=25,并且请求参数在redirectAttributes2()方法中可以直接获取.这种方法直接将参数暴露在链接地址上,请谨慎使用!!!
注意:此种情况下达到的效果和将name和age放在Model中效果一样(在链接地址上直接暴露参数),不能体现RedirectAttributes的高级特性!

利用RedirectAttributes的addFlashAttribute(String attributeName,Object attributeValue)方法

@GetMapping("/redirectAttributes3")
public String redirectAttributes3(RedirectAttributes redirectAttributes){
    redirectAttributes.addFlashAttribute("email","fenglang2016@qq.com");
    redirectAttributes.addFlashAttribute("gender","Male");
    return "redirect:/redirectAttributes4";
}

@GetMapping("/redirectAttributes4")
public String redirectAttributes4(ModelMap map,@ModelAttribute("email")String email,@ModelAttribute("gender")String gender){
    System.out.println(email+"------>"+gender);
    map.clear();
    return "redirect:/redirectAttributes5";
}

@GetMapping("/redirectAttributes5")
public String redirectAttributes5(SessionStatus status){
    System.out.println("redirectAttributes5----->"+status.isComplete());
    return "redirectattributes";
}

说明:redirectAttributes.addFlashAttributie("prama",value)的优点是在发送链接请求时隐藏了请求参数,若跳转至页面,可以在页面中利用jstl表达式取出对应的值,若在方法中要获取param的属性值,需要借助@ModelAttribute注解.

注意:若用户请求http://localhost:8080/redirectAttributes3后重定向到http://localhost:8080/redirectAttributes4,然后重定向到http://localhost:8080/redirectAttributes5,若没有在redirectAttributes4()中使用map.clear(),则地址栏出现的链接为http://localhost:8080/redirectAttributes5?email=fenglang2016@qq.com&gender=Male.

RedirectAttributesModelMap

它实现了RedirectAttributes接口,同时也继承自ModelMap,所以”间接”实现了Model接口的所有方法。

public class RedirectAttributesModelMap extends ModelMap implements RedirectAttributes {
	@Nullable
	private final DataBinder dataBinder;
	private final ModelMap flashAttributes = new ModelMap();
	...
	@Override
	public RedirectAttributesModelMap addAttribute(String attributeName, @Nullable Object attributeValue) {
		super.addAttribute(attributeName, formatValue(attributeValue));
		return this;
	}

	// 可见这里的dataBinder是用于数据转换的
	// 把所有参数都转换为String类型(因为Http都是string传参嘛)
	@Nullable
	private String formatValue(@Nullable Object value) {
		if (value == null) {
			return null;
		}
		return (this.dataBinder != null ? this.dataBinder.convertIfNecessary(value, String.class) : value.toString());
	}
	...

	@Override
	public Map<String, Object> asMap() {
		return this;
	}
	@Override
	public RedirectAttributes addFlashAttribute(String attributeName, @Nullable Object attributeValue) {
		this.flashAttributes.addAttribute(attributeName, attributeValue);
		return this;
	}
	...
}

唯一做的有意义的事:借助DataBinder把添加进来的属性参数会转为String类型

ConcurrentModel

它是Spring5.0后才有的,是线程安全的Model,并没提供什么新鲜东西

ModelMap

ModelMap继承自LinkedHashMap,因此它的本质其实就是个Map而已。
它的特点是:借助Map的能力
间接的实现了org.springframework.ui.Model的接口方法,这种设计技巧更值得我们参考学习的。

这里只需要看看ExtendedModelMap即可。它自己继承自ModelMap,没有啥特点,全部是调用父类的方法完成的接口方法复写

BindingAwareModelMap

注意:它和普通ModelMap的区别是:它能感知数据校验结果(如果放进来的key存在对应的绑定结果,并且你的value不是绑定结果本身。那就移除掉MODEL_KEY_PREFIX + key这个key的键值对)。

public class BindingAwareModelMap extends ExtendedModelMap {

	// 注解复写了Map的put方法,一下子就拦截了所有的addAttr方法。。。
	@Override
	public Object put(String key, Object value) {
		removeBindingResultIfNecessary(key, value);
		return super.put(key, value);
	}
	@Override
	public void putAll(Map<? extends String, ?> map) {
		map.forEach(this::removeBindingResultIfNecessary);
		super.putAll(map);
	}

	// 本类处理的逻辑:
	private void removeBindingResultIfNecessary(Object key, Object value) {
		// key必须是String类型才会给与处理
		if (key instanceof String) {
			String attributeName = (String) key;
			if (!attributeName.startsWith(BindingResult.MODEL_KEY_PREFIX)) {
				String bindingResultKey = BindingResult.MODEL_KEY_PREFIX + attributeName;
				BindingResult bindingResult = (BindingResult) get(bindingResultKey);

				// 如果有校验结果,并且放进来的value值不是绑定结果本身,那就移除掉绑定结果(相当于覆盖掉)
				if (bindingResult != null && bindingResult.getTarget() != value) {
					remove(bindingResultKey);
				}
			}
		}
	}
}

Spring MVC默认使用的就是这个ModelMap,但它提供的感知功能大多数情况下我们都用不着。不过反正也不用你管,乖乖用着呗

ModelAndView

顾名思义,ModelAndView指模型和视图的集合,既包含模型又包含视图;ModelAndView一般可以作为Controller的返回值,所以它的实例是开发者自己手动创建的,这也是它和上面的主要区别(上面都是容器创建,然后注入给我们使用的)。

因为这个类是直接面向开发者的,所以建议里面的一些API还是要熟悉点较好:

public class ModelAndView {
	@Nullable
	private Object view; // 可以是View,也可以是String
	@Nullable
	private ModelMap model;

	// 显然,你也可以自己就放置好一个http状态码进去
	@Nullable
	private HttpStatus status;	
	// 标记这个实例是否被调用过clear()方法~~~
	private boolean cleared = false;

	// 总共这几个属性:它提供的构造函数非常的多  这里我就不一一列出
	public void setViewName(@Nullable String viewName) {
		this.view = viewName;
	}
	public void setView(@Nullable View view) {
		this.view = view;
	}
	@Nullable
	public String getViewName() {
		return (this.view instanceof String ? (String) this.view : null);
	}
	@Nullable
	public View getView() {
		return (this.view instanceof View ? (View) this.view : null);
	}
	public boolean hasView() {
		return (this.view != null);
	}
	public boolean isReference() {
		return (this.view instanceof String);
	}

	// protected方法~~~
	@Nullable
	protected Map<String, Object> getModelInternal() {
		return this.model;
	}
	public ModelMap getModelMap() {
		if (this.model == null) {
			this.model = new ModelMap();
		}
		return this.model;
	}

	// 操作ModelMap的一些方法如下:
	// addObject/addAllObjects

	public void clear() {
		this.view = null;
		this.model = null;
		this.cleared = true;
	}
	// 前提是:this.view == null 
	public boolean isEmpty() {
		return (this.view == null && CollectionUtils.isEmpty(this.model));
	}
	
	// 竟然用的was,歪果仁果然严谨  哈哈
	public boolean wasCleared() {
		return (this.cleared && isEmpty());
	}
}

疑问:为何Controller的处理方法不仅仅可以返回ModelAndView,还可以通过返回Map/Model/ModelMap等来直接向页面传值呢???如果返回值是后三者,又是如何找到view完成渲染的呢?

可参阅ModelMethodProcessor和ModelMethodProcessor对返回值的处理模块

我认为是因为最后都要转为Map来存储处理

绝大多数情况下,我都建议返回ModelAndView,而不是其它那哥三。因为它哥三都没有指定视图名,所以通过DispatcherServlet.applyDefaultViewName()生成的视图名一般都不是我们需要的。(除非你的目录、命名等等都特别特别的规范,那顺便倒是可以省不少事)

  • 在这几者中,ModelAndView、ModelMap、Model是直接可以放在控制器方法的参数中的,在Spring MVC运行的时候,会自动初始化它们。
  • 其中ModelMap和Model必须放在方法参数中才能完成初始化,而ModelAndView既可以放在方法参数中完成初始化,也可以在方法中进行手动实例化。
  • 若放在参数中进行初始化,则不论是Model的参数还是ModelMap的参数,Spring MVC都会对其进行实例化为BindingAwarModelMap,因此Model的参数和ModelMap的参数之间是可以进行相互转换的。
  • 在实例化ModelAndView后,ModeAndView对象中model属性默认为空,当调用它增加数据后,才自动创建实例。
  • 在控制器返回的视图名称为重定向时,如果在数据模型中设置的是String类型,则默认以get的方式添加在重定向url之后。
  • 在控制器返回的视图名称为重定向时,如果要传递的数据不是String类型,则需要使用RedirectAttributes接口的实例进行传递,其中有个方法addFlushAttribute,可以传递自定义数据对象给目标重定向url。其实现原理也很简单,即加入的时候存入session,重定向后从session中将此值取出,然后加到重定向url的请求属性中去,最后从session中删除此数据。
  • 除了在重定向时以字符串的形式传递数据的情况中使用了以请求参数方式传递外,所有的数据模型存储的值的有效范围均为请求(requestScope)作用域。

Model例子:

//ModelAndView
 @RequestMapping(value = "testModelAndView")
 public ModelAndView testModelAndView()  {
  //视图就是sucess页面
  ModelAndView mv=new ModelAndView("sucess");
  Student student=new Student();
  student.setId(1);
  student.setName("lyh");
  mv.addObject("student", student);//相当于request.setAttribute("student", student)
  return mv;   
 }

// 页面参数 ${requestScope.student.id}---${requestScope.student.name}

//ModelAndView
 @RequestMapping(value = "testModelMap")
 public String testModelMap(ModelMap mm)  {  
  Student student=new Student();
  student.setId(1);
  student.setName("lyh");
  mm.put("student1", student);//相当于request.setAttribute("student", student)
  return "sucess"; //返回页面  
 }
 //ModelAndView
 @RequestMapping(value = "testModel")
 public String testModel(Model model)  { 
  Student student=new Student();
  student.setId(1);
  student.setName("lyh");
  model.addAttribute("student2", student);//相当于request.setAttribute("student", student)
  return "sucess";   //返回页面 
 }
 //ModelAndView
 @RequestMapping(value = "testMap")
 public String testMap(Map<String,Object> map)  {
  Student student=new Student();
  student.setId(1);
  student.setName("lyh");
  map.put("student3", student);//相当于request.setAttribute("student", student)
  return "sucess";  //返回页面  
 }

// 页面参数
// testModelMap:${requestScope.student1.id}-- -${requestScope.student1.name}
// testModel:${requestScope.student2.id}---${requestScope.student2.name}
// testMap:${requestScope.student3.id}---${requestScope.student3.name}

疑问:为什么可以通过Model来实现传递参数信息?Model只是一个map的实现

我们可以先来了解一下request.setAttribute和request.getAttribute这两种方法的作用。

  • request.getAttribute("nameOfObj"); 可得到jsp页面表单中输入框内的value。
  • request.setAttribute(position,nameOfObj);属于页面之间的传值

javascript与jsp中不能相互传值,因为javascript运行在客户端,jsp运行在服务器端

  1. 把js直接写在了JSP页面,所以能获取到。如果单独把JS抽离出来是不可能获取到的。前者服务端解析能或许到,后者客户端解析获取不到。
  2. js里是在客户端执行的,不能直接获取,因为EL表达式是在服务器的时候被解析的,所以可以获取到。一个是客户端的东西,一个是服务器端的东西。
  3. jsp能取到,那是因为他本身就是servlet。js不可能有方法直接获取到HttpServletRequest里面的属性值。
  4. setAttribute是服务器行为,到了客户端就无效了,也谈不上如何用。除非你在jsp的时候就写到js变量里,或者使用ajax请求你需要的数据。

ModelFactory

ModelFactory是用来维护Model的,具体包含两个功能

  1. 初始化Model
  2. 处理器执行后将Model中相应的参数更新到SessionAttributes中(处理@ModelAttribute和@SessionAttributes)

@SessionAttributes

该注解顾名思义,作用是将Model中的属性同步到session会话当中,方便在下一次请求中使用(比如重定向场景)。

虽然说Session的概念在当下前后端完全分离的场景中已经变得越来越弱化了,但是若为web开发者来说,我仍旧强烈不建议各位扔掉这个知识点,建议大家能够熟练使用@SessionAttributes来简化平时的开发

这里讲解的是org.springframework.web.bind.annotation.SessionAttributes而非org.springframework.web.bind.annotation.SessionAttribute,它两可完全不是一个概念

这个注解只能标注在类上,用于在多个请求之间传递参数,类似于Session的Attribute。
但不完全一样:一般来说@SessionAttributes设置的参数只用于暂时的传递,而不是长期的保存,长期保存的数据还是要放到Session中。(比如重定向之间暂时传值,用这个注解就很方便)

官方解释:当用@SessionAttributes标注的Controller向其模型Model添加属性时,将根据该注解指定的名称/类型检查这些属性,若匹配上了就顺带也会放进Session里。匹配上的将一直放在Sesson中,直到你调用了SessionStatus.setComplete()方法就消失了

// @since 2.5   它只能标注在类上
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface SessionAttributes {

	// 只有名称匹配上了的  Model上的属性会向session里放置一份~~~
	@AliasFor("names")
	String[] value() default {};
	@AliasFor("value")
	String[] names() default {};

	// 也可以拿类型来约束
	Class<?>[] types() default {};
}

用户可以调用SessionStatus.setComplete来清除,这个方法只是清除@SessionAttributes里的参数,而不会应用于Session中的参数。也就是说使用API自己放进Session内和使用@SessionAttributes注解放进去还是有些许差异的

@Controller
@RequestMapping("/sessionattr/demo")
@SessionAttributes(value = {"book", "description"}, types = {Double.class})
public class RedirectController {

    @RequestMapping("/index")
    public String index(Model model, HttpSession httpSession) {
        model.addAttribute("book", "天龙八部");
        model.addAttribute("description", "我乔峰是个契丹人");
        model.addAttribute("price", new Double("1000.00"));

        // 通过Sesson API手动放一个进去
        httpSession.setAttribute("hero", "fsx");

        //跳转之前将数据保存到Model中,因为注解@SessionAttributes中有,所以book和description应该都会保存到SessionAttributes里(注意:不是session里)
        return "redirect:get";
    }

    // 关于@ModelAttribute 下文会讲
    @RequestMapping("/get")
    public String get(@ModelAttribute("book") String book, ModelMap model, HttpSession httpSession, SessionStatus sessionStatus) {
        //可以从model中获得book、description和price的参数
        System.out.println(model.get("book") + ";" + model.get("description") + ";" + model.get("price"));

        // 从sesson中也能拿到值
        System.out.println(httpSession.getAttribute("book"));
        System.out.println("API方式手动放进去的:" + httpSession.getAttribute("hero"));
        // 使用@ModelAttribute也能拿到值
        System.out.println(book);

        // 手动清除SessionAttributes
        sessionStatus.setComplete();
        return "redirect:complete";
    }

    @RequestMapping("/complete")
    @ResponseBody
    public String complete(ModelMap modelMap, HttpSession httpSession) {
        //已经被清除,无法获取book的值
        System.out.println(modelMap.get("book"));
        System.out.println("API方式手动放进去的:" + httpSession.getAttribute("hero"));
        return "sessionAttributes";
    }

}

只需要访问入口请求/index就可以直接看到控制台输出如下:

天龙八部;我乔峰是个契丹人;1000.0
天龙八部
API方式手动放进去的:fsx
天龙八部
null
API方式手动放进去的:fsx

@SessionAttributes注解设置的参数有3类方式去使用它:

  1. 在视图view中(比如jsp页面等)通过request.getAttribute()或session.getAttribute获取
  2. 在后面请求返回的视图view中通过session.getAttribute或者从model中获取(这个也比较常用)
  3. 自动将参数设置到后面请求所对应处理器的Model类型参数或者有@ModelAttribute注释的参数里面(结合@ModelAttribute一起使用应该是我们重点关注的)

下面从原理层面去分析它的执行过程,实现真正的掌握它。

SessionAttributesHandler

它是@SessionAttributes处理器,也就是解析这个注解的核心。管理通过@SessionAttributes标注了的特定会话属性,存储最终是委托了SessionAttributeStore来实现。

// @since 3.1
public class SessionAttributesHandler {

	private final Set<String> attributeNames = new HashSet<>();
	private final Set<Class<?>> attributeTypes = new HashSet<>();

	// 注意这个重要性:它是注解方式放入session和API方式放入session的关键(它只会记录注解方式放进去的session属性~~)
	private final Set<String> knownAttributeNames = Collections.newSetFromMap(new ConcurrentHashMap<>(4));
	// sessonAttr存储器:它最终存储到的是WebRequest的session域里面去(对httpSession是进行了包装的)
	// 因为有WebRequest的处理,所以达到我们上面看到的效果。complete只会清楚注解放进去的,并不清除API放进去的~~~
	// 它的唯一实现类DefaultSessionAttributeStore实现也简单。(特点:能够制定特殊的前缀,这个有时候还是有用的)
	// 前缀attributeNamePrefix在构造器里传入进来  默认是“”
	private final SessionAttributeStore sessionAttributeStore;

	// 唯一的构造器 handlerType:控制器类型  SessionAttributeStore 是由调用者上层传进来的
	public SessionAttributesHandler(Class<?> handlerType, SessionAttributeStore sessionAttributeStore) {
		Assert.notNull(sessionAttributeStore, "SessionAttributeStore may not be null");
		this.sessionAttributeStore = sessionAttributeStore;

		// 父类上、接口上、注解上的注解标注了这个注解都算
		SessionAttributes ann = AnnotatedElementUtils.findMergedAnnotation(handlerType, SessionAttributes.class);
		if (ann != null) {
			Collections.addAll(this.attributeNames, ann.names());
			Collections.addAll(this.attributeTypes, ann.types());
		}
		this.knownAttributeNames.addAll(this.attributeNames);
	}

	// 既没有指定Name 也没有指定type  这个注解标上了也没啥用
	public boolean hasSessionAttributes() {
		return (!this.attributeNames.isEmpty() || !this.attributeTypes.isEmpty());
	}

	// 看看指定的attributeName或者type是否在包含里面
	// 请注意:name和type都是或者的关系,只要有一个符合条件就成
	public boolean isHandlerSessionAttribute(String attributeName, Class<?> attributeType) {
		Assert.notNull(attributeName, "Attribute name must not be null");
		if (this.attributeNames.contains(attributeName) || this.attributeTypes.contains(attributeType)) {
			this.knownAttributeNames.add(attributeName);
			return true;
		} else {
			return false;
		}
	}

	// 把attributes属性们存储起来  进到WebRequest 里
	public void storeAttributes(WebRequest request, Map<String, ?> attributes) {
		attributes.forEach((name, value) -> {
			if (value != null && isHandlerSessionAttribute(name, value.getClass())) {
				this.sessionAttributeStore.storeAttribute(request, name, value);
			}
		});
	}

	// 检索所有的属性们  用的是knownAttributeNames哦~~~~
	// 也就是说手动API放进Session的 此处不会被检索出来的
	public Map<String, Object> retrieveAttributes(WebRequest request) {
		Map<String, Object> attributes = new HashMap<>();
		for (String name : this.knownAttributeNames) {
			Object value = this.sessionAttributeStore.retrieveAttribute(request, name);
			if (value != null) {
				attributes.put(name, value);
			}
		}
		return attributes;
	}

	// 同样的 只会清除knownAttributeNames
	public void cleanupAttributes(WebRequest request) {
		for (String attributeName : this.knownAttributeNames) {
			this.sessionAttributeStore.cleanupAttribute(request, attributeName);
		}
	}


	// 对底层sessionAttributeStore的一个传递调用~~~~~
	// 毕竟可以拼比一下sessionAttributeStore的实现~~~~
	@Nullable
	Object retrieveAttribute(WebRequest request, String attributeName) {
		return this.sessionAttributeStore.retrieveAttribute(request, attributeName);
	}
}

这个类是对SessionAttribute这些属性的核心处理能力:包括了所谓的增删改查。因为要进一步理解到它的原理,所以要说到它的处理入口,那就要来到ModelFactory了

ModelFactory

Spring MVC对@SessionAttributes的处理操作入口,是在ModelFactory.initModel()方法里会对@SessionAttributes的注解进行解析、处理,然后方法完成之后也会对它进行属性同步。

ModelFactory是用来维护Model的,具体包含两个功能:

  • 处理器执行前,初始化Model
  • 处理器执行后,将Model中相应的参数同步更新到SessionAttributes中(不是全量,而是符合条件的那些)
// @since 3.1
public final class ModelFactory {
	// ModelMethod它是一个私有内部类,持有InvocableHandlerMethod的引用  和方法的dependencies依赖们
	private final List<ModelMethod> modelMethods = new ArrayList<>();
	private final WebDataBinderFactory dataBinderFactory;
	private final SessionAttributesHandler sessionAttributesHandler;

	public ModelFactory(@Nullable List<InvocableHandlerMethod> handlerMethods, WebDataBinderFactory binderFactory, SessionAttributesHandler attributeHandler) {
	
		// 把InvocableHandlerMethod转为内部类ModelMethod
		if (handlerMethods != null) {
			for (InvocableHandlerMethod handlerMethod : handlerMethods) {
				this.modelMethods.add(new ModelMethod(handlerMethod));
			}
		}
		this.dataBinderFactory = binderFactory;
		this.sessionAttributesHandler = attributeHandler;
	}


	// 该方法完成Model的初始化
	public void initModel(NativeWebRequest request, ModelAndViewContainer container, HandlerMethod handlerMethod) throws Exception {
		// 先拿到sessionAttr里所有的属性们(首次进来肯定木有,但同一个session第二次进来就有了)
		Map<String, ?> sessionAttributes = this.sessionAttributesHandler.retrieveAttributes(request);
		// 和当前请求中 已经有的model合并属性信息
		// 注意:sessionAttributes中只有当前model不存在的属性,它才会放进去
		container.mergeAttributes(sessionAttributes);
		// 此方法重要:调用模型属性方法来填充模型  这里ModelAttribute会生效
		// 关于@ModelAttribute的内容  我放到了这里:https://blog.csdn.net/f641385712/article/details/98260361
		// 总之:完成这步之后 Model就有值了~~~~
		invokeModelAttributeMethods(request, container);

		// 最后,最后,最后还做了这么一步操作~~~
		// findSessionAttributeArguments的作用:把@ModelAttribute的入参也列入SessionAttributes(非常重要) 详细见下文
		// 这里一定要掌握:因为使用中的坑坑经常是因为没有理解到这块逻辑
		for (String name : findSessionAttributeArguments(handlerMethod)) {
		
			// 若ModelAndViewContainer不包含此name的属性   才会进来继续处理  这一点也要注意
			if (!container.containsAttribute(name)) {

				// 去请求域里检索为name的属性,若请求域里没有(也就是sessionAttr里没有),此处会抛出异常的~~~~
				Object value = this.sessionAttributesHandler.retrieveAttribute(request, name);
				if (value == null) {
					throw new HttpSessionRequiredException("Expected session attribute '" + name + "'", name);
				}
				// 把从sessionAttr里检索到的属性也向容器Model内放置一份~
				container.addAttribute(name, value);
			}
		}
	}


	// 把@ModelAttribute标注的入参也列入SessionAttributes 放进sesson里(非常重要)
	// 这个动作是很多开发者都忽略了的
	private List<String> findSessionAttributeArguments(HandlerMethod handlerMethod) {
		List<String> result = new ArrayList<>();
		// 遍历所有的方法参数
		for (MethodParameter parameter : handlerMethod.getMethodParameters()) {
			// 只有参数里标注了@ModelAttribute的才会进入继续解析~~~
			if (parameter.hasParameterAnnotation(ModelAttribute.class)) {
				// 关于getNameForParameter拿到modelKey的方法,这个策略是需要知晓的
				String name = getNameForParameter(parameter);
				Class<?> paramType = parameter.getParameterType();

				// 判断isHandlerSessionAttribute为true的  才会把此name合法的添加进来
				// (也就是符合@SessionAttribute标注的key或者type的)
				if (this.sessionAttributesHandler.isHandlerSessionAttribute(name, paramType)) {
					result.add(name);
				}
			}
		}
		return result;
	}

	// 静态方法:决定了parameter的名字  它是public的,因为ModelAttributeMethodProcessor里也有使用
	// 请注意:这里不是MethodParameter.getParameterName()获取到的形参名字,而是有自己的一套规则的

	// @ModelAttribute指定了value值就以它为准,否则就是类名的首字母小写(当然不同类型不一样,下面有给范例)
	public static String getNameForParameter(MethodParameter parameter) {
		ModelAttribute ann = parameter.getParameterAnnotation(ModelAttribute.class);
		String name = (ann != null ? ann.value() : null);
		return (StringUtils.hasText(name) ? name : Conventions.getVariableNameForParameter(parameter));
	}

	// 关于方法这块的处理逻辑,和上差不多,主要是返回类型和实际类型的区分
	// 比如List<String>它对应的名是:stringList。即使你的返回类型是Object~~~
	public static String getNameForReturnValue(@Nullable Object returnValue, MethodParameter returnType) {
		ModelAttribute ann = returnType.getMethodAnnotation(ModelAttribute.class);
		if (ann != null && StringUtils.hasText(ann.value())) {
			return ann.value();
		} else {
			Method method = returnType.getMethod();
			Assert.state(method != null, "No handler method");
			Class<?> containingClass = returnType.getContainingClass();
			Class<?> resolvedType = GenericTypeResolver.resolveReturnType(method, containingClass);
			return Conventions.getVariableNameForReturnType(method, resolvedType, returnValue);
		}
	}

	// 将列为@SessionAttributes的模型数据,提升到sessionAttr里
	public void updateModel(NativeWebRequest request, ModelAndViewContainer container) throws Exception {
		ModelMap defaultModel = container.getDefaultModel();
		if (container.getSessionStatus().isComplete()){
			this.sessionAttributesHandler.cleanupAttributes(request);
		} else { // 存储到sessionAttr里
			this.sessionAttributesHandler.storeAttributes(request, defaultModel);
		}

		// 若该request还没有被处理  并且 Model就是默认defaultModel
		if (!container.isRequestHandled() && container.getModel() == defaultModel) {
			updateBindingResult(request, defaultModel);
		}
	}

	// 将bindingResult属性添加到需要该属性的模型中。
	// isBindingCandidate:给定属性在Model模型中是否需要bindingResult。
	private void updateBindingResult(NativeWebRequest request, ModelMap model) throws Exception {
		List<String> keyNames = new ArrayList<>(model.keySet());
		for (String name : keyNames) {
			Object value = model.get(name);
			if (value != null && isBindingCandidate(name, value)) {
				String bindingResultKey = BindingResult.MODEL_KEY_PREFIX + name;
				if (!model.containsAttribute(bindingResultKey)) {
					WebDataBinder dataBinder = this.dataBinderFactory.createBinder(request, value, name);
					model.put(bindingResultKey, dataBinder.getBindingResult());
				}
			}
		}
	}

	// 看看这个静态内部类ModelMethod
	private static class ModelMethod {
		// 持有可调用的InvocableHandlerMethod 这个方法
		private final InvocableHandlerMethod handlerMethod;
		// 这字段是搜集该方法标注了@ModelAttribute注解的入参们
		private final Set<String> dependencies = new HashSet<>();

		public ModelMethod(InvocableHandlerMethod handlerMethod) {
			this.handlerMethod = handlerMethod;
			// 把方法入参中所有标注了@ModelAttribute了的Name都搜集进来
			for (MethodParameter parameter : handlerMethod.getMethodParameters()) {
				if (parameter.hasParameterAnnotation(ModelAttribute.class)) {
					this.dependencies.add(getNameForParameter(parameter));
				}
			}
		}
		...
	}
}

ModelFactory协助在控制器方法调用之前初始化Model模型,并在调用之后对其进行更新。

  • 初始化时,通过调用方法上标注有@ModelAttribute的方法,使用临时存储在会话中的属性填充模型。
  • 在更新时,模型属性与会话同步,如果缺少,还将添加BindingResult属性。

关于默认名称规则的核心在Conventions.getVariableNameForParameter(parameter)这个方法里。

将一个参数设置到@SessionAttribute中需要同时满足两个条件:

  1. 在@SessionAttribute注解中设置了参数的名字或者类型
  2. 在处理器(Controller)中将参数设置到了Model中(这样方法结束后会自动的同步到SessionAttr里)

总结

@SessionAttributes指的是Spring MVC的Session。向其中添加值得时候,同时会向 HttpSession中添加一条。在sessionStatus.setComplete();的时候,会清空Spring MVC
的Session,同时清除对应键的HttpSession内容,但是通过,request.getSession.setAttribute()方式添加的内容不会被清除掉。其他情况下,Spring MVC的Session和HttpSession使用情况相同。


@ModelAttribute

前面讲了Model相关的类,这里扩展与它相关的注解内容。

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

灵活性非常强,耦合度非常低。

在众多的注解使用中,Spring MVC中有一个非常强大但几乎被忽视的一员:@ModelAttribute。

@ModelAttribute它不是开发必须的注解(不像@RequestMapping那么重要),即使你不知道它依旧能正常书写控制器。尤其是现在前后端分离之后,变的几乎不存在了一样。

首先看看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域相关的模型数据,在上面有讲到哦

关于ModelFactory的介绍上面@SessionAttributes里也提到了,这里做补充

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的处理器的


补充:

@ModelAttribute注解经常在更新时使用

在不改变原有代码的基础上插入一个新方法

在请求该类的每个方法前都会执行一次@ModelAttribute修饰的方法,设计原则是基于一个控制器对应一个功能,使用时需要注意。

并且@ModelAttribute修饰的方法的参数map.put()可以将对象放入即将查询的参数中map.put(k,v)其中的k必须是即将查询方法参数的首字母小写,如果不一致,需要通过 @ModelAttribute注解声明即将查询方法的参数,即将k写入注解中

 //查询
 @ModelAttribute//在任何一次请求前都会执行一次带@ModelAttribute的方法
 public void queryStudentById(Map<String,Object> map) {
  Student student =new Student();
  student.setId(2);
  student.setName("zs");
  student.setAge(20);
  map.put("stu", student);//约定:map的key就是方法参数类型首字母小写,不一致的话需要在参数类型前加@ModelAttribute("key")
 }
 //更新
 @RequestMapping(value = "testModelAttribute")
 public String testModelAttribute(@ModelAttribute("stu") Student student)  {
  student.setName(student.getName());
  System.out.println(student.getId()+student.getName()+student.getAge());
  return "sucess";  //返回页面  
 }

RequestMappingHandlerAdapter

前面有很多的文章提到了这个top级的类

SpringMVC—Web九大组件之HandlerAdapter

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的作用

  1. 绑定请求参数到命令对象(入参对象):放在控制器方法的入参上时,用于将多个请求参数绑定到一个命令对象,从而简化绑定流程,而且自动暴露为模型数据用于视图页面展示时使用;
  2. 暴露表单引用对象为模型数据:放在处理器的一般方法(非功能处理方法,也就是没有@RequestMapping标注的方法)上时,是为表单准备要展示的表单引用数据对象:如注册时需要选择的所在城市等静态信息。它在执行功能处理方法(@RequestMapping 注解的方法)之前,自动添加到模型对象中,用于视图页面展示时使用;
  3. 暴露@RequestMapping方法返回值为模型数据:放在功能处理方法的返回值上时,是暴露功能处理方法的返回值为模型数据,用于视图页面展示时使用。

@ConstructorProperties讲解

自动创建模型对象的时候不仅仅可以使用空的构造函数,还可以使用java.beans.ConstructorProperties这个注解,因此有必须先把它介绍一波:

官方解释:构造函数上的注释,显示该构造函数的参数如何对应于构造对象的getter方法。

// @since 1.6
@Documented 
@Target(CONSTRUCTOR)  // 只能使用在构造器上
@Retention(RUNTIME)
public @interface ConstructorProperties {
    String[] value();
}

如下例子:

@Getter
@Setter
public class Person {
    private String name;
    private Integer age;

	// 标注注解
    @ConstructorProperties({"name", "age"})
    public Person(String myName, Integer myAge) {
        this.name = myName;
        this.age = myAge;
    }
}

这里注解上的name、age的意思是对应着Person这个JavaBean的getName()和getAge()方法。它表示:构造器的第一个参数可以用getName()检索,第二个参数可以用getAge()检索,由于方法/构造器的形参名在运行期就是不可见了,所以使用该注解可以达到这个效果。

此注解它的意义何在?

其实说实话,在现在去xml,完全注解驱动的时代它的意义已经不大了。它使用得比较多的场景是之前像使用xml配置Bean这样:

<bean id="person" class="com.fsx.bean.Person">
    <constructor-arg name="name" value="fsx"/>
    <constructor-arg name="age" value="18"/>
</bean>

这样<constructor-arg>就不需要按照自然顺序参数index(不灵活且容易出错有木有)来了,可以按照属性名来对应,灵活了很多。本来xml配置基本不用了,但恰好在@ModelAttribute解析这块让它又换发的新生

java.beans中还提供了一个注解java.beans.Transient(1.7以后提供的):指定该属性或字段不是永久的。 它用于注释实体类,映射超类或可嵌入类的属性或字段。(可以标注在属性上和get方法上)

DEMO

标注在非功能方法上

@Getter
@Setter
@ToString
public class Person {
    private String name;
    private Integer age;

    public Person() {
    }

    public Person(String myName, int myAge) {
        this.name = myName;
        this.age = myAge;
    }
}

@RestController
@RequestMapping
public class HelloController {

    @ModelAttribute("myPersonAttr")
    public Person personModelAttr() {
        return new Person("非功能方法", 50);
    }

    @GetMapping("/testModelAttr")
    public void testModelAttr(Person person, ModelMap modelMap) {
        //System.out.println(modelMap.get("person")); // 若上面注解没有指定value值,就是类名首字母小写
        System.out.println(modelMap.get("myPersonAttr"));
    }
}

访问:/testModelAttr?name=wo&age=10。打印输出:

Person(name=wo, age=10)
Person(name=非功能方法, age=50)

可以看到入参的Person对象即使没有标注@ModelAttribute也是能够正常被封装进值的(并且还放进了ModelMap里)。

因为没有注解也会使用空构造创建一个Person对象,再使用ServletRequestDataBinder.bind(ServletRequest request)完成数据绑定(当然还可以@Valid校验)

细节需要注意:

1、Person即使没有空构造,借助@ConstructorProperties也能完成自动封装

	// Person只有如下一个构造函数
    @ConstructorProperties({"name", "age"})
    public Person(String myName, int myAge) {
        this.name = myName;
        this.age = myAge;
    }

打印的结果完全同上。

2、即使上面@ConstructorProperties的name写成了myName,结果依旧正常封装。因为只要没有校验bindingResult == null的时候,仍旧还会执行ServletRequestDataBinder.bind(ServletRequest request)再封装一次的。除非加了@Valid校验,那就只会使用@ConstructorProperties封装一次,不会二次bind了(因为Spring认为你已经@Valid过了,那就不要在凑进去了)

3、即使上面构造器上没有标注@ConstructorProperties注解,也依旧是没有问题的。原因:BeanUtils.instantiateClass(ctor, args)创建对象时最多args是[null,null]呗,也不会报错嘛(需要注意:如果你是入参是基本类型int那就报错啦)

4、虽然说@ModelAttribute写不写效果一样。但是若写成这样@ModelAttribute("myPersonAttr") Person person,也就是指定为上面一样的value值,那打印的就是下面:

Person(name=wo, age=10)
Person(name=wo, age=10)

另外还需要知道的是:@ModelAttribute标注在本方法上只会对本控制器有效。但若你使用在@ControllerAdvice组件上,它将是全局的。(当然可以指定basePackages来限制它的作用范围)

标注在功能方法(返回值)上

    @GetMapping("/testModelAttr")
    public @ModelAttribute Person testModelAttr(Person person, ModelMap modelMap) {
    	...
    }

把方法的返回值放入模型中。(注意void、null这些返回值是不会放进去的)

标注在方法的入参上

该使用方式应该是我们使用得最多的方式了,略

和@RequestAttribute/@SessionAttribute一起使用

它俩合作使用是很顺畅的,一般不会有什么问题,也没有什么主意事项

补充:


@RequestAttribute

  1. @RequestAttribute在Spring4.3后才有
  2. 我们可以使用API调用的方式(ServletRequest.getAttribute())来达到目的,而不用注解

Spring提供的这些注解比如@ModelAttribute、@SessionAttributes、@RequestAttribute都是为了简化开发,提高复用性。同时另外一个目的是希望完全屏蔽掉源生Servlet API,增加它的扩展性。

本文我以@RequestAttribute为例进行讲解,因为@SessionAttribute(也是Spring4.3后推出的注解)不管从使用和原理上都是一模一样的。你可以理解成唯一区别是ServletRequest.getAttribute()和HttpSession.getAttribute()的区别
此处再强调一次,这里指的是:org.springframework.web.bind.annotation.SessionAttribute,而非org.springframework.web.bind.annotation.SessionAttributes

它比前面介绍的那些@ModelAttribute、@SessionAttributes等注解要简单很多,它只能使用在方法入参上。作用:从request中取对应的属性值。

很对人对getParameter()和getAttribute()相关方法傻傻分不清楚。建议你可以先弄清楚param和attribute的区别

// @since 4.3
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequestAttribute {
	@AliasFor("name")
	String value() default "";
	@AliasFor("value")
	String name() default "";

	// 默认情况下 这个属性是必须的(没有就报错了)
	boolean required() default true;
}

接下来这句话很重要:@RequestAttribute只负责从request里面取属性值,至于你什么时候往里放值,是有多种方式的可以达到的:

  1. @ModelAttribute注解预存
  2. HandlerInterceptor拦截器中预存
  3. 请求转发带过来

下面分别按照这三种使用场景,给出使用Demo:

@ModelAttribute注解预存

比较简单,在@ModelAttribute标注的方法上使用源生的HttpServletRequest放值即可

@RestController
@RequestMapping
public class HelloController {

	// 放置attr属性值
    @ModelAttribute
    public Person personModelAttr(HttpServletRequest request) {
        request.setAttribute("myApplicationName", "fsx-application");
        return new Person("非功能方法", 50);
    }

    @GetMapping("/testRequestAttr")
    public void testRequestAttr(@RequestAttribute("myApplicationName") String myApplicationName, HttpServletRequest request, ModelMap modelMap) {
        System.out.println(myApplicationName); //fsx-application

        // 从request里获取
        System.out.println(request.getAttribute("myApplicationName")); //fsx-application

        // 从model里获取
        System.out.println(modelMap.get("myApplicationName")); // null 获取不到attr属性的
        System.out.println(modelMap.get("person")); // Person(name=非功能方法, age=50)
    }
}

请求/testRequestAttr,结果打印如下:

fsx-application
fsx-application
null
Person(name=非功能方法, age=50)

这里务必注意:@RequestAttribute("myApplicationName")注解如果省略,是绑定不到attr属性的哦(必须要有注解)

但是,这样是可行的:@RequestAttribute String myApplicationName(若注解没有指定,Spring MVC会再去看形参的名字来确认自动绑定)

但若你写成了这样@RequestAttribute String aaa,那请求就直接400错误了抛出异常:org.springframework.web.bind.ServletRequestBindingException

HandlerInterceptor拦截器中预存

public class SimpleInterceptor implements HandlerInterceptor {
 
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        request.setAttribute("myApplicationName", "fsx-application");
        return true;
    }
 	...
}

forward请求转发带过来

request.setAttribute("myApplicationName", "fsx-application");
request.getRequestDispatcher("/index").forward(request, response); 

其实往里放置属性值只需要遵循一个原则:在调用处理器目标方法之前(参数封装之前)任意地方放置即可,属性值是都能被取到的。

原理剖析

根据经验很容易想到解析它的是一个HandlerMethodArgumentResolver,它就是RequestAttributeMethodArgumentResolver

RequestAttributeMethodArgumentResolver
public class RequestAttributeMethodArgumentResolver extends AbstractNamedValueMethodArgumentResolver {

	// 只处理标注了@RequestAttribute注解的入参
	@Override
	public boolean supportsParameter(MethodParameter parameter) {
		return parameter.hasParameterAnnotation(RequestAttribute.class);
	}

	// 封装此注解的属性到NamedValueInfo 这里关于参数名的处理有这么一个处理
	// info.name.isEmpty()也就说如果自己没有指定,就用形参名parameter.getParameterName()
	@Override
	protected NamedValueInfo createNamedValueInfo(MethodParameter parameter) {
		RequestAttribute ann = parameter.getParameterAnnotation(RequestAttribute.class);
		Assert.state(ann != null, "No RequestAttribute annotation");
		return new NamedValueInfo(ann.name(), ann.required(), ValueConstants.DEFAULT_NONE);
	}

	// 从request请求域去找属性值
	@Override
	@Nullable
	protected Object resolveName(String name, MethodParameter parameter, NativeWebRequest request){
		return request.getAttribute(name, RequestAttributes.SCOPE_REQUEST);
	}


	// 若值不存在,抛出异常ServletRequestBindingException
	@Override
	protected void handleMissingValue(String name, MethodParameter parameter) throws ServletException {
		throw new ServletRequestBindingException("Missing request attribute '" + name +
				"' of type " +  parameter.getNestedParameterType().getSimpleName());
	}

}

源码短小精悍,非常简单。
其实它解析入参方面的核心解析流程在其父类AbstractNamedValueMethodArgumentResolver身上

@RequestAttribute属性required默认为true, request.getAttribute获取不到参数就会抛出异常ServletRequestBindingException;required设置为false,即使没有从request中获取到就忽略跳过,赋值为null;

这里的@RequestAttribute与@SessionAttributes原理一致,(此@SessionAttributes非彼@SessionAttributes)


和@SessionAttributes一起使用

@ModelAttribute它本质上来说:允许我们在调用目标方法前操纵模型数据。@SessionAttributes它允许把Model数据(符合条件的)同步一份到Session里,方便多个请求之间传递数值。

@RestController
@RequestMapping
@SessionAttributes(names = {"name", "age"}, types = Person.class)
public class HelloController {

    @ModelAttribute
    public Person personModelAttr() {
        return new Person("非功能方法", 50);
    }

    @GetMapping("/testModelAttr")
    public void testModelAttr(HttpSession httpSession, ModelMap modelMap) {
        System.out.println(modelMap.get("person"));
        System.out.println(httpSession.getAttribute("person"));
    }
}

为了看到@SessionAttributes的效果,我这里直接使用浏览器连续访问两次(同一个session)看效果:

第一次访问打印:

Person(name=非功能方法, age=50)
null

第二次访问打印:

Person(name=非功能方法, age=50)
Person(name=非功能方法, age=50)

可以看到@ModelAttribute结合@SessionAttributes就生效了。因为第一次是读取Model的数据,往Session里存,第二次才可以打印。

再看下面的变种例子(重要):

@RestController
@RequestMapping
@SessionAttributes(names = {"name", "age"}, types = Person.class)
public class HelloController {

    @GetMapping("/testModelAttr")
    public void testModelAttr(@ModelAttribute Person person, HttpSession httpSession, ModelMap modelMap) {
        System.out.println(modelMap.get("person"));
        System.out.println(httpSession.getAttribute("person"));
    }
}

访问:/testModelAttr?name=wo&age=10。报错了:

 org.springframework.web.HttpSessionRequiredException: Expected session attribute 'person'
	at org.springframework.web.method.annotation.ModelFactory.initModel(ModelFactory.java:117)
	at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:869)

这个错误请务必重视:这是前面我特别强调的一个使用误区,当你在@SessionAttributes和@ModelAttribute一起使用的时候,最容易犯的一个错误。

错误原因代码如下:

	public void initModel(NativeWebRequest request, ModelAndViewContainer container, HandlerMethod handlerMethod) throws Exception {
		Map<String, ?> sessionAttributes = this.sessionAttributesHandler.retrieveAttributes(request);
		container.mergeAttributes(sessionAttributes);
		invokeModelAttributeMethods(request, container);

		// 合并完sesson的属性,并且执行完成@ModelAttribute的方法后,会继续去检测
		// findSessionAttributeArguments:标注有@ModelAttribute的入参  并且isHandlerSessionAttribute()是SessionAttributts能够处理的类型的话
		// 那就必须给与赋值~~~~  注意是必须
		for (String name : findSessionAttributeArguments(handlerMethod)) {
			// 如果model里不存在这个属性(那就去sessionAttr里面找)
			// 这就是所谓的其实@ModelAttribute它是会深入到session里面去找的哦~~~不仅仅是request里
			if (!container.containsAttribute(name)) {
				Object value = this.sessionAttributesHandler.retrieveAttribute(request, name);
				
				// 倘若session里都没有找到,那就报错喽
				// 注意:它并不会自己创建出一个新对象出来,然后自己填值,这就是区别。
				// 至于Spring为什么这么设计 我觉得是值得思考一下子的
				if (value == null) {
					throw new HttpSessionRequiredException("Expected session attribute '" + name + "'", name);
				}
				container.addAttribute(name, value);
			}
		}
	}

注意,这里是initModel()的时候就报错了哟,还没到resolveArgument()呢。Spring这样设计的意图?我大胆猜测一下:控制器上标注了@SessionAttributes注解,如果你入参上还使用了@ModelAttribute,那么你肯定是希望得到绑定的,若找不到肯定是你的程序失误有问题,所以给你抛出异常,显示的告诉你要去排错。

修改如下,本控制器上加上这个方法:

    @ModelAttribute
    public Person personModelAttr() {
        return new Person("非功能方法", 50);
    }

访问:/testModelAttr

Person(name=非功能方法, age=50)
null

再访问:/testModelAttr

Person(name=非功能方法, age=50)
Person(name=非功能方法, age=50)

访问:/testModelAttr?name=wo&age=10

Person(name=wo, age=10)
Person(name=wo, age=10)

注意:此时model和session里面的值都变了哦,变成了最新的的请求链接上的参数值(并且每次都会使用请求参数的值)。

访问:/testModelAttr?age=11111

Person(name=wo, age=11111)
Person(name=wo, age=11111)

可以看到是可以完成局部属性修改的

再次访问:/testModelAttr(无请求参数,相当于只执行非功能方法)

Person(name=fsx, age=18)
Person(name=fsx, age=18)

可以看到这个时候model和session里的值已经不能再被非功能方法上的@ModelAttribute所改变了,这是一个重要的结论。

它的根本原理在这里:

	public void initModel(NativeWebRequest request, ModelAndViewContainer container, HandlerMethod handlerMethod) throws Exception {
		...
		invokeModelAttributeMethods(request, container);
		...
	}

	private void invokeModelAttributeMethods(NativeWebRequest request, ModelAndViewContainer container) throws Exception {
		while (!this.modelMethods.isEmpty()) {
			...
			// 若model里已经存在此key 直接continue了
			if (container.containsAttribute(ann.name())) {
				...
				continue;
			}
			// 执行方法
			Object returnValue = modelMethod.invokeForRequest(request, container);
			// 注意:这里只判断了不为void,因此即使你的returnValue=null也是会进来的
			if (!modelMethod.isVoid()){
				...
				// 也是只有属性不存在 才会生效哦~~~~
				if (!container.containsAttribute(returnValueName)) {
					container.addAttribute(returnValueName, returnValue);
				}
			}
		}
	}

因此最终对于@ModelAttribute和@SessionAttributes共同的使用的时候务必要注意的结论:已经添加进session的数据,在没用使用SessionStatus清除过之前,@ModelAttribute标注的非功能方法的返回值并不会被再次更新进session内

如本文“对您有用”,欢迎随意打赏作者,让我们坚持创作!

0 打赏
Enamiĝu al vi
一个人若有一百只羊,一只走迷了路,你们的意思如何?他岂不撇下这九十九只,往山里去找那只迷路的羊吗?
513文章 67评论 170点赞 294602浏览

默认版本~SpringBoot2.0~2.3
  • Redis—注解接口限流
  • SpringBoot—数据库读写分离
  • Netty—初探与核心
  • Netty—NIO基础
  • Spring—WebClient使用
  • SpringCloud—LoadBalanced负载均衡
  • Spring—ApplicationEvent事件驱动机制
随便看看
ActiveMQ (4) Ajax (13) Docker (7) ElasticSearch (13) Enamiĝu al vi (1) Eureka (2) Feign (6) Freemarker (5) Gateway (6) Git (5) Hystrix (7) Java (72) Java Notes (111) JavaScript (1) jQuery (2) Kotlin Notes (47) Maven (2) More (2) MyBatis (42) MySQL (5) Netty (2) NOSQL (1) OAuth2 (11) PostgreSQL (4) RabbitMQ (6) Redis (18) Ribbon (6) Servlet (3) Spring (70) SpringBoot (85) SpringCloud (14) SpringJPA (4) SpringMVC (46) Spring Notes (43) SpringSecurity (49) SQL (15) SQL Notes (9) SQL Server (2) Thymeleaf (4) Vue (9) Web (12) Web Notes (18) WebSocket (9) XML (1) Zuul (3)
随便看看
  • 2023年2月 (4)
  • 2023年1月 (3)
  • 2022年12月 (1)
  • 2022年11月 (3)
  • 2022年10月 (5)
  • 2022年9月 (8)
  • 2022年8月 (1)
  • 2022年7月 (2)
  • 2022年6月 (4)
  • 2022年5月 (5)
  • 2022年4月 (3)
  • 2022年3月 (7)
  • 2022年2月 (4)
  • 2022年1月 (15)
  • 2021年12月 (16)
  • 2021年11月 (3)
  • 2021年10月 (3)
  • 2021年9月 (3)
  • 2021年8月 (2)
  • 2021年7月 (4)
  • 2021年6月 (16)
  • 2021年5月 (3)
  • 2021年4月 (2)
  • 2021年3月 (13)
  • 2021年2月 (2)
  • 2021年1月 (33)
  • 2020年12月 (13)
  • 2020年11月 (6)
  • 2020年10月 (17)
  • 2020年9月 (26)
  • 2020年8月 (46)
  • 2020年7月 (28)
  • 2020年6月 (4)
  • 2020年5月 (16)
  • 2020年4月 (88)
  • 2020年3月 (104)
随机文章
SpringBoot—整合Druid(阿里巴巴数据库连接池)
3年前
Redis—StringRedisTemplate和RedisTemplate
3年前
Java—DecimalFormat(数字格式化)
3年前
Java—Future与FutureTask的区别与联系
2年前
SpringMVC—HandlerMethodArgumentResolver参数处理器
6个月前
Spring Data Redis 使用(SSM版)
3年前
ENMAL摘要

1、Kotlin:

https://www.liuwj.me/

 

2、webflux:

https://www.cnblogs.com/lixinjie/p/a-brother-of-spring-mvc-is-spring-webflux.html

 

3、Java中的Unsafe

 

4、https://my.oschina.net/quanke/blog/1631990

 

5、https://blog.csdn.net/u013064109/article/details/78786646?ops_request_misc=%25257B%252522request%25255Fid%252522%25253A%252522161214257916780264022540%252522%25252C%252522scm%252522%25253A%25252220140713.130102334.pc%25255Fall.%252522%25257D&request_id=161214257916780264022540&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~first_rank_v2~rank_v29-11-78786646.first_rank_v2_pc_rank_v29&utm_term=kotlin

 

6、待学习除Spring之外的Web框架 — Cloudopt Next、Javalin、jfinal

https://jfinal.com/doc

 

7、kotlin设计模式——https://github.com/AboutKotlin/Design-Patterns-In-Kotlin

 

8、kotlin–ktorm+SpringBoot——https://gitee.com/tianchaohongyu/Spring-Boot-Ktor-Starter

 

9、新技术 — CQRS、jasync-sql、play!framework、akka、asyncdb

 

10、Kotlin Tips

https://gitee.com/lesliekoma/kotlin_tips?_from=gitee_search#tip5–%E6%87%92%E5%88%9D%E5%A7%8B%E5%8C%96by-lazy-%E5%92%8C-%E5%BB%B6%E8%BF%9F%E5%88%9D%E5%A7%8B%E5%8C%96lateinit

 

11、mall项目电商系统

https://github.com/macrozheng/mall

 

12、POI大量读写

https://www.cnblogs.com/swordfall/p/8298386.html

 

13、Gitee

权限RBAC:

https://gitee.com/log4j/pig

 

14、freecodecamp、pf4j

 

15、https://javadoop.com/

 

16、https://www.cnblogs.com/skywang12345/

 

17、Flyway

 

18、https://github.com/kotlin-orm/ktorm/pull/296

 

kt实体类自动生成表

 

https://github.com/tursom/TursomServer/tree/master/ts-database/src/main/kotlin/cn/tursom/database

 

19、蓝狐、支付沙盒、虚拟币

 

20、r2dbc spring强推,vertx这边是quarkus强推 redhat认证

 

21、Keycloak为Web应用和Restful服务提供了一站式的单点登录解决方案。

 

22、RSQL 的形式为 Restful API 带来了 SQL 声明性的便利

https://github.com/vineey/archelix-rsql

https://github.com/ymind/rsql-ktorm

 

23、Kotlin依赖注入

https://github.com/InsertKoinIO/koin

 

24、Kotlin– Alpas

https://github.com/alpas/alpas

一个基于 Kotlin 的 Web 框架,可让您简单快速地创建 Web 应用程序和 API。

 

25、外网学习网站,文章

https://medium.com/nerd-for-tech

 

26、Compose Multiplatform 进入 Alpha 版,统一桌面、Web 和 Android UI

https://blog.jetbrains.com/kotlin/2021/08/compose-multiplatform-goes- alpha/

 

27、Sureness

面向REST API的高性能认证鉴权框架,致力于管理保护API安全

https://gitee.com/dromara/sureness

与Javalin结合

https://javalin.io/2021/04/16/javalin-sureness-example.html

 

28、Kotlin官网合集库

https://kotlinlang.org/lp/server-side/

https://kotlinlang.org/lp/server-side/

https://kotlinlang.org/lp/server-side/

 

29、CLI知识体系

https://juejin.cn/post/6966119324478079007

 

30、面向 Web、移动和 Flutter 开发人员的安全开源后端服务器

https://appwrite.io/

 

31、Java锁

https://blog.csdn.net/hancoder/article/details/120421993

 

32、java—简单的鉴权认证

介绍 – Sa-Token (dev33.cn)

 

33、Effective Kotlin 中文翻译

GitHub – MaxzMeng/Effective-Kotlin-zh-CN: Effective Kotlin 中文翻译

 

34、Nutz—国产Web开源框架

http://www.nutzam.com/core/nutz_preface.html

 

35、Quarkus 夸克—国外开源框架

https://quarkus.io/

 

36、目前6个框架

  1. Spring Reactive → 背靠 Pivotal → 归属 VMware → 归属戴尔
  2. Quarkus 和 Vert.x → 背靠 Eclipse 基金会 → 主要由 Red Hat 支持
  3. Helidon → 背靠 Oracle
  4. Micronaut → 背靠 Object Computing(Grails、OpenDDS)
  5. Lagom → 背靠 Lightbend(Akka)
  6. Ktor → 背靠 JetBrains

 

37、XXL-JOB—–分布式任务调度平台

https://www.xuxueli.com/xxl-job/#%E3%80%8A%E5%88%86%E5%B8%83%E5%BC%8F%E4%BB%BB%E5%8A%A1%E8%B0%83%E5%BA%A6%E5%B9%B3%E5%8F%B0XXL-JOB%E3%80%8B

 

38、领域设计驱动模版

https://myddd.org

 

39、BFF— Backend For Frontend

 

40、面试突击小册

https://snailclimb.gitee.io/javaguide-interview/#/

https://javaguide.cn/

 

41、JeecgBoot 是一款基于代码生成器的低代码开发平台

http://doc.jeecg.com/2043868

 

42、

IdentityServer4 是用于 ASP.NET Core 的 OpenID Connect 和 OAuth 2.0 框架。

https://identityserver4docs.readthedocs.io/zh_CN/latest/index.html

 

43、cn.novelweb 工具类的个人博客

https://blog.novelweb.cn

 

44、分布式链路追踪SkyWalking

 

45、刷题模版

https://blog.csdn.net/fuxuemingzhu/article/details/101900729

 

46、TS中文文档

https://ts.xcatliu.com/

 

47、Rust 中文文档

https://kaisery.github.io/trpl-zh-cn/ch00-00-introduction.html

 

48、Bean Searcher 只读 ORM

https://searcher.ejlchina.com/guide/latest/start.html

 

49、K8S的学习手册

https://kuboard.cn/learning/k8s-basics/kubernetes-basics.html#%E5%AD%A6%E4%B9%A0%E7%9B%AE%E6%A0%87

 

50、fluent-mybatis, mybatis语法增强框架(关键自动生成代码JavaPoet)

https://gitee.com/fluent-mybatis/fluent-mybatis?_from=gitee_search

 

51、程序猿博客

https://qicoder.com/categories/

https://blog.hhui.top/hexblog/

https://fangshixiang.blog.csdn.net/category_7941357_2.html

https://www.zhihu.com/people/zhuo-zi-yang-93

 

52、itxiaoshen大佬的分享

https://www.cnblogs.com/itxiaoshen/

 

53、MySQL实战学习

https://funnylog.gitee.io/mysql45/

 

54、八股文

https://www.javalearn.cn/#/

 

55、两个宣传很牛的IO框架

https://gitee.com/smartboot/smart-socket

https://www.tiocloud.com/doc/taixin/?pageNumber=1

Copyright © 2023 网站备案号: 浙ICP备20017730号
主页
页面
  • 归档
博主
Enamiĝu al vi
Enamiĝu al vi 管理员
If you get tired, learn to rest, not to quit.
513 文章 67 评论 294602 浏览
测试
测试
赞赏作者

请通过微信、支付宝 APP 扫一扫

感谢您对作者的支持!

 支付宝 微信支付