@PreDestroy and Spring AOP compatibility

Viewed 246

I want to do some work on a graceful shutdown.
I've tried an approach shown below, but it doesn't work.
I've found a workaround (putting aspect annotation on @EventListener-marked method for ContextClosedEvent), but I want to understand, why it failed (audit() method not invoked at all without any exceptions).

@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 ...
    }
}

As far as I digged into Spring 5 guts, I found that @PreDestroy is processed by CommonAnnotationBeanPostProcessor and @Aspect classes are transformed into Spring AOP Advisors by AspectJAdvisorFactory (CGLIB on JDK proxies, I guess). Thus, I don't understand, why aspect logic isn't called after sending SIGTERM to the application's JVM process. I even checked an output of System.out.println(this.getClass().getCanonicalName()), which was put into the body of the destroy() method - it looks like, for example, Application$$EnhancerBySpringCGLIB$$25f99bf7. From my current point of view, there is nothing to prevent the aspect from wrapping around @PreDestroy callback method.
Nevertheless, it doesn't work.
Can somebody explain why?

2 Answers

If you would call the pre-destroy method manually like appContext.getBean(Application.class).destroy(), the aspect would be triggered. But in the part of the lifecycle where the application is being destroyed, no aspects seem to be applied anymore.

According to the @PreDestroy javadoc, the annotated target method may be private and even final, i.e. two characteristics contradicting proxy-based Spring AOP usage. I am not a Spring or Java EE user at all and I might be wrong, but to me it looks as though this is not supposed to be working the way you expect it to. Spring experts around here like M. Deinum or R.G might be able to shed more light onto this issue.

Following are my analysis. main() method modified to illustrate a bean method trigger.

@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");
    }
}

When the app.destory() is called within main() method , the call is done on the proxy

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 

and when ApplicationContext shuts down , the lifecycle callback is done by the InitDestroyAnnotationBeanPostProcessor$LifecycleMetadata.invokeDestroyMethods(Object, String) on the actual object and not the proxy , hence no advise happens.

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
Related