第四篇:域对象共享数据

第四篇:域对象共享数据

官方文档

Using Scopes - The Java EE 6 Tutorial

Spring MVC Method Arguments

Spring MVC Return Values

Scope Annotation Duration
Request @RequestScoped A user’s interaction with a web application in a single HTTP request.
Session @SessionScoped A user’s interaction with a web application across multiple HTTP requests.
Application @ApplicationScoped Shared state across all users’ interactions with a web application.
Dependent @Dependent The default scope if none is specified; it means that an object exists to serve exactly one client (bean) and has the same lifecycle as that client (bean).
Conversation @ConversationScoped A user’s interaction with a JavaServer Faces application, within explicit developer-controlled boundaries that extend the scope across multiple invocations of the JavaServer Faces lifecycle. All long-running conversations are scoped to a particular HTTP servlet session and may not cross session boundaries.

我们常用的域,也就是 Request、Session、Application

所谓域共享,其实本质上是因为访问的是同一个对象,一个请求只有一个 request 域对象,在这个请求没有结束的时候,任何时候获取 request 域获取的都是同一个 request 域对象,session 域也是,在同一个 session 中,任何时刻获取 session 域,获取的都是同一个 session 域对象。

总结

往域中添加数据除了是为了在一定的域中进行数据共享,另一大目的时为了视图中能获取到需要的信息,然后进行视图的渲染。

通过 request 域渲染视图

request 域的生命周期为一次请求,请求结束,request 域消失

通过使用 ServletAPI - HttpServletRequest 类型的控制器参数

控制器方法

@Controller
@RequestMapping("/RequestScopeAttribute")
public class RequestScopeAttributeController {

    @RequestMapping("/servletAPI")
    public String servletAPI(HttpServletRequest request) {
        request.setAttribute("userName","xiashuo");
        return "target";
    }

}

target 视图

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>target</title>
</head>
<body>
<p th:text="${userName}"></p>
</body>
</html>

首页引导测试链接

<a th:href="@{/RequestScopeAttribute/servletAPI}">测试ServletAPI向request域添加数据</a>

通过 Model 渲染视图

在使用 Model 之前先简单介绍一下 Model

Model 存放键值对,跟 View 进行绑定,统一为 ModelAndView 对象,传递给各种 ViewResolver,进行视图解析。

在同一个控制器方法的处理链中,model 是可以传递的,但是跨控制器方法就不行,因为每一个控制器方法,最终都会返回一个全新的 ModelAndView 实例

比如控制器方法执行前执行@ModelAttribute 修饰的方法,就没有跨控制器方法,但是重定向,转发,异常处理,就跨了控制器方法。

重定向、转发最终都会再次通过 DispatcherServletdoDispatch 方法处理目的请求,完全重新创建新的 ModelAndeView 对象,所以 Model 无法传递,

异常处理使用的 HandlerExceptionResolver 接口本身就会返回一个新的 ModelAndeView 对象,用于进行异常信息的展示,这个 ModelAndeView 对象也是完全重新构建的,所以 Model 无法传递。

当我们需要将 Model 进行跨请求共享的时候,可以使用@SessionAttributes 注解

如果 Model 也算一个域的话,那么它的生命周期比 request 域还要小:就只有一个控制器方法处理链。

model域 < request域 < session域

Model 和 Request 域的关系 - 很重要

在视频中或者别的博客中,经常直接将 Model 域等同于 request 域,也就是请求域,但是实际上,在控制器方法中,Model 和 @RequestAttribute 获取的值明显是不相关的,这是为什么?如何解释?

其实 Model 域并不是直接跟请求域相等的,而是是进行最终进行视图解析的时候,会将其合并到请求域中,以 ThymeleafView 视图的解析为例,看 ThymeleafView 的 render 方法,实际调用的是 renderFragment 方法,在 viewTemplateEngine.process(templateName, processMarkupSelectors, context, (Writer)templateWriter); 之后,Model 中的所有属性最终都会进入 Request 域,这样在页面中就可以通过访问 request 域拿到 Model 中的值,看起来就像 Model 域就是请求域一样,从视图渲染的角度看,Model 域等同于 Request 域的一个子域,但是大部分的时候,Model 域和 Request 域都是两个不相干的东西。

具体请看《SpringMVC- 第五篇:视图.md》的 ThymeleafViewResolver源码 小节

Model 和 Request 域的异同点

相同:最终都可以在渲染视图的时候,在请求域中访问到

