两个东西都是"横切"用的,写法也像:实现一个接口,把逻辑塞进去,就能在 controller 前后插一脚。
区别在站在哪一层。filter 来自 servlet 规范,是 servlet 容器(tomcat)调用的;interceptor 是 spring mvc 的东西,由 dispatcherservlet 调用。所以 filter 在 dispatcherservlet 外面,interceptor 在 dispatcherservlet 里面,后面那些差别基本都能落到这个位置上。
请求进来的那条链
我们先把请求进来之后的链路画出来:

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() {}
}三个方法里只有 dofilter 是必须实现的。init 和 destroy 分别在这个 filter 第一次用到和容器关闭时各调一次。
dofilter 的放行方式就是调 chain.dofilter(request, response),不调就相当于拦住了,请求不会往下走。下面这几行代码是 filter 最常见的一个形态:
@component
public class costfilter implements filter {
@override
public void dofilter(servletrequest request, servletresponse response, filterchain chain)
throws ioexception, servletexception {
long start = system.currenttimemillis();
try {
chain.dofilter(request, response);
} finally {
system.out.println("cost " + (system.currenttimemillis() - start) + "ms");
}
}
}耗时统计写在 chain.dofilter 前后,因为这一句里面才是后面整条链(包括 dispatcherservlet)。
拿到的 servletrequest 是容器给的原始对象,getrequesturi()、getheader() 这些都得自己 cast 成 httpservletrequest。想往里加东西(比如记一个 traceid 往下传),只能自己包一层 httpservletrequestwrapper:
httpservletrequest wrapper = new httpservletrequestwrapper((httpservletrequest) request) {
@override
public string getheader(string name) {
if ("x-trace-id".equals(name)) {
return traceid;
}
return super.getheader(name);
}
};
chain.dofilter(wrapper, response);响应同理,httpservletresponsewrapper 包一层再传下去。请求体也是这个套路,contentcachingrequestwrapper / contentcachingresponsewrapper 就是 spring 给的现成 wrapper。
这里提醒一句:请求体只能在 filter 里读一次。如果你的 filter 里调了
request.getinputstream()把 body 读出来打印,后面@requestbody拿到的就是空字符串。要么用contentcachingrequestwrapper包一层,要么在 filter 里别碰 body。
jakarta.servlet 还是 javax.servlet 看 spring boot 版本。boot 3 跟着 tomcat 10 换成了 jakarta.*,从旧项目拷 filter 的时候这里必改。
注册
三种方式:
// 1. 直接标 @component,spring boot 会把它注册到 /*
@component
public class costfilter implements filter { ... }
// 2. filterregistrationbean,能指定 urlpatterns、顺序、dispatcher type
@bean
public filterregistrationbean<costfilter> costfilter() {
filterregistrationbean<costfilter> bean = new filterregistrationbean<>(new costfilter());
bean.addurlpatterns("/api/*");
bean.setorder(1);
return bean;
}
// 3. 走 servlet 原生的 @webfilter,需要在启动类上加 @servletcomponentscan
@webfilter(urlpatterns = "/*")
public class costfilter implements filter {
@autowired
private userservice userservice; // 注入不进去,见下面
}第三种要注意,@webfilter 标注的 filter 由 servlet 容器 new 出来,不是 spring 容器里的 bean,@autowired 不生效。
interceptor
public interface handlerinterceptor {
default boolean prehandle(httpservletrequest request, httpservletresponse response, object handler)
throws exception { return true; }
default void posthandle(httpservletrequest request, httpservletresponse response, object handler,
modelandview modelandview) throws exception {}
default void aftercompletion(httpservletrequest request, httpservletresponse response, object handler,
exception ex) throws exception {}
}三个方法的时机:
| 方法 | 时机 | 参数里有什么 |
|---|---|---|
prehandle | controller 方法执行前 | handler 是 handlermethod,返回 false 就不往下走了 |
posthandle | controller 执行完、视图渲染前 | 多了 modelandview,可以往里塞东西 |
aftercompletion | 整个请求结束(视图渲染完) | ex 是这次请求抛出的异常,没异常就是 null |
@requestmapping 匹配到的方法,在这里就是 handlermethod,能拿到 controller 类和 method 对象,也能读它上面的注解。所以"只拦带某个注解的接口"这种需求只有拦截器能做:
public class authinterceptor implements handlerinterceptor {
@override
public boolean prehandle(httpservletrequest request, httpservletresponse response, object handler)
throws exception {
if (!(handler instanceof handlermethod handlermethod)) {
return true; // 静态资源这类不是 handlermethod,直接放行
}
if (!handlermethod.hasmethodannotation(requirelogin.class)) {
return true;
}
string token = request.getheader("authorization");
if (token == null) {
response.setstatus(httpservletresponse.sc_unauthorized);
return false;
}
return true;
}
}注册
拦截器不能标个注解就生效,得往 interceptorregistry 里注册:
@configuration
public class webconfig implements webmvcconfigurer {
@override
public void addinterceptors(interceptorregistry registry) {
registry.addinterceptor(new authinterceptor())
.addpathpatterns("/**")
.excludepathpatterns("/login", "/error", "/static/**")
.order(1);
}
}prehandle 返回 false 之后:请求直接返回,posthandle 不执行;但已经通过 prehandle 的拦截器,它们的 aftercompletion 还是会执行,ex 传 null。这一点和 filter 里"不放行就整个断掉"不一样,资源释放写在 aftercompletion 里是安全的。
对比
| filter | interceptor | |
|---|---|---|
| 所属 | servlet 规范(jakarta.servlet) | spring mvc |
| 谁调用 | servlet 容器 | dispatcherservlet |
| 实现接口 | filter | handlerinterceptor |
| 入口 | dofilter(req, resp, chain) 一个方法 | prehandle / posthandle / aftercompletion |
| 放行方式 | chain.dofilter(),不调就断 | prehandle 返回 true |
| 拦的范围 | 容器收到的所有请求 | dispatcherservlet 找得到 handler 的请求 |
| 看得到 controller 方法 | 看不到 | 能,handler 就是 handlermethod |
| 包装 request / response | 能,包一层再往下传 | 不能,只能直接往 response 里写 |
| 抛异常的归宿 | 容器错误页(boot 的 /error) | handlerexceptionresolver,@controlleradvice 能接 |
| 注册 | filterregistrationbean / @component / @webfilter | webmvcconfigurer#addinterceptors |
| 顺序 | @order、setorder(),数字小的先 | 注册顺序,或 .order() |
| 方向 | 按顺序进、按逆序出 | prehandle 正序,posthandle / aftercompletion 逆序 |
几个容易混的点
静态资源归谁拦
@component 的 filter 默认映射到 /*,容器收到的请求都会经过它,包括那些压根不由 spring mvc 处理的:druid 的 statviewservlet、h2 的控制台,这些都是独立注册到容器上的 servlet,只有 filter 拦得到。
拦截器这边,addpathpatterns("/**") 也会拦到静态资源,因为静态资源在 spring mvc 里也是一个 handler(resourcehttprequesthandler)。所以实际项目里写了 /** 之后一般都会补一句 excludepathpatterns("/static/**", "/favicon.ico"),否则登录校验会拦到 css 上,页面直接白板。
异常去哪了
dispatcherservlet.dodispatch 里的 try 是从 applyprehandle 就开始包的,prehandle 抛出来的异常会和 controller 里抛的一样,走到 processdispatchresult → handlerexceptionresolver → @controlleradvice。所以拦截器里直接 throw new bizexception(...) 是能拿到统一响应体的。
filter 就不行了。它跑在 dispatcherservlet 外面,handlerexceptionresolver 根本没机会看到,异常一路冒到容器,最后交给 boot 的错误处理页面(/error),返回的是 basicerrorcontroller 那个格式。想在 filter 层也保持统一响应体,就得自己在 filter 里 catch 住然后手写 json,或者干脆别在 filter 里做校验。
filter 里能不能@autowired
能,但要看这个 filter 是怎么来的。
@component 注册的 filter 本身就是一个 spring bean,依赖注入、@value、@configurationproperties 都正常。@webfilter + @servletcomponentscan 那条路不行,filter 是容器 new 的,和 spring 容器没关系。
这就是 delegatingfilterproxy 存在的原因:它自己是个 filter(容器 new 出来的没关系,它不需要依赖),dofilter 的时候按名字去 spring 容器里找真正的 filter 然后委托过去。
<filter>
<filter-name>springsecurityfilterchain</filter-name>
<filter-class>org.springframework.web.filter.delegatingfilterproxy</filter-class>
</filter>spring security 的一整条过滤器链就挂在这个名字底下。启动日志里那句 will secure any request with [...],列出来的那些 securityfilterchain 成员,都是被这一层代理转进去的。
顺序
filter 的顺序由 @order 或者 filterregistrationbean.setorder() 决定,数字小的先执行、后返回。拦截器的顺序由 addinterceptors 里注册的先后决定,.order() 也可以指定。
prehandle 正序执行、posthandle 和 aftercompletion 逆序执行,这一点和 filter 的"进去一圈、出来一圈"是一致的,可以照着最上面那张链路图对一遍。
另外 filter 一定在拦截器之前,因为拦截器的调用方 dispatcherservlet 本身就是 filter 链末端的一个 servlet。
异步请求
controller 返回 callable、deferredresult 这类异步结果时,dispatcherservlet 会先把请求线程放掉,posthandle 和 aftercompletion 都不会在那一刻调用。aftercompletion 会在异步结果处理完之后回调,posthandle 则整个跳过。
如果我们依赖 posthandle 做清理,异步接口上就会漏掉。要在请求线程被放掉的那一刻做点事,用 asynchandlerinterceptor,它比 handlerinterceptor 多一个 afterconcurrenthandlingstarted 回调。
到此这篇关于spring 过滤器与拦截器深度对比:filter、interceptor 的区别、执行顺序与选型的文章就介绍到这了,更多相关spring 过滤器与拦截器区别内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论