阅读完需:约 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运行在服务器端
- 把js直接写在了JSP页面,所以能获取到。如果单独把JS抽离出来是不可能获取到的。前者服务端解析能或许到,后者客户端解析获取不到。
- js里是在客户端执行的,不能直接获取,因为EL表达式是在服务器的时候被解析的,所以可以获取到。一个是客户端的东西,一个是服务器端的东西。
- jsp能取到,那是因为他本身就是servlet。js不可能有方法直接获取到HttpServletRequest里面的属性值。
- setAttribute是服务器行为,到了客户端就无效了,也谈不上如何用。除非你在jsp的时候就写到js变量里,或者使用ajax请求你需要的数据。
ModelFactory
ModelFactory
是用来维护Model的,具体包含两个功能
- 初始化Model
- 处理器执行后将
Mode
l中相应的参数更新到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类方式去使用它:
- 在视图view中(比如jsp页面等)通过
request.getAttribute()
或session.getAttribute
获取 - 在后面请求返回的视图view中通过
session.getAttribute
或者从model
中获取(这个也比较常用) - 自动将参数设置到后面请求所对应处理器的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
中需要同时满足两个条件:
- 在
@SessionAttribute
注解中设置了参数的名字或者类型 - 在处理器(
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级的类
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
的作用
- 绑定请求参数到命令对象(入参对象):放在控制器方法的入参上时,用于将多个请求参数绑定到一个命令对象,从而简化绑定流程,而且自动暴露为模型数据用于视图页面展示时使用;
- 暴露表单引用对象为模型数据:放在处理器的一般方法(非功能处理方法,也就是没有@RequestMapping标注的方法)上时,是为表单准备要展示的表单引用数据对象:如注册时需要选择的所在城市等静态信息。它在执行功能处理方法(@RequestMapping 注解的方法)之前,自动添加到模型对象中,用于视图页面展示时使用;
- 暴露@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
-
@RequestAttribute
在Spring4.3
后才有 - 我们可以使用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里面取属性值,至于你什么时候往里放值,是有多种方式的可以达到的:
-
@ModelAttribute
注解预存 -
HandlerInterceptor
拦截器中预存 - 请求转发带过来
下面分别按照这三种使用场景,给出使用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
内