Spring基础知识(30)- Spring Boot (十一)


定制 Spring MVC、拦截器(Interceptor)


1. 定制 Spring MVC

    Spring Boot 抛弃了传统 xml 配置文件,通过配置类(标注 @Configuration 的类,相当于一个 xml 配置文件)以 JavaBean 形式进行相关配置。
    
    Spring Boot 对 Spring MVC 的自动配置可以满足我们的大部分需求,但是我们也可以通过自定义配置类(标注 @Configuration 的类)并实现 WebMvcConfigurer 接口来定制 Spring MVC 配置,例如拦截器、格式化程序、视图控制器等等。

    WebMvcConfigurer 是一个基于 Java  8 的接口,该接口定义了许多与 Spring MVC 相关的方法,其中大部分方法都是 default 类型的,且都是空实现。因此我们只需要定义一个配置类实现 WebMvcConfigurer 接口,并重写相应的方法便可以定制 Spring MVC 的配置。

方法 描述
void configurePathMatch(PathMatchConfigurer configurer) HandlerMappings 路径的匹配规则。
void configureContentNegotiation(ContentNegotiationConfigurer configurer) 内容协商策略(一个请求路径返回多种数据格式)。
void configureAsyncSupport(AsyncSupportConfigurer configurer) 处理异步请求。
void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) 这个接口可以实现静态文件可以像 Servlet 一样被访问。
void addFormatters(FormatterRegistry registry) 添加格式化器或者转化器。
void addInterceptors(InterceptorRegistry registry) 添加 Spring MVC 生命周期拦截器,对请求进行拦截处理。
void addResourceHandlers(ResourceHandlerRegistry registry) 添加或修改静态资源(例如图片,js,css 等)映射;
Spring Boot 默认设置的静态资源文件夹就是通过重写该方法设置的。
void addCorsMappings(CorsRegistry registry) 处理跨域请求。
void addViewControllers(ViewControllerRegistry registry) 主要用于实现无业务逻辑跳转,例如主页跳转,简单的请求重定向,错误页跳转等
void configureViewResolvers(ViewResolverRegistry registry) 配置视图解析器,将 Controller 返回的字符串(视图名称),转换为具体的视图进行渲染。
void addArgumentResolvers(List resolvers) 添加解析器以支持自定义控制器方法参数类型,实现该方法不会覆盖用于解析处理程序方法参数的内置支持;要自定义内置的参数解析支持, 同样可以通过 RequestMappingHandlerAdapter 直接配置 RequestMappingHandlerAdapter 。
void addReturnValueHandlers(List handlers) 添加处理程序来支持自定义控制器方法返回值类型。使用此选项不会覆盖处理返回值的内置支持;要自定义处理返回值的内置支持,请直接配置 RequestMappingHandlerAdapter。
void configureMessageConverters(List> converters) 用于配置默认的消息转换器(转换 HTTP 请求和响应)。
void extendMessageConverters(List> converters) 直接添加消息转换器,会关闭默认的消息转换器列表;实现该方法即可在不关闭默认转换器的起提下,新增一个自定义转换器。
void configureHandlerExceptionResolvers(List resolvers) 配置异常解析器。
void extendHandlerExceptionResolvers(List resolvers) 扩展或修改默认的异常解析器列表。

 
    在 Spring Boot 项目中,我们可以通过以下 2 种形式定制 Spring MVC:

        扩展 Spring MVC
        全面接管 Spring MVC


    1) 扩展 Spring MVC

        如果 Spring Boot 对 Spring MVC 的自动配置不能满足我们的需要,还可以通过自定义一个 WebMvcConfigurer 类型(实现 WebMvcConfigurer 接口)的配置类(标注 @Configuration,但不标注 @EnableWebMvc 注解的类),来扩展 Spring MVC。
        
        这样不但能够保留 Spring Boot 对 Spring MVC 的自动配置,享受 Spring Boot 自动配置带来的便利,还能额外增加自定义的 Spring MVC 配置。

        示例,在 “” 里 SpringbootWeb 项目 的基础上,代码如下。

            (1) 添加 jQuery (https://jquery.com/)

                下载 jQuery 3.6.0 放置到目录 src/main/resources/static/js/jquery-3.6.0.min.js

            (2) 创建 src/main/resources/templates/home.html 文件

 1                 
 2                 "en" xmlns:th="http://www.thymeleaf.org">
 3                 
 4                     "UTF-8">
 5                     Home
 6                     <script language="javascript" src="/js/jquery-3.6.0.min.js"></script>
 7                 
 8                 
 9 
10                     

Home Page

11

jQuery Status: "jquery_status">OFF

12 13 <script type="text/javascript"> 14 $(document).ready(function() { 15 console.log("Home page"); 16 $("#jquery_status").html("ON"); 17 }); 18 </script> 19 20


            (3) 创建 src/main/java/com/example/config/ExtendMvcConfigurer.java 文件

 1                 package com.example.config;
 2 
 3                 import org.springframework.context.annotation.Configuration;
 4                 import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
 5                 import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
 6 
 7                 // 实现 WebMvcConfigurer 接口可以来扩展 SpringMVC 的功能
 8                 @Configuration
 9                 public class ExtendMvcConfigurer implements WebMvcConfigurer {
10 
11                     @Override
12                     public void addViewControllers(ViewControllerRegistry registry) {
13 
14                         // 当访问 “/” 或 “/index.html” 时,都直接跳转到 home
15                         registry.addViewController("/").setViewName("home");
16                         registry.addViewController("/index.html").setViewName("home");
17                     }
18 
19                 }


            (4) 创建 src/main/java/com/example/controller/IndexController.java 文件

 1                 package com.example.controller;
 2 
 3                 import org.springframework.stereotype.Controller;
 4                 import org.springframework.web.bind.annotation.RequestMapping;
 5 
 6                 @Controller
 7                 public class IndexController {
 8                     @RequestMapping("/home")
 9                     public String home() {
10                         return "home";
11                     }
12                 }


            访问 http://localhost:9090/   

                Home Page

                jQuery Status: ON    


    2) 完全接管 Spring MVC

        在一些特殊情况下,可能需要抛弃 Spring Boot 对 Spring MVC 的全部自动配置,完全接管 Spring MVC。此时可以自定义一个 WebMvcConfigurer 类型(实现 WebMvcConfigurer 接口)的配置类,并在该类上标注 @EnableWebMvc 注解,来实现完全接管 Spring MVC。

            注:完全接管 Spring MVC 后,Spring Boot 对 Spring MVC 的自动配置将全部失效。
        
        示例,在上文 SpringbootWeb 项目基础上,代码如下。

            修改 src/main/java/com/example/config/ExtendMvcConfigurer.java 文件

 1                 package com.example.config;
 2 
 3                 import org.springframework.context.annotation.Configuration;
 4                 import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
 5                 import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
 6                 import org.springframework.web.servlet.config.annotation.EnableWebMvc;
 7 
 8                 // 实现 WebMvcConfigurer 接口可以来扩展 SpringMVC 的功能
 9                 @EnableWebMvc  // 完全接管SpringMVC
