【问题标题】:@PreDestroy and Spring AOP compatibility@PreDestroy 和 Spring AOP 兼容性
【发布时间】:2021-09-18 21:12:59
【问题描述】:

我想在正常关机方面做一些工作。
我尝试了如下所示的方法,但它不起作用。
我找到了一种解决方法(在 @EventListener 标记的 ContextClosedEvent 方法上添加方面注释),但我想了解它为什么失败(audit() 方法根本没有被调用,没有任何异常)。

@SpringBootApplication
public class Application {

    public static void main(String args[]) {
       SpringApplication.run(Application.class, args);
    }
    
    @AuditProcess
    @PreDestroy
    public void destroy() {
    }
}
@Aspect
@Order(value = Ordered.HIGHEST_PRECEDENCE)
public class AuditProcessAspect {

    @Pointcut("@annotation(com.aaa.bbb.annotation.AuditProcess) && execution(public * *(..))")
    public void executionOfPublicAuditableMethod() {
    }

    @Around("executionOfPublicAuditableMethod()")
    public Object audit(ProceedingJoinPoint joinPoint) {
        // some business logic ...
    }
}

就我深入研究 Spring 5 的内容而言,我发现 @PreDestroy 由 CommonAnnotationBeanPostProcessor 处理,@Aspect 类由 AspectJAdvisorFactory 转换为 Spring AOP Advisors(我猜是 JDK 代理上的 CGLIB)。因此,我不明白,为什么在将 SIGTERM 发送到应用程序的 JVM 进程后不调用方面逻辑。我什至检查了System.out.println(this.getClass().getCanonicalName()) 的输出,它被放入destroy() 方法的主体中——例如,它看起来像Application$$EnhancerBySpringCGLIB$$25f99bf7。从我目前的角度来看,没有什么可以阻止方面环绕 @PreDestroy 回调方法。
然而,它不起作用。
谁能解释一下原因?

【问题讨论】:

  • 您能否详细说明失败/什么不起作用?有什么例外吗? audit()method 的期望是什么?这个方法没有触发吗?
  • @R.G 后者。 audit() 方法包含一些日志记录逻辑(在调用编织目标方法之前和之后写入安全日志时间戳 + 目标方法名称,用 @AuditProcess 标记)。将 SIGTERM 发送到 JVM 进程后,我在安全日志中看不到任何记录,这看起来好像没有调用该方法。我曾尝试将 System.out.println("aaabbb") 添加到 audit() 正文中 - stdout 终端中没有输出。
  • AuditProcess 注释保留策略是RUNTIME 吗?
  • @R.G 是的,保留策略 RUNTIME 和目标 - 方法
  • 你确定AuditProcessAspect 甚至被Spring的组件扫描发现了吗?您是否使用@Component(或类似)注释标记了方面类?

标签: java spring aop


【解决方案1】:

如果您像appContext.getBean(Application.class).destroy() 那样手动调用预销毁方法,则会触发切面。但是在应用程序被销毁的生命周期部分,似乎不再应用任何方面。

根据@PreDestroy javadoc,注解的目标方法可能是私有的,甚至是最终的,即两个特性与基于代理的 Spring AOP 使用相矛盾。我根本不是 Spring 或 Java EE 用户,我可能错了,但在我看来,这似乎不应该按照您期望的方式工作。 M. Deinum 或 R.G 等这里的 Spring 专家或许能够更深入地了解这个问题。

