SpringMVC 学习笔记

三层架构 和 MVC模型

  • 三层架构:我们在Servlet 学习笔记讲过,当前 Web 应用体系主要为 C/S架构B/S架构C/S架构 是一种历史悠久且技术非常成熟的一种架构,B/S架构C/S架构 演变而来,属于新生代架构,在当今 Web 应用时代,是最被广泛进行使用的架构。

在标准 B/S架构 中,系统分为以下三层:

  • 表现层:也就是我们常说的 web 层,该层主要负责与客户端进行交互,接受客户端请求(控制层)和向客户端响应结果(展示层)。SpringMVC 框架就属于表现层。
  • 业务层:也就是我们常说的 service 层,该层主要负责业务逻辑的处理。Spring 框架其实属于业务层。
  • 持久层:也就是我们常说的 dao(Data Access Object,数据访问对象) 层,该层主要负责数据持久化,也即对数据库进行增删改查操作。MyBatis 框架就属于持久层。

整个服务器 B/S架构 简单示意图如下所示:

B/S架构
  • MVC模型:MVC 是 Model - View - Controller 的缩写,即 模型 - 视图 - 控制器。

MVC模型也是一种分层模型,与 B/S架构 一样,其也分为了三层:

  • 模型(Model):对应于数据模型,该层主要负责对数据进行封装,也就是我们常说的 dao,bean。
  • 视图(View):用于展示模型中的数据,通常指的就是 jsp页面 或 html页面。
  • 控制器(Controller):接收客户端请求,处理程序逻辑,从模型中获取需要的数据,显示到不同的视图上。控制器的典型角色就是 Servlet。

综上:服务器 B/S架构 中,表现层的设计模型一般使用的都是 MVC。更具体来讲,宏观上看,B/S架构 三层架构之间职责明确,各层间存在一定的依赖关系,呈相互合作之势。将表现层拆分成 MVC 模型,层次更加精细,可以说将原本表现层对业务层的依赖,精细化为控制器(Controller)层对业务层的依赖,当表现层控制器(Controller)接收到请求时,就会对请求进行解析,然后将解析结果给到业务层,业务层就会调用持久层进行 CRUD 操作,然后将结果传递给表现层的模型层(Model)进行数据封装,最后返回给客户端。

SpringMVC 简介

  • SpringMVC:其正式名称为 Spring Web MVC,其是以 MVC 作为设计模型,基于 Servlet API 的基础上构建的一套 请求驱动类型(请求 - 响应模型 ) 的轻量级 Web 框架,从最开始的时候就集成到 Spring 框架中。

SpringMVC 目前已成为最主流的 MVC 框架之一,它通过一套注解,可以让一个 POJO 类作为请求处理的控制器(Controller),无须实现任何额外接口,对源码侵入性低,并且它还支持 RESTful 编程风格的请求。

SpringMVC 优势

  • 采用组件化开发,组件可插拔,组件间松散耦合。

  • 基于 MVC 设计模型,各层分工明确,扩展性极强。

  • 无缝集成 Spring 框架,无须额外配置。

SpringMVC 快速入门

举个例子:浏览器请求地址:http://localhost:8080/hello,要求服务器返回页面hello.html

我们使用 IDEA 创建该工程,具体步骤如下:

  1. 创建一个 Maven 的 webapp 工程,如下图所示:
maven-archetype-webapp
  1. IDEA 默认创建的 web 工程目录补全,因此我们需要手动进行补全:
  • 补全源代码目录:在 src/main/ 目录下,创建文件夹 java - 右键该文件夹 - Mark Directory as - Sources Root。

  • 补全源代码资源目录:在 src/main/ 目录下,创建文件夹 resources - 右键该文件夹 - Mark Directory as - Resources Root。

  • 补全测试代码目录:在 src/ 目录下,创建文件夹 test/java - 右键该文件夹 - Mark Directory as - Test Sources Root。

  • 补全测试代码资源目录:在 src/test 目录下,创建文件夹 resources - 右键该文件夹 - Mark Directory as - Test Resources Root。

  1. pom.xml中导入相关依赖:
<?xml version="1.0" encoding="UTF-8"?>

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
   
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
        <!-- 统一版本号 -->
        <spring.version>5.1.9.RELEASE</spring.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>${spring.version}</version>
        </dependency>

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-webmvc</artifactId>
            <version>${spring.version}</version>
        </dependency>

    </dependencies>
</project>
  1. webapp/WEB-INF/目录下,创建文件夹pages,再创建一个hello.jsp页面:
pages
  1. 创建类HellController,接收客户端请求,返回hello.jsp页面:
@Controller
public class HelloController {
    @RequestMapping("/hello")
    public String hello(){       
        return "hello";
    }
}
  1. resources目录下,创建 SpringMVC 的配置文件springmvc.xml,开启注解扫描和 SpringMVC 注解支持,并配置一个视图解析器:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:mvc="http://www.springframework.org/schema/mvc"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        https://www.springframework.org/schema/context/spring-context.xsd
        http://www.springframework.org/schema/mvc
        https://www.springframework.org/schema/mvc/spring-mvc.xsd">

    <!-- 开启注解扫描 -->
    <context:component-scan base-package="com.yn"/>
    <!-- 开启SpringMVC注解 -->
    <mvc:annotation-driven />
    <!-- 视图解析器 -->
    <bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="prefix" value="/WEB-INF/pages/" />
        <property name="suffix" value=".jsp" />
    </bean>
</beans>
  1. webapp/WEB-INF/web.xml中,配置一个前端控制器,当前端控制器启动时,同时加载 Spring 配置文件:
<!DOCTYPE web-app PUBLIC
        "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
        "http://java.sun.com/dtd/web-app_2_3.dtd" >

<web-app>
    <display-name>Archetype Created Web Application</display-name>

    <!-- 配置前端控制器 -->
    <servlet>
        <servlet-name>dispatcherServlet</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <!-- 加载 Spring 配置文件 -->
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>classpath:springmvc.xml</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>dispatcherServlet</servlet-name>
        <!-- 拦截所有 -->
        <url-pattern>/</url-pattern>
    </servlet-mapping>