10                 @Configuration
11                 public class ExtendMvcConfigurer implements WebMvcConfigurer {
12 
13                     @Override
14                     public void addViewControllers(ViewControllerRegistry registry) {
15 
16                         // 当访问 “/” 或 “/index.html” 时,都直接跳转到 home
17                         registry.addViewController("/").setViewName("home");
18                         registry.addViewController("/index.html").setViewName("home");
19                     }
20 
21                 }


            访问 http://localhost:9090/      

                Home Page

                jQuery Status: OFF

            注:Spring Boot 能够访问位于静态资源文件夹中的静态文件,这是在 Spring Boot 对 Spring MVC 的默认自动配置中定义的。@EnableWebMvc 完全接管 Spring MVC 后,Spring Boot 对 Spring MVC 的默认配置都会失效,此时再访问静态资源文件夹中的静态资源(js/jquery-3.6.0.min.js)就会报 404 错误。


2. 拦截器(Interceptor)

    对于拦截器我们并不陌生,无论是 Struts 2 还是 Spring MVC 中都提供了拦截器功能,它可以根据 URL 对请求进行拦截,主要应用于登陆校验、权限验证、乱码解决、性能监控和异常处理等功能上。Spring Boot 同样提供了拦截器功能。

    在 Spring Boot 项目中,使用拦截器功能通常需要以下步骤:

        (1) 定义拦截器;
        (2) 注册拦截器;
        (3) 指定拦截规则(如果是拦截所有,静态资源也会被拦截)。


    1) 定义拦截器

        在 Spring Boot 中定义拦截器十分的简单,只需要创建一个拦截器类,并实现 HandlerInterceptor 接口即可。

        HandlerInterceptor  接口中定义以下 3 个方法,如下表。