不同:在多个控制器方法中,request 域是同一个,是可以共享的,但是 Model 域不是,每进入一个新的控制器方法,就会有新的 Model,因此在重定向,转发的,使用@ExceptionHandler 处理控制器方法异常的时候,Model 都是不可传递的。因为每一个控制器方法都会返回一个新的 ModelAndView 对象。因此,当我们需要将 Model 进行跨请求共享的时候,可以使用@SessionAttributes 注解。

通过使用 Model - Model 类型的控制器参数

控制器方法

@RequestMapping("/Model")
public String Model(Model model) {
    model.addAttribute("userName","xiashuo");
    return "target";
}

target 视图沿用前面的

添加首页引导测试链接

<a th:href="@{/RequestScopeAttribute/Model}">测试Model向视图添加数据</a>

通过使用 ModelMap - ModelMap 类型的控制器参数

控制器

@RequestMapping("/ModelMap")
public String ModelMap(ModelMap modelMap) {
    modelMap.addAttribute("userName","xiashuo");
    return "target";
}

target 视图沿用前面的

添加首页引导测试链接

<a th:href="@{/RequestScopeAttribute/ModelMap}">测试ModelMap向视图添加数据</a>

通过使用 Map - Map 类型的控制器参数

控制器

@RequestMapping("/Map")
public String Map(Map<String,Object> map) {
    map.put("userName","xiashuo");
    return "target";
}

target 视图沿用前面的

添加首页引导测试链接

<a th:href="@{/RequestScopeAttribute/Map}">测试Map向视图添加数据</a>

Model、ModelMap、Map 的关系和区别

Spring MVC Method Arguments 中介绍过 Map\Model\ModelMap 类型的控制器参数,它们的作用是一样的。

通过在以上的三个方法控制器方法中打印 Map\Model\ModelMap 类型参数的实际类型

@RequestMapping("/Model")
public String Model(Model model) {
    model.addAttribute("userName","xiashuo");
    System.out.println(model.getClass().getCanonicalName());
    return "target";
}
@RequestMapping("/ModelMap")
public String ModelMap(ModelMap modelMap) {
    modelMap.addAttribute("userName","xiashuo");
    System.out.println(modelMap.getClass().getCanonicalName());
    return "target";
}

@RequestMapping("/Map")
public String Map(Map<String,Object> map) {
    map.put("userName","xiashuo");
    System.out.println(map.getClass().getCanonicalName());
    return "target";
}

访问的时候,输出

org.springframework.validation.support.BindingAwareModelMap
org.springframework.validation.support.BindingAwareModelMap
org.springframework.validation.support.BindingAwareModelMap

可知实际上 Model\ModelMap\Map 类型的参数其实本质上都是org.springframework.validation.support.BindingAwareModelMap类型的。

简单看一下BindingAwareModelMap的类继承图:

通过使用 ModelAndView 对象 - 推荐使用

SpringMVC 建议我们使用。

控制器方法需要返回这个类型的对象,这个在 Spring MVC Return Values 中也提到过。

控制器方法

@RequestMapping("/ModelAndView")
public ModelAndView ModelAndView() {
    // 这个对象可以方便的将视图和需要在视图中渲染的数据放到一起进行管理,这是个设计的非常合理的类
    ModelAndView modelAndView = new ModelAndView();
    modelAndView.addObject("userName","xiashuo");
    modelAndView.setViewName("target");
    return modelAndView;
}

target 视图沿用前面的

添加首页引导测试链接

<a th:href="@{/RequestScopeAttribute/ModelAndView}">测试ModelAndView向视图添加数据</a>

实际上,前面提到的几种通过添加到 Model 在视图中进行渲染的方式底层都是调用的 ModelAndView。为什么,我们简单看一下源码。

简要分析源码

进入任意的一个控制器方法(controller)的时候,其调用栈中一定会有 doDispatch 方法(DispatcherServlet 类)

进入 doDispatch 方法,可以看到实际调用控制器方法的语句是 ha.handle(processedRequest, response, mappedHandler.getHandler()); 其返回值是 ModelAndView 类型的,这里的意思是,调用任意的一个控制器方法(controller),其返回值,都会被封装成 ModelAndView 类型

@ModelAttribute 注解的使用

@ModelAttribute 注解有三种使用场景

@ModelAttribute 方法也可以通过声明在@ControllerAdvice 中实现在多个控制器之间共享。有关更多细节,《SpringMVC- 第八篇:跨控制器间共享》。

