[spring-projects/spring-boot]启用问题详细信息不适用于代理

2024-05-08 896 views
6

使用 Spring Boot 3.0.2,将问题详细信息(例如,spring.mvc.problemdetails.enabled=true)与代理(例如,由于 AOP)结合起来不起作用,因为类(在和ProblemDetailsExceptionHandler中重复)是。servletreactivefinal

使用案例

我想对返回的问题详细信息执行某些操作(例如日志记录)。为此,我ExceptionHandler使用 AspectJ 拦截返回值。

再生产

考虑以下尝试拦截ExceptionHandlers 以记录ErrorResponse详细信息的示例:

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.http.ResponseEntity;
import org.springframework.lang.Nullable;
import org.springframework.web.ErrorResponse;

@EnableAspectJAutoProxy
@WebMvcTest(value = ProblemTest.TestConfig.class, properties = "spring.mvc.problemdetails.enabled=true")
class ProblemTest {

    @Test
    void contextLoads() {}

    @Configuration
    static class TestConfig {

        @Bean
        ProblemLoggingAspect problemLoggingAspect() {
            return new ProblemLoggingAspect();
        }

    }

    @Aspect
    static class ProblemLoggingAspect {

        @AfterReturning(
                pointcut = "@annotation(org.springframework.web.bind.annotation.ExceptionHandler)",
                returning = "returnValue")
        public void exceptionHandlerIntercept(JoinPoint joinPoint, Object returnValue) {
            ErrorResponse errorResponse = errorResponse(joinPoint, returnValue);
            if (errorResponse != null) {
                System.out.format("problem detail: %s%n", errorResponse.getBody().getDetail());
            }
        }

        @Nullable
        private ErrorResponse errorResponse(JoinPoint ignored, Object returnValue) {
            if (returnValue instanceof ErrorResponse errorResponse) {
                return errorResponse;
            } else if (returnValue instanceof ResponseEntity<?> responseEntity &&
                    responseEntity.getBody() instanceof ErrorResponse errorResponse) {
                return errorResponse;
            }
            return null;
        }

    }

}

运行此测试会导致以下失败:

...
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'problemDetailsExceptionHandler' defined in class path resource [org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration$ProblemDetailsErrorHandlingConfiguration.class]: Could not generate CGLIB subclass of class org.springframework.boot.autoconfigure.web.servlet.ProblemDetailsExceptionHandler: Common causes of this problem include using a final class or a non-visible class
    ...
Caused by: org.springframework.aop.framework.AopConfigException: Could not generate CGLIB subclass of class org.springframework.boot.autoconfigure.web.servlet.ProblemDetailsExceptionHandler: Common causes of this problem include using a final class or a non-visible class
    ...
    at org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator.createProxy(AbstractAutoProxyCreator.java:464) ~[spring-aop-6.0.4.jar:6.0.4]
    at org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator.wrapIfNecessary(AbstractAutoProxyCreator.java:369) ~[spring-aop-6.0.4.jar:6.0.4]
    ...
Caused by: java.lang.IllegalArgumentException: Cannot subclass final class org.springframework.boot.autoconfigure.web.servlet.ProblemDetailsExceptionHandler
    at org.springframework.cglib.proxy.Enhancer.generateClass(Enhancer.java:653) ~[spring-core-6.0.4.jar:6.0.4]
    at org.springframework.cglib.core.DefaultGeneratorStrategy.generate(DefaultGeneratorStrategy.java:26) ~[spring-core-6.0.4.jar:6.0.4]
    ...
笔记

ProblemTest在 Spring Boot 2.x 上成功,并且在spring.mvc.problemdetails.enabled=false3.x时成功。它仅在 3.x 上失败spring.mvc.problemdetails.enabled=true

回答

2

将问题详细信息(例如,spring.mvc.problemdetails.enabled=true)与代理(例如,由于 AOP)结合起来不起作用,因为类(在和ProblemDetailsExceptionHandler中重复)是。servletreactivefinal

