【Java Web】过滤器的核心原理、实现与执行顺序配置

文章目录


前言

过滤器是Java EE标准中Servlet规范提供的功能,也是传统JavaWeb的三大组件之一(Servlet,Filter和Listener)。通过过滤器可以把所有进入Servlet容器的请求都拦截住,无论静态资源还是Controller,从而实现一些通用的操作。


提示:以下是本篇文章正文内容,下面案例可供参考

一、核心功能

自定义过滤器的核心能力来自Filter这个接口。这个接口分别有init(),doFilter()和destroy()方法。init和destroy的生命周期依托于Servlet容器,容器初始化的时候会执行init()方法,容器销毁的时候会执行destroy()方法。

Filter 接口

java 复制代码
package jakarta.servlet;

import java.io.IOException;

public interface Filter {
    default void init(FilterConfig filterConfig) throws ServletException {
    }

    void doFilter(ServletRequest var1, ServletResponse var2, FilterChain var3) throws IOException, ServletException;

    default void destroy() {
    }
}

其中最常用的是doFilter方法,在每一次请求管道中,都会执行我们重写的这个方法。doFilter有一核心的FilterChain参数,它的核心作用是将请求传递给下一个 Filter或最终的控制器。缺少这个调用时,请求会卡在当前 Filter,后续所有处理环节都不会执行。

比如下面的doFilter方法中包含了对filterChain的调用,这样请求才会传递到后面,否则就会中断。

java 复制代码
filterChain.doFilter(servletRequest, servletResponse);

另外doFilter方法中还有ServletRequest和ServletResponse这两个参数,这两个分别对应请求和响应。我们可以通过这两个参数来查看或设置请求/响应

二、实现方式

过滤器本身是Servlet规范提供的功能,在Spring Boot中实现一个过滤器主要通过两种方法,接下来通过两种方式实现一个字符格式化过滤器和日志打印过滤器。

以下两种写法其中本质上还是Spring Boot默认使用FilterRegistrationBean来注册自定义Filter到容器中,两者仅仅是写法上不同

2.1 @WebFilter + @ServletComponentScan

首先我们定义一个CharsetFilter类来实现Filter类,Filter功能由jakarta.servlet这个包提供提供。

注意在自定义类上要使用@WebFilter()注解修饰,里面的参数则是URI路径匹配通配符,这里设置为"/*",表示匹配所有。

CharsetFilter类主要是拦截住请求,该Filter接口里最为核心的方法是doFilter。我们在doFilter的重载里写上自定义的逻辑,比如这里是将客户端到服务器和服务器到客户端的数据流都设置为UTF-8的编码格式。

值得注意的是@WebFilter需要和@ServletComponentScan搭配使用。需要在启动类上通过@ServletComponentScan来控制Spring Boot启动的时候扫描到这个被@WebFilter修饰的自定义过滤器。

通过@WebFilter + @ServletComponentScan这种实现本质上是Spring引导 Servlet 容器扫描@WebFilter最终由Servlet 容器直接调用ServletContext.addFilter()完成过滤器的注册。

java 复制代码
package org.araby.blognovelink.filter;
import jakarta.servlet.*;
import jakarta.servlet.annotation.WebFilter;
import lombok.extern.slf4j.Slf4j;

import java.io.IOException;

@Slf4j
@WebFilter("/*")
public class CharsetFilter implements Filter {

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        log.info("初始化统一字符编码过滤器");
        Filter.super.init(filterConfig);
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        log.info("统一字符编码过滤器开始执行");
        servletRequest.setCharacterEncoding("UTF-8");
        servletResponse.setCharacterEncoding("UTF-8");
        filterChain.doFilter(servletRequest, servletResponse);
    }

    @Override
    public void destroy() {
        log.info("销毁统一字符编码过滤器");
        Filter.super.destroy();
    }
}
java 复制代码
@SpringBootApplication
@ServletComponentScan
public class BlogNovelinkApplication {
    public static void main(String[] args) {
        SpringApplication.run(BlogNovelinkApplication.class, args);
    }
}

执行结果里包含

2.2 @Component修饰自定义过滤器类

第二种通过在自定义过滤器类添加@Component会更加的简单,这样就不需要在启动类上添加@ServletComponentScan。这种方式基于Spring的IOC容器,直接将过滤器作为Bean对象注册到容器中,最后在请求管道里注入自定义过滤器。

但仅用@Component注解标记Filter类时,Spring Boot 会自动注册该过滤器,默认拦截规则为所有请求。如果要手动控制匹配顺序,需要手动实现一个配置类来实现FilterRegistrationBean的注册,这里后面再详细介绍。

这里我们定义一个自定义的日志过滤器,这次只有在类上加一个@Component注解。其底层还是依赖 Spring的FilterRegistrationBean完成注册。

java 复制代码
import jakarta.servlet.*;
import jakarta.servlet.annotation.WebFilter;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;

import java.io.IOException;

@Slf4j
@Component
public class LogFilter implements Filter {

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        log.info("初始化日志过滤器");
        Filter.super.init(filterConfig);
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        log.info("日志过滤器开始执行");
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        String uri = request.getRequestURI();
        String method = request.getMethod();
        String ip = request.getRemoteAddr();
        long startTime = System.currentTimeMillis();
        filterChain.doFilter(servletRequest, servletResponse);
        // 记录响应耗时
        long costTime = System.currentTimeMillis() - startTime;
        log.info("Response - URL: {}, Cost Time: {}ms", uri, costTime);
    }

    @Override
    public void destroy() {
        log.info("销毁日志过滤器");
        Filter.super.destroy();
    }
}

