摘要: 原创出处 http://www.iocoder.cn/Spring-Boot/SpringMVC/ 「芋道源码」欢迎转载,保留摘要,谢谢!

  • 1. 概述
  • 2. 快速入门
  • 3. 测试接口
  • 4. 全局统一返回
  • 5. 全局异常处理
  • 6. HandlerInterceptor 拦截器
  • 7. Servlet、Filter、Listener
  • 8. Cors 跨域
  • 9. HttpMessageConverter 消息转换器
  • 10. 整合 Fastjson
  • 666. 彩蛋

本文在提供完整代码示例,可见 https://github.com/YunaiV/SpringBoot-Labs 的 lab-23 目录。

原创不易,给点个 Star 嘿,一起冲鸭!

1. 概述

如果胖友接触 Java Web 开发比较早,那么可能会了解到如下 Web MVC 框架,当年是 Struts2 与 SpringMVC 双雄争霸的年代。甚至说,我们在面试的时候,就特问:“SpringMVC 和 Struts2 的区别是什么?”。

关于这个问题,如果感兴趣,可以看看 《Struts2 和 SpringMVC 区别?》 讨论。

而现在,SpringMVC 基本已经统治 Web MVC 框架,相信胖友已经很少接触非使用 SpringMVC 的项目了。在艿艿实习那会,大概是 2011 年的时候,还经历了一次将项目从 Struts2 迁移到 SpringMVC 。

相比来说,SpringMVC 的易用性与性能都优于 Struts2 ,整体实现也更加清晰明了。当然,更更更重要的是,它有个好爸爸,Spring 极强的体系与社区活跃度。

因为是一篇 Spring Boot 集成 SpringMVC 入门的文章,艿艿就不多哔哔了,直接快速入门,遨游起来。不过还是提一句,SpringMVC 处理请求的整体流程,一定要能倒背如流。

2. 快速入门

示例代码对应仓库:lab-springmvc-23-01 。

本小节,我们会使用 spring-boot-starter-web 实现 SpringMVC 的自动化配置。然后实现用户的增删改查接口。接口列表如下:

请求方法URL功能
GET/users查询用户列表
GET/users/{id}获得指定用户编号的用户
POST/users添加用户
PUT/users/{id}更新指定用户编号的用户
DELETE/users/{id}删除指定用户编号的用户

下面,开始遨游~

2.1 注解

可能有胖友之前未使用过 SpringMVC ,所以在这个小节,我们来说下它提供的注解。

  • @Controller
  • @RestController
  • @RequestMapping
  • @GetMapping
  • @PostMapping
  • @PutMapping
  • @RequestParam
  • @PathVariable

已经了解过的胖友,可以快速略过或不看。

2.1.1 @Controller

@Controller 注解,添加在类上,表示这是控制器 Controller 对象。属性如下:

  • name 属性:该 Controller 对象的 Bean 名字。允许空。

@RestController 注解,添加在类上,是 @Controller 和 @ResponseBody 的组合注解,直接使用接口方法的返回结果,经过 JSON/XML 等序列化方式,最终返回。也就是说,无需使用 InternalResourceViewResolver 解析视图,返回 HTML 结果。

目前主流的架构,都是 前后端分离 的架构,后端只需要提供 API 接口,仅仅返回数据。而视图部分的工作,全部交给前端来做。也因此,我们项目中 99.99% 使用 @RestController 注解。

往往,我们提供的 API 接口,都是 Restful 或者类 Restful 风格,所以不了解的胖友,推荐看看如下两篇文章:

  • 《RESTful API 最佳实践》
  • 《跟着 Github 学习 Restful HTTP API 的优雅设计》

2.1.2 @RequestMapping

@RequestMapping 注解,添加在类或方法上,标记该类/方法对应接口的配置信息。

@RequestMapping 注解的常用属性,如下:

  • path 属性:接口路径。[] 数组,可以填写多个接口路径。
  • values 属性:和 path 属性相同,是它的别名。
  • method 属性:请求方法 RequestMethod ,可以填写 GETPOSTPOSTDELETE 等等。[] 数组,可以填写多个请求方法。如果为空,表示匹配所有请求方法。

@RequestMapping 注解的不常用属性,如下:

  • name 属性:接口名。一般情况下,我们不填写。

  • params 属性:请求参数需要包含值的参数名。可以填写多个参数名。如果为空,表示匹配所有请你求方法。

  • headers 属性:和 params 类似,只是从参数名变成请求头

  • consumes 属性:和 params 类似,只是从参数名变成请求头的提交内容类型( Content-Type )

  • produces 属性:和 params 类似,只是从参数名变成请求头的( Accept )可接受类型

    艿艿:关于 consumes 和 produces 属性,可以看看 《Http 请求中 Content-Type 和 Accept 讲解以及在 Spring MVC 中的应用》 文章,更加详细。

考虑到让开发更加方便,Spring 给每种请求方法提供了对应的注解:

  • @GetMapping 注解:对应 @GET 请求方法的 @RequestMapping注解。
  • @PostMapping 注解:对应 @POST 请求方法的@RequestMapping 注解。
  • @PutMapping 注解:对应 @PUT 请求方法的 @RequestMapping注解。
  • @DeleteMapping 注解:对应 @DELETE 请求方法的 @RequestMapping 注解。
  • 还有其它几个,就不一一列举了。

2.1.3 @RequestParam

@RequestParam 注解,添加在方法参数上,标记该方法参数对应的请求参数的信息。属性如下:

  • name 属性:对应的请求参数名。如果为空,则直接使用方法上的参数变量名。
  • value 属性:和 name 属性相同,是它的别名。
  • required 属性:参数是否必须传。默认为 true ,表示必传。
  • defaultValue 属性:参数默认值。

@PathVariable 注解,添加在方法参数上,标记接口路径和方法参数的映射关系。具体的,我们在示例中来看。相比 @RequestParam注解,少一个 defaultValue 属性。

下面,让我们快速编写一个 SpringMVC 的示例。

2.2 引入依赖

在 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">

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.3.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>lab-springmvc-23-01</artifactId>

    <dependencies>
        <!-- 实现对 Spring MVC 的自动化配置 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!-- 方便等会写单元测试 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

    </dependencies>

</project>

具体每个依赖的作用,胖友自己认真看下艿艿添加的所有注释噢。

2.3 Application

创建 Application.java 类,配置 @SpringBootApplication 注解即可。代码如下:

@SpringBootApplication
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }

}

先暂时不启动项目。等我们添加好 Controller 。

2.4 UserController

在 cn.iocoder.springboot.lab23.springmvc 包路径下,创建 UserController 类。代码如下:

// UserController.java

@RestController
@RequestMapping("/users")
public class UserController {

    /**
     * 查询用户列表
     *
     * @return 用户列表
     */

    @GetMapping("")
    public List<UserVO> list() {
        // 查询列表
        List<UserVO> result = new ArrayList<>();
        result.add(new UserVO().setId(1).setUsername("yudaoyuanma"));
        result.add(new UserVO().setId(2).setUsername("woshiyutou"));
        result.add(new UserVO().setId(3).setUsername("chifanshuijiao"));
        // 返回列表
        return result;
    }

    /**
     * 获得指定用户编号的用户
     *
     * @param id 用户编号
     * @return 用户
     */

    @GetMapping("/{id}")
    public UserVO get(@PathVariable("id") Integer id) {
        // 查询并返回用户
        return new UserVO().setId(id).setUsername("username:" + id);
    }

    /**
     * 添加用户
     *
     * @param addDTO 添加用户信息 DTO
     * @return 添加成功的用户编号
     */

    @PostMapping("")
    public Integer add(UserAddDTO addDTO) {
        // 插入用户记录,返回编号
        Integer returnId = 1;
        // 返回用户编号
        return returnId;
    }

    /**
     * 更新指定用户编号的用户
     *
     * @param id 用户编号
     * @param updateDTO 更新用户信息 DTO
     * @return 是否修改成功
     */

    @PutMapping("/{id}")
    public Boolean update(@PathVariable("id") Integer id, UserUpdateDTO updateDTO) {
        // 将 id 设置到 updateDTO 中
        updateDTO.setId(id);
        // 更新用户记录
        Boolean success = true;
        // 返回更新是否成功
        return success;
    }

    /**
     * 删除指定用户编号的用户
     *
     * @param id 用户编号
     * @return 是否删除成功
     */

    @DeleteMapping("/{id}")
    public Boolean delete(@PathVariable("id") Integer id) {
        // 删除用户记录
        Boolean success = false;
        // 返回是否更新成功
        return success;
    }

}
  • 在类上,添加 @RestController 注解,表示直接返回接口结果。默认情况下,使用 JSON 作为序列化方式。

  • 在类上,添加 @RequestMapping("/users") 注解,表示 UserController 所有接口路径,以 /users 开头。

  • #list() 方法,查询用户列表。请求对应 GET /users,请求结果为:

    [
        {
            "id"1,
            "username""yudaoyuanma"
        },
        {
            "id"2,
            "username""woshiyutou"
        },
        {
            "id"3,
            "username""chifanshuijiao"
        }
    ]
    • 其中,UserVO 为用户返回 VO 类。
  • #get(Integer id) 方法,获得指定用户编号的用户。请求对应GET /users/{id} 【路径参数】,请求你结果为:

    {
        "id"1,
        "username""username:1"
    }
  • #add(UserAddDTO addDTO) 方法,添加用户。请求对应 POST /users ,请求结果为:

    1
    • 因为我们这里返回的是 Integer 类型,对于非 POJO 对象,所以无需使用 JSON 序列化返回。
    • 其中,UserAddDTO 为用户添加 DTO 类。
  • #update(Integer id, UserUpdateDTO updateDTO) 方法,更新指定用户编号的用户。请求对应 PUT /users/{id} 【路径参数】,请求结果为:

    true
    • 其中,UserUpdateDTO 为用户更新 DTO 类。
  • #delete(Integer id) 方法,删除指定用户编号的用户。请求对应 DELETE /users/{id} 【路径参数】,请求结果为:

    false

