1

背景

Spring中我们经常会对请求做一些特殊处理,比如对请求体的编码解码,做请求的日志,统一修改响应体等等。其中提到了的功能我们会用到Filter,Interceptor,Advice,但是我们感觉都是对请求做操作,什么时候要用哪个呢?下面会进行介绍。

流程图

实际上Filter和Interceptor都是有前置和后置的方法,将请求一层一层包裹起来,而Advice像是扩展点,可以注意一下下面的流程图。
image.png

职责

其实他们的职责需要结合他们的触发时机和返回值去区别,下面时一些他们之间的区别。

  • Filter:用于判断所有请求(可以自行匹配路由,或者如一些服务注册请求等),包装和读请求(字节流)和响应(字节流)。
  • Interceptor:判断进入Spring MVC的请求是否放行,但不能去读请求体(读了之后无法传递),响应体也无法修改,他的后置方法触发时响应体已经发送出去了。
  • Advice:允许拿到请求体(对象),响应体(对象)和异常等,就是用于做统一数据处理的。

实现

Filter

FILter可以通过传参获取请求和响应,虽然请求体直接读会导致ServletRequest 不能被后面读,不过可以用过滤器链继续传递一个新的request。

package com.example.demo.filter;

import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletRequest;
import java.io.IOException;

public class SimpleLogFilter implements Filter {

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        System.out.println("SimpleLogFilter init");
    }

    @Override
    public void doFilter(ServletRequest request,
                         ServletResponse response,
                         FilterChain chain) throws IOException, ServletException {
        HttpServletRequest req = (HttpServletRequest) request;
        long start = System.currentTimeMillis();

        System.out.println("请求进入:" + req.getRequestURI());
        try {
            chain.doFilter(request, response);
        } finally {
            System.out.println("请求结束,耗时:" + (System.currentTimeMillis() - start) + "ms");
        }
    }

    @Override
    public void destroy() {
        System.out.println("SimpleLogFilter destroy");
    }
}

Interceptor

Interceptor传参很像过滤器,但是他没办法读完请求后继续传递给下一个处理环节,所以他不适合读请求,afterCompletion却执行在返回响应之后,所以他的职责就是看是否放行请求。

package com.example.demo.interceptor;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

@Component
public class AuthInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request,
                             HttpServletResponse response,
                             Object handler) throws Exception {

        if (!(handler instanceof HandlerMethod hm)) {
            return true;
        }

        // 方法上的注解
        RequireLogin annotation = hm.getMethodAnnotation(RequireLogin.class);
        if (annotation == null) {
            return true;
        }

        String token = request.getHeader("Authorization");
        if (token == null || token.isEmpty()) {
            response.setStatus(401);
            response.setContentType("application/json;charset=UTF-8");
            response.getWriter().write("{\"code\":401,\"msg\":\"未登录\"}");
            return false;
        }

        // 模拟解析 token
        Long userId = parseUserId(token);
        if (userId == null) {
            response.setStatus(401);
            response.setContentType("application/json;charset=UTF-8");
            response.getWriter().write("{\"code\":401,\"msg\":\"token 无效\"}");
            return false;
        }

        request.setAttribute("userId", userId);
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request,
                           HttpServletResponse response,
                           Object handler,
                           ModelAndView modelAndView) throws Exception {
        // 传统 MVC 场景:可以往 modelAndView 里塞公共数据
        if (modelAndView != null) {
            modelAndView.addObject("appName", "demo");
        }
    }

    @Override
    public void afterCompletion(HttpServletRequest request,
                                HttpServletResponse response,
                                Object handler,
                                Exception ex) throws Exception {
        Long start = (Long) request.getAttribute("startTime");
        if (start != null) {
            System.out.println("耗时:" + (System.currentTimeMillis() - start) + "ms");
        }
        if (ex != null) {
            System.out.println("请求异常:" + ex.getMessage());
        }
    }

    private Long parseUserId(String token) {
        // 模拟
        return 1L;
    }
}

Advice

这里时处理响应体的Advice,这里它的传参有响应体的对象,比较友好。

package com.example.demo.advice;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;

@RestControllerAdvice(basePackages = "com.example.demo.controller")
public class GlobalResponseAdvice implements ResponseBodyAdvice<Object> {

    private final ObjectMapper objectMapper = new ObjectMapper();

    @Override
    public boolean supports(MethodParameter returnType,
                            Class<? extends HttpMessageConverter<?>> converterType) {
        // 跳过已经包装过的、或者加了 @SkipWrap 注解的方法
        return !returnType.hasMethodAnnotation(SkipWrap.class);
    }

    @Override
    public Object beforeBodyWrite(Object body,
                                  MethodParameter returnType,
                                  MediaType selectedContentType,
                                  Class<? extends HttpMessageConverter<?>> selectedConverterType,
                                  ServerHttpRequest request,
                                  ServerHttpResponse response) {

        // 已经是 Result,直接返回
        if (body instanceof Result) {
            return body;
        }

        Result<Object> result = Result.success(body);

        // 如果返回类型是 String,需要手动序列化,否则会 ClassCastException
        if (body instanceof String) {
            try {
                return objectMapper.writeValueAsString(result);
            } catch (JsonProcessingException e) {
                throw new RuntimeException(e);
            }
        }

        return result;
    }
}

三者对比

其实不难发现Filter和Advice已经可以覆盖Interceptor很多的作用,所以基本上在前后端分离的项目很少看见它,不过对于一些独特的需求如:在反序列化之前拦截,基于 HandlerMethod 注解短路等还是需要它的。

维度FilterInterceptorAdvice
所属层Servlet 容器Spring MVCSpring AOP
规范来源Servlet 规范Spring 框架Spring AOP
作用范围所有请求(含静态资源)匹配 Controller 的请求任意 Spring bean 方法
能否拿 HandlerMethod❌✅❌
能否拿方法参数值❌❌✅
能否改返回值❌❌✅
能否读请求体✅(需 Wrapper)❌❌
能否读响应体✅(需 Wrapper)❌❌
能否按路径匹配✅(urlPatterns)✅(addPathPatterns)✅(切点表达式)
能否按注解匹配❌⚠️ 手动判断✅(切点表达式)
能否注入 Spring bean⚠️ 需特殊处理✅✅
异常处理能力只能捕获,难细分afterCompletion 拿到异常@AfterThrowing 精确捕获

最后

希望这篇文章能够帮助您!


猫贼
61 声望7 粉丝

大道至简