作为@Controller 或@ControllerAdvice 类中的方法级注释

一个控制器可以有任意数量的 @ModelAttribute 修饰的方法。所有这些方法都会在同一个控制器中的 @RequestMapping 方法被调用之前调用,这让我们可以在 @ModelAttribute 修饰的方法中执行一些需要在执行控制器方法前执行的前置业务逻辑,非常好用。多个 @ModelAttribute 修饰的方法之间的顺序是按声明顺序排序,排在前面的先调用,@ModelAttribute 方法具有灵活的参数列表。它们支持许多与@RequestMapping 方法相同的参数,除了@ModelAttribute 本身或任何与请求体相关的参数比如@RequestBody,像 @RequestParam、 @RequestAttribute、@SessionAttribute 等注解修饰的参数都是可以生效的。

@ModelAttribute 修饰的方法向 Model 中注入属性有两种写法,一种是直接通过方法返回值,一种是方法返回值为 void,通过在参数中注入 Model mode,手动注入。

当@ModelAttribute 作为@Controller 或@ControllerAdvice 类中的方法级注释的时候,我们一般在此方法中查询数据库,然后将此值放入数据库中,这就是前面所说的控制器方法的前置业务逻辑,如果有需要,我们还可以通过@SessionAttributes 注解让这个从数据库查出来的值在请求中间传递。比如当前用户信息。

控制器代码

@Controller
@RequestMapping("/ModelAttributeMethod")
public class TestModelAttributeMethodController {

    // 按照声明顺序依次执行,此方法先执行
    // 在@RequestMapping自动解析的参数,这里也都可以使用,
    // 比如直接匹配URL中的请求参数,当然也可以使用 @RequestParam、 @RequestAttribute、@SessionAttribute等注解
    // 可以一次写入一个Model属性
    @ModelAttribute(name = "att0")
    public String initModelAttribute0(String name, Integer age) {
        System.out.println("执行业务逻辑1");
        return "value0";
    }

    // 按照声明顺序依次执行,此方法后执行
    // 可以一次写入多个Model属性
    @ModelAttribute
    public void initModelAttribute1(String name, Integer age,Model model) {
        System.out.println("执行业务逻辑2");
        model.addAttribute("att1", "value1");
    }

    @RequestMapping(value = "/getModelAttribute")
    public String testModel(Model model) {
        System.out.println(model);
        return "success";

    }


}

测试类代码

@SpringJUnitWebConfig(locations = "classpath:springMVC.xml")
class TestModelAttributeMethodControllerTest {

    MockMvc mockMvc;

    @BeforeEach
    void setup(WebApplicationContext wac) {
        this.mockMvc = MockMvcBuilders.webAppContextSetup(wac).build();
    }

    @Test
    void testModel() throws Exception {
        mockMvc.perform(get("/ModelAttributeMethod/getModelAttribute")).andDo(print());
    }
}

日志

执行业务逻辑1
执行业务逻辑2
{att0=value0, att1=value1}

最后的响应

MockHttpServletRequest:
      HTTP Method = GET
      Request URI = /ModelAttributeMethod/getModelAttribute
       Parameters = {}
          Headers = []
             Body = <no character encoding set>
    Session Attrs = {}

Handler:
             Type = xyz.xiashuo.springmvchttpparams.controller.TestModelAttributeMethodController
           Method = xyz.xiashuo.springmvchttpparams.controller.TestModelAttributeMethodController#testModel(Model)

Async:
    Async started = false
     Async result = null

Resolved Exception:
             Type = null

ModelAndView:
        View name = success
             View = null
        Attribute = att0
            value = value0
        Attribute = att1
            value = value1

FlashMap:
       Attributes = null

MockHttpServletResponse:
           Status = 200
    Error message = null
          Headers = [Content-Language:"en", Content-Type:"text/html;charset=UTF-8"]
     Content type = text/html;charset=UTF-8
             Body = <!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1>file upload success</h1>
</body>
</html>
    Forwarded URL = null
   Redirected URL = null
          Cookies = []

在@RequestMapping 修饰的方法中标记其返回值的是一个 Model 属性。

注意,此时返回值结果将不会被解析成视图名称,而是作为 Model 的属性,这个时候此请求实际对应的视图,将需要通过默认视图解析来得出,默认就是请求的 URL 去掉应用上下文路径和后缀后剩下的路径,代码就是 DispatcherServlet 中的 doDispatch 中的 applyDefaultViewName(processedRequest, mv);,具体请看《SpringMVC- 第五篇:视图》中的 默认视图解析

