Spring Boot拦截器(Interceptor)

发布时间 2023-05-28 14:48:58作者: ImreW

(一)什么是拦截器?

1.含义

Spring Boot中,拦截器是一种用于拦截和处理HTTP请求的机制。它是Spring框架提供的一种中间件,用于在请求到达控制器(Controller)之前或之后执行一些共享的逻辑

Spring Boot的拦截器基于Spring MVC框架中的HandlerInterceptor接口实现。通过创建一个自定义的拦截器类并实现HandlerInterceptor接口,可以定义拦截器要执行的逻辑和行为。

2.作用

身份验证和权限控制: 拦截器可以用于检查用户的身份验证状态和权限,并根据需要进行相关处理。例如,可以使用拦截器验证用户的登录状态,如果未登录则重定向到登录页面或返回相应的错误信息。

异常处理和统一错误处理: 拦截器可以捕获并处理请求处理过程中发生的异常。可以根据异常类型进行适当的处理,如返回自定义错误页面或错误信息,或执行特定的错误处理逻辑。

当然它还有其它的应用场景,这里就不一一列举了。

(二)用户登录权限效验

1.自定义拦截器

@Component
public class LoginInterceptor implements HandlerInterceptor {

    //调用目标方法之前执行的方法
    //如果返回ture表示拦截器验证成功,执行目标方法
    //如果返回false表示拦截器验证失败,不再继续执行后续业务
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //用户登录判断业务
        HttpSession session = request.getSession(false);

        if(session != null && session.getAttribute("session_userinfo") != null){
            //用户已登录
            return true;
        }
        response.setStatus(401);
        return false;
    }
}

代码中的preHandle方法是拦截器的主要方法,在目标方法调用之前执行。它接收三个参数:HttpServletRequest对象表示当前的HTTP请求,HttpServletResponse对象表示当前的HTTP响应,Object handler表示被拦截的处理器(一般是Controller中的方法)

preHandle方法中,首先通过request.getSession(false)获取当前请求的HttpSession对象(如果存在的话),然后判断该HttpSession对象是否为null并且是否存在名为"session_userinfo"的属性。如果这个条件成立,说明用户已经登录,可以继续执行后续的业务,于是返回true,否则验证失败,将HTTP响应的状态码设置为401,表示未授权,然后返回false,不再继续执行后续业务。

2.将自定义拦截器加入到系统配置

@Configuration
public class MyConfig implements WebMvcConfigurer {

    //注入
    @Autowired
    private LoginInterceptor loginInterceptor;

    //将拦截器
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(loginInterceptor)
                .addPathPatterns("/**") //拦截所有的 url
                .excludePathPatterns("/user/login")//排除url: /user/login (登录)
                .excludePathPatterns("/user/reg") //排除url: /user/reg   (注册)
                .excludePathPatterns("/image/**")//排除 image(图像) 文件夹下的所有文件
                .excludePathPatterns("/**/*.js")//排除任意深度目录下的所有".js"文件
                .excludePathPatterns("/**/*.css");
    }
}

在配置类中,重写了addInterceptors方法,该方法用于注册拦截器。在这里,通过调用InterceptorRegistryaddInterceptor方法来添加拦截器,并设置拦截的路径和排除的路径。

具体地,通过调用addInterceptor(loginInterceptor)来添加LoginInterceptor拦截器。然后使用addPathPatterns方法指定需要拦截的URL路径模式,这里使用"/**"表示拦截所有的URL。使用excludePathPatterns方法来排除一些特定的URL路径,这些路径不会被拦截。

对于"/**/*.js""**":表示零个或多个路径段(目录或文件夹),可以匹配任意深度的目录结构。"/*.js":表示以".js"结尾的文件名。

UserController:

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

    @RequestMapping("/login")
    public String login(){
        return "login";
    }

    @RequestMapping("/index")
    public String index(){
        return "index";
    }

    @RequestMapping("/reg")
    public String reg(){
        return "reg";
    }
}

访问“login”

访问“index”

返回了401

访问“reg”

3.统一访问前缀添加

所有请求地址添加test前缀:   在WebMvcConfigurer接口中,configurePathMatch方法用于配置路径匹配规则。

