From ea04dd7ed40fdc12aed06a36648c410985fb7bf0 Mon Sep 17 00:00:00 2001 From: Mikel Hamer Date: Sat, 1 Nov 2025 22:33:23 -0400 Subject: [PATCH 1/2] Discover annotations on interface methods for AspectJ annotation pointcuts WIP gh-22311 Signed-off-by: Mikel Hamer --- .../aspectj/AspectJExpressionPointcut.java | 38 ++++++++++++++ .../AspectJExpressionPointcutTests.java | 49 +++++++++++++++++++ 2 files changed, 87 insertions(+) diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJExpressionPointcut.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJExpressionPointcut.java index 6452670bf644..d74c5cbee183 100644 --- a/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJExpressionPointcut.java +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJExpressionPointcut.java @@ -16,6 +16,7 @@ package org.springframework.aop.aspectj; +import java.lang.annotation.Annotation; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.lang.reflect.Proxy; @@ -459,6 +460,43 @@ private ShadowMatch getTargetShadowMatch(Method method, Class targetClass) { } } } + else if (containsAnnotationPointcut()) { + try { + // Get the param value inside @annotation(...) + PointcutExpression pointcutExpression = obtainPointcutExpression(); + String expr = pointcutExpression.getPointcutExpression(); + int start = expr.indexOf('(', expr.indexOf("@annotation")); + int end = expr.indexOf(')', start); + String annotationParam = expr.substring(start + 1, end).trim(); + + boolean isParamFullyQualifiedClassName = annotationParam.contains("."); + if (isParamFullyQualifiedClassName) { + @SuppressWarnings("unchecked") + Class annClass = (Class) ClassUtils.forName( + annotationParam, targetClass.getClassLoader()); + // Check for annotation on target method for every interface in the target class hierarchy + for (Class ifc : ClassUtils.getAllInterfacesForClassAsSet(targetClass)) { + try { + Method interfaceMethod = ifc.getMethod(method.getName(), method.getParameterTypes()); + if (interfaceMethod.isAnnotationPresent(annClass)) { + targetMethod = interfaceMethod; + break; + } + } + catch (NoSuchMethodException ignored) { + // The interface doesn't contain the target method + } + } + } + else { + // TODO: Support variable annotation params such as @annotation(transactional) + // WIP gh-22311 + } + } + catch (ClassNotFoundException ignored) { + // Pointcut annotation class not found. Proceed with original target method + } + } return getShadowMatch(targetMethod, method); } diff --git a/spring-aop/src/test/java/org/springframework/aop/aspectj/AspectJExpressionPointcutTests.java b/spring-aop/src/test/java/org/springframework/aop/aspectj/AspectJExpressionPointcutTests.java index 14288c977616..4bc581948392 100644 --- a/spring-aop/src/test/java/org/springframework/aop/aspectj/AspectJExpressionPointcutTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/aspectj/AspectJExpressionPointcutTests.java @@ -23,7 +23,11 @@ import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import test.annotation.EmptySpringAnnotation; import test.annotation.transaction.Tx; @@ -31,6 +35,7 @@ import org.springframework.aop.ClassFilter; import org.springframework.aop.MethodMatcher; import org.springframework.aop.Pointcut; +import org.springframework.aop.aspectj.annotation.AspectJProxyFactory; import org.springframework.aop.framework.ProxyFactory; import org.springframework.aop.support.DefaultPointcutAdvisor; import org.springframework.beans.testfixture.beans.IOther; @@ -513,6 +518,27 @@ void testAnnotationOnMethodArgumentsWithWildcards() throws Exception { ProcessesSpringAnnotatedParameters.class)).isFalse(); } + @Test + void testAnnotationOnInterfaceMethodWithFQN() throws Exception { + String expression = "@annotation(test.annotation.transaction.Tx)"; + AspectJExpressionPointcut ajexp = new AspectJExpressionPointcut(); + ajexp.setExpression(expression); + + assertThat(ajexp.matches(BeanC.class.getMethod("getAge"), BeanC.class)).isTrue(); + } + + @Disabled("WIP gh-22311") + @Test + void testAnnotationOnInterfaceMethodWithAnnotationArgument() { + AspectJProxyFactory proxyFactory = new AspectJProxyFactory(new BeanC()); + proxyFactory.addAspect(PointcutWithTxAnnotationArgument.class); + IBeanC proxiedTestBean = proxyFactory.getProxy(); + + assertThatIllegalStateException() + .isThrownBy(proxiedTestBean::getAge) + .withMessage("Invoked with @Tx"); + } + public static class OtherIOther implements IOther { @@ -602,6 +628,29 @@ public void setName(String name) { } } + interface IBeanC { + + @Tx + int getAge(); + } + + static class BeanC implements IBeanC { + + @Override + public int getAge() { + return 0; + } + } + + @Aspect + static class PointcutWithTxAnnotationArgument { + + @Around("@annotation(tx)") + public Object around(ProceedingJoinPoint pjp, Tx tx) { + throw new IllegalStateException("Invoked with @Tx"); + } + } + } From 36cf25e2bff1b946f220a636884c39bf07614df4 Mon Sep 17 00:00:00 2001 From: Mikel Hamer Date: Sat, 1 Nov 2025 22:33:23 -0400 Subject: [PATCH 2/2] Discover annotations on interface methods for AspectJ annotation pointcuts WIP gh-22311 Signed-off-by: Mikel Hamer --- .../aspectj/AspectJExpressionPointcut.java | 38 ++++++++++++++ .../AspectJExpressionPointcutTests.java | 49 +++++++++++++++++++ 2 files changed, 87 insertions(+) diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJExpressionPointcut.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJExpressionPointcut.java index 6452670bf644..d74c5cbee183 100644 --- a/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJExpressionPointcut.java +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJExpressionPointcut.java @@ -16,6 +16,7 @@ package org.springframework.aop.aspectj; +import java.lang.annotation.Annotation; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.lang.reflect.Proxy; @@ -459,6 +460,43 @@ private ShadowMatch getTargetShadowMatch(Method method, Class targetClass) { } } } + else if (containsAnnotationPointcut()) { + try { + // Get the param value inside @annotation(...) + PointcutExpression pointcutExpression = obtainPointcutExpression(); + String expr = pointcutExpression.getPointcutExpression(); + int start = expr.indexOf('(', expr.indexOf("@annotation")); + int end = expr.indexOf(')', start); + String annotationParam = expr.substring(start + 1, end).trim(); + + boolean isParamFullyQualifiedClassName = annotationParam.contains("."); + if (isParamFullyQualifiedClassName) { + @SuppressWarnings("unchecked") + Class annClass = (Class) ClassUtils.forName( + annotationParam, targetClass.getClassLoader()); + // Check for annotation on target method for every interface in the target class hierarchy + for (Class ifc : ClassUtils.getAllInterfacesForClassAsSet(targetClass)) { + try { + Method interfaceMethod = ifc.getMethod(method.getName(), method.getParameterTypes()); + if (interfaceMethod.isAnnotationPresent(annClass)) { + targetMethod = interfaceMethod; + break; + } + } + catch (NoSuchMethodException ignored) { + // The interface doesn't contain the target method + } + } + } + else { + // TODO: Support variable annotation params such as @annotation(transactional) + // WIP gh-22311 + } + } + catch (ClassNotFoundException ignored) { + // Pointcut annotation class not found. Proceed with original target method + } + } return getShadowMatch(targetMethod, method); } diff --git a/spring-aop/src/test/java/org/springframework/aop/aspectj/AspectJExpressionPointcutTests.java b/spring-aop/src/test/java/org/springframework/aop/aspectj/AspectJExpressionPointcutTests.java index 14288c977616..4bc581948392 100644 --- a/spring-aop/src/test/java/org/springframework/aop/aspectj/AspectJExpressionPointcutTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/aspectj/AspectJExpressionPointcutTests.java @@ -23,7 +23,11 @@ import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import test.annotation.EmptySpringAnnotation; import test.annotation.transaction.Tx; @@ -31,6 +35,7 @@ import org.springframework.aop.ClassFilter; import org.springframework.aop.MethodMatcher; import org.springframework.aop.Pointcut; +import org.springframework.aop.aspectj.annotation.AspectJProxyFactory; import org.springframework.aop.framework.ProxyFactory; import org.springframework.aop.support.DefaultPointcutAdvisor; import org.springframework.beans.testfixture.beans.IOther; @@ -513,6 +518,27 @@ void testAnnotationOnMethodArgumentsWithWildcards() throws Exception { ProcessesSpringAnnotatedParameters.class)).isFalse(); } + @Test + void testAnnotationOnInterfaceMethodWithFQN() throws Exception { + String expression = "@annotation(test.annotation.transaction.Tx)"; + AspectJExpressionPointcut ajexp = new AspectJExpressionPointcut(); + ajexp.setExpression(expression); + + assertThat(ajexp.matches(BeanC.class.getMethod("getAge"), BeanC.class)).isTrue(); + } + + @Disabled("WIP gh-22311") + @Test + void testAnnotationOnInterfaceMethodWithAnnotationArgument() { + AspectJProxyFactory proxyFactory = new AspectJProxyFactory(new BeanC()); + proxyFactory.addAspect(PointcutWithTxAnnotationArgument.class); + IBeanC proxiedTestBean = proxyFactory.getProxy(); + + assertThatIllegalStateException() + .isThrownBy(proxiedTestBean::getAge) + .withMessage("Invoked with @Tx"); + } + public static class OtherIOther implements IOther { @@ -602,6 +628,29 @@ public void setName(String name) { } } + interface IBeanC { + + @Tx + int getAge(); + } + + static class BeanC implements IBeanC { + + @Override + public int getAge() { + return 0; + } + } + + @Aspect + static class PointcutWithTxAnnotationArgument { + + @Around("@annotation(tx)") + public Object around(ProceedingJoinPoint pjp, Tx tx) { + throw new IllegalStateException("Invoked with @Tx"); + } + } + }