</web-app>
  1. 配置 Tomcat 服务器:
配置tomcat
  1. 运行项目,此时浏览器输入:localhost:8080/hello,就可以看到输出了。

SpringMVC 执行模型

在分析上述例子之前,我们首先需要了解下 SpringMVC 的执行模型。

  • SpringMVC 执行模型SpringMVC 是基于组件的方式完成整个请求流程的。具体流程如下图所示:
SpringMVC 执行模型

其中,各个组件的具体功能如下:

  1. 前端控制器(DispatcherServlet):客户端请求都会统一被DispatcherServlet接收,由DispatcherServlet委托给其他组件进行处理,其是整个请求流程控制中心,协同调配各组件共同完成一次 HTTP 请求。DispatcherServlet的存在降低了组件间的耦合。

  2. 处理器映射器(HandlerMapping):HandlerMapping 会根据请求URL 找到对应的处理器(Handler),生成一个处理器执行链(HandlerExecutionChain),其包含一个处理器(Handler)对象(即 Controller)和 0 个或多个拦截器对象(HandlerInterceptor))。

    :处理器(Handler)是对控制器(Controller)的包装,因为 Controller 方法可能存在参数,Handler 可以读入 HTTP 请求和上下文等相关参数传递给 Controller 方法。并且在 Controller 执行完成后,Handler 还可以通过配置信息对 Controller 返回的结果再进行处理。
    HandlerMapping 会解析出 请求URL 的路径,找到对应的 Controller,将请求交由其进行处理。

  3. 处理器适配器(HandlerAdapter):HandlerAdapter 会调用 Hanlder 处理请求,然后将结果适配为一个的 ModelAndView 对象(模型+视图),返回给 DispatcherServlet。

    :HandlerAdapter 采用适配器模式,将不同的 Handler 处理的结果适配为统一的样式 ModelAndView,通过扩展适配器可以对更多类型的 Handler 进行执行。

  4. 视图解析器(ViewResolver):ViewResolver 会接收 ModelAndView 对象,根据其逻辑视图名解析成具体的物理页面地址,即返回一个具体的页面。

现在,我们就可以对上文例子进行解析了。其完整的请求过程如下:

  1. 浏览器访问http://localhost:8080/hello,发送一个请求。

  2. 服务器配置前端请求器(DispatcherServlet)对所有请求进行拦截,因此 DispatcherServlet 会拦截到http://localhost:8080/hello这个请求。

  3. DispatcherServlet 会将请求转发给处理器映射器(HandlerMapping),HandlerMapping 会解析请求URL:http://localhost:8080/hello,得到请求路径/hello,然后根据路径找到了其对应的处理器(Handler)为:HelloController,然后将该 Handler 和 该路径对应的拦截器(HandlerInterceptor)封装到一个处理器执行链(HandlerExecutionChain)对象中,返回给 DispatcherServlet。

  4. DispatcherServlet 获取到 HandlerExecutionChain 后,先执行所有我们定义的拦截器(interceptor)的preHandle方法,然后将处理器(Handler)发送给相应处理器适配器(HandlerAdapter),HandlerAdapter 会执行 Handler,并将其结果封装到一个 ModelAndView 对象中,返回给 DispatcherServlet,最后还会执行拦截器(interceptor)的postHandle方法。

  5. DispatcherServlet 从返回的 ModelAndView 对象中获取到其逻辑视图名,将之传递给视图解析器(ViewResolver),ViewResolver 根据该逻辑视图名就可以解析出实际的具体页面 View,然后将该 View 发送给 DispatcherServlet 。

  6. View 进行视图渲染,View 会根据传进来的 Model 模型数据进行渲染。

  7. 返回控制权给 DispatcherServlet,由 DispatcherServlet 返回响应给用户,完成一个请求流程。

:在 SpringMVC 的各个组件中,处理器映射器(HandlerMapping),处理器适配器(HandlerAdapter)和 视图解析器(ViewResolver)称为 SpringMVC 的三大组件。
当配置了 SpringMVC 注解支持:<mvc:annotation-driven />时,这个配置会自动帮我们加载 处理映射器(RequestMappingHandlerMapping)处理器适配器(RequestMappingHandlerAdapter)。因此,一般情况下, 我们只需配置一个 视图解析器 即可。

上面较详细地阐述了 SpringMVC 的执行模型,为了方便理解,我们可以对其进行简化,大致可以理解为:

  1. 客户端发送请求,被服务器前端控制器(DispatcherServlet)拦截到。

  2. DispatcherServlet 将请求转发给处理器映射器(HandlerMapping)查找得到对应的控制器(Controller)(可以根据 XML配置,注解进行查找)。

  3. DispatcherServlet 通过处理器适配器去执行 Controller 对应的映射方法,然后将结果封装到一个 视图对象(ModelAndView)中。

  4. DispatcherServlet 从该 ModleAndView 中取出逻辑视图名发送给视图解析器(ViewResolver),ViewResolver 根据该逻辑视图名就可找到真实的页面对象(View)。

  5. 最后由 DispatcherServlet 将 ModelAndView 中的 Model 数据传递给页面对象(View),让其进行渲染。

  6. 渲染完成后交由 DispatcherServlet 响应给客户端,完成请求过程。

SpringMVC 常用注解

  • @RequestMapping:用于建立请求URL 和 请求处理方法 之间的对应/映射关系。
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Mapping
public @interface RequestMapping {
    String name() default "";

    @AliasFor("path")
    String[] value() default {};

    @AliasFor("value")
    String[] path() default {};

    RequestMethod[] method() default {};

    String[] params() default {};

    String[] headers() default {};

    String[] consumes() default {};

    String[] produces() default {};
}

@RequestMapping 注解可用于类和方法上,分别作为第一,二级目录,其属性具体含义如下表所示:

Modify and Type Element Description
Sting[] path URL 路径
String[] value 等同于 path
String name 该映射名称
RequestMethod[] method 指定请求方法(默认会响应所有请求)
String[] params 限定请求参数
String[] headers 限定请求头部
String[] consumes 限定请求的媒体资源类型(匹配头部:Content-Type)
String[] produces 限定返回的响应媒体类型(匹配头部:Accept)
  • @GetMapping:映射方法支持 HTTP GET 请求。其是@RequestMapping(method = RequestMethod.GET)的缩写。
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@RequestMapping(
    method = {RequestMethod.GET}
)
public @interface GetMapping {
    @AliasFor(
        annotation = RequestMapping.class
    )
    String name() default "";