这似乎只是第一个障碍。各自的handleException()方法也final。因此,即使ProblemDetailsExceptionHandlerSpring Boot中的类不是final,Spring仍然无法创建一个基于CGLIB的代理来拦截和handleException()中的方法。org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandlerorg.springframework.web.reactive.result.method.annotation.ResponseEntityExceptionHandler

因此,您似乎需要研究一种不同的方法。

1

@sbrannen,我们对 AOP 也不满意,尽管它正在工作。鉴于您建议寻找其他地方,现在我们别无选择。我们只是想捕获(并处理、记录等)所有ErrorResponse返回的数据,我们还能做什么来实现这个功能呢?

4

我们对 AOP 也不满意,尽管它确实有效。

您是说这是一种回归,并且 AspectJ 方法以前对您有用吗?

鉴于您建议寻找其他地方,现在我们别无选择。我们只是想捕获(并处理、记录等)所有ErrorResponse返回的数据,我们还能做什么来实现这个功能呢?

我将听从@rstoyanchev 的指导。

7

您是说这是一种回归,并且 AspectJ 方法以前对您有用吗?

@sbrannen,是的。在 Spring Boot 3 中引入ProblemTest开始失败spring.mvc.problemdetails.enabled=true。使用 AOP 拦截ExceptionHandler返回值在 Spring Boot 2.x 中有效,并且spring.mvc.problemdetails.enabled=false在 3.x 中有效。因此,是的,这对我来说似乎是一种回归。

2

使用AOP拦截ExceptionHandler返回值在Spring Boot 2.x中有效

你是说@ExceptionHandler在自己的非final类上拦截方法(intercepting non-final method)吗?

或者你是在谈论拦截@ExceptionHandler两个ResponseEntityExceptionHandler类的子类中的方法?

9

@AfterReturning(pointcut = "@annotation(org.springframework.web.bind.annotation.ExceptionHandler)", returning = "returnValue")适用于所有ExceptionHandlerbean(包括 Spring Framework 6、Spring Boot 3 和我们自定义的 bean 引入的 bean),除了spring.mvc.problemdetails.enabled=true.

ResponseEntityExceptionHandlerProblemDetailsExceptionHandler等人。是随 Spring Framework 6 和 Spring Boot 3 引入的。这是非类和方法第一次final破坏 AOP。我的印象是,final从这些类和方法中删除修饰符将解决问题并遵守所有 Spring 提供的ExceptionHandlerbean 都是可代理的旧契约。

7

这是非类和方法第一次final破坏 AOP。

好的。现在我明白你所说的“回归”是什么意思了。

这不是 Spring 框架或 Spring Boot 现有代码本身的技术回归,但是......它是一种“回归”,因为并非@ExceptionHandlerSpring 发布的所有方法都能够被代理。

我的印象是,final从这些类和方法中删除修饰符将解决问题并遵守所有 Spring 提供的ExceptionHandlerbean 都是可代理的旧契约。

是的,这可能就足够了。

5

感谢@vy 的报告。

您可以final通过设置spring.mvc.problemdetails.enabled=false和创建类似ProblemDetailsExceptionHandler但不带final.

我也希望这应该可行,在这种情况下,这个问题毕竟属于 Spring Boot。

2

@sbrannen,这当然是真的。这是一个模板方法,它本来就是这样final,而且一直都是这样。更改它会打开类以在不应该扩展的地方进行扩展。从这个意义上说,我仍然看不到我们可以在 Spring 框架中做任何事情。

相比之下,考虑到这个用例,我不认为 Boot 类无法打开进行扩展的原因。

或者,由于ResponseEntityExceptionHandler预计 的子类会重写受保护的方法handleExceptionInternal以进行单点处理,因此也应该可以使用建议进行拦截。我不知道这是否是确切的语法,但无论如何它应该是可能的:

@AfterReturning(
    pointcut = "execution(protected * org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler.handleExceptionInternal(..)))",
    returning = "returnValue")