@Configuration
public class MyConfig implements WebMvcConfigurer {
        //统一访问前缀的添加
        @Override
        public void configurePathMatch(PathMatchConfigurer configurer) {
            configurer.addPathPrefix("test", new Predicate<Class<?>>() {
                @Override
                public boolean test(Class<?> aClass) {
                    return true;
                }
            });
        }
}

在这个例子中,传递给addPathPrefix方法的前缀是"test",而Predicate对象是一个匿名内部类,实现了Predicate<Class<?>>接口。Predicate接口是Java 8中引入的函数式接口,它的test方法用于判断传入的类是否符合条件。

在这个匿名内部类中,test方法被重写为总是返回true,这意味着所有的类都符合条件,都会被添加统一访问前缀。

因此,通过这段代码的配置,所有的请求路径都会在前面添加"test"前缀。例如,原始路径为"/example",添加了前缀后的路径就变为"/test/example"。这样可以实现对请求路径的统一处理。

注意:如果加了前缀,拦截器的排除路径也要跟着改动:

 //将拦截器
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(loginInterceptor)
                .addPathPatterns("/**") //拦截所有的 url
                .excludePathPatterns("/**/user/login")//排除url: /user/login (登录)
                .excludePathPatterns("/**/user/reg") //排除url: /user/reg   (注册)
                .excludePathPatterns("/**/image/**")//排除 image(图像) 文件夹下的所有文件
                .excludePathPatterns("/**/*.js")//排除任意深度目录下的所有".js"文件
                .excludePathPatterns("/**/*.css");
    }

(三)统一异常处理

下面的代码在访问后会返回什么?

@RestController
@RequestMapping("/user")
public class UserController {
    @RequestMapping("/login")
    public Integer login(){
        Object object = null;
        object.hashCode();
        return 1;
    }
}

答案是:

有没有一种手段可以在发生异常的时候返回有用的信息(此时的响应状态为200),而不是冰冷的报错信息呢?

那就是统⼀异常处理:

@ControllerAdvice
@ResponseBody
public class MyExceptionAdvice {
    @ExceptionHandler(NullPointerException.class)
    public HashMap<String,Object> doNullPointerException(NullPointerException e){
        HashMap<String,Object> result = new HashMap<>();
        result.put("code",-1);
        result.put("msg","空指针:" + e.getMessage());
        result.put("data",null);
        return result;
    }
}
  1. @ControllerAdvice注解标识该类是一个全局异常处理器,它将捕获应用程序中抛出的异常,并执行相应的处理逻辑。
  2. @ExceptionHandler(NullPointerException.class)注解指定了处理NullPointerException类型异常的方法doNullPointerException()
  3. doNullPointerException()方法的参数是NullPointerException类型的异常对象,表示捕获到的具体异常实例。
  4. doNullPointerException()方法返回一个HashMap<String, Object>对象,用于封装异常处理结果。

这段代码的作用是当捕获到NullPointerException异常时,执行doNullPointerException()方法,并返回一个包含异常处理结果的HashMap对象。该结果以JSON格式返回给客户端。

当有多个异常处理器的时候,它们的处理次序:

@ControllerAdvice
@ResponseBody
public class MyExceptionAdvice {

    //处理 NullPointerException 异常
    @ExceptionHandler(NullPointerException.class)
    public HashMap<String,Object> doNullPointerException(NullPointerException e){
        //处理
        HashMap<String,Object> result = new HashMap<>();
        result.put("code",-1);
        result.put("msg","NullPointerException:" + e.getMessage());
        result.put("data",null);
        return result;
    }
    
    //处理 Exception 异常
    @ExceptionHandler(Exception.class)
    public HashMap<String,Object> doException(Exception e){
        //处理
        HashMap<String,Object> result = new HashMap<>();
        result.put("code",-1);
        result.put("msg","Exception:" + e.getMessage());
        result.put("data",null);
        return result;
    }
}
@RestController
@RequestMapping("/user")
public class UserController {
    @RequestMapping("/login")
    public Integer login(){
        Object object = null;
        object.hashCode();
        return 1;
    }
}

结论:如果能匹配就子类优先,如果没有匹配到就找父类。

(四)统一数据返回格式

