【问题标题】:Can I determine in a servlet filter whether a HttpServletRequest maps to a particular Spring controller class我可以在 servlet 过滤器中确定 HttpServletRequest 是否映射到特定的 Spring 控制器类
【发布时间】:2021-07-05 13:13:47
【问题描述】:

我正在开发一个应用程序,该应用程序使用OncePerRequestFilter 使用传入的 Web 请求执行一些自定义的类似日志的行为。此行为同时使用HttpServletRequestHttpServletResponse。此外,过滤器同时使用ContentCachingRequestWrapperContentCachingResponseWrapper 来访问请求/响应正文。

我们决定只在调用特定 Spring 控制器的方法时才执行此行为,因为我们不想为其他控制器/执行器端点/等执行此操作。有没有办法判断传入的请求是否会(或曾经)映射到控制器?

public class ExampleFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(
            HttpServletRequest request, HttpServletResponse response,
            FilterChain filterChain) throws ServletException, IOException {
        // Can I tell here whether this will be mapping to an endpoint in
        // ExampleController or NestedExampleController?

        ContentCachingRequestWrapper requestToUse = new ContentCachingRequestWrapper(request);
        ContentCachingResponseWrapper responseToUse = new ContentCachingResponseWrapper(response);

        try {
            filterChain.doFilter(requestToUse, responseToUse);

            // Can I tell here whether this was mapped to an endpoint in
            // ExampleController or OtherExampleController?
        } finally {
            responseToUse.copyBodyToResponse(); // Write the cached body back to the real response
        }
    }
}
@RestController
@RequestMapping("/example")
public class ExampleController {
  @GetMapping("/{id}")
  public Example retrieveExample() {
    return getValue(); // Retrieve the value
  }

  // ...
}
@RestController
@RequestMapping("/example/{id}/nested")
public class NestedExampleController {
  @GetMapping("/{nestedId}")
  public NestedExample retrieveNestedExample() {
    return getValue(); // Retrieve the value
  }

  // ...
}

我已经对 Spring MVC/Boot 内部进行了一些研究,但我不确定是否有一种方法可以轻松地做到这一点。作为替代方案,我可以进行一些手动 URL 模式匹配,这可能不一定与控制器中的方法完全匹配,但可能会让我足够接近以成为可接受的解决方案。

总结一下:网络过滤器中有没有办法判断传入的请求是映射到控制器(在执行过滤器链之前)还是映射到控制器(在执行过滤器链之后)?

【问题讨论】:

  • 必须是特定的控制器类吗?说@Controller类中的具体方法?从措辞来看,这听起来很容易解决,使用 AOP 和特定类的切面 + 切入点(或匹配方法,如果您需要更具体的话)。
  • 我只需要特定的课程。
  • 那么 AOP 应该可以很好地满足您的需求,尤其是因为您只想记录日志——这是最常见的用例之一。如果您愿意,我会在几分钟内写出答案。
  • 您也可以通过HandlerInterceptorstackoverflow.com/questions/51352320/… 获取类,这不是您的确切请求,但可以使用它来告诉类/方法该请求已映射到
  • @DarrenForsythe 这看起来很有希望。我去看看。

标签: java spring spring-boot spring-mvc servlet-filters


【解决方案1】:

你想要的基本上是一个cross-cutting concern,它针对你的应用程序的特定部分——在本例中是日志记录。

这是面向方面编程最常见的用例之一,Spring 使用 AspectJ 风格的切入点对此提供了内置支持。

你需要:

  1. 在配置类的 Spring 配置中启用 AOP,如下所示:
@Configuration
@EnableAspectJAutoProxy
public class AopConfiguration {
}
  1. 定义一个方面,例如如下:
@Aspect
public class LoggingAspect {

    Logger log = ...; // define logger

    // Matches all executions in com.example.ExampleController,
    // with any return value, using any parameters   
    @Pointcut("execution(* com.example.ExampleController.*(..))")
    public void controllerExecutionPointcut() {}

    @Around("controllerExecutionPointcut()")
    public Object aroundTargetControllerInvocation(ProceedingJoinPoint pjp) {
    
        log.debug("About to invoke method: {}", pjp.getSignature().getName());
        
        try {
            return pjp.proceed(); 
        } catch (Throwable t) {
            // note that getArgs() returns an Object[],
            // so you may want to map it to a more readable format
            log.debug("Encountered exception while invoking method with args {}", pjp.getArgs());
            throw t;
        }
        
        log.debug("Sucessfully finished invocation");
    }
}

参见例如this 指南以了解有关切入点表达式的更多信息。

另一个常见的用例是为您的方法调用计时,尽管使用 @Timed 的 Micrometer(和 Spring 的 Micrometer 适配器)可能会更好。

您可能还希望通读reference documentation,它提供了大量有关 Spring 中 AOP 工作原理的信息。

注意:与几乎所有其他 Spring 代理机制一样,来自目标对象内的调用不会被代理,即 this.otherControllerMethod() 不会被拦截上述建议。同样,private 方法也不能被拦截。有关详细信息,请参阅参考文档的 5.4.3 部分。

最后一点,如果性能非常重要,您应该查看 AspectJ 编译时或加载时编织,它消除了 Spring 的代理机制引入的一些开销(这是 Spring AOP 在引擎盖)。在您的情况下,这很可能不是必需的,但请记住这一点。


编辑评论:

谢谢!这种方法的一个警告是它不能让我访问 HttpServletRequest 或 HttpServletResponse,这是我正在使用的东西。如果这不是我需要的东西,我可以看到这会有所帮助。我发现我在我的问题中没有明确说明这个要求,所以我会相应地更新。

