Skip to content

Spring MVC

简介

Spring MVC 是Spring 生态基于MVC模式的轻量级Web请求框架,以DispatcherServlet前端控制器为统一入口,接收浏览器请求,通过处理器映射器、适配器匹配并执行Controller控制器,封装Model数据,再由视图解析器解析视图、渲染页面,实现请求分发、业务处理、视图解耦,职责清晰、开箱即用,是Java Web及SSM、SpringBoot后端接口开发的核心基础。Spring MVC工作原理图如下:

flowchart LR %% 外层:请求/响应 Front[前端]:::info -->|1 HTTP请求| FrontController subgraph Servlet engine %% 核心组件 M1[模型
POJO]:::primary FrontController[前端控制器
DispatcherServlet]:::success Controller[控制器
Controller]:::danger %% 控制器流程 FrontController -->|2 分发请求| Controller Controller -->|2 处理请求
创建模型| M1 M1 -->|POJO
3 委托结果渲染| FrontController %% 视图渲染流程 FrontController -->|POJO
4 分派视图渲染| VT VT[视图解析器]:::warning -->|5 渲染 | TE[模板引擎
Thymeleaf]:::danger TE -->|6 视图 | VT VT -->|7 返回控制| FrontController end FrontController -->|8 响应| Front
Spring MVC工作原理图

Spring Boot集成

Spring Boot 通过spring-boot-starter-web实现Spring MVC自动集成,自动完成DispatcherServlet、处理器映射器、视图解析器等核心组件的配置;开发者仅需引入依赖、编写控制器即可完成请求处理数据封装视图渲染

添加web依赖自动集成Spring MVC;添加模板引擎依赖用于视图渲染。

1
2
3
4
5
6
7
8
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

SpringMVC启动类用于初始化Spring容器、扫描控制器、加载Web与MVC核心配置,整合web环境,开启请求处理与接口访问功能。是Spring Boot应用的入口,通过@SpringBootApplication注解开启自动配置,扫描项目中的组件,加载DispatcherServlet等核心组件。

1
2
3
4
5
6
7
@SpringBootApplication
public class MvcApplication {
    // 主方法,用于启动Spring Boot应用
    public static void main(String[] args) {
        SpringApplication.run(MvcApplication.class, args);
    }
}

控制器是MVC模式中的C(Controller),是业务逻辑的调度中心,主要涉及以下任务:

  • 映射URL路径(路由);通过@RequestMapping注解将请求映射到控制器方法。

  • 接收参数(输入);通过@RequestParam@RequestHeader@CookieValue等注解绑定请求参数。

  • 封装数据;将业务逻辑处理结果封装到Model,返回视图名用于渲染。

  • 视图渲染(输出);根据返回的视图名,由视图解析器解析视图,渲染页面。