控制器方法

@Controller
@RequestMapping("/ModelAttributeMethod")
public class TestModelAttributeWithRequestMappingMethodController {


    @RequestMapping(value = "/returnValue")
    @ModelAttribute(name = "returnValue")
    public String getReturnValueAsModelAttribute() {
        // 实际的视图名称需要通过默认视图解析来得出,默认就是请求的URL去掉应用上下文路径和后缀后剩下的路径
        return "success";
    }

}

测试类

@SpringJUnitWebConfig(locations = "classpath:springMVC.xml")
class TestModelAttributeWithRequestMappingMethodControllerTest {
    MockMvc mockMvc;

    @BeforeEach
    void setup(WebApplicationContext wac) {
        this.mockMvc = MockMvcBuilders.webAppContextSetup(wac).build();
    }

    @Test
    void getReturnValueAsModelAttribute() throws Exception {
        mockMvc.perform(get("/ModelAttributeMethod/returnValue")).andDo(print());
    }
}

请求的默认路径是 /ModelAttributeMethod/returnValueThymeleafViewResolver 配置的视图解析前缀,是 /WEB-INF/templates/,也就是说 WEB-INF 下的 templates 文件夹下要有 ModelAttributeMethod 路径,同时下面还得有一个 returnValue.html。不然就会请求报错

Caused by: java.io.FileNotFoundException: Could not open ServletContext resource [/WEB-INF/templates/ModelAttributeMethod/returnValue.html]

returnValue.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1 th:text="${returnValue}"></h1>
</body>
</html>

建好文件夹和文件之后,可以看到响应中,ModelAndView 中有我们返回的 success,同时这个 Model 属性在 html 中正常地解析了。

MockHttpServletRequest:
      HTTP Method = GET
      Request URI = /ModelAttributeMethod/returnValue
       Parameters = {}
          Headers = []
             Body = <no character encoding set>
    Session Attrs = {}

Handler:
             Type = xyz.xiashuo.springmvchttpparams.controller.TestModelAttributeWithRequestMappingMethodController
           Method = xyz.xiashuo.springmvchttpparams.controller.TestModelAttributeWithRequestMappingMethodController#getReturnValueAsModelAttribute()

Async:
    Async started = false
     Async result = null

Resolved Exception:
             Type = null

ModelAndView:
        View name = ModelAttributeMethod/returnValue
             View = null
        Attribute = returnValue
            value = success

FlashMap:
       Attributes = null

MockHttpServletResponse:
           Status = 200
    Error message = null
          Headers = [Content-Language:"en", Content-Type:"text/html;charset=UTF-8"]
     Content type = text/html;charset=UTF-8
             Body = <!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1>success</h1>
</body>
</html>
    Forwarded URL = null
   Redirected URL = null
          Cookies = []

在@RequestMapping 方法中的方法参数上使用 - 重头戏

具体分两种情况,简单明了,后面我们再写代码测试。

简单总结:

在控制器方法的参数上使用 @ModelAttribute 的效果可以简单的理解为控制器方法参数本身就有的功能(数据绑定) + 强制实例化并放入 Model 域中。

这些功能实际上对应着参数解析器 ServletModelAttributeMethodProcessor,具体的分析请看《SpringMVC-RequestMappingHandlerAdapter 源码解析.md》的 ServletModelAttributeMethodProcessor 小节。


将请求中的参数填充到控制器方法的参数中就是数据绑定,注意,这不是 @ModelAttribute 的特性,而是控制器方法参数自带的特性。@ModelAttribute 也可以设置成 @ModelAttribute(binding = false) 表示仅仅从 Model 中取得属性,而忽略请求中的参数。

数据绑定的过程中,会有错误,比如请求参数名跟目标类型的属性名相同,但是无法转化,此时就会报错,你可以在控制器方法参数中添加 BindingResult result 获取匹配的错误,注意,BindingResult 类型的参数要直接跟在它要检测的字段的后面,比如 public String beanParameter(@ModelAttribute("userObj") User user, BindingResult result, Model model) 。如果不添加,Model 中会自动添加参数的绑定结果 BindingResult,一个参数一个,非常和谐。

但是有的时候,参数匹配的错误会导致无法进入控制器,所以这个时候需要配合 @ExceptionHandler 进行异常处理。

