New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fix execution order of @AfterReturning and @After methods in ReflectiveAspectJAdvisorFactory #24673
Conversation
…urning and @after in the AOP annotation implementation.
Can you please explain that in more detail? Does the current behavior contradict existing documentation (either in Spring or AspectJ)? In other words, what is the rationale for the proposed change? |
1. What is the problem?I found this problem when using annotation AOP to implement transaction management, so to illustrate this problem, I wrote a demo of simulating transaction processing to show the problem, the complete demo can be found at:https://github.com/Dafengsu/DemoForAnnotionAOP. following is the source code of the demo: The spring configbean.xml<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<!--package scan-->
<context:component-scan base-package="com.dafengsu"/>
<!-- TransactionManager-->
<bean id="XMLTransactionManager" class="com.dafengsu.aop.XMLTransactionManager"/>
<!--aop-->
<aop:config>
<aop:pointcut id="daoTransfer" expression="execution(* com.dafengsu.service.impl.*.*(..))"/>
<aop:aspect id="txAdvice" ref="XMLTransactionManager">
<aop:before method="beginTransaction" pointcut-ref="daoTransfer"/>
<aop:after-returning method="commit" pointcut-ref="daoTransfer"/>
<aop:after-throwing method="rollback" pointcut-ref="daoTransfer"/>
<aop:after method="release" pointcut-ref="daoTransfer"/>
</aop:aspect>
</aop:config>
<aop:aspectj-autoproxy/>
</beans> The service CodeTransactionService.javapackage com.dafengsu.service;
public interface TransactionService {
void transaction();
} TransactionServiceImpl.javapackage com.dafengsu.service.impl;
import com.dafengsu.service.TransactionService;
import org.springframework.stereotype.Service;
@Service("transactionService")
public class TransactionServiceImpl implements TransactionService {
@Override
public void transaction() {
System.out.println("....Execute transaction....");
}
} Implemented the AOP function by the XMLXMLTransactionManager.javapackage com.dafengsu.aop;
public class XMLTransactionManager {
// Indicates whether the connection is closed
private boolean isClosed = false;
public void beginTransaction() {
System.out.println("XML: beginTransaction");
}
public void commit() {
if (isClosed) {
throw new RuntimeException("XML: Connection is closed");
}
System.out.println("XML: commit...");
}
public void rollback() {
System.out.println("XML: rollback...");
}
public void release() {
isClosed = true;
System.out.println("XML: Connection released.");
}
} Implemented the AOP function by the annotionAnnotationTransactionManager.javapackage com.dafengsu.aop;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
@Component("annotationTransactionManager")
@Aspect
public class AnnotationTransactionManager {
//Indicates whether the connection is closed
private boolean isClosed = false;
@Pointcut("execution(* com.dafengsu.service.impl.*.*(..))")
private void daoTransfer() {
}
@Before("daoTransfer()")
public void beginTransaction() {
System.err.println("Annotation: beginTransaction");
}
@AfterReturning("daoTransfer()")
public void commit() {
if (isClosed) {
throw new RuntimeException("Annotation: Connection is closed");
}
System.err.println("Annotation: commit...");
}
@AfterThrowing("daoTransfer()")
public void rollback() {
System.err.println("Annotation: rollback...");
}
@After("daoTransfer()")
public void release() {
isClosed = true;
System.err.println("Annotation: Connection released.");
}
} JUnit Jupiter TestTransactionServiceTest.javapackage com.dafengsu.service;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
@ExtendWith(SpringExtension.class)
@ContextConfiguration(locations = "classpath:bean.xml")
class TransactionServiceTest {
@Autowired
private TransactionService transactionService;
@Test
void transaction() {
transactionService.transaction();
}
} If all goes well, the method invocation order of the two AOP implementations should be the same and no exception will be thrown. This is the result of the running before the modification
It can be seen that the order of AOP method invocations implemented by the annotations is inconsistent with the XML implementation, and an exception is thrown. The order of the invocation we want is:
But the calling sequence of AOP implementation is:
Because the AOP implementation calls When I made this commited modification, recompiled and replaced the spring-aop jar package. This is the result of re-running:
Everything is normal and the two implementations are consistent. 2. Does the current behavior contradict existing documentation (either in Spring or AspectJ) ?NO! Fragment of ReflectiveAspectJAdvisorFactory.java static {
Comparator<Method> adviceKindComparator = new ConvertingComparator<>(
new InstanceComparator<>(
Around.class, Before.class, AfterReturning.class, AfterThrowing.class, After.class),
(Converter<Method, Annotation>) method -> {
AspectJAnnotation<?> annotation =
AbstractAspectJAdvisorFactory.findAspectJAnnotationOnMethod(method);
return (annotation != null ? annotation.getAnnotation() : null);
});
Comparator<Method> methodNameComparator = new ConvertingComparator<>(Method::getName);
METHOD_COMPARATOR = adviceKindComparator.thenComparing(methodNameComparator);
}
private List<Method> getAdvisorMethods(Class<?> aspectClass) {
final List<Method> methods = new ArrayList<>();
ReflectionUtils.doWithMethods(aspectClass, method -> {
// Exclude pointcuts
if (AnnotationUtils.getAnnotation(method, Pointcut.class) == null) {
methods.add(method);
}
}, ReflectionUtils.USER_DECLARED_METHODS);
if (methods.size() > 1) {
methods.sort(METHOD_COMPARATOR);
}
return methods;
} Through the IDEA Findusages function, we can find that the statically constructed 3. what is the rationale for the proposed change?AOP's annotation implementation should perform the correct call sequence and be consistent with the XML implementation. Although we can use @around or XML to circumvent this flaw, it is best to fix it for the completeness of the AOP annotation implementation. |
Thanks so much for the very detailed response!
That basically sums it up. 👍 |
This behavior contradicts existing documentation. After Returning AdviceAfter returning advice runs when a matched method execution returns normally. You can declare it by using the @AfterReturning annotation. After (Finally) AdviceAfter (finally) advice runs when a matched method execution exits. It is declared by using the @after annotation. After advice must be prepared to handle both normal and exception return conditions. It is typically used for releasing resources and similar purposes. |
The AspectJPrecedenceComparator was designed to mimic the precedence order enforced by the AspectJ compiler with regard to multiple 'after' methods defined within the same aspect whose pointcuts match the same joinpoint. Specifically, if an aspect declares multiple @after, @AfterReturning, or @AfterThrowing advice methods whose pointcuts match the same joinpoint, such 'after' advice methods should be invoked in the reverse order in which they are declared in the source code. When the AspectJPrecedenceComparator was introduced in Spring Framework 2.0, it achieved its goal of mimicking the AspectJ compiler since the JDK at that time (i.e., Java 5) ensured that an invocation of Class#geDeclaredMethods() returned an array of methods that matched the order of declaration in the source code. However, Java 7 removed this guarantee. Consequently, in Java 7 or higher, AspectJPrecedenceComparator no longer works as it is documented or as it was designed when sorting advice methods in a single @aspect class. Note, however, that AspectJPrecedenceComparator continues to work as documented and designed when sorting advice configured via the <aop:aspect> XML namespace element. PR gh-24673 highlights a use case where AspectJPrecedenceComparator fails to assign the highest precedence to an @after advice method declared last in the source code. Note that an @after advice method with a precedence higher than @AfterReturning and @AfterThrowing advice methods in the same aspect will effectively be invoked last due to the try-finally implementation in AspectJAfterAdvice.invoke() which invokes proceed() in the try-block and invokeAdviceMethod() in the finally-block. Since Spring cannot reliably determine the source code declaration order of annotated advice methods without using ASM to analyze the byte code, this commit introduces reliable invocation order for advice methods declared within a single @aspect. Specifically, the getAdvisors(...) method in ReflectiveAspectJAdvisorFactory now hard codes the declarationOrderInAspect to `0` instead of using the index of the current advice method. This is necessary since the index no longer has any correlation to the method declaration order in the source code. The result is that all advice methods discovered via reflection will now be sorted only according to the precedence rules defined in the ReflectiveAspectJAdvisorFactory.METHOD_COMPARATOR. Specifically, advice methods within a single @aspect will be sorted in the following order (with @after advice methods effectively invoked after @AfterReturning and @AfterThrowing advice methods): @around, @before, @after, @AfterReturning, @AfterThrowing. The modified assertions in AspectJAutoProxyAdviceOrderIntegrationTests demonstrate the concrete effects of this change. Closes gh-25186
Thanks again for the PR and for bringing this inconsistency to our attention! After thorough analysis I determined that the cause of the undesired behavior was not in the The proposed change in this PR actually breaks the following test which is inherited by Lines 511 to 525 in 33181da
Further information can be found in #25186 and 0998bd4, and a follow-up issue has been raised for advice configured via the Please note that the change in #25186 has already been released (today) in Spring Framework 5.2.7. In addition, the following excerpt from the Advice Ordering section of the reference manual provides guidance with regard to the ordering of multiple advice methods within a single
I am closing this PR since it has been effectively superseded by #25186. |
The AspectJPrecedenceComparator was designed to mimic the precedence order enforced by the AspectJ compiler with regard to multiple 'after' methods defined within the same aspect whose pointcuts match the same joinpoint. Specifically, if an aspect declares multiple @after, @AfterReturning, or @AfterThrowing advice methods whose pointcuts match the same joinpoint, such 'after' advice methods should be invoked in the reverse order in which they are declared in the source code. When the AspectJPrecedenceComparator was introduced in Spring Framework 2.0, it achieved its goal of mimicking the AspectJ compiler since the JDK at that time (i.e., Java 5) ensured that an invocation of Class#geDeclaredMethods() returned an array of methods that matched the order of declaration in the source code. However, Java 7 removed this guarantee. Consequently, in Java 7 or higher, AspectJPrecedenceComparator no longer works as it is documented or as it was designed when sorting advice methods in a single @aspect class. Note, however, that AspectJPrecedenceComparator continues to work as documented and designed when sorting advice configured via the <aop:aspect> XML namespace element. PR spring-projectsgh-24673 highlights a use case where AspectJPrecedenceComparator fails to assign the highest precedence to an @after advice method declared last in the source code. Note that an @after advice method with a precedence higher than @AfterReturning and @AfterThrowing advice methods in the same aspect will effectively be invoked last due to the try-finally implementation in AspectJAfterAdvice.invoke() which invokes proceed() in the try-block and invokeAdviceMethod() in the finally-block. Since Spring cannot reliably determine the source code declaration order of annotated advice methods without using ASM to analyze the byte code, this commit introduces reliable invocation order for advice methods declared within a single @aspect. Specifically, the getAdvisors(...) method in ReflectiveAspectJAdvisorFactory now hard codes the declarationOrderInAspect to `0` instead of using the index of the current advice method. This is necessary since the index no longer has any correlation to the method declaration order in the source code. The result is that all advice methods discovered via reflection will now be sorted only according to the precedence rules defined in the ReflectiveAspectJAdvisorFactory.METHOD_COMPARATOR. Specifically, advice methods within a single @aspect will be sorted in the following order (with @after advice methods effectively invoked after @AfterReturning and @AfterThrowing advice methods): @around, @before, @after, @AfterReturning, @AfterThrowing. The modified assertions in AspectJAutoProxyAdviceOrderIntegrationTests demonstrate the concrete effects of this change. Closes spring-projectsgh-25186
When using the
@AfterReturning
and@After
to implement AOP function, the program always call the method annotated by@After
before the method annotated by@AfterReturning
. It's opposite to the normal execution order of AOP function implemented by the XML.By tracing and debugging, I found the reason is that the annotated methods is sorted by a wrong comparator which was not constructed correctly in the process of parsing annotations. Finally, the problem was fixed by adjusting the order of parameters (annotation classes) when the comparator be constructed. It was be verified after repair and compilation. So, I created this commit . If I misunderstand something, please tell me. thanks !