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
This commit is contained in:
Sam Brannen
2023-09-18 18:22:08 +02:00
parent 9120f87897
commit 865fa33927
9 changed files with 172 additions and 14 deletions

View File

@@ -53,12 +53,14 @@ import org.springframework.beans.factory.config.MethodInvokingFactoryBean;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.beans.testfixture.beans.ITestBean;
import org.springframework.beans.testfixture.beans.TestBean;
import org.springframework.cglib.proxy.Factory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.context.annotation.Scope;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.context.support.GenericApplicationContext;
import org.springframework.core.DecoratingProxy;
@@ -208,6 +210,31 @@ class AspectJAutoProxyCreatorTests {
assertThat(adrian1.getAge()).isEqualTo(3);
}
@Test // gh-31238
void cglibProxyClassIsCachedAcrossApplicationContextsForPerTargetAspect() {
Class<?> configClass = PerTargetProxyTargetClassTrueConfig.class;
TestBean testBean1;
TestBean testBean2;
// Round #1
try (ConfigurableApplicationContext context = new AnnotationConfigApplicationContext(configClass)) {
testBean1 = context.getBean(TestBean.class);
assertThat(AopUtils.isCglibProxy(testBean1)).as("CGLIB proxy").isTrue();
assertThat(testBean1.getClass().getInterfaces())
.containsExactlyInAnyOrder(Factory.class, SpringProxy.class, Advised.class);
}
// Round #2
try (ConfigurableApplicationContext context = new AnnotationConfigApplicationContext(configClass)) {
testBean2 = context.getBean(TestBean.class);
assertThat(AopUtils.isCglibProxy(testBean2)).as("CGLIB proxy").isTrue();
assertThat(testBean2.getClass().getInterfaces())
.containsExactlyInAnyOrder(Factory.class, SpringProxy.class, Advised.class);
}
assertThat(testBean1.getClass()).isSameAs(testBean2.getClass());
}
@Test
void twoAdviceAspect() {
ClassPathXmlApplicationContext bf = newContext("twoAdviceAspect.xml");
@@ -615,6 +642,23 @@ class ProxyTargetClassFalseConfig extends AbstractProxyTargetClassConfig {
class ProxyTargetClassTrueConfig extends AbstractProxyTargetClassConfig {
}
@Configuration
@EnableAspectJAutoProxy(proxyTargetClass = true)
class PerTargetProxyTargetClassTrueConfig {
@Bean
@Scope("prototype")
TestBean testBean() {
return new TestBean("Jane", 34);
}
@Bean
@Scope("prototype")
PerTargetAspect perTargetAspect() {
return new PerTargetAspect();
}
}
@FunctionalInterface
interface MessageGenerator {
String generateMessage();

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2021 the original author or authors.
* Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -21,6 +21,7 @@ import java.util.concurrent.atomic.AtomicLong;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.springframework.aop.support.AopUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
@@ -45,6 +46,7 @@ import static org.springframework.context.testfixture.cache.CacheTestUtils.asser
* Tests that represent real use cases with advanced configuration.
*
* @author Stephane Nicoll
* @author Sam Brannen
*/
class EnableCachingIntegrationTests {
@@ -83,6 +85,25 @@ class EnableCachingIntegrationTests {
assertCacheHit(key, value, cache);
}
@Test // gh-31238
public void cglibProxyClassIsCachedAcrossApplicationContexts() {
ConfigurableApplicationContext ctx;
// Round #1
ctx = new AnnotationConfigApplicationContext(FooConfigCglib.class);
FooService service1 = ctx.getBean(FooService.class);
assertThat(AopUtils.isCglibProxy(service1)).as("FooService #1 is not a CGLIB proxy").isTrue();
ctx.close();
// Round #2
ctx = new AnnotationConfigApplicationContext(FooConfigCglib.class);
FooService service2 = ctx.getBean(FooService.class);
assertThat(AopUtils.isCglibProxy(service2)).as("FooService #2 is not a CGLIB proxy").isTrue();
ctx.close();
assertThat(service1.getClass()).isSameAs(service2.getClass());
}
@Test
void barServiceWithCacheableInterfaceCglib() {
this.context = new AnnotationConfigApplicationContext(BarConfigCglib.class);