确实,不幸的是,这种方法无法直接实现。如果你真的需要这个请求,那么@DarrenForsythe 提到的HandlerInterceptor 方法是另一种可能。如果您只想记录日志,我看不出您绝对需要该请求的理由 - 除非您希望提取特定的标头并记录它们。

在这种情况下,IMO,您最初尝试的 OncePerRequestFilter 会好得多,因为您可以控制对哪些请求应用过滤器(使用 shouldNotFilter(HttpServletRequest request) 并在 URL 上进行匹配)。

【讨论】:

  • 谢谢!这种方法的一个警告是它不能让我访问HttpServletRequestHttpServletResponse,这是我正在使用的东西。如果这不是我需要的东西,我可以看到这会有所帮助。我发现我在问题中没有明确说明该要求,因此我将相应更新。
  • 是的,它从请求和响应中提取特定信息(URL、响应状态、使用包装的 servlet 请求/响应的请求/响应主体等),切入点和 HandlerInterceptor 都不允许为。
  • 我明白了。在这种情况下,使用过滤器很可能是最实用的方法。
【解决方案2】:

经过一些额外的探索和反复试验,我发现控制器可以通过RequestMappingHandlerMapping bean 访问。当请求可以由控制器处理时,这会将请求映射到HandlerMethod,用于控制器的请求处理方法。

public class ExampleFilter extends OncePerRequestFilter {
    private RequestMappingHandlerMapping requestMappingHandlerMapping;

    @Override
    protected void doFilterInternal(
            HttpServletRequest request, HttpServletResponse response,
            FilterChain filterChain) throws ServletException, IOException {
        Object handler = getHandlerBean(request);
        boolean isHandledController = handler instanceof ExampleController
                || handler instanceof NestedEampleController;

        if (!isHandledController) {
            filterChain.doFilter(request, response);
            return;
        }

        // ...
    }

    private Object getHandlerBean(HttpServletRequest request) {
        try {
            HandlerExecutionChain handlerChain = requestMappingHandlerMapping.getHandler(request);
            if (handlerChain != null) {
                Object handler = handlerChain.getHandler();
                if (handler instanceof HandlerMethod) {
                    return ((HandlerMethod) handler).getBean();
                }
            }
            return null;
        } catch (Exception e) {
            return null;
        }
    }

    @Override
    protected void initFilterBean() {
        WebApplicationContext appContext = WebApplicationContextUtils.getWebApplicationContext(getServletContext());
        requestMappingHandlerMapping = appContext.getBean(RequestMappingHandlerMapping.class);
    }
}

为了更加彻底和真正模仿 Spring 的处理程序逻辑,可以使用/模仿 DispatcherServlet 逻辑,而不是直接引用 RequestMappingHandlerMapping。这将咨询所有处理程序,而不仅仅是RequestMappingHandlerMapping

public class ExampleFilter extends OncePerRequestFilter {
    private DispatcherServlet dispatcherServlet;

    @Override
    protected void doFilterInternal(
            HttpServletRequest request, HttpServletResponse response,
            FilterChain filterChain) throws ServletException, IOException {
        Object handler = getHandlerBean(request);
        boolean isHandledController = handler instanceof ExampleController
                || handler instanceof NestedEampleController;

        if (!isHandledController) {
            filterChain.doFilter(request, response);
            return;
        }

        // ...
    }

    private Object getHandlerBean(HttpServletRequest request) {
        try {
            HandlerExecutionChain handlerChain = getHandler(request);
            if (handlerChain != null) {
                Object handler = handlerChain.getHandler();
                if (handler instanceof HandlerMethod) {
                    return ((HandlerMethod) handler).getBean();
                }
            }
            return null;
        } catch (Exception e) {
            return null;
        }
    }

    /**
     * Duplicates the protected "getHandler" method logic from DispatcherServlet.
     */
    private HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
        List<HandlerMapping> handlerMappings = dispatcherServlet.getHandlerMappings();
        if (handlerMappings != null) {
            for (HandlerMapping mapping : handlerMappings) {
                HandlerExecutionChain handler = mapping.getHandler(request);
                if (handler != null) {
                    return handler;
                }
            }
        }
        return null;
    }

    @Override
    protected void initFilterBean() {
        WebApplicationContext appContext = WebApplicationContextUtils.getWebApplicationContext(getServletContext());
        dispatcherServlet = appContext.getBean(DispatcherServlet.class);
    }
}

我不确定是否有更惯用的方法,而且它确实感觉像是跳过了一些圈子并过多地挖掘了 Spring 的内部结构。但它似乎确实有效,至少在 spring-web 5.2.7.RELEASE 上。

【讨论】:

  • 它会起作用吗,是的,部分原因是因为可能有更多的HandlerMappings 可用,所有这些都需要咨询。它是否会返回匹配项,但是它会对性能产生影响,因为每个请求现在被映射两次(一次在您的过滤器中,另一个在 DispatcherServlet 中)。所以是的,它可能会起作用,但它会对您的吞吐量产生相当大的影响。因此,Filter 的建议用于包装请求,HandlerInterceptor 用于日志记录。映射没有额外的开销,您可以知道调用了哪个处理程序。
  • @M.Deinum 有道理。我也可以研究一下这种方法。谢谢!
  • @M.Deinum 通过咨询DispatcherServlet 而不是RequestMappingHandlerMapping 解决了咨询所有HandlerMappings 的问题。
猜你喜欢
  • 2017-05-14
  • 2011-07-29
  • 2016-09-04
  • 2014-11-16
  • 1970-01-01
  • 2018-12-24
  • 2021-06-09
  • 2011-04-10
  • 2015-09-05
相关资源
最近更新 更多