Skip to content

Commit 865fa33

Browse files
committed
Cache CGLIB proxy classes properly again
The introduction of AdvisedSupport.AdvisorKeyEntry in Spring Framework 6.0.10 resulted in a regression regarding caching of CGLIB generated proxy classes. Specifically, equality checks for the proxy class cache became based partially on identity rather than equivalence. For example, if an ApplicationContext was configured to create a class-based @transactional proxy, a second attempt to create the ApplicationContext resulted in a duplicate proxy class for the same @transactional component. On the JVM this went unnoticed; however, when running Spring integration tests within a native image, if a test made use of @⁠DirtiesContext, a second attempt to create the test ApplicationContext resulted in an exception stating, "CGLIB runtime enhancement not supported on native image." This is because Test AOT processing only refreshes a test ApplicationContext once, and the duplicate CGLIB proxy classes are only requested in subsequent refreshes of the same ApplicationContext which means that duplicate proxy classes are not tracked during AOT processing and consequently not included in a native image. This commit addresses this regression as follows. - AdvisedSupport.AdvisorKeyEntry is now based on the toString() representations of the ClassFilter and MethodMatcher in the corresponding Pointcut instead of the filter's and matcher's identities. - Due to the above changes to AdvisorKeyEntry, ClassFilter and MethodMatcher implementations are now required to implement equals(), hashCode(), AND toString(). - Consequently, the following now include proper equals(), hashCode(), and toString() implementations. - CacheOperationSourcePointcut - TransactionAttributeSourcePointcut - PerTargetInstantiationModelPointcut Closes gh-31238
1 parent 9120f87 commit 865fa33

File tree

9 files changed

+172
-14
lines changed

9 files changed

+172
-14
lines changed