    @AliasFor(
        annotation = RequestMapping.class
    )
    String[] value() default {};

    @AliasFor(
        annotation = RequestMapping.class
    )
    String[] path() default {};

    @AliasFor(
        annotation = RequestMapping.class
    )
    String[] params() default {};

    @AliasFor(
        annotation = RequestMapping.class
    )
    String[] headers() default {};

    @AliasFor(
        annotation = RequestMapping.class
    )
    String[] consumes() default {};

    @AliasFor(
        annotation = RequestMapping.class
    )
    String[] produces() default {};
}

同理:@PostMapping@PutMapping@DeleteMapping@PatchMapping 分别支持 Post,Put,Delete,Patch 请求。

  • @RequestBody:表示将请求体作为Controller方法的参数。@RequestBody 会读取请求体内容,然后通过系统默认配置的HttpMessageConverter进行解析,并将解析结果绑定到Controller方法参数上。
@Target({ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequestBody {
    boolean required() default true;
}

:Get 方法不适用该注解。

示例:

@PostMapping("/requestBody")
public String requestBody(@RequestBody String body) {
    return body;
}
  • @ResponseBody:表示方法返回值作为响应体内容。即将Controller方法返回的对象,通过适当的HttpMessageConverter转换为指定格式后,写入到Response对象的响应体中。
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ResponseBody {
}
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Controller
@ResponseBody
public @interface RestController {
    @AliasFor(
        annotation = Controller.class
    )
    String value() default "";
}
  • @RequestParam:用于把请求携带的参数赋值给控制器方法参数。
@Target({ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequestParam {
    @AliasFor("name")
    String value() default "";

    @AliasFor("value")
    String name() default "";

    boolean required() default true;

    String defaultValue() default "\n\t\t\n\t\t\n\ue000\ue001\ue002\n\t\t\t\t\n";
}

@RequestParam 注解只能用于参数上,其属性具体含义如下表所示:

Modifier and Type Element Description
String name 请求参数名称
String value 等同于 name
String defaultValue 默认值。当请求参数未提供或为空时,使用该默认值。
boolean required 请求参数是否必须提供此参数

示例:

@GetMapping("/requestParam")
public String requestParam(@RequestParam("name") String username) {
    return username;
}
@Target({ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequestHeader {
    @AliasFor("name")
    String value() default "";

    @AliasFor("value")
    String name() default "";

    boolean required() default true;

    String defaultValue() default "\n\t\t\n\t\t\n\ue000\ue001\ue002\n\t\t\t\t\n";
}

示例:

@GetMapping("/requestHeader")
public String requestHeader(@RequestHeader("User-Agent") String userAgent) {
    return userAgent;
}
  • @PathVariable:用于绑定 URL 占位符,赋值给方法参数。比如,url为/delete/{id},则{id}即为占位符。
@Target({ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface PathVariable {
    @AliasFor("name")
    String value() default "";

    @AliasFor("value")
    String name() default "";

    boolean required() default true;
}

示例:

@GetMapping("/pathVariable/{uid}")
public String pathVariable(@PathVariable("uid") Integer userId) {
    return "Your id is " + userId;
}
  • @ModelAttribute:用于把一个方法参数或方法返回值绑定到一个模型属性上。
@Target({ElementType.PARAMETER, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ModelAttribute {
    @AliasFor("name")
    String value() default "";

    @AliasFor("value")
    String name() default "";

    boolean binding() default true;
}

该注解可用于方法和参数上:

  1. 注解于参数上时,当该模型不存在时(没有 @ModelAttribute 注解的方法返回相应模型对象),会自动实例化一个新的模型对象,并且当请求携带有与模型字段相匹配的参数时,模型会自动将这些参数值绑定到对应域变量上(数据绑定)。

示例:

@RestController
public class UserController {
    static class User implements Serializable {
        String name;
        Integer age;

        public User(String name, Integer age) {
            this.name = name;
            this.age = age;
        }

        @Override
        public String toString() {
            if (this.age == null) {
                return String.format("[%s]", this.name);
            }
            return String.format("[%s:%d years old]", this.name, this.age);
        }
    }

    @RequestMapping("/modelAttribute")
    public String modelAttribute(@ModelAttribute User user) {
        return user.toString();
    }
}

分析:当客户端请求/modelAttribute时,被 @ModelAttribute 注解的参数user会自动创建一个实例对象,如果请求携带参数:/modelAttribute?name=Whyn,则该实例对象的属性name就会自动获取请求对应值,即:user.name=Whyn

  1. 注解于方法上时,当接收到请求时,会先执行被 @ModelAttribute 注解的方法,然后再执行控制器对应请求映射方法。

    注解于方法上可分为两种情形:

    1)方法没有返回值,也即不提供模型对象给控制器方法参数。不过一般使用 @ModelAttribute,都是为了提供自定义的模型对象给到控制器映射方法,因此,对于没有返回值的方法,若想提供模型对象,则需要Model对象进行辅助:

    @ModelAttribute
    public void getUser(@RequestParam("name") String name,
                        @RequestParam(value = "age", required = false) Integer age,
                        Model model) {
        User user = new User(name, age);
        model.addAttribute("user",user);
    }
    
    @RequestMapping("/modelAttribute")
    public String modelAttribute(@ModelAttribute("user") User user) {
        return user.toString();
    }
    

    :参数Model具体类型为BindingAwareModelMap,其实质就是一个Map键值对对象,使用该Map帮助我们存储模型对象,内部的具体实现其实是将键值对存储到Request域中,因此我们可以在控制器内不同方法间共享该模型对象。
    控制器方法参数的 @ModelAttribute 注解属性value用于指定Model键值,可忽略,忽略时系统直接根据参数名称进行获取。
    控制器方法参数的 @ModelAttribute 注解可忽略不写,系统会自动查找相应模型进行注入。

    2)当 @ModelAttribute 注解的方法有返回值时,该返回值会被传入给控制器请求映射方法的参数上。

    @ModelAttribute("user")
    public User getUser(@RequestParam("name") String name,
                        @RequestParam(value = "age", required = false) Integer age) {
        return new User(name, age);
    }
    
    @RequestMapping("/modelAttribute")
    public String modelAttribute(@ModelAttribute("user") User user) {
        return user.toString();
    }
    

    分析:方法getUser其实就相当于调用了model.addAttribute("user",user),键从注解 @ModelAttribute 获取,值即为方法返回值。

  • @ExceptionHandler:用于捕获控制器/方法抛出的异常的处理方案。当Controller或映射方法抛出异常时,如果该异常符合 @ExceptionHandler 注解声明的异常时,则会被该注解声明的方法进行捕获。
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ExceptionHandler {
    Class<? extends Throwable>[] value() default {};
}

示例:

@Controller
public class ExceptionController {

    @GetMapping("/runtimeexception")
    public String exception() {
        throw new RuntimeException("runtime exception occured!");
    }

    @ResponseStatus(HttpStatus.NOT_FOUND) // 404
    @ExceptionHandler({IOException.class, RuntimeException.class})
    public ModelAndView handlerException(Exception e) {
        e.printStackTrace();
        return new ModelAndView("error");
    }
}
  • @InitBinder:该注解用于初始化 WebDataBinder,从而可以对前台请求参数进行预处理。
    @InitBinder 支持 @RequestMapping 注解方法上的所有参数,除了命令/表单对象和相应的验证结果对象。
    @InitBinder 注解的方法不能有返回结果,通常都设置返回值为void
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface InitBinder {
    String[] value() default {};
}

:当前端控制器(DispatcherServlet)接收到请求时,在调用相应控制器(Controller)映射方法前,都会调用 @InitBinder 注解的方法对相应请求参数进行预处理。

示例:对前台请求参数日期数据(形如2019-08-30)进行转换,使控制器映射方法能接收该参数:

@GetMapping("/date")
public String today(@RequestParam("date") Date date){
    return date.toString();
}

@InitBinder
public void transformDate(WebDataBinder binder){
    binder.addCustomFormatter(new DateFormatter("yyyy-MM-dd"));
}

分析:对于大多数请求参数,Spring 已经为我们实现了自动数据绑定功能。但是总有一些自定义的参数数据,Spring 无法自动进行转换,因此需要我们手动提供一个转换器,就比如对于上例自定义上传的日期格式(yyyy-MM-dd),Spring 没有提供相应转换器,此时我们就可以借助 @InitBinder 对参数进行预处理,使之能转换成相应的类型。

@InitBinder 的属性只有value,其用于声明要进行预处理的命令/表单属性 或者 请求参数名称。默认则对所有参数进行预处理。

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface ControllerAdvice {
    @AliasFor("basePackages")
    String[] value() default {};

    @AliasFor("value")
    String[] basePackages() default {};

    Class<?>[] basePackageClasses() default {};

    Class<?>[] assignableTypes() default {};

    Class<? extends Annotation>[] annotations() default {};
}

@ControllerAdvice 只能作用于类上,其属性具体含义如下表所示:

Modifier and Type Element Description
String[] basePackages 指定要进行拦截的包(根包)
String[] value 等同于 basePackages
Class<?>[] basePackageClasses 指定要进行拦截的类所在的包(根包)
Class<?>[] assignableTypes 指定要进行拦截的具体类(类型)
Class<? extends Annotation>[] annotations 指定拦截被相关注解注解的类

示例:为所有控制器(Controller)设置一个全局异常处理器:

@ControllerAdvice
public class ExceptionController {

    @ResponseStatus(HttpStatus.NOT_FOUND) // 404
    @ExceptionHandler({IOException.class, RuntimeException.class})
    public ModelAndView handlerException(Exception e) {
        e.printStackTrace();
        return new ModelAndView("error");
    }
}

数据绑定

当我们发送一个请求时,一般都会携带请求参数,服务器则需要解析请求获取参数值。

比如,我们发送了请求:curl "localhost:8080/params?name=Whyn&password=1234",对于服务器来说,其需要获取参数namepassword的值,进行使用。

对于原生 Servlet 来说,获取请求参数方法如下:

@WebServlet("/params")
public class BindServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        String name = request.getParameter("name");
        String password = request.getParameter("password");
        System.out.println(String.format("%s:%s",name,password));
    }
}

SpringMVC 中,我们同样可以在控制器中接收HttpServletRequestHttpServletResponse对象,从而获取请求参数:

@Controller
public class BindController {

    @GetMapping("/params")
    public void bindParams(HttpServletRequest request, HttpServletResponse response) throws IOException {
        String name = request.getParameter("name");
        String password = request.getParameter("password");
        String result = String.format("%s:%s", name, password);
        System.out.println(result);
        response.getWriter().write(result);
    }
}

上述例子的做法其实是直接使用底层 Servlet API 进行参数获取,这样一方面数据获取需要手动操作,比较繁琐;另一方面,控制器(Controller)由于映射方法接收了HttpServletRequest类型等作为参数,使得Controller和 Servlet 产生了耦合。

其实,SpringMVC 已经为我们提供了对请求参数进行解析和绑定的功能,我们称之为 数据绑定

SpringMVC 提供了对 默认支持类型简单类型实体类型(Bean)复杂类型 以及 自定义类型 的数据绑定功能。

  • 默认支持的类型:如下所示的类型,处理器适配器会自动进行识别并注入到Controller对应参数上:
  1. HttpServletRequest:自动绑定请求对象request
  2. HttpServletResponse:自动绑定响应对象response
  3. HttpSession:自动绑定会话对象session
  4. InputStreamOutputStream:自动绑定输入,输出字节流对象
  5. ReaderWriter:自动绑定输入,输出字符流对象
  6. Map/Model/ModelMap:自动绑定一个模型对象(具体为BindingAwareModelMap),通过该模型对象默认作用域为request域,通过该模型对象可以保存数据到request域中,一般用于向页面传递数据。
    ModelMap继承自LinkedHashMapModel是一个接口,而BindingAwareModelMap extends ModelMap implement Model
  • 简单类型:即自动绑定基本数据类型和字符串数据。
    示例:发送请求:curl "localhost:8080/params?name=Whyn&age=10"
    要求:解析得到参数值
    代码:
@GetMapping("/params")
@ResponseBody
public String bindParams(@RequestParam("name") String name, @RequestParam("age") Integer age) {
    return String.format("%s:%s", name, age);
}

:参数注解@RequestParam指定了请求参数名称,此处可忽略不写,则 SpringMVC 会根据参数名称自动进行数据绑定,通常建议加上@RequestParam注解进行显示指定参数名称。

  • 实体类型:即自动绑定请求参数到 Java Bean 中。
    示例:发送请求:curl "localhost:8080/params?name=Whyn&age=10"
    要求:解析参数,将其值封装到一个User类中。
    代码:
@Controller
public class BindController {

    static class User implements Serializable {
        // 请求参数必须于属性名一致
        private String name;
        private Integer age;

        // 需要提供 setter 方法,让 Spring 进行注入
        public void setName(String name) {
            this.name = name;
        }

        public void setAge(Integer age) {
            this.age = age;
        }

        @Override
        public String toString() {
            return "User{" +
                    "name='" + name + '\'' +
                    ", age=" + age +
                    '}';
        }
    }

    @GetMapping("/params")
    @ResponseBody
    public String bindParams(User user) {
        return user.toString();
    }
}

解析:当请求参数要封装到 Java Bean 类时,要求 Bean 类对象属性名与请求参数名一致才会进行注入:即name赋给user.nameage赋给user.age(通过反射setter函数注入)。

如果 Java Bean 内还嵌套有 Java Bean,如下代码所示:

static class Book implements Serializable {
    private String name;
    private Double price;

    public void setName(String name) {
        this.name = name;
    }

    public void setPrice(Double price) {
        this.price = price;
    }

    @Override
    public String toString() {
        return "Book{" +
                "name='" + name + '\'' +
                ", price=" + price +
                '}';
    }
}

static class User implements Serializable {
    // 请求参数必须于属性名一致
    private String name;
    private Integer age;
    // 嵌套 Java Bean
    private Book book;

    // 需要提供 setter 方法,让 Spring 进行注入
    public void setName(String name) {
        this.name = name;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    public void setBook(Book book) {
        this.book = book;
    }

    // 对于嵌套 bean 类,需要提供 getter 函数
    public Book getBook() {
        return book;
    }

    @Override
    public String toString() {
        return "User{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", book=" + book +
                '}';
    }
}

:对于嵌套 Bean 需提供相应getter函数才能注入成功。

User类内部嵌套了一个 Bean 类Book,此时如果还想能进行自动数据绑定,则需要在发送请求时,指明参数具体类型,比如:
示例:发送请求:curl "localhost:8080/params?name=Whyn&age=10&book.name=SpringMVC&book.price=100.00"
要求:解析参数,自动绑定到一个User类中。
代码:

@RequestMapping("/params")
@ResponseBody
public String bindParams(User user) {
    return user.toString();
}

分析:请求参数book.namebook.price,前缀book表示嵌套 Bean 在外部 Bean 的名称,后缀表示嵌套 Bean 的各个属性名。

  • 复杂类型:即自动绑定集合类型。集合类型大致可以分为以下两种:
  1. 绑定数组类型:主要用于当请求参数具备一个或多个相同名称的时候。
    示例:发送请求:curl "localhost:8080/params?name=Whyn01&name=Whn02"
    要求:解析参数,将参数名相同的值封装到数组中。
    代码:
@RequestMapping("/params")
@ResponseBody
public String bindParams(@RequestParam("name") String[] names) {
    return Arrays.toString(names);
}
  1. 绑定集合对象(ListSetMap:对于集合类型,在 SpringMVC 中,我们需要单独设置一个包装类进行辅助,其属性为请求参数的集合类型(比如List<Book>Set<Book>Map<String,Book>)。
    示例:发送请求:curl "localhost:8080/params" -d "bookList[0].name=SpringMVC01&bookList[0].price=10.0&bookSet[0].name=SpringMVC02&bookSet[0].price=20.0&bookMap['one'].name=SpringMVC03&bookMap['one'].price=30.0"
    要求:解析参数,将参数名相同的值封装到相应集合中。
    代码:
static class BookVo implements Serializable {
    private List<Book> bookList;
    private Set<Book> bookSet;
    private Map<String, Book> bookMap;

    public BookVo() {
        // Set 集合必须进行初始化,且每次传参时,不能超过该集合大小
        this.bookSet = new HashSet<>();
        // Set 集合大小为 1
        this.bookSet.add(new Book());
    }

    public List<Book> getBookList() {
        return bookList;
    }

    public void setBookList(List<Book> bookList) {
        this.bookList = bookList;
    }

    public Set<Book> getBookSet() {
        return bookSet;
    }

    public void setBookSet(Set<Book> bookSet) {
        this.bookSet = bookSet;
    }

    public Map<String, Book> getBookMap() {
        return bookMap;
    }

    public void setBookMap(Map<String, Book> bookMap) {
        this.bookMap = bookMap;
    }

    @Override
    public String toString() {
        return "BookVo{" +
                "bookList=" + bookList +
                ", bookSet=" + bookSet +
                ", bookMap=" + bookMap +
                '}';
    }
}

@PostMapping("/params")
@ResponseBody
public String bindParams(BookVo bookVo) {
    return bookVo.toString();
}

SpringMVC 对于复杂类型的数据绑定支持并不是很好,实际使用中,对于复杂类型,更多的是通过 JSON,XML 等数据格式进行传输。

  • 自定义类型:前台传递的请求参数,对于后台来说,都是字符串类型。对于大多数常见类型,SpringMVC 已经提供了相关转换器进行转换,但对于自定义类型数据,SpringMVC 无法进行转换,此时就需要我们自己手动实现一个转换器,并注册给 SpringMVC

    :在 SpringMVC 中,是通过WebDataBinder来获取参数的,其主要作用是解析 HTTP 请求的上下文,然后在控制器(Controller)调用之前转换参数并提供验证功能,为调用 Controller 方法做准备。

    WebDataBinder内部含有三种转换器:

    1. Converter:普通类型转换器,用于普通类型之间的一对一的转换(即一种类型转换为另一种类型),比如StringInteger
    2. Formatter:格式化转换器,用于日期,货币等字符串按约定的格式转换为对应类型。
    3. GenericConverter:泛型转换器,用于将 HTTP 请求参数转换为数组。

    示例:我们为类Book增加一个属性:发布日期publishDate,其格式为yyyy-MM-dd
    前台会发送请求:curl "localhost:8080/params?name=SpringMVC&price=100.0&publishDate=2019-08-31"
    要求:解析参数,封装到一个Book对象中。
    分析:对于请求参数namepriceSpringMVC 内置转换器已可进行数据类型正确转换,但对于参数publishDateSpringMVC 无法进行转换,因此,我们需要手动实现一个转换器,并注册给 SpringMVC,具体操作如下:

    1. 自定义一个转换器:String2DateConverter
    import org.springframework.core.convert.converter.Converter;
    
    public class String2DateConverter implements Converter<String, Date> {
        @Override
        public Date convert(String source) {
            String dateFormat = "yyyy-MM-dd";
            try {
                return new SimpleDateFormat(dateFormat).parse(source);
            } catch (ParseException e) {
                throw new IllegalArgumentException("无效日期格式,请使用:" + dateFormat);
            }
        }
    }
    
    1. 注册自定义转换器,并配置让其生效:
    <!-- springmvc.xml -->
    <beans xmlns="http://www.springframework.org/schema/beans"
        ...
        <!-- 配置类型转换器 -->
        <bean id="converterService" class="org.springframework.format.support.FormattingConversionServiceFactoryBean">
            <property name="converters">
                <set>
                    <!-- 注入自定义类型转换器 -->
                    <bean class="com.yn.convverter.String2DateConverter"/>
                </set>
            </property>
        </bean>
        <!-- 开启SpringMVC注解,并使能自定义类型转换器 -->
        <mvc:annotation-driven conversion-service="converterService"/>
    </beans>
    
    1. 完成控制器代码
    @Controller
    public class DataBindController {
    
        static class Book implements Serializable {
            private String name;
            private Double price;
    
            private Date publishDate;
    
            public Date getPublishDate() {
                return publishDate;
            }
    
            public void setPublishDate(Date publishDate) {
                this.publishDate = publishDate;
            }
            ...
        }
    
        @RequestMapping("/params")
        @ResponseBody
        public String bindParams(Book book) {
            return book.toString();
        }
    }
    

拦截器(HandlerInterceptor)

  • 拦截器(HandlerInterceptor)SpringMVC 中的拦截器(HandlerInterceptor)与 Servlet 中的过滤器(Filter)功能类似,都是用于拦截请求。区别在于 Filter 是对 Servlet 进行拦截,HandlerInterceptor 是对 Controller 进行拦截。

Filter 和 HandlerInterceptor 的一次完整请求过程大致如下图所示:

理解上面这张图,应该就可以很清晰地掌握 Filter 和 HandlerInterceptor 的关系与执行模型。

:对于 tomcat 服务器来说,所有的资源访问都是通过 Servlet 来实现的,对于静态资源,其使用的是 DefaultServlet 来处理。
因此,Filter 可以拦截静态资源,而 HandlerInterceptor 由于只能拦截 Controller 方法,故其不能拦截静态资源。

下面看下 SpringMVC 的 HandlerInterceptor 接口:

HandlerInterceptor

HandlerInterceptor 提供了三个接口方法:

  • preHandle:该方法在Controller映射方法执行前调用,用以对请求做一些预处理操作。需要注意的是,该方法是 HandlerInterceptor 接口方法中唯一一个有返回值的接口,其返回值类型为boolean:当返回true时,表示继续执行处理器执行链(HandlerExecutionChain)的下一结点;当返回false时,表示打断处理器执行链,直接由该方法处理请求给到 DispatcherServlet,完成请求。因此,处理器执行链的后续节点不会得到执行。
  • postHandle:该方法在Controller映射方法执行后,返回ModelAndView之前执行。
  • afterCompletion:该方法视图页面渲染完成后执行。

postHandle对于@ResponseBodyResponseEntity方法来说作用相对有限,因为这些方法在HandlerAdapter内部的响应就会被写入并进行提交,时序上优先于postHandle,因此postHandle此时就无法对响应进行修改。在这种场景下,可以通过实现ResponseBodyAdvice,并将其声明为一个 Controller Advice bean 或者直接通过RequestMappingHandlerAdapter进行配置。

构建一个 HandlerInterceptor 很简单,只需两个步骤:

  1. 首先构建一个 HandlerInterceptor 类:
public class InterceptorOne implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println(this.getClass().getSimpleName()+" preHandle");
        return true; // don't break the chain
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        System.out.println(this.getClass().getSimpleName()+" postHandler");
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        System.out.println(this.getClass().getSimpleName()+" afterCompletion");
    }
}
  1. 对创建的 HandlerInterceptor 类进行配置:
<!-- ApplicatioinContext.xml -->
<!-- 拦截器 -->
<mvc:interceptors>
    <!-- 配置拦截器 -->
    <mvc:interceptor>
        <!-- 配置拦截路径,/**:表示拦截所有URL及其子路径-->
        <mvc:mapping path="/**"/>
        <!-- 配置拦截器对象 -->
        <bean class="com.yn.interceptor.InterceptorOne"/>
    </mvc:interceptor>
</mvc:interceptors>

文件上传/下载

  • 文件上传原理:文件上传是以multipart/form-data的 MIME 类型进行上传,此时请求报文的Content-Type会随机生成一个 boundary(分界符),这个 boundary 标记请求体的一段数据(多个文件用多个 boundary 进行分隔)。也即服务端只需查找请求头Content-Type,获取 boundary,然后根据该 boundary,在请求体中第一次出现该 boundary 的位置到第二次出现该 boundary 的位置的中间部分即为上传文件的内容,具体格式如下所示:
文件上传格式

:文件上传 from 表单的请求正文类型enctype必须为multipart/form-data,其他客户端则设置Content-Type:multipart/datamethod的取值必须为post

这里我们借助开源库 commons-fileupload 对上传文件进行解析,首先导入该库:

<dependency>
    <groupId>commons-fileupload</groupId>
    <artifactId>commons-fileupload</artifactId>
    <version>1.4</version>
</dependency>

对于文件上传,传统方式是对request对象进行解析,获取请求体内容,再解析出文件内容,进行保存。

而在 SpringMVC 中,其是由组件构建完成请求,因此,我们只需添加一个文件上传解析器,SpringMVC 就能自动帮我们将上传文件解析处理(具体为MultipartFile对象),因此,控制器映射方法只需接收解析出来的文件即可。

文件解析器配置方法如下:

<!-- springmvc.xml -->
<!-- 配置文件上传解析器 -->
<!-- id 必须为 multipartResolver -->
<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver"/>

到这里,我们就可以实现文件上传了。

常见的文件上传有三种方式,以下依次进行讲解:

  • 单文件上传:每次只上传一个文件,具体步骤如下:
  1. 前端页面写一个单文件上传 form 表单:
<h1>上传单文件</h1>
<form enctype="multipart/form-data" method="post" action="/uploadSingleFile">
    <input type="file" name="uploadFile"/>
    <input type="submit" value="提交"/>
</form>
  1. 控制器映射方法接收上传文件,并进行磁盘保存操作:
@RestController
public class UploadController {

    // 支持单文件上传
    @PostMapping("/uploadSingleFile")
    public String uploadSingleFile(HttpServletRequest request,
                                   @RequestParam("uploadFile") MultipartFile uploadFile) throws IOException {
        // 创建上传目录
        String path = request.getServletContext().getRealPath("/uploads/");
        File dir = new File(path);
        if (!dir.exists()) {
            dir.mkdir();
        }

        // 获取上传文件名
        String filename = uploadFile.getOriginalFilename();
        // 写入磁盘
        uploadFile.transferTo(new File(dir, filename));
        return filename + " upload successful";
    }
}
  • 多文件上传:每次上传多个文件,具体步骤如下:
  1. 前端页面 form 表单支持多文件选择:
<h1>上传多文件</h1>
<form enctype="multipart/form-data" method="post" action="/uploadMultiFiles">
    <input type="file" name="uploadFiles" multiple="multiple"/>
    <input type="submit" value="提交"/>
</form>

:其实就是多个multiple属性。

  1. 多文件上传与单文件上传其实实现很相似,借助于 SpringMVC 数据绑定功能,我们只需将控制器映射方法设成数组即可:
@RestController
public class UploadController {
    // 支持多文件上传
    @PostMapping("/uploadMultiFiles")
    public String uploadMultiFiles(HttpServletRequest request,
                                  @RequestParam("uploadFiles")MultipartFile[] uploadFiles) throws IOException {
        // 创建上传目录
        String path = request.getServletContext().getRealPath("/uploads/");
        File dir = new File(path);
        if (!dir.exists()) {
            dir.mkdir();
        }


        for (MultipartFile file : uploadFiles) {
            // 获取上传文件名
            String filename = file.getOriginalFilename();
            System.out.println("upload file: " + filename);
            // 写入磁盘
            file.transferTo(new File(dir, filename));
            System.out.println("upload file done: " + filename);
        }
        return "upload files done";
    }
}
  • 多文件上传+额外表单字段:有时候可能需要对上传文件进行一些额外信息描述,此时除了上传文件外,还需上传一些额外字段:
  1. 前端页面 form 表单增加额外字段:
<h1>上传多文件</h1>
<form enctype="multipart/form-data" method="post" action="/uploadMultiFiles">
    name:<input type="text" name="name"/>
    <br>
    description:<input type="text" name="description"/>
    <br>
    upload files:<input type="file" name="uploadFiles" multiple="multiple"/>
    <br>
    <input type="submit" value="提交"/>
</form>
  1. 后端我们使用一个新的 Java Bean 类,用于 SpringMVC 封装表单数据:
@RestController
public class UploadController {

    // 封装表单数据
    static class UploadFiles implements Serializable {
        private String name;
        private String description;
        private List<MultipartFile> uploadFiles;

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        public String getDescription() {
            return description;
        }

        public void setDescription(String description) {
            this.description = description;
        }

        public List<MultipartFile> getUploadFiles() {
            return uploadFiles;
        }

        public void setUploadFiles(List<MultipartFile> uploadFiles) {
            this.uploadFiles = uploadFiles;
        }

        @Override
        public String toString() {
            return "UploadFiles{" +
                    "name='" + name + '\'' +
                    ", description='" + description + '\'' +
                    ", uploadFiles=" + uploadFiles +
                    '}';
        }
    }

    // 支持多文件上传
    @PostMapping("/uploadMultiFiles")
    public String uploadMultiFiles(HttpServletRequest request,
                                   @ModelAttribute UploadFiles uploadFiles) throws IOException {
        // 创建上传目录
        String path = request.getServletContext().getRealPath("/uploads/");
        File dir = new File(path);
        if (!dir.exists()) {
            dir.mkdir();
        }

        System.out.println(uploadFiles.getName());
        System.out.println(uploadFiles.getDescription());

        List<MultipartFile> files = uploadFiles.getUploadFiles();
        for (MultipartFile file : files) {
            // 获取上传文件名
            String filename = file.getOriginalFilename();
            System.out.println("upload file: " + filename);
            // 写入磁盘
            file.transferTo(new File(dir, filename));
            System.out.println("upload file done: " + filename);
        }
        return "upload files done";
    }
}
  • 文件下载原理:服务端response对象可以获取输出流,通过输出流将文件输入流写到浏览器即可完成文件下载功能。

文件下载的关键是设置以下两个头信息:

  1. 设置响应文件的 MIME 类型:ContentType:xxx/yyyy
    :如果想浏览器一直弹出下载对话框,可直接设置:Content-Type:application/octet-streamContent-Type:application/x-msdownload

  2. 设置文件下载名称:Content-Disposition:attachement;filename=下载名称

示例:下载一个图片文件。

具体步骤如下:

  1. 假设我们要下载的图片的真实路径为/WEB-INF/downloads/images/
downloads
  1. 由于/WEB-INF目录为默认安全目录,浏览器无法直接访问。因此服务器端通过一个控制器进行转发获取资源:
@Controller
public class DownloadController {
    @GetMapping("/downloads/{filename:.+}") // .+ 表示不过滤 PathVariable 点后缀
    public void download(HttpServletRequest request,
                         HttpServletResponse response,
                         @PathVariable("filename") String filename) throws IOException {
        ServletContext context = request.getServletContext();
        String downloadDir = context.getRealPath("WEB-INF/downloads/images/");
        Path file = Paths.get(downloadDir, filename);
        System.out.println(filename + " exists? " + Files.exists(file));
        if (Files.exists(file)) {
            String fileMime = context.getMimeType(filename);
            System.out.println(filename + ":" + fileMime);
            response.setContentType(fileMime);
            response.addHeader("Content-Disposition", "attachment; filename=" + filename);
            Files.copy(file, response.getOutputStream());
            response.getOutputStream().flush();
        }
    }
}

其他

  • 不拦截静态资源:在 SpringMVC 中,前端控制器(DispatcherServlet)一般配置为对所有请求进行拦截,这同时包括了对静态资源(js,css,images...)的拦截,导致静态资源无法获取。因此,我们需要配置前端控制器,让其不对静态资源进行拦截,具体的配置方法有如下两种:
    1. 配置 default servlet,优先处理请求:
    <!-- springmvc.xml -->
    <mvc:default-servlet-handler/>
    
    1. 手动指定静态资源映射及路径,让前端控制器不进行拦截:
    <!-- springmvc.xml -->
    <mvc:resources mapping="/static/**" location="/static/" />
    <mvc:resources mapping="/resources/**" location="/resources/" />
    <mvc:resources mapping="/images/**" location="/images/" />
    <mvc:resources mapping="/js/**" location="/js/" />
    
  • JSON 支持:为控制器添加接收 JSON 参数和返回 JSON 数据。当前很多应用都采用了 Restful 请求,通讯格式基本都采用 JSON 进行数据交互,我们可以配置 SpringMVC,让其支持 JSON 数据绑定,具体步骤如下:
  1. 添加 JSON 支持库,这里我们使用 jackson
<properties>
    <jackson.version>2.9.9</jackson.version>
</properties>

<dependencies>
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-core</artifactId>
        <version>${jackson.version}</version>
    </dependency>

    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
        <version>${jackson.version}</version>
    </dependency>
</dependencies>
  1. 编写后端接收 JSON 数据控制器:
@Controller
@RequestMapping("/user")
public class JsonController {

    static class User implements Serializable {
        private String name;
        private Integer age;

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        public Integer getAge() {
            return age;
        }

        public void setAge(Integer age) {
            this.age = age;
        }

        @Override
        public String toString() {
            return "User{" +
                    "name='" + name + '\'' +
                    ", age=" + age +
                    '}';
        }
    }
    @RequestMapping("/json")
    public @ResponseBody User jsonUser(@RequestBody User user){
        System.out.println(user);
       return user;
    }
}
  1. 前台页面使用 jQuery 发送 JSON 数据:
<script src="https://cdn.bootcss.com/jquery/3.4.1/jquery.min.js"></script>
<script>
    $(function () {
       $('#btn').click(()=>{
           $.ajax({
               url:'user/json',
               type:'post',
               contentType:'application/json;charset=utf-8',
               data:'{"name":"Whyn","age":10}',
               success:function(data){
                   alert(data);
               }
           });
       });
    });
</script>

@ResponseBody 注解可以通过内置的 9 种HttpMessageConverter,匹配不同的Controller返回值类型,然后进行不同的消息转换处理。
对于 POJO 类型数据,使用的是MappingJacksonHttpMessageConverter转换器,该转换器可以将 Java 对象转换成 JSON 数据返回给客户端。

  • 解决中文乱码:请求体和响应体都可能携带中文:
  1. 请求携带中文:
    1)对于 Get 请求,在 Tomcat 8 以后,统一采用 UTF-8 格式接收请求,此时就无须进行编码转换了(前提:网页编码使用的是 UTF-8)。
    2)对于 Post 请求,参数位于请求体,如果请求头Content-Type没有指定请求体编码方式,那么 SpringMVC 默认使用 ISO-8859-1 进行处理,那么对于客户端发送的 utf-8 数据,则会产生乱码。

    解决方案:SpringMVC 为我们提供了一个编码过滤器,可以通过设置该编码过滤器让 web 容器使用 utf-8 编码解析请求参数:

    <!-- web.xml -->
    <!-- 解决中文乱码过滤器 -->
    <filter>
        <filter-name>characterEncodingFilter</filter-name>
        <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
        <init-param>
            <param-name>encoding</param-name>
            <param-value>UTF-8</param-value>
        </init-param>
        <init-param>
            <param-name>forceEncoding</param-name>
            <param-value>true</param-value>
        </init-param>
    </filter>
    <filter-mapping>
        <filter-name>characterEncodingFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>
    
  2. 对于响应:使用 @ResponseBody 返回字符串响应时,SpringMVC 默认使用 ISO-8859-1 格式进行编码,因此客户端接收到数据后,使用 utf-8 格式进行解码,此时就会出现乱码。

    解决方案:配置 SpringMVC 响应使用 utf-8 编码:

    <!-- springmvc.xml -->
    <!-- 开启SpringMVC注解  -->
    <mvc:annotation-driven >
        <!-- 消息转换器 -->
        <mvc:message-converters register-defaults="true">
            <bean class="org.springframework.http.converter.StringHttpMessageConverter">
                <property name="supportedMediaTypes">
                    <list>
                        <value>text/plain;charset=UTF-8</value>
                        <value>text/html;charset=UTF-8</value>
                    </list>
                </property>
            </bean>
        </mvc:message-converters>
    </mvc:annotation-driven>
    

参考

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,602评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,442评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,878评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,306评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,330评论 5 373
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,071评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,382评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,006评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,512评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,965评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,094评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,732评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,283评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,286评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,512评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,536评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,828评论 2 345