以上的测试,肯定需要通过运行 Application ,启动项目。这里,补充下它的启动日志如下:

2019-11-15 18:46:00.671  INFO 99493 --- [           main] c.i.s.lab23.springmvc.Application        : Starting Application on MacBook-Pro-8 with PID 99493 (/Users/yunai/Java/SpringBoot-Labs/lab-23/lab-springmvc-23-01/target/classes started by yunai in /Users/yunai/Java/SpringBoot-Labs)
2019-11-15 18:46:00.673  INFO 99493 --- [           main] c.i.s.lab23.springmvc.Application        : No active profile set, falling back to default profiles: default
2019-11-15 18:46:01.593  INFO 99493 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 8080 (http)
2019-11-15 18:46:01.613  INFO 99493 --- [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
2019-11-15 18:46:01.613  INFO 99493 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet engine: [Apache Tomcat/9.0.16]
2019-11-15 18:46:01.619  INFO 99493 --- [           main] o.a.catalina.core.AprLifecycleListener   : The APR based Apache Tomcat Native library which allows optimal performance in production environments was not found on the java.library.path: [/Users/yunai/Library/Java/Extensions:/Library/Java/Extensions:/Network/Library/Java/Extensions:/System/Library/Java/Extensions:/usr/lib/java:.]
2019-11-15 18:46:01.684  INFO 99493 --- [           main] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2019-11-15 18:46:01.684  INFO 99493 --- [           main] o.s.web.context.ContextLoader            : Root WebApplicationContext: initialization completed in 976 ms
2019-11-15 18:46:01.844  INFO 99493 --- [           main] o.s.s.concurrent.ThreadPoolTaskExecutor  : Initializing ExecutorService 'applicationTaskExecutor'
2019-11-15 18:46:01.987  INFO 99493 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''
2019-11-15 18:46:01.990  INFO 99493 --- [           main] c.i.s.lab23.springmvc.Application        : Started Application in 1.559 seconds (JVM running for 2.146)
  • 我们可以看到,Spring Boot 在启动 SpringMVC 时,会默认初始化一个内嵌的 Tomcat ,监听 8080 端口的请求。

2.5 UserController2

在日常的项目开发中,艿艿只使用 GET 和 POST 请求方法。主要是,实际场景下,因为业务比较复杂,标准的 Restful API 并不能满足所有的操作。例如说,订单有用户取消,管理员取消,修改收货地址,评价等等操作。所以,我们更多的是,提供 Restful API 。

对于 SpringMVC 提供的 @PathVariable 路径参数,艿艿目前也并没有在项目中使用,主要原因如下:

  • 1、封装的权限框架,基于 URL 作为权限标识,暂时是不支持带有路径参数的 URL 。
  • 2、基于 URL 进行告警,而带有路径参数的 URL ,“相同” URL 实际对应的是不同的 URL ,导致无法很方便的实现按照单位时间请求错误次数告警。
  • 3、@PathVariable 路径参数的 URL ,会带来一定的 SpringMVC 的性能下滑。具体可以看看 《SpringMVC RESTful 性能优化》 文章。

所以,我们创建 UserController2 类,修改 API 接口。最终代码如下:

// UserController2.java

@RestController
@RequestMapping("/users2")
public class UserController2 {

    /**
     * 查询用户列表
     *
     * @return 用户列表
     */

    @GetMapping("/list"// URL 修改成 /list
    public List<UserVO> list() {
        // 查询列表
        List<UserVO> result = new ArrayList<>();
        result.add(new UserVO().setId(1).setUsername("yudaoyuanma"));
        result.add(new UserVO().setId(2).setUsername("woshiyutou"));
        result.add(new UserVO().setId(3).setUsername("chifanshuijiao"));
        // 返回列表
        return result;
    }

    /**
     * 获得指定用户编号的用户
     *
     * @param id 用户编号
     * @return 用户
     */

    @GetMapping("/get"// URL 修改成 /get
    public UserVO get(@RequestParam("id") Integer id) {
        // 查询并返回用户
        return new UserVO().setId(id).setUsername(UUID.randomUUID().toString());
    }

    /**
     * 添加用户
     *
     * @param addDTO 添加用户信息 DTO
     * @return 添加成功的用户编号
     */

    @PostMapping("add"// URL 修改成 /add
    public Integer add(UserAddDTO addDTO) {
        // 插入用户记录,返回编号
        Integer returnId = UUID.randomUUID().hashCode();
        // 返回用户编号
        return returnId;
    }

    /**
     * 更新指定用户编号的用户
     *
     * @param updateDTO 更新用户信息 DTO
     * @return 是否修改成功
     */

    @PostMapping("/update"// URL 修改成 /update ,RequestMethod 改成 POST
    public Boolean update(UserUpdateDTO updateDTO) {
        // 更新用户记录
        Boolean success = true;
        // 返回更新是否成功
        return success;
    }

    /**
     * 删除指定用户编号的用户
     *
     * @param id 用户编号
     * @return 是否删除成功
     */

    @DeleteMapping("/delete"// URL 修改成 /delete ,RequestMethod 改成 DELETE
    public Boolean delete(@RequestParam("id") Integer id) {
        // 删除用户记录
        Boolean success = false;
        // 返回是否更新成功
        return success;
    }

}
  • 每一处的修改,看下 @XXXMapping 注解后的注释说明。

3. 测试接口

示例代码对应仓库:lab-springmvc-23-01 。

在开发完接口,我们会进行接口的自测。一般情况下,我们先启动项目,然后使用 Postman、curl、浏览器,手工模拟请求后端 API 接口。

实际上,SpringMVC 提供了测试框架 MockMvc ,方便我们快速测试接口。下面,我们对 「2.4 UserController」 提供的接口,进行下单元测试。也就是说,本小节,我们会继续在 lab-springmvc-23-01 示例的基础上修改。

MockMvc 提供了集成测试和单元测试的能力,我们分成 「3.1 集成测试」 和 「3.2 单元测试」 来看。如果胖友对测试这块不太了解,可以看看如下两篇文章:

  • 《小谈 Java 单元测试》
  • 《谈谈单元测试》

3.1 集成测试

创建 UserControllerTest 测试类,我们来测试一下简单的 UserController 的每个操作。核心代码如下:

// UserControllerTest.java

@RunWith(SpringRunner.class)
@SpringBootTest(classes = Application.class)
@AutoConfigureMockMvc
public class UserControllerTest {

    @Autowired
    private MockMvc mvc;

    @Test
    public void testList() throws Exception {
        // 查询用户列表
        ResultActions resultActions = mvc.perform(MockMvcRequestBuilders.get("/users"));
        // 校验结果
        resultActions.andExpect(MockMvcResultMatchers.status().isOk()); // 响应状态码 200
        resultActions.andExpect(MockMvcResultMatchers.content().json("[\n" +
                "    {\n" +
                "        \"id\": 1,\n" +
                "        \"username\": \"yudaoyuanma\"\n" +
                "    },\n" +
                "    {\n" +
                "        \"id\": 2,\n" +
                "        \"username\": \"woshiyutou\"\n" +
                "    },\n" +
                "    {\n" +
                "        \"id\": 3,\n" +
                "        \"username\": \"chifanshuijiao\"\n" +
                "    }\n" +
                "]")); // 响应结果
    }

    @Test
    public void testGet() throws Exception {
        // 获得指定用户编号的用户
        ResultActions resultActions = mvc.perform(MockMvcRequestBuilders.get("/users/1"));
        // 校验结果
        resultActions.andExpect(MockMvcResultMatchers.status().isOk()); // 响应状态码 200
        resultActions.andExpect(MockMvcResultMatchers.content().json("{\n" +
                "\"id\": 1,\n" +
                "\"username\": \"username:1\"\n" +
                "}")); // 响应结果
    }

    @Test
    public void testAdd() throws Exception {
        // 获得指定用户编号的用户
        ResultActions resultActions = mvc.perform(MockMvcRequestBuilders.post("/users")
            .param("username""yudaoyuanma")
            .param("passowrd""nicai"));
        // 校验结果
        resultActions.andExpect(MockMvcResultMatchers.status().isOk()); // 响应状态码 200
        resultActions.andExpect(MockMvcResultMatchers.content().string("1")); // 响应结果
    }

    @Test
    public void testUpdate() throws Exception {
        // 获得指定用户编号的用户
        ResultActions resultActions = mvc.perform(MockMvcRequestBuilders.put("/users/1")
                .param("username""yudaoyuanma")
                .param("passowrd""nicai"));
        // 校验结果
        resultActions.andExpect(MockMvcResultMatchers.status().isOk()); // 响应状态码 200
        resultActions.andExpect(MockMvcResultMatchers.content().string("true")); // 响应结果
    }

    @Test
    public void testDelete() throws Exception {
        // 获得指定用户编号的用户
        ResultActions resultActions = mvc.perform(MockMvcRequestBuilders.delete("/users/1"));
        // 校验结果
        resultActions.andExpect(MockMvcResultMatchers.status().isOk()); // 响应状态码 200
        resultActions.andExpect(MockMvcResultMatchers.content().string("false")); // 响应结果
    }

}
  • 在类上,我们添加了 @AutoConfigureMockMvc 注解,用于自动化配置我们稍后注入的 MockMvc Bean 对象 mvc 。在后续的测试中,我们会看到都是通过 mvc 调用后端 API 接口。而每一次调用后端 API 接口,都会执行真正的后端逻辑。因此,整个逻辑,走的是集成测试,会启动一个真实的 Spring 环境。
  • 每次 API 接口的请求,都通过 MockMvcRequestBuilders 来构建。构建完成后,通过 mvc 执行请求,返回 ResultActions 结果。
  • 执行完请求后,通过调用 ResultActions 的 andExpect(ResultMatcher matcher) 方法,添加对结果的预期,相当于做断言。如果不符合预期,则会抛出异常,测试不通过。

另外,ResultActions 还有两个方法:

  • #andDo(ResultHandler handler) 方法,添加 ResultHandler 结果处理器,例如说调试时打印结果到控制台,人肉看看结果是否正确。
  • #andReturn() 方法,最后返回相应的 MvcResult 结果。后续,自己针对 MvcResult 写一些自定义的逻辑。

例如说,我们将 #testGet() 方法,使用上述介绍的两个,进行补充改写如下:

// UserControllerTest.java

@Test
public void testGet2() throws Exception {
    // 获得指定用户编号的用户
    ResultActions resultActions = mvc.perform(MockMvcRequestBuilders.get("/users/1"));
    // 校验结果
    resultActions.andExpect(MockMvcResultMatchers.status().isOk()); // 响应状态码 200
    resultActions.andExpect(MockMvcResultMatchers.content().json("{\n" +
            "\"id\": 1,\n" +
            "\"username\": \"username:1\"\n" +
            "}")); // 响应结果

    // <1> 打印结果
    resultActions.andDo(MockMvcResultHandlers.print());

    // <2> 获得 MvcResult ,后续执行各种自定义逻辑
    MvcResult mvcResult = resultActions.andReturn();
    System.out.println("拦截器数量:" + mvcResult.getInterceptors().length);
}
  • <1> 处,打印请求和响应信息。输出如下:

    MockHttpServletRequest:
          HTTP Method = GET
          Request URI = /users/1
           Parameters = {}
              Headers = []
                 Body = null
        Session Attrs = {}

    Handler:
                 Type = cn.iocoder.springboot.lab23.springmvc.controller.UserController
               Method = public cn.iocoder.springboot.lab23.springmvc.vo.UserVO cn.iocoder.springboot.lab23.springmvc.controller.UserController.get(java.lang.Integer)

    Async:
        Async started = false
         Async result = null

    Resolved Exception:
                 Type = null

    ModelAndView:
            View name = null
                 View = null
                Model = null

    FlashMap:
           Attributes = null

    MockHttpServletResponse:
               Status = 200
        Error message = null
              Headers = [Content-Type:"application/json;charset=UTF-8"]
         Content type = application/json;charset=UTF-8
                 Body = {"id":1,"username":"username:1"}
        Forwarded URL = null
       Redirected URL = null
              Cookies = []
  • <2> 处,获得 MvcResult 后,打印下拦截器的数量。输出如下:

    拦截器数量:2

3.2 单元测试

为了更好的展示 SpringMVC 单元测试的示例,我们需要改写 UserController 的代码,让其会依赖 UserService 。修改点如下:

  • 在 cn.iocoder.springboot.lab23.springmvc.service 包路径下,创建 UserService 类。代码如下:

    // UserService.java

    @Service
    public class UserService {

        public UserVO get(Integer id) {
            return new UserVO().setId(id).setUsername("test");
        }

    }
  • 在 UserController 类中,增加 GET /users/v2/{id} 接口,获得指定用户编号的用户。代码如下:

    // UserController.java

    @Autowired
    private UserService userService;

    /**
     * 获得指定用户编号的用户
     *
     * @param id 用户编号
     * @return 用户
     */

    @GetMapping("/v2/{id}")
    public UserVO get2(@PathVariable("id") Integer id) {
        return userService.get(id);
    }
    • 在代码中,我们注入了 UserService Bean 对象 userService,然后在新增的接口方法中,会调用 UserService#get(Integer id) 方法,获得指定用户编号的用户。

创建 UserControllerTest2 测试类,我们来测试一下简单的 UserController 的新增的这个 API 操作。代码如下:

// UserControllerTest2.java

@RunWith(SpringRunner.class)
@WebMvcTest(UserController.class)
public class UserControllerTest2 {

    @Autowired
    private MockMvc mvc;

    @MockBean
    private UserService userService;

    @Test
    public void testGet2() throws Exception {
        // Mock UserService 的 get 方法
        System.out.println("before mock:" + userService.get(1)); // <1.1>
        Mockito.when(userService.get(1)).thenReturn(
                new UserVO().setId(1).setUsername("username:1")); // <1.2>
        System.out.println("after mock:" + userService.get(1)); // <1.3>

        // 查询用户列表
        ResultActions resultActions = mvc.perform(MockMvcRequestBuilders.get("/users/v2/1"));
        // 校验结果
        resultActions.andExpect(MockMvcResultMatchers.status().isOk()); // 响应状态码 200
        resultActions.andExpect(MockMvcResultMatchers.content().json("{\n" +
                "    \"id\": 1,\n" +
                "    \"username\": \"username:1\"\n" +
                "}")); // 响应结果
    }

}
  • 在类上添加 @WebMvcTest 注解,并且传入的是 UserController 类,表示我们要对 UserController 进行单元测试。

  • 同时,@WebMvcTest 注解,是包含了 @AutoConfigureMockMvc的组合注解,所以它会自动化配置我们稍后注入的 MockMvc Bean 对象 mvc 。在后续的测试中,我们会看到都是通过 mvc调用后端 API 接口。但是!每一次调用后端 API 接口,并不会执行真正的后端逻辑,而是走的 Mock 逻辑。也就是说,整个逻辑,走的是单元测试会启动一个 Mock 的 Spring 环境。

    艿艿:注意上面每个加粗的地方!

  • userService 属性,我们添加了 @MockBean 注解,实际这里注入的是一个使用 Mockito 创建的 UserService Mock 代理对象。如下图所示:

    • 打印的就是我们 Mock 返回的 UserVO 对象。
    • 结果竟然返回的是 null 空。理论来说,此时应该返回一个id = 1 的 UserVO 对象。实际上,因为此时的userService 是通过 Mockito 来 Mock 出来的对象,其所有调用它的方法,返回的都是空。
    • UserController 中,也会注入一个 UserService 属性,此时注入的就是该 Mock 出来的 UserService Bean 对象。

    • 默认情况下,

    • <1.1> 处,我们调用 UserService#get(Integer id) 方法,然后打印返回结果。执行结果如下:

      before mock:null
    • <1.2> 处,通过 Mockito 进行 Mock userService 的 #get(Integer id) 方法,当传入的 id = 1 方法参数时,返回 id = 1 并且 username = "username:1" 的 UserVO 对象。

    • <1.3> 处,再次调用 UserService#get(Integer id) 方法,然后打印返回结果。执行结果如下:

      after mock:cn.iocoder.springboot.lab23.springmvc.vo.UserVO@23202c31
  • 后续,使用 mvc 完成一次后端 API 调用,并进行断言结果是否正确。执行成功,单元测试通过。

可能胖友对单元测试不是很了解,这里在额外推荐一本书 《有效的单元测试》 。很薄,周末抽几个小时就能读完。

如果觉得本小节还不够,可以看看 《Testing the Web Layer》 文章,Spring 关于这块入门的官方文档。

4. 全局统一返回

示例代码对应仓库:lab-springmvc-23-02 。

在我们提供后端 API 给前端时,我们需要告前端,这个 API 调用结果是否成功:

  • 如果成功,成功的数据是什么。后续,前端会取数据渲染到页面上。
  • 如果失败,失败的原因是什么。一般,前端会将原因弹出提示给用户。

这样,我们就需要有统一的返回结果,而不能是每个接口自己定义自己的风格。一般来说,统一的全局返回信息如下:

  • 成功时,返回成功的状态码 + 数据
  • 失败时,返回失败的状态码 + 错误提示

在标准的 RESTful API 的定义,是推荐使用 HTTP 响应状态码 返回状态码。一般来说,我们实践很少这么去做,主要有如下原因:

  • 业务返回的错误状态码很多,HTTP 响应状态码无法很好的映射。例如说,活动还未开始、订单已取消等等。
  • 国内开发者对 HTTP 响应状态码不是很了解,可能只知道 200、403、404、500 几种常见的。这样,反倒增加学习成本。

所以,实际项目在实践时,我们会将状态码放在 Response Body 响应内容中返回。

在全局统一返回里,我们至少需要定义三个字段:

  • code:状态码。无论是否成功,必须返回。

    关于这一块,也有团队实践时,增加了 success 字段,通过true 和 false 表示成功还是失败。这个看每个团队的习惯吧。艿艿的话,还是偏好基于约定,返回 0 时表示成功。

    • 成功时,状态码为 0 。
    • 失败时,对应业务的错误码。
  • data:数据。成功时,返回该字段。

  • message:错误提示。失败时,返回该字段。

那么,让我们来看两个示例:

// 成功响应
{
    code: 0,
    data: {
        id: 1,
        username: "yudaoyuanma"
    }
}

// 失败响应
{
    code: 233666,
    message: "徐妈太丑了"
}

下面,我们来看一个示例。

艿艿:考虑到不破坏 「2. 快速入门」 和 「3. 测试接口」 提供的示例,我们需要重新弄搭建一个。

4.1 引入依赖

在 「2.2 引入依赖」 一致。

4.2 Application

在 「2.3 Application」 一致。

4.3 CommonResult

在 cn.iocoder.springboot.lab23.springmvc.core.vo 包路径,创建 CommonResult 类,用于全局统一返回。代码如下:

// CommonResult.java

public class CommonResult<Timplements Serializable {

    public static Integer CODE_SUCCESS = 0;

    /**
     * 错误码
     */

    private Integer code;
    /**
     * 错误提示
     */

    private String message;
    /**
     * 返回数据
     */

    private T data;

    /**
     * 将传入的 result 对象,转换成另外一个泛型结果的对象
     *
     * 因为 A 方法返回的 CommonResult 对象,不满足调用其的 B 方法的返回,所以需要进行转换。
     *
     * @param result 传入的 result 对象
     * @param <T> 返回的泛型
     * @return 新的 CommonResult 对象
     */

    public static <T> CommonResult<T> error(CommonResult<?> result) {
        return error(result.getCode(), result.getMessage());
    }

    public static <T> CommonResult<T> error(Integer code, String message) {
        Assert.isTrue(!CODE_SUCCESS.equals(code), "code 必须是错误的!");
        CommonResult<T> result = new CommonResult<>();
        result.code = code;
        result.message = message;
        return result;
    }

    public static <T> CommonResult<T> success(T data) {
        CommonResult<T> result = new CommonResult<>();
        result.code = CODE_SUCCESS;
        result.data = data;
        result.message = "";
        return result;
    }

    @JsonIgnore // 忽略,避免 jackson 序列化给前端
    public boolean isSuccess() // 方便判断是否成功
        return CODE_SUCCESS.equals(code);
    }

    @JsonIgnore // 忽略,避免 jackson 序列化给前端
    public boolean isError() // 方便判断是否失败
        return !isSuccess();
    }

    // ... 省略 setting/getting/toString 方法

}
  • 每个字段,胖友自己看相应的注释。

4.4 GlobalResponseBodyHandler

在 cn.iocoder.springboot.lab23.springmvc.core.web 包路径,创建 GlobalResponseBodyHandler 类,全局统一返回的处理器。代码如下:

// GlobalResponseBodyHandler.java

@ControllerAdvice(basePackages = "cn.iocoder.springboot.lab23.springmvc.controller")
public class GlobalResponseBodyHandler implements ResponseBodyAdvice {

    @Override
    public boolean supports(MethodParameter returnType, Class converterType) {
        return true;
    }

    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType,
                                  ServerHttpRequest request, ServerHttpResponse response)
 
{
        // 如果已经是 CommonResult 类型,则直接返回
        if (body instanceof CommonResult) {
            return body;
        }
        // 如果不是,则包装成 CommonResult 类型
        return CommonResult.success(body);
    }

}
  • 在 SpringMVC 中,可以使用通过实现 ResponseBodyAdvice 接口,并添加 @ControllerAdvice 接口,拦截 Controller 的返回结果。注意,我们这里 @ControllerAdvice 注解,设置了 basePackages 属性,只拦截 "cn.iocoder.springboot.lab23.springmvc.controller" 包,也就是我们定义的 Controller 。为什么呢?因为在项目中,我们可能会引入 Swagger 等库,也使用 Controller 提供 API 接口,那么我们显然不应该让 GlobalResponseBodyHandler 去拦截这些接口,毕竟它们并不需要我们去替它们做全局统一的返回
  • 实现 #supports(MethodParameter returnType, Class converterType) 方法,返回 true 。表示拦截 Controller 所有 API 接口的返回结果。
  • 实现 #beforeBodyWrite(...) 方法,当返回的结果不是 CommonResult 类型时,则包装成 CommonResult 类型。这里有两点要注意:
    • 第一点,可能 API 接口的返回结果已经是 CommonResult 类型,就无需做二次包装了。
    • 第二点,API 接口既然返回结果,被 GlobalResponseBodyHandler 拦截到,约定就是成功返回,所以使用 CommonResult#success(T data) 方法,进行包装成成功的 CommonResult 返回。那么,如果我们希望 API 接口是失败的返回呢?我们约定在 Controller 抛出异常,这点我们会在 「5. 全局异常处理」 看到。

4.5 UserController

在 cn.iocoder.springboot.lab23.springmvc.controller 包路径下,创建 UserController 类。代码如下:

// UserController.java

@RestController
@RequestMapping("/users")
public class UserController {

    /**
     * 获得指定用户编号的用户
     *
     * 提供不使用 CommonResult 包装
     *
     * @param id 用户编号
     * @return 用户
     */

    @GetMapping("/get")
    public UserVO get(@RequestParam("id") Integer id) {
        // 查询并返回用户
        return new UserVO().setId(id).setUsername(UUID.randomUUID().toString());
    }

    /**
     * 获得指定用户编号的用户
     *
     * 提供使用 CommonResult 包装
     *
     * @param id 用户编号
     * @return 用户
     */

    @GetMapping("/get2")
    public CommonResult<UserVO> get2(@RequestParam("id") Integer id) {
        // 查询用户
        UserVO user = new UserVO().setId(id).setUsername(UUID.randomUUID().toString());
        // 返回结果
        return CommonResult.success(user);
    }

}
  • 在 #get(Integer id) 方法,返回的结果是 UserVO 类型。这样,结果会被 GlobalResponseBodyHandler 拦截,包装成 CommonResult 类型返回。请求结果如下:

    {
        "code"0,
        "message""",
        "data": {
            "id"10,
            "username""f0ab9401-062f-4697-bcc9-1dc70c1c1310"
        }
    }
    • 会有 "message": "" 的返回的原因是,我们使用 SpringMVC 提供的 Jackson 序列化,对于 CommonResult 此时的 message = null 的情况下,会序列化它成 "message": "" 返回。实际情况下,不会影响前端处理。
  • 在 #get2(Integer id) 方法,返回的结果是 Common类型。结果虽然也会被 GlobalResponseBodyHandler 拦截,但是不会二次再重复包装成 CommonResult 类型返回。

4.6 小小的讨论

至此,我们已经完成了全局统一返回的示例。不过呢,艿艿在这里想抛出一个话题?我们是否应该使用 GlobalResponseBodyHandler 这样的方式,拦截 Controller 的返回结果,返回全局统一返回的示例呢?

我们会发现,网上大量的文章,其实也是使用这样的方式,实现全局统一返回。这样看起来,好像也并没有什么问题。当然,实际在项目中使用,没有问题,没有坑,放放心食用。这里,只是艿艿的一个纠结点。我们在使用 AOP 的时候,修改一个方法的返回结果的类型,真的合适吗?艿艿个人的意见,不合适。所以,艿艿在做 onemall 开源项目的时候,是强制要求 Controller 返回结果的类型是 CommonResult ,即 Controller#get2(Integer id) 的方式。

不过呢,自己公司团队的项目,我们还是采用 GlobalResponseBodyHandler 拦截的方式。嘿嘿。也就说,两种方式,都可以,按照自己的理念即可。

另外,ResponseBodyAdvice 用于对响应内容 Response Body 进行切面拦截。而相对的,SpringMVC 也提供了 RequestBodyAdvice ,用于对请求内容 Request Body 进行切面拦截。使用比较简单,和 ResponseBodyAdvice 是类似的,就不重复赘述了。当然,貌似 ResponseBodyAdvice 使用也比较少,胖友知道有这么个东西即可,等到有需要的时候,再来瞅瞅。

5. 全局异常处理

示例代码对应仓库:lab-springmvc-23-02 。

在 「4. 全局统一返回」 中,我们已经定义了使用 CommonResult 全局统一返回,并且看到了成功返回的示例与代码。这一小节,我们主要是来全局异常处理,最终能也是通过 CommonResult 返回。

那么,我们就不哔哔,直接看着示例代码,遨游起来。

友情提示:该示例,基于 「4. 全局统一返回」 的 lab-springmvc-23-02 的基础上,继续改造。

5.1 ServiceExceptionEnum

在 cn.iocoder.springboot.lab23.springmvc.constants 包路径,创建 ServiceExceptionEnum 枚举类,枚举项目中的错误码。代码如下:

// ServiceExceptionEnum.java

public enum ServiceExceptionEnum {

    // ========== 系统级别 ==========
    SUCCESS(0"成功"),
    SYS_ERROR(2001001000"服务端发生异常"),
    MISSING_REQUEST_PARAM_ERROR(2001001001"参数缺失"),

    // ========== 用户模块 ==========
    USER_NOT_FOUND(1001002000"用户不存在"),

    // ========== 订单模块 ==========

    // ========== 商品模块 ==========
    ;

    /**
     * 错误码
     */

    private int code;
    /**
     * 错误提示
     */

    private String message;

    ServiceExceptionEnum(int code, String message) {
        this.code = code;
        this.message = message;
    }

    // ... 省略 getting 方法

}
  • 因为错误码是全局的,最好按照模块来拆分。如下是艿艿在 onemall 项目的实践:

    /**
     * 服务异常
     *
     * 参考 https://www.kancloud.cn/onebase/ob/484204 文章
     *
     * 一共 10 位,分成四段
     *
     * 第一段,1 位,类型
     *      1 - 业务级别异常
     *      2 - 系统级别异常
     * 第二段,3 位,系统类型
     *      001 - 用户系统
     *      002 - 商品系统
     *      003 - 订单系统
     *      004 - 支付系统
     *      005 - 优惠劵系统
     *      ... - ...
     * 第三段,3 位,模块
     *      不限制规则。
     *      一般建议,每个系统里面,可能有多个模块,可以再去做分段。以用户系统为例子:
     *          001 - OAuth2 模块
     *          002 - User 模块
     *          003 - MobileCode 模块
     * 第四段,3 位,错误码
     *       不限制规则。
     *       一般建议,每个模块自增。
     */

5.2 ServiceException

我们在一起讨论下 Service 逻辑异常的时候,如何进行返回。这里的逻辑异常,我们指的是,例如说用户名已经存在,商品库存不足等。一般来说,常用的方案选择,有两种:

  • 封装统一的业务异常类 ServiceException ,里面有错误码和错误提示,然后进行 throws 抛出。
  • 封装通用的返回类 CommonResult ,里面有错误码和错误提示,然后进行 return 返回。

一开始,我们选择了 CommonResult ,结果发现如下情况:

  • 因为 Spring @Transactional 声明式事务,是基于异常进行回滚的,如果使用 CommonResult 返回,则事务回滚会非常麻烦。
  • 当调用别的方法时,如果别人返回的是 CommonResult 对象,还需要不断的进行判断,写起来挺麻烦的。

所以,后来我们采用了抛出业务异常 ServiceException 的方式。

在 cn.iocoder.springboot.lab23.springmvc.core.exception 包路径,创建 ServiceException 异常类,继承 RuntimeException 异常类,用于定义业务异常。代码如下:

// ServiceException.java

public final class ServiceException extends RuntimeException {

    /**
     * 错误码
     */

    private final Integer code;

    public ServiceException(ServiceExceptionEnum serviceExceptionEnum) {
        // 使用父类的 message 字段
        super(serviceExceptionEnum.getMessage());
        // 设置错误码
        this.code = serviceExceptionEnum.getCode();
    }

    // ... 省略 getting 方法

}
  • 提供传入 serviceExceptionEnum 参数的构造方法。具体的处理,看下代码和注释。

5.3 GlobalExceptionHandler

在 cn.iocoder.springboot.lab23.springmvc.core.web 包路径,创建 GlobalExceptionHandler 类,全局统一返回的处理器。代码如下:

// GlobalExceptionHandler.java

@ControllerAdvice(basePackages = "cn.iocoder.springboot.lab23.springmvc.controller")
public class GlobalExceptionHandler {

    private Logger logger = LoggerFactory.getLogger(getClass());

    /**
     * 处理 ServiceException 异常
     */

    @ResponseBody
    @ExceptionHandler(value = ServiceException.class)
    public CommonResult serviceExceptionHandler(HttpServletRequest req, ServiceException ex) {
        logger.debug("[serviceExceptionHandler]", ex);
        // 包装 CommonResult 结果
        return CommonResult.error(ex.getCode(), ex.getMessage());
    }

    /**
     * 处理 MissingServletRequestParameterException 异常
     *
     * SpringMVC 参数不正确
     */

    @ResponseBody
    @ExceptionHandler(value = MissingServletRequestParameterException.class)
    public CommonResult missingServletRequestParameterExceptionHandler(HttpServletRequest req, MissingServletRequestParameterException ex) {
        logger.debug("[missingServletRequestParameterExceptionHandler]", ex);
        // 包装 CommonResult 结果
        return CommonResult.error(ServiceExceptionEnum.MISSING_REQUEST_PARAM_ERROR.getCode(),
                ServiceExceptionEnum.MISSING_REQUEST_PARAM_ERROR.getMessage());
    }

    /**
     * 处理其它 Exception 异常
     */

    @ResponseBody
    @ExceptionHandler(value = Exception.class)
    public CommonResult exceptionHandler(HttpServletRequest req, Exception e) {
        // 记录异常日志
        logger.error("[exceptionHandler]", e);
        // 返回 ERROR CommonResult
        return CommonResult.error(ServiceExceptionEnum.SYS_ERROR.getCode(),
                ServiceExceptionEnum.SYS_ERROR.getMessage());
    }

}
  • 在类上,添加 @ControllerAdvice 注解。这一点,和 「4.4 GlobalResponseBodyHandler」 是一样的。不过,不会实现 ResponseBodyAdvice 接口,因为咱不需要拦截接口返回结果,进行修改。
  • 我们定义了三个方法,通过添加 @ExceptionHandler 注解,定义每个方法对应处理的异常。并且,也添加了 @ResponseBody注解,标记直接使用返回结果作为 API 的响应。
  • #serviceExceptionHandler(...) 方法,拦截处理 ServiceException 业务异常,直接使用该异常的 code +message 属性,构建出 CommonResult 对象返回。
  • #missingServletRequestParameterExceptionHandler(...) 方法,拦截处理 MissingServletRequestParameterException 请求参数异常,构建出错误码为 ServiceExceptionEnum.MISSING_REQUEST_PARAM_ERROR 的 CommonResult 对象返回。
  • #exceptionHandler(...) 方法,拦截处理 Exception 异常,构建出错误码为 ServiceExceptionEnum.SYS_ERROR 的 CommonResult 对象返回。这是一个兜底的异常处理,避免有一些其它异常,我们没有在 GlobalExceptionHandler 中,提供自定义的处理方式。

注意,在 #exceptionHandler(...) 方法中,我们还多使用logger 打印了错误日志,方便我们接入 ELK 等日志服务,发起告警,通知我们去排查解决。如果胖友的系统里暂时没有日志服务,可以记录错误日志到数据库中,也是不错的选择。而其它两个方法,因为是更偏业务的,相对正常的异常,所以无需记录错误日志。

5.4 UserController

在 UserController 类中,我们添加两个 API 接口,抛出异常,方便我们测试全局异常处理的效果。代码如下:

// UserController.java

/**
 * 测试抛出 NullPointerException 异常
 */

@GetMapping("/exception-01")
public UserVO exception01() {
    throw new NullPointerException("没有粗面鱼丸");
}

/**
 * 测试抛出 ServiceException 异常
 */

@GetMapping("/exception-02")
public UserVO exception02() {
    throw new ServiceException(ServiceExceptionEnum.USER_NOT_FOUND);
}
  • 在 #exception01() 方法,抛出 NullPointerException 异常。这样,异常会被GlobalExceptionHandler#exceptionHandler(...) 方法来拦截,包装成 CommonResult 类型返回。请求结果如下:

    {
        "code"2001001000,
        "message""服务端发生异常",
        "data"null
    }
  • 在 #exception02() 方法,抛出 ServiceException 异常。这样,异常会被 GlobalExceptionHandler#serviceExceptionHandler(...) 方法来拦截,包装成 CommonResult 类型返回。请求结果如下:

    {
        "code"1001002000,
        "message""用户不存在",
        "data"null
    }

6. HandlerInterceptor 拦截器

示例代码对应仓库:lab-springmvc-23-02 。

在使用 SpringMVC 的时候,我们可以使用 HandlerInterceptor ,拦截 SpringMVC 处理请求的过程,自定义前置和处理的逻辑。例如说:

  • 日志拦截器,记录请求与响应。这样,我们可以知道每一次请求的参数,响应的结果,执行的时长等等信息。
  • 认证拦截器,我们可以解析前端传入的用户标识,例如说 access_token 访问令牌,获得当前用户的信息,记录到 ThreadLocal 中。这样,后续的逻辑,只需要通过 ThreadLocal 就可以获取到用户信息。
  • 授权拦截器,我们可以通过每个 API 接口需要的授权信息,进行判断,当前请求是否允许访问。例如说,用户是否登陆,是否有该 API 操作的权限等等。
  • 限流拦截器,我们可以通过每个 API 接口的限流配置,进行判断,当前请求是否超过允许的请求频率,避免恶意的请求,打爆整个系统。

HandlerInterceptor 接口,定义了三个拦截点。代码如下:

// HandlerInterceptor.java

public interface HandlerInterceptor {

 default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
   throws Exception 
{
  return true;
 }

 default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
   @Nullable ModelAndView modelAndView)
 throws Exception 
{
 }

 default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
   @Nullable Exception ex)
 throws Exception 
{
 }

}
  • 首先,我们要普及一个概念。我们的每一个 API 请求,会对应到一个 handler 处理器。如下图所示:

    • 我们可以看到,这一次 users/exception_03 请求,handler 对应上了 UserController 的 #exception03()方法。
    • 也因此,HandlerInterceptor 的接口名上,是以 Handler 开头,基于 Handler 的拦截器。
  • 然后,我们先来看一段伪代码,看看这三个拦截点和 handler的执行过程,是怎么结合的。代码如下:

    // 伪代码
    Exception ex = null;
    try {
        // 前置处理
        if (!preHandle(request, response, handler)) {
            return;
        }

        // 执行处理器,即执行 API 的逻辑
        handler.execute();

        // 后置处理
        postHandle(request, response, handler);
    catch(Exception exception) {
        // 如果发生了异常,记录到 ex 中
        ex = exception;
    finally {
        afterCompletion(request, response, handler);
    }
    • 结合艿艿的代码注释,胖友理解下整个处理的过程。
    • 当然,这里仅仅是伪代码,并没有考虑多个拦截器的情况下。我们会在下文中,提供具体的代码示例,方便胖友更加深入的理解整个执行过程。
  • #preHandle(...) 方法,实现 handler 的置处理逻辑。当返回 true 时,继续后续 handler 的执行;当返回 false 时,不进行后续 handler 的执行。

    例如说,判断用户是否已经登录,如果未登录,返回 false不进行后续 handler 的执行。

  • #postHandle(...) 方法,实现 handler 的置处理逻辑。

    例如说,在视图 View 在渲染之前,做一些处理。不过因为目前都前后端分离,所以这个后置拦截点,使用的就已经比较少了。

  • #afterCompletion(...) 方法,整个 handler 执行完成,并且拦截器都执行完前置和后置的拦截逻辑,实现请求完成后的处理逻辑。注意,只有 #preHandle(...) 方法返回 true 的 HandlerInterceptor 拦截器,才能执行 #afterCompletion(...)方法,因为这样要算 HandlerInterceptor 执行完成才有效。

    例如说,释放资源。比如,清理认证拦截器产生的 ThreadLocal 线程变量,避免“污染”下一个使用到该线程的请求。

    又例如说,处理 handler 执行过程中发生的异常,并记录异常日志。不过因为现在一般通过 「5. 全局异常处理」 来处理,所以很少这么做了。

    再例如说,记录请求结束时间,这样我们就可以计算出整个请求的耗时。

在上文中,考虑到让 HandlerInterceptor 的理解更加简洁,一直漏说了一个概念,多个 HandlerInterceptor 们,可以组成一个 Chain 拦截器链。那么,整个执行的过程,就变成:

  • 首先,按照 HandlerInterceptor 链的正序,执行 #preHandle(...) 方法。
  • 然后,执行 handler 的逻辑处理。
  • 之后,按照 HandlerInterceptor 链的倒序,执行 #postHandle(...) 方法。
  • 最后,按照 HandlerInterceptor 链的倒序,执行 #afterCompletion(...) 方法。

这里,我们先只说了正常执行的情况。那么异常执行的情况呢?例如说:

  • 某个 HandlerInterceptor 执行 #preHandle(...) 方法,返回 false 的情况。
  • handler 的逻辑处理,抛出 Exception 异常的情况。
  • 某个 HandlerInterceptor 执行 #afterCompletion(...) 方法,抛出 Exception 异常的情况。
  • ... 等等

不要慌,我们下面会举例,说说列举的这些异常情况。下面,开始遨游我们本小节的示例。

友情提示:该示例,基于 「5. 全局异常处理」 的 lab-springmvc-23-02 的基础上,继续改造。

6.1 自定义 HandlerInterceptor

在 cn.iocoder.springboot.lab23.springmvc.core.interceptor 包路径,我们创建三个自定义 HandlerInterceptor 拦截器。

  • FirstInterceptor 代码如下:

    // FirstInterceptor.java

    public class FirstInterceptor implements HandlerInterceptor {

        private Logger logger = LoggerFactory.getLogger(getClass());

        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
            logger.info("[preHandle][handler({})]", handler);
            return true;
        }

        @Override
        public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
            logger.info("[postHandle][handler({})]", handler);
        }

        @Override
        public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
            logger.info("[afterCompletion][handler({})]", handler, ex);
        }

    }
    • 每个方法中,打印日志。
  • SecondInterceptor 代码如下:

    // SecondInterceptor.java

    public class SecondInterceptor implements HandlerInterceptor {

        private Logger logger = LoggerFactory.getLogger(getClass());

        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
            logger.info("[preHandle][handler({})]", handler);
            return false// 故意返回 false
        }

        @Override
        public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
            logger.info("[postHandle][handler({})]", handler);
        }

        @Override
        public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
            logger.info("[afterCompletion][handler({})]", handler, ex);
        }

    }
    • 和 「FirstInterceptor」 基本一致,差别在于 #preHandle(...) 方法,返回 false 。
  • ThirdInterceptor 代码如下:

    // ThirdInterceptor.java

    public class ThirdInterceptor implements HandlerInterceptor {

        private Logger logger = LoggerFactory.getLogger(getClass());

        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
            logger.info("[preHandle][handler({})]", handler);
            return true;
        }

        @Override
        public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
            logger.info("[postHandle][handler({})]", handler);
        }

        @Override
        public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
            logger.info("[afterCompletion][handler({})]", handler, ex);
            throw new RuntimeException("故意抛个错误"); // 故意抛出异常
        }

    }
    • 和 「FirstInterceptor」 基本一致,差别在于 #afterCompletion(...) 方法,抛出 RuntimeException 异常。

