如何在javax.servlet.Filter中注入ResourceInfo?服务方法时间指标注册遇阻
这个问题我之前帮人排查过,核心矛盾在于:javax.servlet.Filter是由Servlet容器(比如Tomcat、Jetty)管理生命周期的,而ResourceInfo是JAX-RS规范提供的上下文对象,只有在JAX-RS的请求处理链中才会被初始化并注入。直接在Servlet Filter里用@Inject或者@Context注入ResourceInfo,容器根本找不到这个对象,自然会失败。
给你两个可行的方案,根据你的场景选择:
方案一:改用JAX-RS原生Container Filter(推荐)
如果你的目标就是拦截JAX-RS服务方法并记录指标,优先用JAX-RS自带的ContainerRequestFilter和ContainerResponseFilter,这是规范原生支持的方式,能直接注入ResourceInfo,完全不需要额外的上下文处理。
示例代码如下:
import javax.ws.rs.container.ContainerRequestFilter; import javax.ws.rs.container.ContainerResponseFilter; import javax.ws.rs.container.ResourceInfo; import javax.ws.rs.core.Context; import javax.ws.rs.ext.Provider; import java.io.IOException; import java.util.concurrent.TimeUnit; @Provider // 标记为JAX-RS提供者,会被自动注册 public class ServiceTimingFilter implements ContainerRequestFilter, ContainerResponseFilter { @Context // 直接注入ResourceInfo private ResourceInfo resourceInfo; @Override public void filter(ContainerRequestContext requestContext) throws IOException { // 记录请求开始时间,存在请求上下文里 long startTime = System.currentTimeMillis(); requestContext.setProperty("serviceStartTime", startTime); } @Override public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) throws IOException { long startTime = (long) requestContext.getProperty("serviceStartTime"); long duration = System.currentTimeMillis() - startTime; // 获取服务类和方法的全限定名 String serviceClass = resourceInfo.getResourceClass().getName(); String serviceMethod = resourceInfo.getResourceMethod().getName(); // 这里替换成你的指标注册逻辑,比如用Micrometer、Prometheus客户端等 // meterRegistry.timer("service_method_duration", "class", serviceClass, "method", serviceMethod) // .record(duration, TimeUnit.MILLISECONDS); System.out.printf("服务方法 %s#%s 执行耗时:%d ms%n", serviceClass, serviceMethod, duration); } }
这个方案的优势:
- 完全符合JAX-RS规范,不依赖任何特定实现(Resteasy、Jersey都能用)
- 能精准拦截到每个JAX-RS服务方法,获取的
ResourceInfo信息准确 - 生命周期和JAX-RS请求链完全同步,不会出现上下文缺失的问题
方案二:在Servlet Filter中通过JAX-RS实现特定API获取ResourceInfo
如果因为某些原因必须使用javax.servlet.Filter,那只能通过具体JAX-RS实现的上下文API来获取ResourceInfo——因为JAX-RS规范本身没有在Servlet Filter层面暴露ResourceInfo的标准方式,所以这个方案是实现相关的。
以常用的Resteasy为例,代码如下:
import javax.servlet.*; import javax.servlet.annotation.WebFilter; import javax.ws.rs.container.ResourceInfo; import org.jboss.resteasy.core.ResteasyContext; import java.io.IOException; @WebFilter(urlPatterns = "/api/*") // 匹配你的JAX-RS服务根路径 public class ServletTimingFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { long startTime = System.currentTimeMillis(); try { // 继续执行请求链,让JAX-RS处理请求 chain.doFilter(request, response); } finally { long duration = System.currentTimeMillis() - startTime; // 从Resteasy的上下文里获取ResourceInfo ResourceInfo resourceInfo = ResteasyContext.getContextData(ResourceInfo.class); if (resourceInfo != null) { String serviceClass = resourceInfo.getResourceClass().getName(); String serviceMethod = resourceInfo.getResourceMethod().getName(); System.out.printf("服务方法 %s#%s 执行耗时:%d ms%n", serviceClass, serviceMethod, duration); // 这里添加你的指标注册逻辑 } else { // 非JAX-RS请求,或者JAX-RS上下文已销毁,做降级处理 System.out.printf("非服务方法请求耗时:%d ms%n", duration); } } } @Override public void init(FilterConfig filterConfig) throws ServletException {} @Override public void destroy() {} }
如果用的是Jersey(GlassFish的JAX-RS实现),你需要替换成Jersey的上下文获取方式,比如通过InjectionManager或者RequestScope来获取ResourceInfo,这会更繁琐一些。
这个方案的局限性:
- 依赖具体的JAX-RS实现,换实现就得改代码
- 只能在请求处理完成后(finally块)获取
ResourceInfo,因为Servlet Filter的执行时机早于JAX-RS上下文初始化,提前获取会拿到null - 无法区分哪些请求是JAX-RS服务请求,需要自己做判断
总结
如果你的需求是给JAX-RS服务方法注册时间指标,强烈推荐方案一——既符合规范,又通用可靠。只有当你必须用Servlet Filter处理非JAX-RS请求,同时又要兼顾服务方法指标时,才考虑方案二。
内容的提问来源于stack exchange,提问作者Jack

