Filter在JavaWeb里算是最基础但又最容易讲不清楚的一个组件。网上讲Filter的文章不少但多数要么只贴代码不讲原理要么从Servlet规范讲到源码分析把新手直接劝退。这篇文章我换个思路从实际开发里遇到的场景出发把Filter到底是什么、生命周期怎么走、配置怎么写、实战怎么用、和拦截器有什么区别一次性讲透。我尽量少说空话每个点都配代码和踩坑经历读完之后你应该能自己动手把过滤器用得明明白白。1. Filter到底是个什么东西——从“门禁”说起1.1 没有Filter的时代代码有多狼狈在引入过滤器之前很多公共逻辑都是散落在每个Servlet里重复写的。比如用户登录校验你得在每个Servlet的doGet或doPost方法第一行写一遍“检查Session是否有用户、没有就重定向到登录页”。项目里三五个Servlet还好等到几十个接口你会发现同样的代码复制粘贴了几十遍改一处需求就得全局搜索替换。最难受的是那些“横切”需求。想做统一编码处理想在控制台打印每个请求的耗时想给接口加个简单的访问次数统计这些逻辑跟业务本身没有半毛钱关系但你不得不在每个业务方法里插入对应代码。代码越来越臃肿维护成本越来越高新同事接手项目时对着满屏重复代码直接懵了。这些问题本质上都是一个需求在一批Servlet或者说资源执行之前和之后插入一段可以被统一管理和复用的逻辑。JavaWeb规范里专门为这个场景设计了Filter也就是过滤器。1.2 Filter的本质一条流水线上的“质检员”把整个请求-响应过程想象成一条工厂流水线。客户端浏览器、App发出的HTTP请求是一块原料Servlet/JSP是最终加工出成品的工人而这个原料在到达工人手里之前、在成品送出工厂之后都可以经过若干道“质检工序”这些工序就是Filter。你可以在原料进车间之前拦下来检查比如验证用户是否登录如果不合格就当场打回重定向到登录页根本不让它到达Servlet。你可以在原料上做预处理比如把请求体的字符编码统一改成UTF-8。你还可以在加工完成、准备把成品发给客户之前偷偷把商标贴上去给响应头加一些统一字段。从代码角度看Filter是一个实现了javax.servlet.Filter接口的普通Java类Servlet 3.0之后也可以用注解直接声明。它被Web容器Tomcat、Jetty等管理配置在某个URL规则上当请求的路径匹配到这个规则时容器就会在调用目标Servlet之前先调用Filter的doFilter方法。这个方法的第三个参数是FilterChain你可以把它理解成一个“放行令牌”——调用了chain.doFilter(request, response)才会继续往下走不调用的话请求就直接被Filter截停在这个环节。1.3 Filter到底能解决哪些问题——四个高频场景Filter能解决的典型问题我按开发频率排个序统一编码处理全站设置请求和响应的字符集解决中文乱码问题这是入门时最常写的过滤器。登录/权限校验拦截未登录用户的访问请求重定向到登录页或返回401状态码。日志记录记录每个请求的IP、URI、耗时、参数用于排查问题和统计分析。跨域处理在响应头里加上Access-Control-Allow-Origin等字段让前端跨域调用接口时不报错。敏感词过滤把用户输入里的敏感词汇替换成“*”号后再传给下游处理。除此之外还有压缩响应内容、缓存静态资源、XSS攻击防护、接口幂等性校验等场景。总结一句话凡是“需要在多个Servlet执行前后统一做的事”都可以塞进Filter。它不是业务逻辑而是横切进去的基础设施。注意Filter依赖Servlet容器也就是说必须运行在Tomcat、Jetty这样的Web容器中才能生效。如果你用纯Spring Boot内嵌Tomcat那没问题但如果脱离容器跑单元测试Filter是不会被自动执行的。2. Filter的核心API与生命周期——把三个方法吃透2.1 三个核心方法一个都不能少Filter接口一共有三个方法我先把签名和作用列出来public interface Filter { default void init(FilterConfig filterConfig) throws ServletException {} void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException; default void destroy() {} }init(FilterConfig)过滤器初始化方法。容器在启动时创建Filter实例后调用只调用一次。FilterConfig可以读取web.xml或注解里配置的初始化参数比如默认编码值、需要跳转的登录页路径。doFilter(ServletRequest, ServletResponse, FilterChain)真正的过滤逻辑就在这里。每次请求只要匹配拦截路径容器就会调用它。你需要在这个方法里判断“放行”还是“拦截”放行时别忘了调用chain.doFilter(request, response)。destroy()容器销毁过滤器实例之前调用一般用来释放资源比如关闭数据库连接池或线程池。也只会执行一次。这里有个容易被坑的点doFilter的参数是ServletRequest和ServletResponse这两个是父接口。如果你要用HttpServletRequest特有的方法比如getSession()、getRequestURI()需要先向下转型成HttpServletRequest和HttpServletResponse否则编译都过不了。2.2 生命周期从创建到销毁的完整时间线理解Filter生命周期对排查那些“为什么我的全局变量乱套了”“为什么过滤器不生效”的问题很有帮助。生命周期分三个阶段创建与初始化Web应用启动时也就是Tomcat完成应用部署时容器读取过滤器配置实例化Filter类的对象然后调用init方法。注意这里不是“第一次请求时才创建”而是启动即创建这与Servlet的懒加载不同。服务应用运行期间每当有请求匹配过滤规则容器就从线程池里挑一个线程来调用doFilter。这里要特别注意Filter在单例模式下被多线程并发调用也就是说所有请求共享同一个Filter实例。如果你在Filter里定义了成员变量并在doFilter里修改它那线程安全性完全取决于你。所以Filter里尽量别写可变成员变量非写不可就要用局部变量或者加锁。销毁应用关闭或容器停止时容器调用destroy方法然后释放实例。如果这时候有请求还在处理中并不会因为destroy而突然中断容器会等待当前请求处理完毕后才彻底销毁。2.3 配置方式web.xml、注解、Spring Boot三件套传统的配置方式是写web.xmlfilter filter-nameencodingFilter/filter-name filter-classcom.example.filter.EncodingFilter/filter-class init-param param-nameencoding/param-name param-valueUTF-8/param-value /init-param /filter filter-mapping filter-nameencodingFilter/filter-name url-pattern/*/url-pattern /filter-mappingServlet 3.0之后可以用注解省掉配置文件WebFilter( filterName encodingFilter, urlPatterns /*, initParams WebInitParam(name encoding, value UTF-8) ) public class EncodingFilter implements Filter { // ... }注意使用注解方式时别忘了在启动类上加上ServletComponentScan注解否则Spring Boot不会扫描到这些WebFilter。如果你用原生Tomcat部署war包容器会自动扫描WebServlet、WebFilter等注解这一点和Spring Boot有点差异。Spring Boot更推荐用Java配置类来注册过滤器把过滤器的生命周期交给Spring容器管理还能自由注入其他BeanConfiguration public class FilterConfig { Bean public FilterRegistrationBeanEncodingFilter encodingFilterRegistration() { FilterRegistrationBeanEncodingFilter registration new FilterRegistrationBean(); registration.setFilter(new EncodingFilter()); registration.addUrlPatterns(/*); registration.setName(encodingFilter); registration.setOrder(1); // 设置初始化参数 registration.addInitParameter(encoding, UTF-8); return registration; } }用FilterRegistrationBean的好处是你可以控制多个过滤器之间的执行顺序setOrder的值越小越先执行。这里先记住结论数字越小优先级越高后面讲执行顺序时还会详细说。踩坑提示在Spring Boot里直接用Component标记Filter类也能生效但此时无法通过FilterRegistrationBean控制顺序。如果项目里过滤器多了强烈建议全部用FilterRegistrationBean注册别混用混用后顺序会变得非常难排查。2.4 多个Filter的执行顺序——一张图看懂FilterChain很多初学者搞不懂多个过滤器时的调用序列。我先定义一个场景项目里配置了两个过滤器A是登录校验B是日志记录A的order是1B的order是2它们都拦截“/*”。请求过来后的执行顺序是这样的请求进入A的doFilterA做一些登录校验逻辑。A调用chain.doFilter()把请求传给链上的下一个环节——B。B的doFilter开始执行记录请求开始时间。B调用chain.doFilter()请求终于到达目标Servlet。Servlet执行完业务逻辑响应返回注意响应回传时会重新经过B执行B在chain.doFilter()之后的代码比如记录耗时。响应继续回传到A执行A在chain.doFilter()之后的代码。所以关键结论是chain.doFilter()之前是请求方向的预处理chain.doFilter()之后是响应方向的收尾处理。FilterChain就像一个嵌套的洋葱请求从外层剥到内层响应从内层返回到外层一层层走出来。再说回order。order数字越小越先执行“预处理”也就越晚执行“收尾”。打个比方第一个过滤器像小区大门保安第二个过滤器像单元楼门禁——进门时先过保安再刷门禁出门时先过单元门禁再出小区大门顺序正好反过来。3. 过滤器配置里的门道——路径、参数、顺序细节一次理清3.1 URL匹配规则什么时候拦、什么时候放Filter的url-pattern写错是“过滤器不生效”的常见原因。规则其实很简单/\*匹配所有路径最常用。注意这里不是*也不是空字符串。/api/*匹配/api/下的所有子路径包括/api/login、/api/user/list等。*.do扩展名匹配匹配所有以.do结尾的请求。注意这种写法前面不能有斜杠。/匹配根路径这个比较特殊一般不建议用在过滤器上容易造成困惑。精确路径比如/login只匹配这一个路径。注意Servlet容器的匹配规则是“最长匹配优先”这一点Filter和Servlet是共用规则。比如同时配置了/\*和/api/*请求/api/user只会命中后者不会先执行/\*再执行/api/*——这两个是并行的映射关系而不是链式关系。如果你想多个过滤器前后执行得靠多个Filter加不同order而不是靠配置多个url-pattern。3.2 初始化参数把魔法值从代码里挪出去Filter的init参数是个好东西能把一些环境相关的配置从代码中解耦出来。比如编码过滤器的编码值、登录页路径、允许跨域的域名列表都可以通过init-param配置这样不同环境部署时只需要改配置不用改代码重新编译。用FilterConfig.getInitParameter(encoding)就能取到。如果是Spring Boot的FilterRegistrationBean就用addInitParameter(encoding, UTF-8)来设。本质上两种方式殊途同归都是往FilterConfig里塞键值对。有一个小坑想提醒一下HttpServletRequest和HttpServletResponse在容器里是以包装类形式存在的比如RequestFacade在Filter里拿到的请求对象不一定是纯Servlet实现。有些新手在Filter里直接把request强转成自定义类型结果抛ClassCastException。所以强转前最好先用instanceof判断一下。3.3 注解、web.xml、FilterRegistrationBean到底该选哪个我的建议是传统war包部署用web.xml配置清晰运维友好不依赖IDE生成代码。Spring Boot 项目无脑选FilterRegistrationBean。它能控制优先级、能注入Spring管理的Bean、还能动态设置参数最适合现代开发习惯。注解方式适合快速Demo、小型项目。但一旦Filter数量超过两个注解就无法控制执行顺序后续维护会很痛苦。我自己在实际项目中见过很多团队一开始图省事用WebFilter结果后面加过滤器时发现顺序完全不可控最后又被迫改成FilterRegistrationBean。所以我的经验是项目一开始就用FilterRegistrationBean别走弯路。3.4 Filter的线程模型单例多线程别乱用成员变量这一点再强调一次因为踩坑的人实在太多了。Filter实例在容器中只有一个所有请求并发访问时走的都是同一个对象的doFilter方法。如果你在Filter里写了private String currentUser; public void doFilter(...) { currentUser getFromSession(request); chain.doFilter(request, response); }你以为每个请求各存各的user实际上多个线程同时改同一个变量结果就是用户A的请求可能读到用户B的信息。这是严重的并发bug而且现象时有时无特别难排查。正确做法是用局部变量存请求级的数据或者用ThreadLocal来处理线程私有数据。哪怕是计数器这种简单的成员变量都要用AtomicLong或者加锁。这是我见过最多的Filter翻车现场写代码时一定长个心眼。4. 四个实战场景从能用写到用得漂亮4.1 字符编码过滤器中文乱码的终极解药JavaWeb乱码问题分三种请求参数乱码POST、请求参数乱码GET、响应乱码。GET乱码还涉及Tomcat的URIEncoding配置这个Filter管不了需要改server.xml但POST和响应乱码一个过滤器就能全搞定。public class EncodingFilter implements Filter { private String encoding UTF-8; Override public void init(FilterConfig filterConfig) throws ServletException { String param filterConfig.getInitParameter(encoding); if (param ! null !param.trim().isEmpty()) { encoding param; } } Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { request.setCharacterEncoding(encoding); response.setCharacterEncoding(encoding); response.setContentType(text/html;charset encoding); chain.doFilter(request, response); } }三点说明第一setCharacterEncoding必须在读取请求参数之前调用这就是放在chain.doFilter之前的原因。第二响应编码建议和ContentType一起设置只设setCharacterEncoding的话有些情况响应头里还是会有乱码。第三如果你用了Jackson、Fastjson往响应里写JSON建议在Filter里统一设成application/json;charsetUTF-8或者由SpringMVC的MappingJackson2HttpMessageConverter来处理两者别冲突。4.2 登录校验过滤器让“未登录就滚去登录页”变得体面这是我在企业项目里写的最多的过滤器类型。直接放一个能跑的版本public class LoginFilter implements Filter { private String loginPage /login.jsp; private String redirectUrl /login.jsp; Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; HttpServletResponse response (HttpServletResponse) resp; String uri request.getRequestURI(); // 白名单静态资源和登录接口直接放行 if (uri.endsWith(.css) || uri.endsWith(.js) || uri.endsWith(.png) || uri.contains(/login) || uri.equals(/)) { chain.doFilter(request, response); return; } Object user request.getSession().getAttribute(user); if (user null) { // 判断是否是AJAX请求如果是AJAX请求返回JSON提示未登录 String requestedWith request.getHeader(X-Requested-With); if (XMLHttpRequest.equals(requestedWith)) { response.setContentType(application/json;charsetUTF-8); response.getWriter().write({\code\:401,\message\:\未登录或登录已过期\}); return; } response.sendRedirect(request.getContextPath() redirectUrl); return; } chain.doFilter(request, response); } }这里有两个细节值得说一是白名单设计。登录接口、登录页面、静态资源这些不需要鉴权的路径必须放行。不然会出现“登录页面被重定向到登录页面”的循环重定向问题。白名单建议配置在init参数里方便不同环境调整。二是AJAX请求的特殊处理。如果你对AJAX请求直接sendRedirect前端拿到的是登录页的HTML而不是JSON数据axios、fetch里根本看不见正常提示。所以这里判断X-Requested-With头对AJAX返回JSON状态码让前端拦截器统一跳转登录页。这个细节能让前后端联调少很多脑血栓时刻。4.3 日志过滤器打点不求人请求耗时统计日志过滤器是排查线上问题的一把好手代码不长但信息量很大public class AccessLogFilter implements Filter { private static final Logger log LoggerFactory.getLogger(AccessLogFilter.class); Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; long start System.currentTimeMillis(); try { chain.doFilter(request, resp); } finally { long duration System.currentTimeMillis() - start; log.info({} {} from {}, cost {} ms, request.getMethod(), request.getRequestURI(), request.getRemoteAddr(), duration); } } }注意我是把chain.doFilter放在try-finally里的这样即使下游Servlet抛异常日志也能正常打印。还有一个加分操作给请求生成一个traceIdUUID通过MDC放进日志上下文这样一次请求的所有日志都能串起来排查问题时特别方便。如果你还没用过MDC建议了解一下这是日志追踪的基础技能。4.4 敏感词过滤与跨域处理两个高频需求敏感词过滤的核心思路是对请求参数做一层清洗。最简单的做法是在getParameter里做文章public class SensitiveWordFilter implements Filter { private ListString words List.of(敏感词1, 敏感词2); Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; CustomRequest customRequest new CustomRequest(request); chain.doFilter(customRequest, resp); } class CustomRequest extends HttpServletRequestWrapper { public CustomRequest(HttpServletRequest request) { super(request); } Override public String getParameter(String name) { String value super.getParameter(name); if (value null) { return null; } for (String word : words) { value value.replace(word, ***); } return value; } } }这里用到了HttpServletRequestWrapper这是装饰者模式的经典应用。包装类可以拦截对getParameter、getParameterValues、getInputStream等方法的调用在不改Servlet代码的情况下透明地修改请求内容。有一说一生产级别的敏感词过滤不会用简单的replace那只是Demo级别生产环境要用Trie树、DFA算法或者集成第三方服务但包装类的思路是通用的。跨域过滤器的原理是在响应里追加CORS相关头。需要注意两点一是不要用*通配符同时搭配allowCredentialstrue某些浏览器会拒绝二是预检请求OPTIONS要直接返回204不再继续往下走因为预检请求不需要业务处理。5. Filter vs Interceptor vs Listener——面试高频对比题5.1 三个组件的定位区别Filter过滤器属于Servlet规范作用于Servlet容器层。它拦截的是请求-响应这一层与具体的Controller或Servlet无关。在Spring Boot里Filter在请求进入DispatcherServlet之前就会执行所以它能覆盖Static资源、Servlet、JSP等所有容器管理的资源。Interceptor拦截器属于Spring MVC框架层。它由Spring容器管理只能拦截经过DispatcherServlet分发到Controller的请求对于静态资源默认是不拦截的。它能拿到HandlerMethod就是Controller方法的信息所以可以做更细粒度的业务校验比如方法级权限判断。Listener监听器也是Servlet规范的组件但它不拦截请求而是监听Web应用中的事件比如应用启动/销毁、Session创建/销毁、属性增删改。它是“旁观者”而不是“拦截者”。5.2 执行时机对比表组件拦截时机所属规范能否拿到HandlerMethod典型应用Filter请求进入容器后、进入DispatcherServlet前Servlet规范不能编码、跨域、登录、日志InterceptorHandler执行前/后、视图渲染前/后Spring MVC能权限校验、通用数据填充Listener应用启动/销毁、会话变化等事件Servlet规范不能初始化数据、在线人数统计5.3 选型建议我的建议很直接通用型横切逻辑用Filter编码设置、CORS跨域、请求日志、字符流包装这些不依赖Spring MVC机制放Filter最稳。因为Filter在容器层就算你将来把SpringMVC换成别的框架Filter照样生效。Controller映射相关的逻辑用Interceptor比如需要判断“是哪个Controller处理当前请求”才能决定是否放行、需要往ModelAndView塞公共数据的放Interceptor更合适。生命周期事件用Listener启动时初始化数据字典、关闭时释放资源、统计在线用户数这类场景Listener是唯一选择。有个很经典的面试连环题请求到达Controller经过了哪些组件一个完整的链路大致是Tomcat线程接收请求 - Filter链 - DispatcherServlet - HandlerMapping定位Handler - HandlerInterceptor预处理 - 执行HandlerController- 返回ModelAndView - HandlerInterceptor后处理 - 视图渲染 - Filter链返回响应。理解这个链路后很多“为什么Filter里拿不到Spring MVC的异常处理器处理结果”的问题就迎刃而解了。6. 过滤器开发常见问题排查——把血流成河的坑摆在台面上6.1 过滤器不执行先查这四个地方过滤器配置了但就是不生效99%是下面四个原因类没有实现javax.servlet.Filter或jakarta.servlet.Filter接口新老版本包名千万别混Tomcat 10用jakarta老项目用javax写错包名编译能过但容器识别不了。路径匹配规则写错最常见的是漏了/*前面的斜杠或者写了*全匹配Spring的Ant路径里能用但Filter的url-pattern里不行。没有把它放进容器的管理范围web.xml里写了filter但忘了写filter-mapping或者注解方式下Spring Boot没有配ServletComponentScan或者FilterRegistrationBean没交给Spring管理只是new出来没注册。项目里有多个Filter但当前请求被更早的Filter拦截了比如登录校验Filter没放行白名单后续的日志Filter自然执行不到。查看Tomcat启动日志时注意有没有Filter的初始化信息没有就说明注册有问题。6.2 过滤器执行顺序乱了怎么办顺序问题分两种一是Spring Boot下的Filter除了FilterRegistrationBean的order还有可能和Spring Security等框架自带过滤器混在一起。Spring Security的过滤器链优先级默认是-100优先级最高如果不想让业务Filter抢在安全过滤之前做事就要把业务Filter的order调大。二是和web.xml混用时web.xml里的Filter优先级只按声明顺序这一点和FilterRegistrationBean的order机制不一样。混用以后执行顺序会变得很难判断。我的建议是一套项目只用一个注册机制不要web.xml、注解、Bean三种混着来。6.3 在Filter里抛了异常会发生什么Filter的doFilter抛异常后会直接冒泡到容器层Tomcat默认返回500错误页。如果你想在Filter里统一处理异常可以用try-catch包住chain.doFilter然后在catch里对异常分类处理。但注意Filter不是处理业务异常的第一选择——真正的业务异常还是交给Spring MVC的ControllerAdvice统一处理更优雅。Filter阶段处理的应该是更底层的异常比如编码解析错误、请求体过大之类的问题。6.4 Filter里能注入Spring Bean吗能用FilterRegistrationBean注册的Filter因为Filter是Spring的Bean所以能直接Autowired注入其他Spring组件。但注意如果你是new出来的Filter比如还没交给Spring里面用了Autowired字段是null一调用就空指针。如果WebFilter注解方式下的Filter也需要注入Bean就要用静态工具类或者WebApplicationContextUtils拿到容器具体网上有挺多教程核心思路是拿ApplicationContext然后getBean。这里有个更让人困惑的点Spring Boot里WebFilter注解标注的Filter虽然被容器扫描注册了但它默认不是Spring BeanAutowired依然无效。要让注解Filter也能注入得在Filter类上加Component但这又回到了之前说的顺序问题。所以还是那个建议老老实实用FilterRegistrationBean。6.5 过滤器常见问题速查表现象可能原因解决思路请求中文乱码编码Filter未配置或配置在读取参数之后把setCharacterEncoding放到chain.doFilter之前登录页无限重定向登录页和登录接口没加入白名单检查放行路径是否覆盖登录页静态资源和登录URLAJAX拿不到错误JSON登录Filter对AJAX直接重定向判断X-Requested-With头返回JSON过滤器不生效url-pattern写错/漏注册/包名错误检查配置、启动日志、依赖包名多Filter顺序错乱混用多种注册方式统一用FilterRegistrationBean管理成员变量被并发修改Filter是单例多线程用局部变量或ThreadLocal请求参数被读后修改失效getParameter读取时机早于包装生效使用HttpServletRequestWrapper保证第一次读取即被包装静态资源也被过滤Filter设置了/*在白名单里放行静态资源或单独配置静态目录7. 写在最后——过滤器不是炫技是工程习惯过滤器这个东西单独拎出来看每个API都简单但真正在项目里用好需要的是对“横切逻辑”的深刻理解。我见过不少开发业务代码里到处塞HttpSession判断、塞编码设置、塞日志打印不仅代码难看而且一旦漏了一处就是线上事故。反过来我也见过过度设计的过滤器——为了AOP、为了可扩展写了几层自定义的过滤器框架结果一个多月后没人敢动那段代码。过滤器是锦上添花的基础设施不是展示设计模式的地方简单、清晰、可配置才是它的归宿。我自己做项目的习惯是搭建初期就把编码过滤器、日志过滤器、登录校验过滤器三件套先准备好业务代码里绝不留《Servlet规范》做的事情每次新增一个需要“统一前置处理”的需求先问一句“这能不能用Filter实现”。等到项目大了、Filter多了用FilterRegistrationBean统一管顺序再配好白名单和初始化参数后续几乎不需要再动过滤器代码。如果你刚接触Filter建议照着这篇文章手写一遍三个基础过滤器然后在真实项目里改造成自己的版本。代码量不多但动手写过一次理解深度完全不一样。后面你还可以继续研究Spring Security的过滤器链设计、HttpServletRequestWrapper的更多玩法把过滤器从“会写”进阶到“会设计”。