6.2 SpringMVCConfiguration

在 cn.iocoder.springboot.lab23.springmvc.config 包路径下,创建 SpringMVCConfiguration 配置类。代码如下:

// SpringMVCConfiguration.java

@Configuration
public class SpringMVCConfiguration implements WebMvcConfigurer {

    @Bean
    public FirstInterceptor firstInterceptor() {
        return new FirstInterceptor();
    }

    @Bean
    public SecondInterceptor secondInterceptor() {
        return new SecondInterceptor();
    }

    @Bean
    public ThirdInterceptor thirdInterceptor() {
        return new ThirdInterceptor();
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 拦截器一
        registry.addInterceptor(this.firstInterceptor()).addPathPatterns("/**");
        // 拦截器二
        registry.addInterceptor(this.secondInterceptor()).addPathPatterns("/users/current_user");
        // 拦截器三
        registry.addInterceptor(this.thirdInterceptor()).addPathPatterns("/**");
    }

}
  • 该配置类,实现 WebMvcConfigurer 接口,实现 SpringMVC 的自定义配置。当然,类上还是要加上 @Configuration 注解,表明 SpringMVCConfiguration 是个配置类。
  • #addInterceptors(InterceptorRegistry registry) 方法,添加自定义的 HandlerInterceptor 拦截器,到 InterceptorRegistry 拦截器注册表中。其中,SecondInterceptor 拦截器,我们配置拦截的是 /users/current_user 路径,用于测试 SecondInterceptor#preHandle(...) 前置拦截返回 false 的情况。

