Springboot配置拦截过滤的方式

165 阅读8分钟

前言

遇到一些场景,需要对指定的请求数据做前置或者后置的封装处理。 此时我们应该能想到的就是过滤器,拦截器。还有之前提到的全局异常的输出封装。这些都是也属于对数据的统一逻辑前后置处理。

过滤器

在 Spring Boot 中,Filter(过滤器)是一种用于拦截和处理 HTTP 请求和响应的组件。它可以在请求到达目标资源(如控制器方法)之前进行预处理,也可以在目标资源处理完请求并生成响应之后进行后处理。

作用: 字符编码,跨域处理等作用

实现过程:

  • 通过自定义继承Filter实现的规则类
  • 使用注解配置应用/使用FilterRegistrationBean配置应用

自定义过滤器逻辑

  • init方法:服务启动时候,初始化设置配置操作
  • doFilter 方法: 处理请求和响应调用FilterChain的doFilter将请求传递给下一个 Filter 或者目标资源
  • destroy方法:服务关闭的时候,用于释放 Filter 在初始化或使用过程中占用的资源

自定义CustomerFilter过滤器,如下我们对请求前后增加日志的输出初始配置的值

public class CustomerFilter implements Filter {

    private static final Logger logger = LoggerFactory.getLogger(CustomerFilter.class);

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        logger.info("Filter handle before");
        // 传递给下一个Filter执行
        chain.doFilter(request, response);
        logger.info("Filter handle after");
    }
}

代码实现绑定过滤器

使用 FilterRegistrationBean 绑定上我们自定义的过滤器,配置生效的路径和初始化参数,优先级等信息。

注意: order数字越小,优先级越高

@Bean
public FilterRegistrationBean customerFilter() {
    FilterRegistrationBean registration = new FilterRegistrationBean();
    // 设置过滤器
    registration.setFilter(new CustomerFilter());
    // 拦截路由规则
    registration.addUrlPatterns("/intercept/*");
    // 设置过滤器的名称
    registration.setName("CustomerFilter");
    // 设置优先级等信息
    registration.setOrder(1);
    return registration;
}

通过注解绑定过滤器

使用 @WebFilter 配置拦截路径,过滤器名称,初始化值,使用@Order 配置优先级

  • urlPatterns : 过滤器的拦截路径
  • filterName : 过滤器的名称

注意: @Order注解的value也是数字越小,优先级越高

@Component
@WebFilter(urlPatterns = "/intercept/*", filterName = "customerFilter")
@Order(1)
public class CustomerFilter implements Filter {
    // 上述的自定义过滤器
}

对应的执行流程 :

  1. 启动服务后,会执行所有Filter的init方法
  2. 执行符合过滤器请求路径的请求,
    • 执行优先级高的过滤器前置请求
    • 执行优先级低的过滤器的前置请求
    • 执行优先级低的过滤器的后置响应
    • 执行优先级高的过滤器的后置响应
  3. 关闭服务后,会执行所有Filter的destory方法

拦截器

拦截器(Interceptor),用于在请求处理过程中进行拦截和处理。它可以在控制器方法被调用之前(pre - handle)、之后(post - handle)以及视图渲染之后(after - completion)进行操作

拦截器与过滤器的不同

  • 过滤器作为顶级,最先进行数据的预处理,也可以对静态资源进行处理。
    • 作用于 Servlet 容器级别。
    • 主要应用于 字符编码,跨域处理等场景
  • 拦截器作为请求到达了Spring MVC框架内,对控制层请求前后拦截处理。
    • 作用于 Spring MVC 框架内部
    • 应用于 权限认证,日志记录等场景

声明自定义个拦截器

主要实现org.springframework.web.servlet.HandlerInterceptor接口

  • preHandle方法:在请求处理之前被调用。它接收HttpServletRequestHttpServletResponseObject(代表被调用的处理器,通常是控制器方法)作为参数。
    • 返回值如果返回true,表示允许请求继续进行;
    • 返回值如果返回false,则表示拦截请求,后续的处理器(如控制器方法)将不会被调用。
  • postHandle方法:在处理器(控制器方法)执行之后、视图渲染之前被调用。它可以对模型和视图进行操作,例如添加或修改模型中的数据。
  • afterCompletion方法:在整个请求处理完成(包括视图渲染)之后被调用。它主要用于资源清理等操作。
public class LoggingInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println("请求开始时间:" + new Date());
        System.out.println("请求URL:" + request.getRequestURL());
        System.out.println("请求URI:" + request.getRequestURI());
        return true;
    }
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        System.out.println("请求结束时间:" + new Date());
    }
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        // 可以在这里进行资源清理等操作
        System.out.println("资源清理操作");
    }
}

注册拦截器配置

创建一个配置类来注册拦截器。这个配置类需要实现org.springframework.web.servlet.config.annotation.WebMvcConfigurer接口,并覆盖addInterceptors方法

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoggingInterceptor()).addPathPatterns("/**");
    }
}

执行的过程

  • 在请求符合拦截要求时
  • 先到拦截器PreHandle的方法,如果返回false,则中断。
  • 如果返回true, 再到具体请求controller中
  • 再到postHandle,最后到afterCompletion方法中