1
2
3
4
5
6
7
8
@Controller
public class IndexController {
@GetMapping("/index") //请求处理
public String index(Model model, ...输入参数) {
    // 业务逻辑处理...
    model.addAttribute("msg", "Spring MVC渲染成功"); // 数据封装
    return "index"; // 视图渲染,返回视图名,用于渲染index.html
}

模板页面接收Model数据,完成HTML渲染。

resources/templates/index.html
1
2
3
4
5
<html xmlns:th="http://www.thymeleaf.org">
<body>
    <h1 th:text="${msg}"></h1>
</body>
</html>

请求→DispatcherServlet→Controller→视图解析→渲染页面→响应客户端。


输入

主要的注解

类型 说明
@Controller 指示Spring类的实例是一个控制器;返回视图名
@RestController 指示Spring类的实例是一个Restful控制器;@RestController = @Controller + @ResponseBody;返回JSON数据
类型 说明
@RequestMapping 映射一个请求或一个方法
@RequestParam 绑定请求参数
@RequestHeader 绑定请求头参数
@CookieValue 绑定Cookie的值
@PathVariable 绑定URL中的变量
@GetMapping 匹配GET方式的请求
@PostMapping 匹配POST方式的请求
类型 说明
@ResponseBody 指示返回JSON数据

控制器与HTTP请求

parse


请求URL

URL表达式

@RequestMapping不但支持标准的URL,还支持Ant风格的和带{xxx}占位符的URL。以下URL都是合法的:

URL 匹配地址
/user/*/createUser /user/aaa/createUser、/user/bbb/createUser等URL。
/user/**/createUser /user/createUser、/user/aaa/bbb/createUser等URL。
/user/createUser?? /user/createUseraa、/user/createUserbb等URL。
/user/{userId} user/123、user/abc等URL。
/user/**/{userId} user/aaa/bbb/123、user/aaa/456等URL。
company/{companyId}/user/{userId}/detail company/123/user/456/detail等的URL。

绑定{xxx}中的值

URL中的{xxx}占位符可以通过@PathVariable("xxx")绑定到操作方法的入参中,如:

1
2
3
4
5
6
7
@RequestMapping("/{userId}")
public ModelAndView showDetail(@PathVariable("userId") String userId){
    ModelAndView mav = new ModelAndView();
    mav.setViewName("user/showDetail");
    mav.addObject("user", userService.getUserById(userId));
    return mav;
}

如果@PathVariable不指定参数名,只有在编译时打开debug开关(javac -debug=no)时才可行!!(不建议)

@Controller
@RequestMapping("/owners/{ownerId}")
public class RelativePathUriTemplateController {

   @RequestMapping("/pets/{petId}")
   public void findPet(@PathVariable String ownerId, 
                                 @PathVariable String petId, Model model) {
     
   }
}

请求方法

请求方法,在HTTP中这被叫做动词(verb),除了两个大家熟知的(GET和POST)之外,标准方法集合中还包含PUT、DELETE、HEAD和OPTIONS。这些方法的含义连同行为许诺都一起定义在HTTP规范之中。一般浏览器只支持GET和POST方法。

常见的HTTP请求方法有以下几种:

方法 描述
GET 请求指定的页面信息,并返回实体主体。
HEAD 类似于GET请求,只不过返回的响应中没有具体的内容,用于获取报头。
POST 向指定资源提交数据进行处理请求(例如提交表单或者上传文件)。数据被包含在请求体中。
PUT 从客户端向服务器传送的数据取代指定的文档的内容。
DELETE 请求服务器删除指定的页面。
CONNECT HTTP/1.1协议中预留给能够将连接改为管道方式的代理服务器。
OPTIONS 允许客户端查看服务器的性能。
TRACE 回显服务器收到的请求,主要用于测试或诊断。
PATCH 对资源进行部分修改。

报文头及报文体示例

1
2
3
@RequestMapping(value="/delete", params="userId") 
public String test1(@RequestParam("userId") String userId){
}
@RequestMapping(value="/show",headers="content-type=text/*")
public String test2(@RequestParam("userId") String userId){}
1
2
3
4
5
6
@RequestMapping(value="/handle1")
public String handle1(@RequestParam("userName") String userName,
              @RequestParam("password") String password,
              @RequestParam("realName") String realName){
    ...
}
1
2
3
4
5
@RequestMapping(value="/handle2")
public String handle2(@CookieValue("JSESSIONID") String sessionId,
       @RequestHeader("Accept-Language") String accpetLanguage){
  ...
}

表单对象绑定

所谓命令/表单对象并不需要实现任何接口,仅是一个拥有若干属性的POJO。Spring MVC按:

  • "HTTP请求参数名 = 命令/表单对象的属性名"

的规则,自动绑定请求数据,支持"级联属性名",自动进行基本类型数据转换。

1
2
3
4
@RequestMapping(value = "/handle14")
public String handle14(User user) {
    //…
}
graph LR A["userName=xxxx password=yyyy"]:::success B["class User{
private String userName;
private String password;
}"]:::warning A --> B
表单-对象绑定

使用Servlet API对象作为入参

在Spring MVC中,控制器类可以不依赖任何Servlet API对象,但是Spring MVC并不阻止我们使用Servlet API的类作为处理方法的入参。值得注意的是,如果处理方法自行使用HttpServletResponse返回响应,则处理方法的返回值设置成void即可。

1
2
3
4
5
@RequestMapping(value = "/handle21")
public void handle21(HttpServletRequest request,HttpServletResponse response) {
  String userName = WebUtils.findParameterValue(request, "userName");
  response.addCookie(new Cookie("userName", userName));
}
1
2
3
4
public String handle23(HttpSession session) {
  session.setAttribute("sessionId", 1234);
  return "success";     
}
1
2
3
4
5
public String handle24(HttpServletRequest request,
                                    @RequestParam("userName")String userName) {
  //…
  return "success";
}

使用Spring的Servlet API代理类

Spring MVC在org.springframework.web.context.request包中定义了若干个可代理Servlet原生API类的接口,如WebRequest和NativeWebRequest,它们也允许作为处理类的入参,通过这些代理类可访问请求对象的任何信息。

1
2
3
4
5
@RequestMapping(value = "/handle25")
public String handle25(WebRequest request) {
    String userName = request.getParameter("userName");
    return "success";
}

使用IO对象作为入参

Spring MVC允许控制器的处理方法使用java.io.InputStream/java.io.Reader及java.io.OutputStream/java.io.Writer作为方法的入参

1
2
3
4
5
@RequestMapping(value = "/handle31")
public void handle31(OutputStream os) throws IOException{
       Resource res = new ClassPathResource("/image.jpg");//读取类路径下的图片文件
       FileCopyUtils.copy(res.getInputStream(), os);//将图片写到输出流中
}

Spring MVC将获取ServletRequest的InputStream/Reader或ServletResponse的OutputStream/Writer,然后按类型匹配的方式,传递给控制器的处理方法入参。


其他类型的参数

控制器处理方法的入参除支持以上类型的参数以外,还支持java.util.Locale、java.security.Principal,可以通过Servlet的HttpServletRequest 的getLocale()及getUserPrincipal()得到相应的值。如果处理方法的入参类型为Locale或Principal,Spring MVC自动从请求对象中获取相应的对象并传递给处理方法的入参。

1
2
3
4
@RequestMapping(value = "/handle32")
public void handle31(Locale locale) throws IOException{
  //...
}

输出

控制器返回值

@Controller

  • String; 可以跳转视图,但不能携带数据

  • ModelAndView; 可以添加Model数据,并指定视图

  • View

@RestController

  • Model

  • Map

  • void; 在异步请求时使用,它只返回数据,而不会跳转视图

  • HttpEntity<?>或ResponseEntity<?>

  • Callable<?>

  • DeferredResult<?>


HttpMessageConverter

HttpMessageConverter 是 SpringMVC 中专门负责【HTTP 请求/响应报文】与【Java 对象】之间自动转换的核心组件,帮我们省去手动解析 JSON、拼接数据的繁琐工作。

  1. 接收请求时(入参):JSON → Java 对象

    把前端传来的 JSON/XML/表单 原始报文

    自动转换成 Java 对象

    JSON字符串 → @RequestBody 对象
    
  2. 响应数据时(出参):Java 对象 → JSON

    把 Java 对象 自动转换成 JSON/XML 响应报文 返回给前端