在某些情况下,您可能希望访问 Model 的属性而不希望根据请求参数对此属性进行更新(即不进行数据绑定)。此时您可以将 Model 注入到控制器方法的参数列表中并直接访问它,或者使用 @ModelAttribute(binding=false) 类型的参数。

测试简单参数

控制器方法

@Controller
@RequestMapping("/ModelAttributeParameter")
public class TestModelAttributeParameterController {

    @RequestMapping("/plainParameter")
    public String plainParameter(@ModelAttribute("name") String name, @ModelAttribute("age") Integer age, Model model) {
        System.out.println(name + "--" + age);
        // model中会自动添加数据绑定的结果BindingResult到Model域中,一个参数一个结果对象,还蛮方便的
        System.out.println(model);
        return "success";
    }

}

测试类

@SpringJUnitWebConfig(locations = "classpath:springMVC.xml")
class TestModelAttributeParameterControllerTest {
    MockMvc mockMvc;

    @BeforeEach
    void setup(WebApplicationContext wac) {
        this.mockMvc = MockMvcBuilders.webAppContextSetup(wac).build();
    }

    @Test
    void plainParameter() throws Exception {
        mockMvc.perform(get("/ModelAttributeParameter/plainParameter").queryParam("name", "xiashuo").queryParam("age", "12")).andDo(print());
    }

    @Test
    void plainParameterNullValue() throws Exception {
        mockMvc.perform(get("/ModelAttributeParameter/plainParameter")).andDo(print());
    }

    @Test
    void plainParameterError() throws Exception {
        mockMvc.perform(get("/ModelAttributeParameter/plainParameter").queryParam("name", "xiashuo").queryParam("age", "aaa")).andDo(print());
    }

}

执行 plainParameter 方法,可以观察到 model 中除了由于配置了 @ModelAttribute 进入 Model 中的两个属性,还有这两个参数对应的 BindingResult 也进入 Model

运行 plainParameterNullValue 会报错,因为没有传递 age 参数,但是为什么 plainParameter 方法不报错呢,因为 Integer 方法只有带参数的构造函数,没有不带参数的构造函数,所以在不传跟对应的 @ModelAttribute 的 name 属性同名的参数的时候,无法构造出 Integer 实例。

No primary or single public constructor found for class java.lang.Integer - and no default constructor found either

执行 plainParameterError 方法,因为 aaa 无法转化为 Integer 类型而爆出错误 TypeMismatchException,这个错误导致直接无法进入控制器方法,于是我们新建 @ExceptionHandler 方法用于处理异常

查看《SpringMVC- 第七篇:控制器方法异常处理》了解控制器方法异常处理

@ExceptionHandler(value = TypeMismatchException.class)
public void exceptionHandler(TypeMismatchException exception, Model model) {
    model.addAttribute("exception", exception);
}

同时注意,因为 @ExceptionHandler 返回 void,所以会采用默认视图解析策略,最终视图为 /ModelAttributeParameter/plainParameter,跟 在@RequestMapping修饰的方法中标记其返回值的是一个Model属性。 小节同样的环境下,我们在 WEB-INF 下的 templates 文件夹下要有 ModelAttributeParameter 路径,同时下面还得有一个 plainParameter.html。

查看《SpringMVC- 第五篇:视图》了解默认视图解析策略

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>异常</title>
</head>
<body>
<h1 th:text="${exception}"></h1>
</body>
</html>

运行日志,异常被@ExceptionHandler 方法正常处理,plainParameter.html 也正常的渲染了 exception 属性。

MockHttpServletRequest:
      HTTP Method = GET
      Request URI = /ModelAttributeParameter/plainParameter
       Parameters = {name=[xiashuo], age=[aaa]}
          Headers = []
             Body = <no character encoding set>
    Session Attrs = {}

Handler:
             Type = xyz.xiashuo.springmvchttpparams.controller.TestModelAttributeParameterController
           Method = xyz.xiashuo.springmvchttpparams.controller.TestModelAttributeParameterController#plainParameter(String, Integer, Model)

Async:
    Async started = false
     Async result = null

Resolved Exception:
             Type = org.springframework.beans.TypeMismatchException

ModelAndView:
        View name = ModelAttributeParameter/plainParameter
             View = null
        Attribute = exception
            value = org.springframework.beans.TypeMismatchException: Failed to convert value of type 'java.lang.String' to required type 'java.lang.Integer'; nested exception is java.lang.NumberFormatException: For input string: "aaa"

FlashMap:
       Attributes = null