请求体和响应体的数据处理

SpringMvc中还提供了对请求体内容的数据预先处理。RequestBodyAdviceResponseBodyAdvice接口。

  • RequestBodyAdvice是用于在请求体(RequestBody)被读取和绑定到控制器方法的参数之前,对请求体进行处理。
    • 作用:用于对请求数据进行通用的预处理
    • 作用场景: 解密、数据格式转换、数据验证等操作
  • ResponseBodyAdvice是用于在控制器方法执行完毕,响应体(ResponseBody)被写入到 HTTP 响应之前,对响应体进行处理。
    • 作用: 它可以用于对响应的数据进行通用的后处理,
    • 作用场景: 加密、数据格式转换、添加额外信息等操作

实现RequestBodyAdvice接口

  • supports方法:用于判断当前的RequestBodyAdvice是否支持处理特定的控制器方法和请求类型。
  • beforeBodyRead方法:在请求体被读取之前被调用。这个方法可以对请求的HttpInputMessage(包含请求体的输入流等信息)进行操作,如修改请求头、替换输入流等,从而实现对请求体的预处理。
  • afterBodyRead方法:在请求体已经被读取并转换为对象(但还没有绑定到控制器方法的参数)之后被调用。可以对读取后的对象进行进一步的处理
  • handleEmptyBody方法:当请求体为空时被调用。可以根据具体情况返回一个默认的对象或者进行其他处理。
@ControllerAdvice
public class MyRequestBodyAdvice implements RequestBodyAdvice {
    @Override
    public boolean supports(MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
        // 假设只支持处理带有@RequestBody注解的参数
        return parameter.hasParameterAnnotation(org.springframework.web.bind.annotation.RequestBody.class);
    }
    @Override
    public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) throws IOException {
        return inputMessage;
    }
    @Override
    public Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
        // 假设body是一个包含用户名和密码的User对象,输出用户名
        if (body instanceof User) {
            User user = (User) body;
            System.out.println(user.getUsername())
        }
        return body;
    }
    @Override
    public Object handleEmptyBody(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
        return body;
    }
}

实现ResponseBodyAdvice接口

  • supports方法:用于判断当前的ResponseBodyAdvice是否支持处理特定的控制器方法和响应类型。
  • beforeBodyWrite方法:在响应体被写入之前被调用。这个方法可以对响应体进行操作
@ControllerAdvice
public class MyResponseBodyAdvice implements ResponseBodyAdvice {
    @Override
    public boolean supports(MethodParameter parameter, Class converterType) {
        // 假设支持所有的控制器方法和消息转换器
        return true;
    }
    @Override
    public Object beforeBodyWrite(Object body, MethodParameter parameter, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
        // 假设将所有的响应体(如果是字符串类型)转换为大写
        if (body instanceof String) {
            return ((String) body).toUpperCase();
        }
        return body;
    }
}

使用这个和我们之前提到的全局解决异常封装的 @ExceptionHandler 使用有些很相似,但是它的场景是异常问题,统一的响应结果输出。而上述我们提到的则是正常场景情况下的body内容的统一调整的输出。

AOP的处理

AOP 通过动态代理的方式在目标方法执行前后或出现异常时等时机插入额外的逻辑, 使得开发者从横切关注点从业务逻辑中抽离出来,形成单独模块去实现。

添加使用依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

具体实现

  • 定义切面类(实现切面逻辑的类),@Aspect 标记spring能扫描到。
  • 定义切点,@Pointcut指定哪些方法将被应用到此逻辑
  • 定义具体插入的逻辑
    • @Before : 目标方法执行前逻辑
    • @After : 目标方法执行后逻辑
    • @Around : 目标方法前后执行逻辑
    • @AfterThrowing : 目标方法抛出异常后执行逻辑
@Aspect // 定义切面类
@Component
public class LoggingAspect {
    // Pointcut表达式中指第一个*任意返回值, 第二个..* 当前包和子包下, 第三个.*任意方法名, 第四个.. 任意方法参数和个数
    // service方法名只是一个切点的标识,本身没有逻辑,用于下面Around的指代使用
    @Pointcut("execution(* com.example..*.*(..)))")
    public void service(){} 
    
    // 前后执行的逻辑, `ProceedingJoinPoint`对象用于获取目标方法等信息
    @Around("service()") 
    public Object aroundPrint(ProceedingJoinPoint joinPoint) throws Throwable {
        Object[] args = joinPoint.getArgs();
        log.info("args:{}", args);
        // joinPoint.proceed() 的结果为调用方法的返回值
        Object proceed = joinPoint.proceed();
        log.info("proceed:{}", proceed);
        return proceed;
    }
    
}

针对ProceedingJoinPoint对象的使用

  • getTarget() : 获取未经过代理的对象信息
  • getThis() : 获取代理对象的信息
  • getArgs() : 获取被拦截方法的参数列表 ,结果值是参数的数组
  • getSignature() : 获取到被拦截的方法的全限定名等信息
  • proceed(): 控制目标方法的执行

总结

至此,以上就是常用的对数据包装和修改的增强使用。