1.为什么需要统一数据返回格式?

  1. 统一数据返回格式可以帮助前端程序员更好地接收和解析后端数据接口返回的数据,
  2. 规范性和可读性:统一数据返回格式可以定义一种统一的数据结构和字段,使得不同接口的返回数据具有一致性。这样可以提高代码的可读性和维护性,降低前后端开发人员之间的沟通成本。
  3. 统一数据维护和修改:通过统一数据返回格式,项目中的所有接口都遵循相同的数据格式,使得数据维护和修改变得更加方便和统一。如果需要修改数据返回的结构或字段,只需要在统一数据返回格式的定义处进行修改,而不需要逐个接口进行修改。
  4. 有利于制定后端技术规范标准:统一数据返回格式可以作为后端技术部门制定的一项规范标准。它可以避免出现各种奇怪的返回内容,提供统一的数据规范和结构,使得开发人员在后端开发过程中能够更好地遵循规范,提高代码质量和可维护性。

2.统一数据返回格式案例

假设这里我们以{"msg": *,"code": *,"data": *}这种形式来作为标准返回格式。使用 @ControllerAdvice 注解结合 ResponseBodyAdvice 接口可以实现统一的数据返回格式。

@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {

    /**
     * @return 如果为 true,就执行 beforeBodyWrite
     */
    @Override
    public boolean supports(MethodParameter returnType, Class converterType){
        return true;
    }

    /**
     * 返回数据之前进行数据重写
     * @param body 原始返回值
     * @return
     */
    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType,
                                  MediaType selectedContentType, Class selectedConverterType,
                                  ServerHttpRequest request, ServerHttpResponse response) {

        // 在返回数据之前进行处理,可以修改、包装响应体,添加额外信息等
        // 这里可以对返回的数据进行统一的格式处理
        if(body instanceof HashMap){
            // 如果已经是统一响应格式,则直接返回
            return body;
        }
        // 如果不是统一响应格式,则进行包装
        HashMap<String,Object> result = new HashMap<>();
        result.put("code",200);
        result.put("data",body);
        result.put("msg","");
        return result;
    }
}
@RestController
@RequestMapping("/user")
public class UserController {
    @RequestMapping("/login")
    public HashMap<String,Object> login(){
        HashMap<String,Object> hashMap = new HashMap<>();
        hashMap.put("code",200);
        hashMap.put("data",1);
        hashMap.put("msg","");
        return hashMap;
    }
    @RequestMapping("/reg")
    public Integer reg(){
        return 10;
    }
}

3.返回值为 String 的情况

假如我要对下面的代码进行格式统一处理:

@RequestMapping("/hi")
public String hi(){
    return "Hello World";
}

可以发现报错了,为什么没有成功呢?

返回执行流程:

  1. 方法返回的是 String
  2. 统一数据返回之前处理:将 String 装入 HashMap 中。
  3. 将 HashMap 转换为 application/json 字符串给前端。

问题就出在第三步,在进行转换 application/json 的时候,会先判断原 Body 的类型,如果是 String 类型,将会启用 StringHttpMessageConverter 进行类型转换,如果不是 String 类型,就用HttpMessageConverter来进行转换。

在 Spring MVC 中HttpMessageConverter 负责处理请求和响应的消息体,将请求的数据转换为方法参数的类型,以及将方法返回值转换为响应的数据格式。StringHttpMessageConverter 特别用于处理字符串类型的数据。

StringHttpMessageConverter只能把String转换为其他类型,例如将字符串作为响应的内容返回给客户端。而代码中是把 HashMap 转换为 application/json字符串。所以报错了。

解决方法:

  • 直接对 String 做特殊处理:直接把 HashMap 转为 json 字符串再发送给前端。
@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {

    @Autowired
    private ObjectMapper objectMapper;

    .......
        略
    .......
        
    //重写返回结果
    HashMap<String,Object> result = new HashMap<>();
    result.put("code",200);
    result.put("data",body);
    result.put("msg","");
    if(body instanceof String){
        //将 HashMap 转换为 json 字符串
        return objectMapper.writeValueAsString(request);
    }

    .......
        略
    .......
}
  • 删除 StringHttpMessageConverter
@Configuration
public class MyConfig implements WebMvcConfigurer {
    /**
     * 移除 StringHttpMessageConverter
     *
     * @param converters
     */
    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        converters.removeIf(converter -> converter instanceof StringHttpMessageConverter);
    }
}