MockHttpServletResponse:
           Status = 200
    Error message = null
          Headers = [Content-Language:"en", Content-Type:"text/html;charset=UTF-8"]
     Content type = text/html;charset=UTF-8
             Body = <!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>异常</title>
</head>
<body>
<h1>org.springframework.beans.TypeMismatchException: Failed to convert value of type &#39;java.lang.String&#39; to required type &#39;java.lang.Integer&#39;; nested exception is java.lang.NumberFormatException: For input string: "aaa"</h1>
</body>
</html>
    Forwarded URL = null
   Redirected URL = null
          Cookies = []
测试 JavaBean 类型的参数

继续添加控制器方法

@RequestMapping("/beanParameter")
public String beanParameter(@ModelAttribute(name = "userObj") User user, Model model) {
    // 自动通过公共无参构造创建User实例,然后将同名参数注入到字段中,是不使用@ModelAttribute注解也有的功能
    // 其实 在控制器方法的参数上使用 @ModelAttribute 的效果可以简单的理解为控制器方法参数本身就有的功能(数据绑定) + 强制实例化并放入Model域中
    // 当然,这里还可以配合BeanValidation一起使用,这里就不实验了,具体请查看 《SpringMVC整合JavaBeanValidation及拓展》
    return "success";
}

测试方法

@Test
void beanParameterNullBean() throws Exception {
    mockMvc.perform(get("/ModelAttributeParameter/beanParameter")
    ).andDo(print());
}

@Test
void beanParameter() throws Exception {
    mockMvc.perform(get("/ModelAttributeParameter/beanParameter")
            .queryParam("username", "xiashuo")
            .queryParam("gender", "male")
            .queryParam("age", "12")).andDo(print());
}

执行 beanParameterNullBean 可以看到,会默认创建一个空的 User 对象,并且 Model 中会自动添加此属性 + 此属性对应绑定结果 BindingResult 属性

运行 beanParameter 方法,请求中的查询参数会填充到新建的空的 User 对象中,然后此饱满的对象被添加到 Model 中,当然 Model 中还有 user 对象对应的 BindingResult

如果 Model 中原来就有 userObj 会怎么样?在控制器中添加

@ModelAttribute
public void initUserModel(Model model) {
    // 如果 Model已经有了此属性,则直接用,而不是初始化新的对象,然后根据请求中对应的参数更新对象的属性。
    model.addAttribute("userObj", new User("aaa","female",15));
}

执行 beanParameterNullBean 可以看到,user 为 initUserModel 中添加的 user 对象

运行 beanParameter 方法,user 为根据请求参数创建的 user 对象

添加测试方法

@Test
void beanParameterPart() throws Exception {
    mockMvc.perform(get("/ModelAttributeParameter/beanParameter")
            .queryParam("age", "12")).andDo(print());
}

执行可以看到,user 为 initUserModel 中添加的 user 对象,但是 age 属性被更新为请求中的请求参数的值,因此我们可以得出结论:如果 Model 已经有了此属性,则直接用,而不是初始化新的对象。然后根据请求中对应的参数更新对象的属性。

测试不使用数据绑定

添加控制器方法

@RequestMapping("/beanParameterNotBinding")
public String beanParameterNotBinding(@ModelAttribute(name = "userObj", binding = false) User user, Model model) {
    //获得的仅仅是initUserModel方法中填入的userObj属性,请求中的匹配的参数名不会更新userObj的值
    return "success";
}

添加测试方法

@Test
void beanParameterNotBinding() throws Exception {
    mockMvc.perform(get("/ModelAttributeParameter/beanParameterNotBinding")
            .queryParam("username", "xiashuo")
            .queryParam("gender", "male")
            .queryParam("age", "12")).andDo(print());
}

执行之后,可以看到,user 为 initUserModel 中添加的 user 对象,跟请求参数一点关系都没有。请求中的匹配的参数名不会更新 userObj 的值。

注意,假设此时没有 initUserModel 方法已经事先已经在 Model 中添加的 user 对象,配置了 binding = false@ModelAttribute 依然会调用 User 默认的构造函数初始化一个 user 对象,然后添加到 Model 域中,逻辑是统一的。

测试省略@ModelAttribute 注解

我们发现,省略掉 @ModelAttribute 注解,上面的功能依然正常,而此时,跟《SpringMVC- 第三篇:在控制器方法中获取请求参数和构建响应.md》中 直接用POJO(JavaBean)来接受参数 小节描述的情况一样了。