6.3 UserController

本小节,我们会在 UserController 接口,测试拦截器链的表现。

① /users/do_something 接口

// UserController.java

@GetMapping("/do_something")
public void doSomething() {
    logger.info("[doSomething]");
}

调用该接口,执行日志如下:

// 首先,按照 HandlerInterceptor 链的**正序**,执行 `#preHandle(...)` 方法。
2019-11-17 12:31:38.049  INFO 28157 --- [nio-8080-exec-1] c.i.s.l.s.c.i.FirstInterceptor           : [preHandle][handler(public void cn.iocoder.springboot.lab23.springmvc.controller.UserController.doSomething())]
2019-11-17 12:31:38.050  INFO 28157 --- [nio-8080-exec-1] c.i.s.l.s.c.i.ThirdInterceptor           : [preHandle][handler(public void cn.iocoder.springboot.lab23.springmvc.controller.UserController.doSomething())]
2019-11-17 12:31:38.055  INFO 28157 --- [nio-8080-exec-1] c.i.s.l.s.controller.UserController      :

// 然后,执行 `handler` 的逻辑处理。
[doSomething]

// 之后,按照 HandlerInterceptor 链的**倒序**,执行 `#postHandle(...)` 方法。
2019-11-17 12:31:38.109  INFO 28157 --- [nio-8080-exec-1] c.i.s.l.s.c.i.ThirdInterceptor           : [postHandle][handler(public void cn.iocoder.springboot.lab23.springmvc.controller.UserController.doSomething())]
2019-11-17 12:31:38.109  INFO 28157 --- [nio-8080-exec-1] c.i.s.l.s.c.i.FirstInterceptor           : [postHandle][handler(public void cn.iocoder.springboot.lab23.springmvc.controller.UserController.doSomething())]