7

@sbrannen,这当然是真的。这是一个模板方法,它本来就是这样final,而且一直都是这样。更改它会打开类以在不应该扩展的地方进行扩展。从这个意义上说,我仍然看不到我们可以在 Spring 框架中做任何事情。

正确的。在这些观点上我同意你的观点。

相比之下,考虑到这个用例,我不认为 Boot 类无法打开进行扩展的原因。

我也同意这一点。让我们看看 @bclozel 怎么说,因为他在 Boot 中编写了这些类。

或者,由于ResponseEntityExceptionHandler预计 的子类会重写受保护的方法handleExceptionInternal以进行单点处理,因此也应该可以使用建议进行拦截。我不知道这是否是确切的语法,但无论如何它应该是可能的:

@AfterReturning(
  pointcut = "execution(protected * org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler.handleExceptionInternal(..)))",
  returning = "returnValue")

是的,确实,我们可以将ResponseEntityExceptionHandler类中的其他方法作为切入点。

但是,如果 @vy 想要继续将他现有的建议应用于@ExceptionHandler其他类中的方法(不会遇到 的问题final),他将需要投入更多的工作来正确应用切入点。具体来说,他需要引入一些共享@Pointcut方法来匹配这两个类,然后他需要将各种切入点与布尔逻辑组合起来——以达到继续使用现有逻辑ResponseEntityExceptionHandler的效果。pointcut = "exceptionHandlerMethod() && !inResponseEntityExceptionHandler()"

总之,如果 Boot 删除了final这两个类的声明,@vy 应该能够让他的建议发挥作用,但不能不更改他的切入点。

4

相比之下,考虑到这个用例,我不认为 Boot 类无法打开进行扩展的原因。

这里的主要目标是提供ResponseEntityExceptionHandler支持; Spring Boot 并不打算为此创建新的公共合约或扩展。该类已经是包私有的,所以我想我们可以删除final那里的关键字。请注意,这不会阻止应用程序开发人员使用自定义处理程序完全执行此操作。

我们对 AOP 也不满意,尽管它确实有效。鉴于您建议寻找其他地方,现在我们别无选择。我们只是想捕获(并处理、记录等)所有返回的 ErrorResponse,我们还能做什么来实现此功能?

我不知道是否有更好的方法来实现这一目标。如果这个更改对您来说不错的话,我很高兴再次将问题转移到 Spring Boot @vy。

0

@rstoyanchev、@sbrannen、@bclozel,非常感谢您的及时回复。我将在本地尝试删除final修饰符并更新方面切入点,并在这里分享结果。请给我一些时间进行此项检查。

2

我已经确认从类中删除修饰符ProblemTest时会成功。尽管切入点确实错过了.我也可以扩展切入点来覆盖,但这使得该方法非常脆弱:finalProblemDetailsExceptionHandlerResponseEntityExceptionHandler#handleException()handleExceptionInternal()

@AfterReturning(
        pointcut = "@annotation(org.springframework.web.bind.annotation.ExceptionHandler)",
        returning = "returnValue")
@AfterReturning(
        pointcut = "@annotation(org.springframework.web.bind.annotation.ExceptionHandler) || execution(protected * org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler.handleExceptionInternal(..)))",
        returning = "returnValue")

之前的意图是明确而紧凑的。现在看起来好像有漏洞,我们正在覆盖它们,并祈祷我们的覆盖仍然是完整的。是否也可以从方法final中删除修饰符handleException()

4

恐怕不是。该类是一个模板类,模板的部分开放用于扩展,公开为受保护的方法。这只是设计的一部分。切入点涉及更多,但可以编写测试以确保其按预期工作。

4

如果handleException()打不开,那就没什么好讨论的了。我认为这个问题可以移回到 Spring Boot 并通过从类final中删除修饰符来解决ProblemDetailsExceptionHandler

2

结束有利于 PR #34503