本质是因为这两种情况的本质都是参数解析器 ServletModelAttributeMethodProcessor 在发挥作用。

具体的分析请看《SpringMVC-RequestMappingHandlerAdapter 源码解析.md》的 ServletModelAttributeMethodProcessor 小节。

BeanValidation

当然,这里还可以配合 BeanValidation 一起使用,这里就不实验了,具体请查看 《SpringMVC 整合 JavaBeanValidation 及拓展》

简单总结

自动通过公共无参构造创建 User 实例,然后将同名参数注入到字段中,是不使用@ModelAttribute 注解也有的功能。在控制器方法的参数上使用 @ModelAttribute 的效果可以简单的理解为控制器方法参数本身就有的功能(数据绑定) + 强制实例化并放入 Model 域中

其实,话说回来,有的时候,不推荐直接使用 @ModelAttribute 将请求中的参数绑定到 Model 中,这样不够显式,我们一般都是通过参数绑定的方式获取了转换之后的参数之后,再手动添加到 model 中使用

通过 session 域渲染视图

session 域的生命周期为一次会话,会话关闭,session 域消失。

通过使用 HttpSession(ServletAPI)

控制器

@Controller
@RequestMapping("/RequestScopeAttribute")
public class SessionScopeAttributeController {

    @RequestMapping("/HttpSession")
    public String HttpSession(HttpSession session) {
        session.setAttribute("userName", "xiashuo");
        return "target-session";
    }

}

访问链接

<a th:href="@{/RequestScopeAttribute/HttpSession}">测试HttpSession向session域添加数据</a>

视图,在 Thymeleaf 中,访问 session 域要用 ${session.XXX}

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>target</title>
</head>
<body>
<p th:text="${session.userName}"></p>
</body>
</html>

@SessionAttributes 控制器类注解 + 控制器方法参数 SessionStatus

与之类似的@SessionAttribute 主要用于控制器方法参数,具体使用看 《SpringMVC- 第三篇:在控制器方法中获取请求参数和构建响应》

@SessionAttributes 是类级注解,用于在访问 @SessionAttributes 修饰的控制器中的方法时,自动将 Model 中的指定的名称或者类型的属性同步到 Session 域中,同时在调用此控制器内的其他控制器方法时,自动将同步到 session 中的 model 属性添加到 Model 中,实现以 Session 域为中介的跨控制器 Model 属性共享,最终,可以在此控制器的任意控制器方法中调用 status.setComplete(); 清除在此控制器中从 Model 同步到 Session 的特定属性,注意,只清除此控制器同步的,别的 @SessionAttributes 修饰的控制器同步的属性不会受影响。

详细流程如下:

访问此控制器下的控制器方法,在 Model 添加上特定的属性,自动同步到 session 中,一旦添加到 session 域中,只要 session 没过期,在任何地方(任意控制器的任意控制器方法)都可以访问。

访问此控制器下的其他控制器方法,看看 session 域中有没有从 Model 域同步过来的属性,有的话,自动添加到 Model 参数中,或者同步到被@ModelAttribute 注解修饰的参数中。

如果想要清除同步的属性,则访问此控制器的某个控制器方法,通过 SessionStatus status 参数,调用 status.setComplete(); 清除 session 中同步的特定属性。注意,必须是在此控制器内的控制器方法调用 status.setComplete(); 才有效,在别的控制器中调用无效。

即,从 Model 同步特定属性到 Session 再从 Session 中同步属性回 Model 的同步操作和清除操作,必须在同一个 @SessionAttributes 注解修饰的控制器内部才可以。

而且调用 status.setComplete(); 的时候,不是从 Model 中同步过来的 session 属性不会受影响,别的 @SessionAttributes 注解修饰的控制器同步的特定属性也不会受影响。

即:多个控制器同时使用 @SessionAttributes 的时候,其对同步的属性的管理都是分离的,删除的时候也只会删除当前控制器中同步的特定属性。

删除从 Model 同步到 Session 中的属性之后,把上面的操作再来一遍,特定属性会再次同步到 Session 中

这个注解,打通了 Model 和 Session 域,实现了 Model 的同一个控制器内跨控制器方法的共享,不同控制器,也可以通过 Session 域手动共享到 Model 中,非常的牛逼和方便,比如用来同步当前用户的 User 信息

实践

控制器方法

同步 userInfo 的控制器

@Controller
@RequestMapping("/SessionAttributesMain")
@SessionAttributes("userInfo")
public class testSessionAttributesMainController {

