Skip to content

SpringMVC教程 - 8 域对象

下面先来介绍一下 Web 项目中的一些基本概念,并介绍在 SpringMVC 中的使用。


8.1 域对象

什么是域对象?

在 Web 开发中,域对象(Scope Object)是服务器端用于在不同组件之间传递与共享数据的容器。它解决的问题是:当我们在一个 Servlet、Controller 或页面中产生了数据,如何在后续的页面或请求中继续使用这些数据。

8.1.1 域对象简介

Java Web 中最常用的域对象主要是以下三种:

1 Request请求

对应的类是 HttpServletRequest ,在前端或页面中,每次发送一次请求就是新的 Request 对象。

一般前端页面向 Controller 传递数据,Controller 处理后再将数据转发到页面模板。这两步操作都处于同一个请求过程(即同一个 Request 对象)。因此,在页面中可以直接访问由 Controller 设置在 Request 域中的属性。


2 Session会话

对应的类是 HttpSession ,Session 的整个执行流程如下:

  1. 当用户访问服务器时,当服务器端的代码**第一次调用 **request.getSession() 时,Web容器会创建新的Session对象,并生成Session ID,通过 Set-Cookie 发给客户端浏览器;
  2. 浏览器在收到服务器响应头里的 Set-Cookie: JSESSIONID=xxx 时,会自动保存到本地 Cookie 存储;
  3. 之后访问同域名时,浏览器会自动把这个 Cookie 附加在请求头里;
  4. 服务器的Web容器会自动解析请求头的 Cookie,找到 JSESSIONID,并去 Session 管理器里查找对应的 Session 对象,判断是否是同一个 Session。

这个操作流程是不需要我们介入的,是浏览器和 Web 容器(例如Tomcat)自己完成的,我们只需要关心如何向 Session 对象中存入信息,并验证其中的信息就可以了。

Session 是有过期时间的,默认 30 分钟(可在 web.xml 或容器中修改)。客户端每次发送携带 JSESSIONID 的请求时,服务器都会刷新 Session 的有效期。因此,从 Session 创建到浏览器关闭之间,只要 Session 未过期,就属于同一个会话。如果 Session 已过期,再次调用 request.getSession() 时会重新创建新的 Session。

在传统项目中(非前后端分离),用户请求服务器登录,在服务器端我们可以将用户登录信息保存在 Session 会话中,然后在后面每次请求,我们可以验证会话中有没有登录信息,如果有登录信息,那么就可以成功访问后续资源;如果没有,就跳转到登录页面。

但是这也会存在问题,就是一般服务器压力过大,我们会使用负载均衡,将请求分发到多台服务器上,但是 Session 是没有办法跨服务器的,所以一次请求到达服务器A,将登录信息保存在服务器A 的 Session 对象中,后面请求被分发到服务器B,那么服务器B上没有登录信息,就会认为没有登录。针对这种情况,一般使用 Redis 缓存数据库来保存用户登录信息。同样在前后端分离的项目,一般也是将用户登录信息保存到 Redis 缓存数据库中。当然也可以不使用 Redis,使用 JWT 的方式,通过验证 JWT 的 token 合法性来确认请求是否有效,在本课程的最后,我们会演示一下。


3 Application应用

对应的类是 ServletContextApplication 是整个应用共享的,从 Web 启动到关闭都有效,所有用户都能访问同一份数据。

一般用于全局配置或全局统计数据,例如通过多少人访问网站,可以使用 Application 对象。


8.1.2 域对象的常用方法

所有域对象都有以下的操作方法,用于设置、获取、删除属性:

方法说明
setAttribute(String name, Object value)存入数据
getAttribute(String name)获取数据
removeAttribute(String name)移除数据

举个栗子:

java
request.setAttribute("msg", "请求范围的数据");
session.setAttribute("user", new User("Tom"));
getServletContext().setAttribute("count", 100);

SpringMVC 是建立在 Servlet 之上的框架,因此同样支持这些域对象,而且 SpringMVC 提供了更优雅的方式来操作和传递数据。

下面就来学习一下。


8.2 SpringMVC中使用Request

首先介绍一下在 Controller 中获取和使用 request 域对象。

1 使用原生HttpServletRequest

编写 Controller 如下:

java
package com.foooor.hellospringmvc.controller;

import jakarta.servlet.http.HttpServletRequest;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class IndexController {

    @GetMapping("/")
    public String index(HttpServletRequest request) {  // springmvc会自动注入request对象

        // 向request对象中设置参数
        request.setAttribute("message", "Hello For技术栈");

        return "index";  // 转发到index视图 index.html
    }
}
  • 当请求 / 会请求上面的接口,然后向 request 对象中设置属性参数,然后转发到视图,视图中可以获取到参数。

index.html

html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"><!-- 引入Thymeleaf命名空间 -->
<head>
  <meta charset="UTF-8">
  <title>首页</title>
</head>
<body>

<div th:text="${message}"></div>

</body>
</html>
  • 页面上就可以获取到 request 对象中的 attribute 属性值。
  • 但是这种方式是使用的原生的 Servlet API,不利于单元测试,因为要单元测需要创建 request 对象 ,而单元测试中没有 Web 环境。模拟 request 对象会比较麻烦。

2 使用Model

在前面编写 CRUD 的时候,已经使用了 Model 接口,SpringMVC 会自动将 Model 对象注入到 Controller 的方法中,通过在 model 对象中设置属性,就可以在页面中获取到数据。

举个栗子:

java
package com.foooor.hellospringmvc.controller;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class IndexController {

    @GetMapping("/")
    public String index(Model model) {  // springmvc会自动注入model对象

        // 向request域中存储参数
        model.addAttribute("message", "www.foooor.com");

        return "index";  // 转发到index视图 index.html
    }
}
  • 通过 addAttribute 向 model 对象中添加属性。在页面中获取参数是一样的。

3 使用Map

使用Map也是可以的,和 Model 方式一样。

举个栗子:

java
package com.foooor.hellospringmvc.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import java.util.Map;

@Controller
public class IndexController {

    @GetMapping("/")
    public String index(Map<String, Object> map) {  // springmvc会自动注入map对象

        // 向request域中存储参数
        map.put("message", "www.foooor.com2");

        return "index";  // 转发到index视图 index.html
    }
}
  • 使用方式基本是一样的,只是使用 put 方法添加参数,页面获取方式也是一样的。

4 使用ModelMap

还可以使用 ModelMap ,功能与 Model 类似,但它本质是一个 Map。

使用方式也是一样的,举个栗子:

java
package com.foooor.hellospringmvc.controller;

import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class IndexController {

    @GetMapping("/")
    public String index(ModelMap map) {  // springmvc会自动注入ModelMap对象

        // 向request域中存储参数
        map.addAttribute("message", "www.foooor.com");

        // 或者使用put方法
        // map.put("message", "www.foooor.com2");

        return "index";  // 转发到index视图 index.html
    }
}

5 使用ModelAndView

还可以使用 ModelAndView,它同时包含数据与视图。ModelAndView 有一些不同,使用时需要同时设置视图和数据,并返回。

举个栗子:

java
package com.foooor.hellospringmvc.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.servlet.ModelAndView;

@Controller
public class IndexController {

    @GetMapping("/")
    public ModelAndView index(ModelAndView modelAndView) {  // springmvc会自动注入ModelAndView对象

        modelAndView.setViewName("index");  // 转发到index视图 index.html
        // 向request域中存储参数
        modelAndView.addObject("message", "www.foooor.com");

        return modelAndView;  // 转发到index视图 index.html
    }
}
  • 注意方法的返回值是 ModelAndView 类型,其中包含了视图名称和数据。页面获取数据方式是一样的。

上面的方式有很多,但其实区别不大,对于 ModelMapModelMap 而言,底层都是使用的同一个类对象,org.springframework.validation.support.BindingAwareModelMap ,而 BindingAwareModelMap 继承了 ExtendedModelMap 类,ExtendedModelMap 继承了 ModelMap 类,并实现了 Model 接口,ModelMap 继承了 LinkedHashMap 类并实现了 Map 接口。所以效果是一样的,只是为了方便使用而已,最常用的还是 Model 吧。

但不管使用什么方式,调用 Controller 方法的结果最终都会被封装为 ModelAndView 返回给 DispatcherServlet,然后 DispatcherServlet 调用视图解析器将逻辑视图(视图名称index)转换为物理视图(具体的模板文件WEB-INF/template/index.html),最终调用视图对象的渲染方法,将模板结合数据转换为 HTML 代码返回给浏览器。