// 最后,按照 HandlerInterceptor 链的**倒序**,执行 `#afterCompletion(...)` 方法。
2019-11-17 12:31:38.109  INFO 28157 --- [nio-8080-exec-1] c.i.s.l.s.c.i.ThirdInterceptor           : [afterCompletion][handler(public void cn.iocoder.springboot.lab23.springmvc.controller.UserController.doSomething())]
java.lang.RuntimeException: 故意抛个错误 // ... 省略异常堆栈

2019-11-17 12:31:38.116  INFO 28157 --- [nio-8080-exec-1] c.i.s.l.s.c.i.FirstInterceptor           : [afterCompletion][handler(public void cn.iocoder.springboot.lab23.springmvc.controller.UserController.doSomething())]
  • 整个执行日志,看下艿艿添加在其上的注释。
  • 因为 SecondInterceptor 拦截的是 /users/current_user 路径,所以它没有拦截本次 API 调用。
  • 注意,虽然说,ThirdInterceptor 在 #afterCompletion(...) 方法中,抛出异常,但是不影响 FirstInterceptor 的 #afterCompletion(...) 的后续执行。

② /users/current_user 接口

// UserController.java

@GetMapping("/current_user")
public UserVO currentUser() {
    logger.info("[currentUser]");
    return new UserVO().setId(10).setUsername(UUID.randomUUID().toString());
}