三、过滤器执行的顺序

3.1 现象讨论

假想一下有三个过滤器在请求管道里,分别是过滤器a,过滤器b和过滤器c。它们都是通过@WebFilter + @ServletComponentScan的方式初始化。

观察执行结果发现了一个神奇的现象,初始化和销毁顺序 不等于执行顺序。初始化/销毁顺序是A-C-B,而 执行顺序是A-B-C。这是因为@WebFilter + @ServletComponentScan默认情况下自定义过滤器其初始化顺序由Servlet容器加载Servlet组件的规则决定,而执行顺序由Servlet容器构建FilterChain时的排序规则,像Tomcat默认会对所有@WebFilter按类名排序。

执行结果

bash 复制代码
[INFO ] [2025-12-02 22:14:18.365] [main] [] o.araby.blognovelink.filter.AFilter - 初始化A过滤器
[INFO ] [2025-12-02 22:14:18.365] [main] [] o.araby.blognovelink.filter.CFilter - 初始化C过滤器
[INFO ] [2025-12-02 22:14:18.365] [main] [] o.araby.blognovelink.filter.BFilter - 初始化B过滤器
[INFO ] [2025-12-02 22:14:36.764] [http-nio-9090-exec-2] [] o.araby.blognovelink.filter.AFilter - A过滤器开始执行
[INFO ] [2025-12-02 22:14:36.765] [http-nio-9090-exec-2] [] o.araby.blognovelink.filter.BFilter - B过滤器开始执行
[INFO ] [2025-12-02 22:14:36.765] [http-nio-9090-exec-2] [] o.araby.blognovelink.filter.CFilter - C过滤器开始执行
[INFO ] [2025-12-02 22:14:42.262] [SpringApplicationShutdownHook] [] o.araby.blognovelink.filter.AFilter - 销毁A过滤器
[INFO ] [2025-12-02 22:14:42.262] [SpringApplicationShutdownHook] [] o.araby.blognovelink.filter.CFilter - 销毁C过滤器
[INFO ] [2025-12-02 22:14:42.262] [SpringApplicationShutdownHook] [] o.araby.blognovelink.filter.BFilter - 销毁B过滤器

3.2 自定义顺序

既然都已经是在Spring Boot中使用过滤器,那么我们就应该用Spring的方式管理Filter,而不是依赖 Servlet容器的隐式行为。这里其实是采用FilterRegistrationBean来手动定义注册的顺序,所以过滤器类可以不用@WebFilter + @ServletComponentScan或者@Component修饰。启动类上也不需要@ServletComponentScan。

这样注册过滤器的的过程由Spring Boot通过FilterRegistrationBean统一管理,而非直接依赖Servlet 容器的注解扫描。

这里我们定义一个配置类WebConfig ,用@Configuration注解修饰。里面手动实现FilterRegistrationBean来管理定义的过滤器。

  • setFilter:添加过滤器类
  • addUrlPatterns:添加匹配路径
  • setOrder:指定顺序,数值越小,过滤器doFilter越先执行
java 复制代码
import org.araby.blognovelink.filter.AFilter;
import org.araby.blognovelink.filter.BFilter;
import org.araby.blognovelink.filter.CFilter;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class WebConfig {
    @Bean
    public FilterRegistrationBean<AFilter> aFilter() {
        FilterRegistrationBean<AFilter> bean = new FilterRegistrationBean<>();
        bean.setFilter(new AFilter());
        bean.addUrlPatterns("/*");
        bean.setOrder(1); // 最先执行
        return bean;
    }

    @Bean
    public FilterRegistrationBean<BFilter> bFilter() {
        FilterRegistrationBean<BFilter> bean = new FilterRegistrationBean<>();
        bean.setFilter(new BFilter());
        bean.addUrlPatterns("/*");
        bean.setOrder(2);
        return bean;
    }

    @Bean
    public FilterRegistrationBean<CFilter> cFilter() {
        FilterRegistrationBean<CFilter> bean = new FilterRegistrationBean<>();
        bean.setFilter(new CFilter());
        bean.addUrlPatterns("/*");
        bean.setOrder(3); // 最后执行
        return bean;
    }
}

相关推荐
稚辉君.MCA_P8_Java1 小时前
Gemini永久会员 Java 返回最长有效子串长度
java·数据结构·后端·算法
极光代码工作室1 小时前
基于SpringBoot的停车场收费管理系统的设计与实现
spring boot·后端·产品运营
我超级能吃的1 小时前
线程池核心原理及使用
java·开发语言
路边草随风1 小时前
java 实现 flink 读 kafka 写 delta
java·大数据·flink·kafka
逆风局?1 小时前
后端Web实战(部门管理)——日志技术
java·前端
g***26791 小时前
Springboot中mybatis的使用
spring boot·后端·mybatis
小马爱打代码1 小时前
Spring AI:ChatClient实现对话效果
java·人工智能·spring
无敌最俊朗@1 小时前
C++ 内存管理与编译原理 (面试复习2)
java·开发语言·jvm
赴前尘1 小时前
docker 配置ipv6地址
java·docker·容器