而从 Request 的角度讲,无论你使用 HttpServletRequestModelMapModelMap 还是 ModelAndView,它们的最终效果都是:request.setAttribute("key", value),然后在渲染视图前,由 SpringMVC 将这些数据带入模板引擎。所以说 Model(以及它的几个变体如 ModelMapModelAndView)只是方便开发者使用的上层封装,这些数据最终都会被放入 request 域中

8.3 SpringMVC中使用Session

下面介绍一下 Session 的使用。

1 使用原生HttpSession

HttpServletRequest 使用方式一样,只需要在 Controller 的方法上添加形参即可。

java
package com.foooor.hellospringmvc.controller;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpSession;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class IndexController {

    @GetMapping("/")
    public String index(HttpServletRequest request, HttpSession session) {  // springmvc会自动注入对象

        // 向request域中存储参数
        request.setAttribute("message", "For技术栈");
        // 向session域中存储参数
        session.setAttribute("message", "www.foooor.com");

        return "index";  // 转发到index视图 index.html
    }
}
  • Controller 的方法上可以添加多个参数(一个肯定可以),会自动注入 session 对象。

在页面上,获取 session 域中的参数,需要使用它 ${session.参数名} ,如下:

html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"><!-- 引入Thymeleaf命名空间 -->
<head>
  <meta charset="UTF-8">
  <title>首页</title>
</head>
<body>

  <!-- 从request域中获取参数 -->
  <div th:text="${message}"></div>
  
  <!-- 从session域中获取参数 -->
  <div th:text="${session.message}"></div>

</body>
</html>

其实直接通过 request 对象也可以获取到 session 对象:

java
@GetMapping("/")
public String index(HttpServletRequest request) {  // springmvc会自动注入map对象

    // 通过request获取session对象
    HttpSession session = request.getSession();

    // 向request域中存储参数
    request.setAttribute("message", "For技术栈");
    // 向session域中存储参数
    session.setAttribute("message", "www.foooor.com3");

    return "index";  // 转发到index视图 index.html
}

2 使用@SessionAttributes

通过 @SessionAttributes 注解,可以将 Model 对象中的部分数据放入 Session 域中。

举个例子:

java
package com.foooor.hellothymeleaf.controller;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.SessionAttributes;

@Controller
@SessionAttributes("message")
public class IndexController {

    @GetMapping("/")
    public String index(Model model) {  // springmvc会自动注入对象

        // 使用了@SessionAttributes 注解,会同时放入 request 和 session
        model.addAttribute("message", "For技术栈");

        return "index";  // 转发到index视图 index.html
    }
}
  • 上面使用 @SessionAttributes("message") 指定了,将 message 参数同时放入 request 和 session 域中,所以在页面通过request域和session域读取方式都能获取到数据。

页面两种方式都能读取到数据:

html
<!-- 从request域中获取参数 -->
<div th:text="${message}"></div>

<!-- 从session域中获取参数 -->
<div th:text="${session.message}"></div>

8.4 SpringMVC中使用Application

通过 request 对象和 session 对象都可以获取到 Application 对象:

java
package com.foooor.hellothymeleaf.controller;

import jakarta.servlet.ServletContext;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpSession;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Slf4j
@Controller
public class IndexController {

    @GetMapping("/")
    public String index(HttpServletRequest request, HttpSession session) {  // springmvc会自动注入对象

        ServletContext context1 = request.getServletContext();
        ServletContext context2 = session.getServletContext();
        log.info("context1 == context2: " + (context1 == context2)); // true

        context1.setAttribute("message", "For技术栈");  // 向application中放入数据

        return "index";  // 转发到index视图 index.html
    }
}
  • 获取到的结果是同一个对象。

在页面上获取 Application 对象中的参数,通过 ${application.参数名称} ,如下:

html
<!-- 从application域中获取参数 -->
<div th:text="${application.message}"></div>

在前后端分离的项目中,Session 域已经很少使用了,因为每次请求都是独立,完整的,不依赖于服务器保存状态,分布式环境下还会存在 Session 同步问题,所以前后端分离项目会尽量避免使用 Session

内容未完......