spring-aop/src/main/java/org/springframework/aop/ClassFilter.java

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,17 @@
2323
* <p>Can be used as part of a {@link Pointcut} or for the entire targeting of
2424
* an {@link IntroductionAdvisor}.
2525
*
26-
* <p>Concrete implementations of this interface typically should provide proper
27-
* implementations of {@link Object#equals(Object)} and {@link Object#hashCode()}
28-
* in order to allow the filter to be used in caching scenarios &mdash; for
29-
* example, in proxies generated by CGLIB.
26+
* <p><strong>WARNING</strong>: Concrete implementations of this interface must
27+
* provide proper implementations of {@link Object#equals(Object)},
28+
* {@link Object#hashCode()}, and {@link Object#toString()} in order to allow the
29+
* filter to be used in caching scenarios &mdash; for example, in proxies generated
30+
* by CGLIB. As of Spring Framework 6.0.13, the {@code toString()} implementation
31+
* must generate a unique string representation that aligns with the logic used
32+
* to implement {@code equals()}. See concrete implementations of this interface
33+
* within the framework for examples.
3034
*
3135
* @author Rod Johnson
36+
* @author Sam Brannen
3237
* @see Pointcut
3338
* @see MethodMatcher
3439
*/

spring-aop/src/main/java/org/springframework/aop/MethodMatcher.java

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,17 @@
4141
* state changes they have produced in parameters or {@code ThreadLocal} state will
4242
* be available at the time of evaluation.
4343
*
44-
* <p>Concrete implementations of this interface typically should provide proper
45-
* implementations of {@link Object#equals(Object)} and {@link Object#hashCode()}
46-
* in order to allow the matcher to be used in caching scenarios &mdash; for
47-
* example, in proxies generated by CGLIB.
44+
* <p><strong>WARNING</strong>: Concrete implementations of this interface must
45+
* provide proper implementations of {@link Object#equals(Object)},
46+
* {@link Object#hashCode()}, and {@link Object#toString()} in order to allow the
47+
* matcher to be used in caching scenarios &mdash; for example, in proxies generated
48+
* by CGLIB. As of Spring Framework 6.0.13, the {@code toString()} implementation
49+
* must generate a unique string representation that aligns with the logic used
50+
* to implement {@code equals()}. See concrete implementations of this interface
51+
* within the framework for examples.
4852
*
4953
* @author Rod Johnson
54+
* @author Sam Brannen
5055
* @since 11.11.2003
5156
* @see Pointcut
5257
* @see ClassFilter

spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/InstantiationModelAwarePointcutAdvisorImpl.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import org.springframework.aop.support.DynamicMethodMatcherPointcut;
3333
import org.springframework.aop.support.Pointcuts;
3434
import org.springframework.lang.Nullable;
35+
import org.springframework.util.ObjectUtils;
3536

3637
/**
3738
* Internal implementation of AspectJPointcutAdvisor.
@@ -40,6 +41,7 @@
4041
*
4142
* @author Rod Johnson
4243
* @author Juergen Hoeller
44+
* @author Sam Brannen
4345
* @since 2.0
4446
*/
4547
@SuppressWarnings("serial")
@@ -297,6 +299,23 @@ public boolean matches(Method method, Class<?> targetClass, Object... args) {
297299
private boolean isAspectMaterialized() {
298300
return (this.aspectInstanceFactory == null || this.aspectInstanceFactory.isMaterialized());
299301
}
302+
303+
@Override
304+
public boolean equals(@Nullable Object other) {
305+
return (this == other || (other instanceof PerTargetInstantiationModelPointcut that &&
306+
ObjectUtils.nullSafeEquals(this.declaredPointcut.getExpression(), that.declaredPointcut.getExpression())));
307+
}
308+
309+
@Override
310+
public int hashCode() {
311+
return ObjectUtils.nullSafeHashCode(this.declaredPointcut.getExpression());
312+
}
313+
314+
@Override
315+
public String toString() {
316+
return PerTargetInstantiationModelPointcut.class.getName() + ": " + this.declaredPointcut.getExpression();
317+
}
318+
300319
}
301320

302321
}

spring-aop/src/main/java/org/springframework/aop/framework/AdvisedSupport.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@
6262
*
6363
* @author Rod Johnson
6464
* @author Juergen Hoeller
65+
* @author Sam Brannen
6566
* @see org.springframework.aop.framework.AopProxy
6667
*/
6768
public class AdvisedSupport extends ProxyConfig implements Advised {
@@ -653,8 +654,8 @@ public AdvisorKeyEntry(Advisor advisor) {
653654
this.adviceType = advisor.getAdvice().getClass();
654655
if (advisor instanceof PointcutAdvisor pointcutAdvisor) {
655656
Pointcut pointcut = pointcutAdvisor.getPointcut();
656-
this.classFilterKey = ObjectUtils.identityToString(pointcut.getClassFilter());
657-
this.methodMatcherKey = ObjectUtils.identityToString(pointcut.getMethodMatcher());
657+
this.classFilterKey = pointcut.getClassFilter().toString();
658+
this.methodMatcherKey = pointcut.getMethodMatcher().toString();
658659
}
659660
else {
660661
this.classFilterKey = null;

spring-context/src/main/java/org/springframework/cache/interceptor/CacheOperationSourcePointcut.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
*
3333
* @author Costin Leau
3434
* @author Juergen Hoeller
35+
* @author Sam Brannen
3536
* @since 3.1
3637
*/
3738
@SuppressWarnings("serial")
@@ -86,6 +87,27 @@ public boolean matches(Class<?> clazz) {
8687
}
8788
return (cacheOperationSource == null || cacheOperationSource.isCandidateClass(clazz));
8889
}
90+
91+
private CacheOperationSource getCacheOperationSource() {
92+
return cacheOperationSource;
93+
}
94+
95+
@Override
96+
public boolean equals(@Nullable Object other) {
97+
return (this == other || (other instanceof CacheOperationSourceClassFilter that &&
98+
ObjectUtils.nullSafeEquals(cacheOperationSource, that.getCacheOperationSource())));
99+
}
100+
101+
@Override
102+
public int hashCode() {
103+
return CacheOperationSourceClassFilter.class.hashCode();
104+
}
105+
106+
@Override
107+
public String toString() {
108+
return CacheOperationSourceClassFilter.class.getName() + ": " + cacheOperationSource;
109+
}
110+
89111
}
90112

91113
}

spring-context/src/test/java/org/springframework/aop/aspectj/autoproxy/AspectJAutoProxyCreatorTests.java

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,12 +53,14 @@
5353
import org.springframework.beans.factory.support.RootBeanDefinition;
5454
import org.springframework.beans.testfixture.beans.ITestBean;
5555
import org.springframework.beans.testfixture.beans.TestBean;
56+
import org.springframework.cglib.proxy.Factory;
5657
import org.springframework.context.ApplicationContext;
5758
import org.springframework.context.ConfigurableApplicationContext;
5859
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
5960
import org.springframework.context.annotation.Bean;
6061
import org.springframework.context.annotation.Configuration;
6162
import org.springframework.context.annotation.EnableAspectJAutoProxy;
63+
import org.springframework.context.annotation.Scope;
6264
import org.springframework.context.support.ClassPathXmlApplicationContext;
6365
import org.springframework.context.support.GenericApplicationContext;
6466
import org.springframework.core.DecoratingProxy;
@@ -208,6 +210,31 @@ void perTargetAspect() throws SecurityException, NoSuchMethodException {
208210
assertThat(adrian1.getAge()).isEqualTo(3);
209211
}
210212

213+
@Test // gh-31238
214+
void cglibProxyClassIsCachedAcrossApplicationContextsForPerTargetAspect() {
215+
Class<?> configClass = PerTargetProxyTargetClassTrueConfig.class;
216+
TestBean testBean1;
217+
TestBean testBean2;
218+
219+
// Round #1
220+
try (ConfigurableApplicationContext context = new AnnotationConfigApplicationContext(configClass)) {
221+
testBean1 = context.getBean(TestBean.class);
222+
assertThat(AopUtils.isCglibProxy(testBean1)).as("CGLIB proxy").isTrue();
223+
assertThat(testBean1.getClass().getInterfaces())
224+
.containsExactlyInAnyOrder(Factory.class, SpringProxy.class, Advised.class);
225+
}
226+
227+
// Round #2
228+
try (ConfigurableApplicationContext context = new AnnotationConfigApplicationContext(configClass)) {
229+
testBean2 = context.getBean(TestBean.class);
230+
assertThat(AopUtils.isCglibProxy(testBean2)).as("CGLIB proxy").isTrue();
231+
assertThat(testBean2.getClass().getInterfaces())
232+
.containsExactlyInAnyOrder(Factory.class, SpringProxy.class, Advised.class);
233+
}
234+
235+
assertThat(testBean1.getClass()).isSameAs(testBean2.getClass());
236+
}
237+
211238
@Test
212239
void twoAdviceAspect() {
213240
ClassPathXmlApplicationContext bf = newContext("twoAdviceAspect.xml");
@@ -615,6 +642,23 @@ class ProxyTargetClassFalseConfig extends AbstractProxyTargetClassConfig {
615642
class ProxyTargetClassTrueConfig extends AbstractProxyTargetClassConfig {
616643
}
617644

645+
@Configuration
646+
@EnableAspectJAutoProxy(proxyTargetClass = true)
647+
class PerTargetProxyTargetClassTrueConfig {
648+
649+
@Bean
650+
@Scope("prototype")
651+
TestBean testBean() {
652+
return new TestBean("Jane", 34);
653+
}
654+
655+
@Bean
656+
@Scope("prototype")
657+
PerTargetAspect perTargetAspect() {
658+
return new PerTargetAspect();
659+
}
660+
}
661+
618662
@FunctionalInterface
619663
interface MessageGenerator {
620664
String generateMessage();

spring-context/src/test/java/org/springframework/cache/config/EnableCachingIntegrationTests.java

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2021 the original author or authors.
2+
* Copyright 2002-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -21,6 +21,7 @@
2121
import org.junit.jupiter.api.AfterEach;
2222
import org.junit.jupiter.api.Test;
2323

24+
import org.springframework.aop.support.AopUtils;
2425
import org.springframework.beans.factory.annotation.Autowired;
2526
import org.springframework.cache.Cache;
2627
import org.springframework.cache.CacheManager;
@@ -45,6 +46,7 @@
4546
* Tests that represent real use cases with advanced configuration.
4647
*
4748
* @author Stephane Nicoll
49+
* @author Sam Brannen
4850
*/
4951
class EnableCachingIntegrationTests {
5052

@@ -83,6 +85,25 @@ private void fooGetSimple(FooService service) {
8385
assertCacheHit(key, value, cache);
8486
}
8587

88+
@Test // gh-31238
89+
public void cglibProxyClassIsCachedAcrossApplicationContexts() {
90+
ConfigurableApplicationContext ctx;
91+
92+
// Round #1
93+
ctx = new AnnotationConfigApplicationContext(FooConfigCglib.class);
94+
FooService service1 = ctx.getBean(FooService.class);
95+
assertThat(AopUtils.isCglibProxy(service1)).as("FooService #1 is not a CGLIB proxy").isTrue();
96+
ctx.close();
97+
98+
// Round #2
99+
ctx = new AnnotationConfigApplicationContext(FooConfigCglib.class);
100+
FooService service2 = ctx.getBean(FooService.class);
101+
assertThat(AopUtils.isCglibProxy(service2)).as("FooService #2 is not a CGLIB proxy").isTrue();
102+
ctx.close();
103+
104+
assertThat(service1.getClass()).isSameAs(service2.getClass());
105+
}
106+
86107
@Test
87108
void barServiceWithCacheableInterfaceCglib() {
88109
this.context = new AnnotationConfigApplicationContext(BarConfigCglib.class);

spring-tx/src/main/java/org/springframework/transaction/interceptor/TransactionAttributeSourcePointcut.java

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,15 @@
2727
import org.springframework.util.ObjectUtils;
2828

2929
/**
30-
* Abstract class that implements a {@code Pointcut} that matches if the underlying
30+
* Internal class that implements a {@code Pointcut} that matches if the underlying
3131
* {@link TransactionAttributeSource} has an attribute for a given method.
3232
*
3333
* @author Juergen Hoeller
34+
* @author Sam Brannen
3435
* @since 2.5.5
3536
*/
3637
@SuppressWarnings("serial")
37-
class TransactionAttributeSourcePointcut extends StaticMethodMatcherPointcut implements Serializable {
38+
final class TransactionAttributeSourcePointcut extends StaticMethodMatcherPointcut implements Serializable {
3839

3940
@Nullable
4041
private TransactionAttributeSource transactionAttributeSource;
@@ -87,6 +88,27 @@ public boolean matches(Class<?> clazz) {
8788
}
8889
return (transactionAttributeSource == null || transactionAttributeSource.isCandidateClass(clazz));
8990
}
91+
92+
private TransactionAttributeSource getTransactionAttributeSource() {
93+
return transactionAttributeSource;
94+
}
95+
96+
@Override
97+
public boolean equals(@Nullable Object other) {
98+
return (this == other || (other instanceof TransactionAttributeSourceClassFilter that &&
99+
ObjectUtils.nullSafeEquals(transactionAttributeSource, that.getTransactionAttributeSource())));
100+
}
101+
102+
@Override
103+
public int hashCode() {
104+
return TransactionAttributeSourceClassFilter.class.hashCode();
105+
}
106+
107+
@Override
108+
public String toString() {
109+
return TransactionAttributeSourceClassFilter.class.getName() + ": " + transactionAttributeSource;
110+
}
111+
90112
}
91113

92114
}

spring-tx/src/test/java/org/springframework/transaction/annotation/EnableTransactionManagementTests.java

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2022 the original author or authors.
2+
* Copyright 2002-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -69,6 +69,25 @@ public void transactionProxyIsCreated() {
6969
ctx.close();
7070
}
7171

72+
@Test // gh-31238
73+
public void cglibProxyClassIsCachedAcrossApplicationContexts() {
74+
ConfigurableApplicationContext ctx;
75+
76+
// Round #1
77+
ctx = new AnnotationConfigApplicationContext(EnableTxConfig.class, TxManagerConfig.class);
78+
TransactionalTestBean bean1 = ctx.getBean(TransactionalTestBean.class);
79+
assertThat(AopUtils.isCglibProxy(bean1)).as("testBean #1 is not a CGLIB proxy").isTrue();
80+
ctx.close();
81+
82+
// Round #2
83+
ctx = new AnnotationConfigApplicationContext(EnableTxConfig.class, TxManagerConfig.class);
84+
TransactionalTestBean bean2 = ctx.getBean(TransactionalTestBean.class);
85+
assertThat(AopUtils.isCglibProxy(bean2)).as("testBean #2 is not a CGLIB proxy").isTrue();
86+
ctx.close();
87+
88+
assertThat(bean1.getClass()).isSameAs(bean2.getClass());
89+
}
90+
7291
@Test
7392
public void transactionProxyIsCreatedWithEnableOnSuperclass() {
7493
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(

0 commit comments

Comments
 (0)