    @ModelAttribute
    public void initUserModel(Model model) {
        User user = new User("xiashuo", "male", 18);
        model.addAttribute("userInfo", user);
    }

    @RequestMapping("/addModel")
    public String addModel(Model model, HttpSession session) {
        session.setAttribute("defaultInfo", "111111111111");
        return "sessionAttr";
    }

    @RequestMapping("/clearSession")
    public String clearModel(SessionStatus status, Model model, HttpSession session) {
        status.setComplete();
        Object userInfo = session.getAttribute("userInfo");
        return "sessionAttr";
    }

}

同步 orderInfo 的控制器

@Controller
@RequestMapping("/SessionAttributesMain2")
@SessionAttributes("orderInfo")
public class testSessionAttributesMain2Controller {

    @RequestMapping("/addModel")
    public String addModel(Model model, HttpSession session) {
        model.addAttribute("orderInfo", "-------------orderList-------------");
        session.setAttribute("defaultInfo", "111111111111");
        return "sessionAttr";
    }

    @RequestMapping("/testModel")
    public String testModel(Model model, HttpSession session) {
        return "sessionAttr";
    }

    @RequestMapping("/clearSession")
    public String clearModel(SessionStatus status) {
        status.setComplete();
        return "sessionAttr";
    }

}

测试 session 域属性

@Controller
@RequestMapping("/SessionAttributesTest")
public class testSessionAttributesTestController {

    @RequestMapping(value = "/testSession")
    public String testRequest(Model model){
        return "sessionAttr";
    }

    @RequestMapping("/clearSession")
    public String clearModel(SessionStatus status) {
        status.setComplete();
        return "sessionAttr";
    }

}

Session 域和 Model 域(合并到了 request 域中)属性展示页面,sessionAttr.html,

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1>Session</h1>
<h1 th:text="${session.userInfo}"></h1>
<h1 th:text="${session.orderInfo}"></h1>
<h1 th:text="${session.defaultInfo}"></h1>
<hr/>
<h1>Model(合并到了Request域中)</h1>
<h1 th:text="${userInfo}"></h1>
<h1 th:text="${orderInfo}"></h1>
<h1 th:text="${defaultInfo}"></h1>
</body>
</html>

测试链接,这次用 Spring-test 就不没法测试同一个 session 的效果了,只能启动 Tomcat 来测试

<hr/>
<a th:href="@{/SessionAttributesMain/addModel}">1. 测试@SessionAttributes自动初始化session属性-userInfo</a><br/>
<a th:href="@{/SessionAttributesMain2/addModel}">2. 测试@SessionAttributes自动初始化session属性-defaultInfo</a><br/>
<a th:href="@{/SessionAttributesMain2/testModel}">3. 测试@SessionAttributes自动初始化model属性</a><br/>
<a th:href="@{/SessionAttributesTest/testSession}">4. 测试@SessionAttributes测试</a><br/>
<a th:href="@{/SessionAttributesMain/clearSession}">5. 测试@SessionAttributes清理session属性-userInfo</a><br/>
<a th:href="@{/SessionAttributesMain2/clearSession}">6. 测试@SessionAttributes清理session属性-defaultInfo</a><br/>
<a th:href="@{/SessionAttributesTest/clearSession}">7. 测试@SessionAttributes清理session属性-无效</a><br/>

依次点击链接,即可验证前面的总结,很简单,这里就不分析了。

application 域数据共享

application 域的声明周期为 web 应用开启到结束,只要应用未结束,application 域就一直存在。

application 域实际上就是 ServletContext。可以通过 HttpServletRequest.getServletContext() 获取,也可以通过 HttpSession.getServletContext() 获取

通过使用 ServletContext(HttpSession.getServletContext())

控制器

@Controller
@RequestMapping("/ApplicationScopeAttribute")
public class ApplicationScopeAttributeController {

    @RequestMapping("/ServletContext")
    public String HttpSession(HttpSession session) {
        ServletContext application = session.getServletContext();
        application.setAttribute("userName", "xiashuo");
        return "target-application";
    }

}

前端访问链接

<a th:href="@{/ApplicationScopeAttribute/ServletContext}">测试ServletContext向application域添加数据</a>

视图

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>target</title>
</head>
<body>
<p th:text="${application.userName}"></p>
</body>
</html>

注意

当响应的消息体的媒体类型为 JSON 类型的时候,是没有 ModelAndView,因为只有返回视图进行视图解析的时候,才会有 ModelAndView