调用该接口,执行日志如下:

// 首先,按照 HandlerInterceptor 链的**正序**,执行 `#preHandle(...)` 方法。
2019-11-17 12:48:37.357  INFO 28157 --- [nio-8080-exec-5] c.i.s.l.s.c.i.FirstInterceptor           : [preHandle][handler(public cn.iocoder.springboot.lab23.springmvc.vo.UserVO cn.iocoder.springboot.lab23.springmvc.controller.UserController.currentUser())]
2019-11-17 12:48:37.357  INFO 28157 --- [nio-8080-exec-5] c.i.s.l.s.c.i.SecondInterceptor          : [preHandle][handler(public cn.iocoder.springboot.lab23.springmvc.vo.UserVO cn.iocoder.springboot.lab23.springmvc.controller.UserController.currentUser())]

//【不存在】然后,执行 `handler` 的逻辑处理。

//【不存在】之后,按照 HandlerInterceptor 链的**倒序**,执行 `#postHandle(...)` 方法。

// 最后,按照 HandlerInterceptor 链的**倒序**,执行 `#afterCompletion(...)` 方法。
2019-11-17 12:48:37.358  INFO 28157 --- [nio-8080-exec-5] c.i.s.l.s.c.i.FirstInterceptor           : [afterCompletion][handler(public cn.iocoder.springboot.lab23.springmvc.vo.UserVO cn.iocoder.springboot.lab23.springmvc.controller.UserController.currentUser())]
  • 因为只有 FirstInterceptor 完成了 #preHandle(...) 方法的执行,所以也只有 FirstInterceptor 的 #afterCompletion(...) 方法被执行。
  • 在 handler 未执行逻辑处理的情况下,HandlerInterceptor 的#postHandle(...) 方法不会执行。答案也很显而易见,handler 丫根就没完成执行,何来的后置处理,嘿嘿。