    @ResponseBody 对象 → JSON字符串
    

graph TD %% 左侧:请求流程 reqIn["HttpServletRequest"]:::success converter["HttpMessageConverter<T>"]:::danger reqOut["@RequestBody/HttpEntity<T>"]:::success reqIn -- HTTP请求报文 --> converter converter --> reqOut %% 右侧:响应流程 respOut["HttpServletResponse"] respIn["@ResponseBody/ResponseEntity<T>"]:::primary respIn --> converter converter -- HTTP响应报文 --> respOut:::primary
前后端数据转换

HttpMessageConverter实现类

converter_impl


使用@RequestBody/@ResponseBody

将HttpServletRequest的getInputStream()内容绑定到入参,将处理方法返回值写入到HttpServletResponse的getOutputStream()中。

1
2
3
4
5
@RequestMapping(value = "/handle41")
public String handle41(@RequestBody  String requestBody ) {
      System.out.println(requestBody);
      return "success";
}
1
2
3
4
5
6
7
8
@ResponseBody
@RequestMapping(value = "/handle42/{imageId}")
public byte[] handle42(@PathVariable("imageId") String imageId) throws IOException {
       System.out.println("load image of "+imageId);
       Resource res = new ClassPathResource("/image.jpg");
       byte[] fileData =FileCopyUtils.copyToByteArray(res.getInputStream());
       return fileData;
}

优点:处理方法签名灵活不受限

缺点:只能访问报文体,不能访问报文头


使用HttpEntity/ResponseEntity

1
2
3
4
5
6
@RequestMapping(value = "/handle43")
public String handle43(HttpEntity<String> httpEntity){                             
     long contentLen = httpEntity.getHeaders().getContentLength();  
     System.out.println(httpEntity.getBody());
      return "success";
}
@RequestMapping(params = "method=login")   
public ResponseEntity<String> doFirst(){   
    HttpHeaders headers = new HttpHeaders();   
    MediaType mt=new MediaType("text","html",Charset.forName(UTF-8"));   
    headers.setContentType(mt);   
    ResponseEntity<String> re=null;   
    String return = new String("test");   
    re=new ResponseEntity<String>(return,headers, HttpStatus.OK);   
    return re;   
}

优点:处理方法签名受限

缺点:不但可以访问报文体,还能访问报文头


数据转换、校验

数据绑定机理

data_bind


ConversionService

由于ConversionService在进行类型转换时,可以使用到Bean所在宿主类的上下文信息(包括类结构,注解信息),所以可以实施更加高级的类型转换,如注解驱动的格式化等功能。

1
2
3
4
public class User {
  @DateTimeFormat(pattern="yyyy-MM-dd")
  private Date birthday;
}

以上User类,通过一个@DateTimeFormat注解,为类型转换提供了一些“额外”的信息,即代表日期的“源字符器”格式是“yyyy-MM-dd”


定义自定义的类型转换器

在Spring Boot中,可以通过实现Converter接口或者继承GenericConverter类来自定义类型转换器。具体步骤如下:

  1. 创建一个类,实现Converter接口或者继承GenericConverter类。

  2. 在类上使用@Component注解,将其注册为Spring Bean。

  3. 在Spring Boot应用的配置类中,使用@Import注解将自定义类型转换器类导入。

  4. 在Spring MVC的配置类中,使用addFormatters方法将自定义类型转换器添加到FormatterRegistry中。

例如,下面是一个将字符串转换为日期类型的自定义类型转换器的示例代码:

@Component
public class StringToDateConverter implements Converter<String, Date> {

    private static final String DATE_FORMAT = "yyyy-MM-dd";

    @Override
    public Date convert(String source) {
        SimpleDateFormat dateFormat = new SimpleDateFormat(DATE_FORMAT);
        try {
            return dateFormat.parse(source);
        } catch (ParseException e) {
            throw new IllegalArgumentException("Invalid date format. Please use " + DATE_FORMAT);
        }
    }
}

在Spring MVC的配置类中,可以这样添加自定义类型转换器:

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    @Autowired
    private StringToDateConverter stringToDateConverter;

    @Override
    public void addFormatters(FormatterRegistry registry) {
        registry.addConverter(stringToDateConverter);
    }
}

这样,在Spring Boot应用中,就可以将字符串转换为日期类型了。


数据校验框架

Spring 3.0拥有自己独立的数据校验框架,同时支持JSR 303标准的校验框架。Spring 的DataBinder在进行数据绑定时,可同时调用校验框架完成数据校验工作。

在Spring MVC中,则可直接通过注解驱动的方式进行数据校验。

Spring的org.springframework.validation是校验框架所在的包


JSR 303

JSR 303是Java为Bean数据合法性校验所提供的标准框架,它已经包含在Java EE 6.0中。JSR 303通过在Bean属性上标注类似于@NotNull、@Max等标准的注解指定校验规则,并通过标准的验证接口对Bean进行验证。

可以通过http://jcp.org/en/jsr/detail?id=303了解JSR 303的详细内容。

注解 功能说明
@Null 被注释的元素必须为 null
@NotNull 被注释的元素必须不为 null
@AssertTrue 被注释的元素必须为 true
@AssertFalse 被注释的元素必须为 false
@Min(value) 被注释的元素必须是一个数字,其值必须大于等于指定的最小值
@Max(value) 被注释的元素必须是一个数字,其值必须小于等于指定的最大值
@DecimalMin(value) 被注释的元素必须是一个数字,其值必须大于等于指定的最小值
@DecimalMax(value) 被注释的元素必须是一个数字,其值必须小于等于指定的最大值
@Size(max, min) 被注释的元素的大小必须在指定的范围内
@Digits (integer, fraction) 被注释的元素必须是一个数字,其值必须在可接受的范围内
@Past 被注释的元素必须是一个过去的日期
@Future 被注释的元素必须是一个将来的日期

数据校验框架

<mvc:annotation-driven/>会默认装配好一个LocalValidatorFactoryBean,通过在处理方法的入参上标注@Valid注解即可让Spring MVC在完成数据绑定后执行数据校验的工作。

注意:Spring本身没有提供JSR 303的实现,所以必须将JSR 303的实现者(如Hibernate Validator)的jar文件放到类路径下,Spring将自动加载并装配好JSR 303的实现者。

public class User {   
  @Pattern(regexp="w{4,30}")
  private String userName;

  @Length(min=2,max=100)
  private String realName;

  @Past 
  @DateTimeFormat(pattern="yyyy-MM-dd")
  private Date birthday;

  @DecimalMin(value="1000.00")
  @DecimalMax(value="100000.00") 
  @NumberFormat(pattern="#,###.##")
  private long salary;
}

如何使用注解驱动的校验

在已经标注了JSR 303注解的表单/命令对象前标注一个@Valid,Spring MVC框架在将请求数据绑定到该入参对象后,就会调用校验框架根据注解声明的校验规则实施校验。

@Controller
@RequestMapping("/user")
public class UserController {
  @RequestMapping(value = "/handle91")
  public String handle91(@Valid  User user,
    BindingResult bindingResult){       if(bindingResult.hasErrors()){
      return "/user/register3";
    }else{
          return "/user/showUser";
    }
  }
}

使用校验功能时,处理方法要如何签名??

Spring MVC是通过对处理方法签名的规约来保存校验结果的:前一个表单/命令对象的校验结果保存在其后的入参中,这个保存校验结果的入参必须是BindingResult或Errors类型,这两个类都位于org.springframework.validation包中。

sign


校验错误信息存放在什么地方?

error_message

4.Spring MVC将HttpServletRequest对象数据绑定到处理方法的入参对象中(表单/命令对象);

5.将绑定错误信息、检验错误信息都保存到隐含模型中;

6.本次请求的对应隐含模型数据存放到HttpServletRequest的属性列表中,暴露给视图对象。


页面如何显示错误信息

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="c"      uri="http://java.sun.com/jsp/jstl/core" %>   
<%@ taglib prefix="form"   uri="http://www.springframework.org/tags/form" %>
<html>
<head>
<title>注册用户</title>
  <style>.errorClass{color:red}</style>
</head>
<body> 
  <form:form modelAttribute="user"  action="user/handle91.html">
    <form:errors path="*"/>
    <table>
      <tr>
        <td>用户名</td>
        <td>
          <form:errors path="userName" cssClass="errorClass"/>
          <form:input path="userName" />
        </td>
      </tr>
                   
    </table>
  </form:form>
</body>
</html>

如何对错误信息进行国际化1

一个属性发生校验错误时,Spring MVC会产生一系列对应的错误码键:

1
2
3
4
public class User {   
      @Pattern(regexp="w{4,30}")->假设发生错误
      private String userName;
}

如果userName的@Pattern校验规则未通过,则会在“隐含模型”中产生如下的错误键,这些错误键可以作为“国际化消息”的属性键。

  • Pattern.user.userName

  • Pattern.userName

  • Pattern.String

  • Pattern


如何对错误信息进行国际化2

我们在conf/i18n/下添加基名为messages的国际化资源,一个是默认的messages.properties,另一个是对应中国大陆的messages_zh_CN.properties。来看一下messages_zh_ CN.properties资源文件的内容:

i18n

form


数据模型控制

数据模型访问结构

data_model_access


访问数据模型:ModelAndView

通过ModelAndView

1
2
3
4
5
6
7
8
@RequestMapping(method = RequestMethod.POST)
public ModelAndView createUser(User user) {
    userService.createUser(user);
    ModelAndView mav = new ModelAndView();
    mav.setViewName("user/createSuccess");
    mav.addObject("user", user);
    return mav;
}

访问数据模型:@ModelAttribute

1
2
3
4
5
@RequestMapping(value = "/handle61")
public String  handle61(@ModelAttribute("user") User user){
  user.setUserId("1000");
  return "/user/createSuccess";
}

Spring MVC将HTTP请求数据绑定到user入参中,然后再将user对象添加到数据模型中。

//访问UserController中任何一个请求处理方法前,Spring MVC先执行该方法,并将返回值以user为键添加到模型中
@ModelAttribute("user")
public User getUser(){
    User user = new User();
    user.setUserId("1001"); 
    return user;
}

@RequestMapping(value = "/handle62")  
public String  handle62(@ModelAttribute("user") User user){
  //在此,模型数据会赋给User的入参,然后再根据HTTP请求消息进一步填充覆盖user对象
    user.setUserName("tom");
    return "/user/showUser";
}

访问数据模型:Map及Model

1
2
3
4
5
6
7
@RequestMapping(value = "/handle63")
public String  handle63(ModelMap modelMap){
     modelMap.addAttribute("testAttr","value1");
     User user = (User)modelMap.get("user");
     user.setUserName("tom");       
     return "/user/showUser";
}
Spring MVC一旦发现处理方法有Map或Model类型的入参,就会将请求内在的隐含模型对象的引用传给这些入参。


访问数据模型:@SessionAttributes

如果希望在多个请求之间共用某个模型属性数据,则可以在控制器类标注一个@SessionAttributes,Spring MVC会将模型中对应的属性暂存到HttpSession中:

@Controller
@RequestMapping("/user")
@SessionAttributes(user)  //①将②处的模型属性自动保存到HttpSession中
public class UserController {

    @RequestMapping(value = "/handle71")
    public String  handle71(@ModelAttribute(user) User user){ //②
        user.setUserName("John");
        return "redirect:/user/handle72.html";
    }

    @RequestMapping(value = "/handle72")
    public String  handle72(ModelMap modelMap,SessionStatus sessionStatus){
        User user = (User)modelMap.get(user); //③读取模型中的数据
        if(user != null){
            user.setUserName("Jetty");
            sessionStatus.setComplete();  //④让Spring MVC清除本处理器对应的会话属性

        }
        return "/user/showUser";
    }       
}

视图及解析器

view_resolver


视图解析器类型

完成单一解析逻辑的视图解析器:

  • InternalResourceViewResolver

  • FreeMarkerViewResolver

  • BeanNameViewResolver

  • XmlViewResolver

  • ...