【讨论】:

    【解决方案2】:

    以下是我的分析。 main() 方法被修改以说明一个 bean 方法触发器。

    @SpringBootApplication
    public class MainApp {
        
        public static void main(String[] args) {
            ConfigurableApplicationContext context = new SpringApplication(MainApp.class).run(args);
            MainApp app = context.getBean(MainApp.class);
            app.destroy();
        }
    
    
        @PreDestroy
        @AuditProcess
        public void destroy() {
            System.out.println("PreDestroy");
        }
    }
    

    当在main() 方法中调用app.destory() 时,调用在代理上完成

    MainApp$$EnhancerBySpringCGLIB$$f2c7a1b4(MainApp).destroy() line: 25    
    MainApp$$FastClassBySpringCGLIB$$8fbee297.invoke(int, Object, Object[]) line: not available 
    MethodProxy.invoke(Object, Object[]) line: 218  
    ... 
    MyAspect.preDestroyLog(ProceedingJoinPoint) line: 27    
    ... 
    CglibAopProxy$DynamicAdvisedInterceptor.intercept(Object, Method, Object[], MethodProxy) line: 692  
    MainApp$$EnhancerBySpringCGLIB$$b98f9ed6.destroy() line: not available  
    MainApp.main(String[]) line: 18 
    

    当 ApplicationContext 关闭时,生命周期回调由实际对象上的 InitDestroyAnnotationBeanPostProcessor$LifecycleMetadata.invokeDestroyMethods(Object, String) 完成,而不是代理,因此不会发生任何建议。

    MainApp$$EnhancerBySpringCGLIB$$f2c7a1b4(MainApp).destroy() line: 25    
    NativeMethodAccessorImpl.invoke0(Method, Object, Object[]) line: not available [native method]  
    NativeMethodAccessorImpl.invoke(Object, Object[]) line: 62  
    DelegatingMethodAccessorImpl.invoke(Object, Object[]) line: 43  
    Method.invoke(Object, Object...) line: 566  
    InitDestroyAnnotationBeanPostProcessor$LifecycleElement.invoke(Object) line: 389    
    InitDestroyAnnotationBeanPostProcessor$LifecycleMetadata.invokeDestroyMethods(Object, String) line: 347 
    CommonAnnotationBeanPostProcessor(InitDestroyAnnotationBeanPostProcessor).postProcessBeforeDestruction(Object, String) line: 177    
    DisposableBeanAdapter.destroy() line: 242   
    DefaultListableBeanFactory(DefaultSingletonBeanRegistry).destroyBean(String, DisposableBean) line: 587  
    DefaultListableBeanFactory(DefaultSingletonBeanRegistry).destroySingleton(String) line: 559 
    DefaultListableBeanFactory.destroySingleton(String) line: 1152  
    DefaultListableBeanFactory(DefaultSingletonBeanRegistry).destroySingletons() line: 520  
    DefaultListableBeanFactory.destroySingletons() line: 1145   
    AnnotationConfigApplicationContext(AbstractApplicationContext).destroyBeans() line: 1111    
    AnnotationConfigApplicationContext(AbstractApplicationContext).doClose() line: 1080 
    AbstractApplicationContext$1.run() line: 996
    

    【讨论】:

    • 非常感谢,这说明了一切。正如我所怀疑的,CommonAnnotationBeanPostProcessor 首先发挥作用,并存储对 bean 的“原始”实例的引用,该实例尚未代理。
    • 这不是我在回答中所说的吗?即使是创建 bean 并手动调用它的示例,也是一样的。还有一些额外的日志记录,我在本地分析问题后编写自己的答案时没有费心发布,但基本上它只是证实了我之前所说的。我什至指出 javadoc 解释了为什么不能指望它首先起作用。此外,在规范关闭情况下,destroy 方法在与手动情况下完全相同的 CGLIB 代理上调用。所以这部分解释似乎不正确或至少不清楚。
    • 生命周期回调在实际 bean 方法上的推断基于以下差异 MainApp$$EnhancerBySpringCGLIB$$f2c7a1b4(MainApp).destroy()MainApp$$EnhancerBySpringCGLIB$$b98f9ed6.destroy() 。前者是实际 bean 方法被调用的时候。这种安排的原因应该是因为@kriegaex 的回答,因此也是我对此表示赞成的原因。
    • 注意:“原始”应用程序对象已经是一个代理,尽管不是 AOP 代理。您可以在调试模式下运行应用程序并在destroy() 方法中设置断点。在那里,您可以检查原始对象(断点处的this)以及当作为bean 方法调用它并单击AOP 代理的destroy() 方法的堆栈帧时,AOP 代理本身。您可以评估 this.getClass().getDeclaredFields()this.getClass().getDeclaredMethods() 之类的代码以检查差异。
    • 然后你会看到原始对象只有来自BeanMethodInterceptorBeanFactoryAwareMethodInterceptor的回调,而AOP代理有来自CglibAopProxy的拦截器和调度器。因此,当我们在这种情况下谈论代理时,我们必须准确并解释我们正在谈论的两种类型中的哪一种。应用程序实例的 CGLIB 代理不是 AOP 代理,这就是 Spring AOP 无法按设计工作的原因。
    猜你喜欢
    • 1970-01-01
    • 2019-05-01
    • 2017-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-04-08
    • 2017-11-11
    • 2013-06-06
    • 1970-01-01
    相关资源
    最近更新 更多