③ /users/exception-03 接口

// UserController.java

@GetMapping("/exception-03")
public void exception03() {
    logger.info("[exception03]");
    throw new ServiceException(ServiceExceptionEnum.USER_NOT_FOUND);
}

调用该接口,执行日志如下:

// 首先,按照 HandlerInterceptor 链的**正序**,执行 `#preHandle(...)` 方法。
2019-11-17 12:54:45.029  INFO 28157 --- [nio-8080-exec-7] c.i.s.l.s.c.i.FirstInterceptor           : [preHandle][handler(public void cn.iocoder.springboot.lab23.springmvc.controller.UserController.exception03())]
2019-11-17 12:54:45.029  INFO 28157 --- [nio-8080-exec-7] c.i.s.l.s.c.i.ThirdInterceptor           : [preHandle][handler(public void cn.iocoder.springboot.lab23.springmvc.controller.UserController.exception03())]

// 然后,执行 `handler` 的逻辑处理。
2019-11-17 12:54:45.029  INFO 28157 --- [nio-8080-exec-7] c.i.s.l.s.controller.UserController      : [exception03]

//【不存在】之后,按照 HandlerInterceptor 链的**倒序**,执行 `#postHandle(...)` 方法。

// 最后,按照 HandlerInterceptor 链的**倒序**,执行 `#afterCompletion(...)` 方法。
2019-11-17 12:54:45.036  INFO 28157 --- [nio-8080-exec-7] c.i.s.l.s.c.i.ThirdInterceptor           : [afterCompletion][handler(public void cn.iocoder.springboot.lab23.springmvc.controller.UserController.exception03())]
2019-11-17 12:54:45.037 ERROR 28157 --- [nio-8080-exec-7] o.s.web.servlet.HandlerExecutionChain    : HandlerInterceptor.afterCompletion threw exception
java.lang.RuntimeException: 故意抛个错误 // ... 省略异常堆栈

2019-11-17 12:54:45.037  INFO 28157 --- [nio-8080-exec-7] c.i.s.l.s.c.i.FirstInterceptor           : [afterCompletion][handler(public void cn.iocoder.springboot.lab23.springmvc.controller.UserController.exception03())]
  • handler 的逻辑处理,抛出 Exception 异常的情况,HandlerInterceptor 的 #postHandle(...) 方法不会执行。为什么呢?答案我们已经在 /users/current_user 接口中,进行了说明。

6.4 拓展阅读

这里,艿艿整理了一些不同场景下的 HandlerInterceptor 实现类,胖友可以选择性看看:

  • 限流拦截器:《Spring Boot 项目的 API 接口防刷》
  • 访问日志拦截器:AccessLogInterceptor.java
  • 用户认证 + 授权拦截器:UserSecurityInterceptor.java
  • 管理员认证 + 授权拦截器:AdminSecurityInterceptor.java

如果胖友有其他场景下的 HandlerInterceptor 示例,欢迎给艿艿留言。

7. Servlet、Filter、Listener

虽然绝大多数情况下,我们无需在 SpringMVC 中,直接使用 java.servlet 提供的 Servlet、Filter、Listener 了,但是在使用一些三方类库时,它们更多的提供的是 java.servlet 中的组件,毕竟它们需要提供更加公用的组件,而不能和 SpringMVC 耦合在一起。

例如说,在使用 Shiro 做权限认证相关方面的功能时,我们就需要配置 Shiro 提供的 ShiroFilterFactoryBean 。

我们有两种方式,使用 Java 代码的方式,配置 Servlet、Filter、Listener 。分别是:

  • 通过 Bean 的方式
  • 通过注解的方式

下面,就让我们看看这两种方式的示例。

友情提示:该示例,基于 「6. HandlerInterceptor 拦截器」 的 lab-springmvc-23-02 的基础上,继续改造。

7.1 通过 Bean 的方式

在 SpringMVCConfiguration 配置类中,我们来添加 Servlet、Filter、Listener 三个 Bean 的配置。代码如下:

// SpringMVCConfiguration.java

@Bean
public ServletRegistrationBean testServlet01() {
    ServletRegistrationBean servletRegistrationBean = new ServletRegistrationBean<>(new HttpServlet() {

        @Override
        protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
            logger.info("[doGet][uri: {}]", req.getRequestURI());
        }

    });
    servletRegistrationBean.setUrlMappings(Collections.singleton("/test/01"));
    return servletRegistrationBean;
}

@Bean
public FilterRegistrationBean testFilter01() {
    FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean<>(new Filter() {

        @Override
        public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
            logger.info("[doFilter]");
            filterChain.doFilter(servletRequest, servletResponse);
        }

    });
    filterRegistrationBean.setUrlPatterns(Collections.singleton("/test/*"));
    return filterRegistrationBean;
}

@Bean
public ServletListenerRegistrationBean<?> testListener01() {
    return new ServletListenerRegistrationBean<>(new ServletContextListener() {

        @Override
        public void contextInitialized(ServletContextEvent sce) {
            logger.info("[contextInitialized]");
        }

        @Override
        public void contextDestroyed(ServletContextEvent sce) {
        }

    });
}
  • 在 Spring Boot 中,提供了 ServletRegistrationBean 来配置 Servlet Bean、FilterRegistrationBean 来配置 Filter Bean、ServletListenerRegistrationBean 来配置 Listener Bean 。
  • 这里,我们为了让示例比较简洁,所以采用了内部类。实际在使用时,还是正常去定义类,嘿嘿。

7.2 通过注解的方式

在 Servlet3.0 的新特性里,提供了 @WebServlet@WebFilter@WebListener 三个注解,方便配置 Servlet、Filter、Listener 。

而在 SpringBoot 中,我们仅需要在 Application 类上,添加 @ServletComponentScan 注解,开启对 @WebServlet@WebFilter@WebListener 注解的扫描。不过要注意,当且仅当使用内嵌的 Web Server 才会生效。

在 cn.iocoder.springboot.lab23.springmvc.core.servlet 包路径,我们创建了三个示例。代码如下:

// TestServlet02.java
@WebServlet(urlPatterns = "/test/02")
public class TestServlet02 extends HttpServlet {

    private Logger logger = LoggerFactory.getLogger(getClass());

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        logger.info("[doGet][uri: {}]", req.getRequestURI());
    }

}

// TestFilter02.java
@WebFilter("/test/*")
public class TestFilter02 implements Filter {