方法 描述
boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) 该方法在控制器处理请求方法前执行,其返回值表示是否中断后续操作,返回 true 表示继续向下执行,返回 false 表示中断后续操作。
void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView)  该方法在控制器处理请求方法调用之后、解析视图之前执行,可以通过此方法对请求域中的模型和视图做进一步修改。
void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) 该方法在视图渲染结束后执行,可以通过此方法实现资源清理、记录日志信息等工作。


        实现 HandlerInterceptor 接口,代码如下:

 1             public class LoginInterceptor implements HandlerInterceptor {
 2 
 3                 @Override
 4                 public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
 5                                         Object handler) throws Exception {
 6 
 7                 }
 8 
 9                 @Override
10                 public void postHandle(HttpServletRequest request, HttpServletResponse response,
11                                         Object handler, ModelAndView modelAndView) throws Exception {
12 
13                 }
14 
15                 @Override
16                 public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
17                                             Object handler, Exception ex) throws Exception {
18 
19                 }
20             } 

    2) 注册拦截器

        创建一个实现了 WebMvcConfigurer 接口的配置类(使用了 @Configuration 注解的类),重写 addInterceptors() 方法,并在该方法中调用 registry.addInterceptor() 方法将自定义的拦截器注册到容器中。

        实现 WebMvcConfigurer 接口,代码如下:

 1             // 实现 WebMvcConfigurer 接口可以来扩展 SpringMVC 的功能
 2             @Configuration
 3             public class ExtendMvcConfig implements WebMvcConfigurer {
 4                 ...
 5 
 6                 @Override
 7                 public void addInterceptors(InterceptorRegistry registry) {
 8                     registry.addInterceptor(new LoginInterceptor());
 9                 }
10             }

    3) 指定拦截规则

        修改 ExtendMvcConfig 配置类中 addInterceptors() 方法的代码,继续指定拦截器的拦截规则,代码如下。

 1         @Configuration
 2         public class ExtendMvcConfig implements WebMvcConfigurer {
 3             ...
 4 
 5             @Override
 6             public void addInterceptors(InterceptorRegistry registry) {
 7                 registry.addInterceptor(new LoginInterceptor())
 8                         .addPathPatterns("/**") // 拦截所有请求,包括静态资源文件
 9                         .excludePathPatterns("/", "/index.html", "/user/login", "/user/login/post", "/css/**",
10                             "/images/**", "/js/**"); // 放行登录页,登陆操作,静态资源
11             }
12         }


        在指定拦截器拦截规则时,调用了两个方法,这两个方法的说明如下:

            addPathPatterns:该方法用于指定拦截路径,例如拦截路径为 “/**”,表示拦截所有请求,包括对静态资源的请求。
            excludePathPatterns:该方法用于排除拦截路径,即指定不需要被拦截器拦截的请求。

    示例,在 “” 里 SpringbootWeb 项目 的基础上,代码如下。

        (1) 创建 src/main/resources/templates/login.html 文件

 1             
 2             "en" xmlns:th="http://www.thymeleaf.org">
 3             
 4                 "UTF-8">
 5                 Login
 6             
 7             
 8 
 9                 

Login

10 11

"${msg}">

12 13
"/user/login/post" method="POST"> 14 15

Username: "text" name="username" value="admin" />

16

Password: "text" name="password" value="123456" />

17 18

"submit" value="Submit" />

19 20
21 22 23


        (2) 创建 src/main/java/com/example/interceptor/LoginInterceptor.java 文件

 1             package com.example.interceptor;
 2 
 3             import javax.servlet.http.HttpServletRequest;
 4             import javax.servlet.http.HttpServletResponse;
 5             import org.springframework.web.servlet.HandlerInterceptor;
 6             import org.springframework.web.servlet.ModelAndView;
 7 
 8             public class LoginInterceptor implements HandlerInterceptor {
 9 
10                 @Override
11                 public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
12                                         Object handler) throws Exception {
13                     Object loginUser = request.getSession().getAttribute("loginUser");
14                     if (loginUser == null) {
15                         request.setAttribute("msg", "No authentication, try login");
16                         request.getRequestDispatcher("/index.html").forward(request, response);
17                         return false;
18                     } else {
19                         return true;
20                     }
21                 }
22 
23                 @Override
24                 public void postHandle(HttpServletRequest request, HttpServletResponse response,
25                                     Object handler, ModelAndView modelAndView) throws Exception {
26                     System.out.println("LoginInterceptor -> postHandle(): modelAndView = " + modelAndView);
27                 }
28 
29                 @Override
30                 public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
31                                             Object handler, Exception ex) throws Exception {
32                     System.out.println("LoginInterceptor -> afterCompletion()");
33                 }
34             }


        (3) 创建 src/main/java/com/example/config/ExtendMvcConfigurer.java 文件

 1             package com.example.config;
 2 
 3             import org.springframework.context.annotation.Configuration;
 4             import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
 5             import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
 6 
 7             // 实现 WebMvcConfigurer 接口可以来扩展 SpringMVC 的功能
 8             @Configuration
 9             public class ExtendMvcConfigurer implements WebMvcConfigurer {
10 
11                 @Override
12                 public void addViewControllers(InterceptorRegistry registry) {
13 
14                     // 当访问 “/” 或 “/index.html” 时,都直接跳转到登陆页面
15                     registry.addViewController("/").setViewName("login");
16                     registry.addViewController("/index.html").setViewName("login");
17                     registry.addViewController("/home.html").setViewName("home");
18                 }
19 
20                 @Override
21                 public void addInterceptors(InterceptorRegistry registry) {
22 
23                     registry.addInterceptor(new LoginInterceptor())
24                             .addPathPatterns("/**") // 拦截所有请求,包括静态资源文件
25                             .excludePathPatterns("/", "/index.html", "/user/login", "/user/login/post",
26                                     "/css/**", "/images/**", "/js/**"); // 放行登录页,登陆操作,静态资源
27                 }                
28 
29             }


        (4) 创建 src/main/java/com/example/controller/UserController.java 文件

 1             package com.example.controller;
 2 
 3             import java.util.Map;
 4             import javax.servlet.http.HttpSession;
 5             import org.springframework.stereotype.Controller;
 6             import org.springframework.web.bind.annotation.RequestMapping;
 7             import org.springframework.web.bind.annotation.RequestMethod;
 8 
 9             @Controller
10             @RequestMapping("/user")
11             public class UserController {
12 
13                 @RequestMapping("/login")
14                 public String login() {
15                     return "login";
16                 }
17 
18                 @RequestMapping(value = "/login/post")
19                 public String loginPost(String username, String password, 
20                                         Map map, HttpSession session) {
21 
22                     if ("admin".equals(username) && "123456".equals(password)) {
23                         session.setAttribute("loginUser", username);
24                         return "redirect:/home.html";
25                     } else {
26                         map.put("msg", "Invalid username or password");
27                     }
28 
29                     return "login";
30                 }
31             }


        访问 http://localhost:9090/ 或 http://localhost:9090/home.html