    private Logger logger = LoggerFactory.getLogger(getClass());

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        logger.info("[doFilter]");
        filterChain.doFilter(servletRequest, servletResponse);
    }

}

// TestServletContextListener02.java
@WebListener
public class TestServletContextListener02 implements ServletContextListener {

    private Logger logger = LoggerFactory.getLogger(getClass());

    @Override
    public void contextInitialized(ServletContextEvent sce) {
        logger.info("[contextInitialized]");
    }

    @Override
    public void contextDestroyed(ServletContextEvent sce) {
    }

}

8. Cors 跨域

示例代码对应仓库:lab-springmvc-23-02 。

在前后端分离之后,我们会碰到跨域的问题。例如说,前端在 http://www.iocoder.cn 域名下,而后端 API 在 http://api.iocoder.cn 域名下。

对跨域不是很了解的胖友,可以看看阮大的 《跨域资源共享 CORS 详解》 文章。

当然,也可以先继续本文的阅读。

解决跨域的方式有很多,例如说,在 Nginx 上配置处理跨域请求的参数。又例如说,项目中有网关服务,统一配置处理。当然,本文既然是 Spring Boot SpringMVC 入门,那么必然只使用 SpringMVC 来解决跨域。目前一共有三种方案:

  • 方式一,使用 @CrossCors 注解,配置每个 API 接口。
  • 方式二,使用 CorsRegistry.java 注册表,配置每个 API 接口。
  • 方案三,使用 CorsFilter.java 过滤器,处理跨域请求。

其中,方案一和方案二,本质是相同的方案,只是配置方式不同。想要理解底层原理的胖友,可以看看 CorsInterceptor 拦截器

友情提示:该示例,基于 「7. Servlet、Filter、Listener」 的 lab-springmvc-23-02 的基础上,继续改造。

8.1 @CrossCors

@CrossCors 注解,添加在类或方法上,标记该类/方法对应接口的 Cors 信息。

@CrossCors 注解的常用属性,如下:

  • origins 属性,设置允许的请求来源。[] 数组,可以填写多个请求来源。默认值为 * 。
  • value 属性,和 origins 属性相同,是它的别名。
  • allowCredentials 属性,是否允许客户端请求发送 Cookie 。默认为 false ,不允许请求发送 Cookie 。
  • maxAge 属性,本次预检请求的有效期,单位为秒。默认值为 1800 秒。

@CrossCors 注解的不常用属性,如下:

  • methods 属性,设置允许的请求方法。[] 数组,可以填写多个请求方法。默认值为 GET + POST 。
  • allowedHeaders 属性,允许的请求头 Header 。[] 数组,可以填写多个请求来源。默认值为 * 。
  • exposedHeaders 属性,允许的响应头 Header 。[] 数组,可以填写多个请求来源。默认值为 * 。

一般情况下,我们在每个 Controller 上,添加 @CrossCors 注解即可。当然,如果某个 API 接口希望做自定义的配置,可以在 Method 方法上添加。示例如下:

// TestController.java

@RestController
@RequestMapping("/test")
@CrossOrigin(origins = "*", allowCredentials = "true"// 允许所有来源,允许发送 Cookie
public class TestController {

    /**
     * 获得指定用户编号的用户
     *
     * @return 用户
     */

    @GetMapping("/get")
    @CrossOrigin(allowCredentials = "false"// 允许所有来源,不允许发送 Cookie
    public UserVO get() {
        return new UserVO().setId(1).setUsername(UUID.randomUUID().toString());
    }

}

在绝大数场合下,肯定是在 Controller 上,添加 @CrossOrigin(allowCredentials = "true") 即可

坑坑坑坑坑

在前端使用符合 CORS 规范的网络库时,例如说 Vue 常用的网络库 axios ,在发起非简单请求时,会自动先先发起 OPTIONS “预检”请求,要求服务器确认是否能够这样请求。这样,这个请求就会被 SpringMVC 的拦截器所处理。

此时,如果我们的拦截器认为 handler 一定是 HandlerMethod 类型时,就会导致报错。例如说,艿艿在 UserSecurityInterceptor 拦截器中,会认为 handler 是 HandlerMethod 类型,然后通过它获得@RequiresLogin 注解信息,判断是否需要登陆。然后,实际上,此时 handler 是 PreFlightHandler 类型,则会导致抛出异常。如下图所示:

此时,有两种解决方案:

  • 1)检查每个拦截器的实现,是不是依赖于 handler 是 HandlerMethod 的逻辑,进行修复。
  • 2)不使用该方案,而是采用 「8.3 CorsFilter」 过滤器,避免 OPTIONS 预检查走到拦截器里。

显然,1) 的成本略微有点高,所以一般情况下,推荐 2) 。目前艿艿的项目,也是采用 「8.3 CorsFilter」 过滤器的方式。

在 《【SpringMVC】与权限拦截器冲突导致的 Cors 跨域设置失效问题》 文章中,我们也可以看到有开发者,碰到一样的问题。

8.2 CorsRegistry

显然,在每个 Controller 上配置 @CrossOrigin 注解,是挺麻烦一事。所以更多的情况下,我们会选择配置 CorsRegistry 注册表。

修改 SpringMVCConfiguration 配置类,增加 CorsRegistry 相关的配置。代码如下:

// SpringMVCConfiguration.java

@Override
public void addCorsMappings(CorsRegistry registry) {
    // 添加全局的 CORS 配置
    registry.addMapping("/**"// 匹配所有 URL ,相当于全局配置
            .allowedOrigins("*"// 允许所有请求来源
            .allowCredentials(true// 允许发送 Cookie
            .allowedMethods("*"// 允许所有请求 Method
            .allowedHeaders("*"// 允许所有请求 Header
//                .exposedHeaders("*") // 允许所有响应 Header
            .maxAge(1800L); // 有效期 1800 秒,2 小时
}
  • 这里配置匹配的路径是 /** ,从而实现全局 CORS 配置。
  • 如果想要配置单个路径的 CORS 配置,可以通过 CorsRegistry#addMapping(String pathPattern) 方法,继续往其中添加 CORS 配置。
  • 如果胖友想要更安全,可以 originns 属性,只填写允许的前端域名地址。

这种方式,一样会存在 「8.1 @CrossCors」 提供到的坑坑坑坑坑,因为这两者的实现方式是一致的。所以,继续看 「8.3 CorsFilter」 方案。

8.3 CorsFilter

在 Spring Web 中,内置提供 CorsFilter 过滤器,实现对 CORS 的处理。

配置方式很简单,既然是 Filter 过滤器,就可以采用 「7.1 通过 Bean 的方式」 ,进行配置。所以修改 SpringMVCConfiguration 配置类,增加 CorsFilter 相关的配置。代码如下:

// SpringMVCConfiguration.java

@Bean
public FilterRegistrationBean<CorsFilter> corsFilter() {
    // 创建 UrlBasedCorsConfigurationSource 配置源,类似 CorsRegistry 注册表
    UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    // 创建 CorsConfiguration 配置,相当于 CorsRegistration 注册信息
    CorsConfiguration config = new CorsConfiguration();
    config.setAllowedOrigins(Collections.singletonList("*")); // 允许所有请求来源
    config.setAllowCredentials(true); // 允许发送 Cookie
    config.addAllowedMethod("*"); // 允许所有请求 Method
    config.setAllowedHeaders(Collections.singletonList("*")); // 允许所有请求 Header
    // config.setExposedHeaders(Collections.singletonList("*")); // 允许所有响应 Header
    config.setMaxAge(1800L); // 有效期 1800 秒,2 小时
    source.registerCorsConfiguration("/**", config);
    // 创建 FilterRegistrationBean 对象
    FilterRegistrationBean<CorsFilter> bean = new FilterRegistrationBean<>(
            new CorsFilter(source)); // 创建 CorsFilter 过滤器
    bean.setOrder(0); // 设置 order 排序。这个顺序很重要哦,为避免麻烦请设置在最前
    return bean;
}
  • 艿艿已经添加了详细的注释,胖友自己看下噢。效果上,和 「8.2 CorsRegistry」 是一致的。

至此,我们已经学习完三种 SpringMVC 配置 CORS 的方式。**结论就是,使用 「8.3 CorsFilter」 方式。**©著作权归作者所有:来自51CTO博客作者mb5ff80520dfa04的原创作品,如需转载,请注明出处,否则将追究法律责任

更多相关文章

  1. Debian设置允许root用户以ssh方式登录
  2. MySQL中自增ID起始值修改方法
  3. Python3版本下创建计算给定日期范围内工作日方法
  4. 芋道 Spring Boot JPA 入门(二)之基于方法名查询
  5. 给用户一个否减弱动画效果的选择[每日前端夜话0x8B]
  6. Azure DevTest Lab体验(二)用户测试
  7. Windows系统设置多用户同时远程登录
  8. DoDAF2.0方法论探究
  9. http协议请求方法都有哪些?网络安全学习提升

随机推荐

  1. 布局 遗忘 整理
  2. Android自带的图标集合
  3. Gradle sync failed: Could not GET 'htt
  4. Android:res之selector背景选择器
  5. android预定义样式简述
  6. android 加载图片到gallery
  7. Android Home's favorite.xml
  8. JEECG 移动端解决方案
  9. Android Imageview 图片居左居右,自定义
  10. (转)android中的ellipsize