Skip to content

Commit 44c652e

Browse files
committed
Introduce ProxiedInterfacesCache for JdkDynamicAopProxy
Closes gh-30499
1 parent cd8bc2f commit 44c652e

File tree

3 files changed

+110
-73
lines changed

3 files changed

+110
-73
lines changed

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

+30-14
Original file line numberDiff line numberDiff line change
@@ -102,8 +102,23 @@ public class AdvisedSupport extends ProxyConfig implements Advised {
102102
*/
103103
private List<Advisor> advisors = new ArrayList<>();
104104

105+
/**
106+
* List of minimal {@link AdvisorKeyEntry} instances,
107+
* to be assigned to the {@link #advisors} field on reduction.
108+
* @since 6.0.10
109+
* @see #reduceToAdvisorKey
110+
*/
105111
private List<Advisor> advisorKey = this.advisors;
106112

113+
/**
114+
* Optional field for {@link AopProxy} implementations to store metadata in.
115+
* Used for {@link JdkDynamicAopProxy.ProxiedInterfacesCache}.
116+
* @since 6.1.3
117+
* @see JdkDynamicAopProxy#JdkDynamicAopProxy(AdvisedSupport)
118+
*/
119+
@Nullable
120+
transient Object proxyMetadataCache;
121+
107122

108123
/**
109124
* No-arg constructor for use as a JavaBean.
@@ -491,6 +506,7 @@ public List<Object> getInterceptorsAndDynamicInterceptionAdvice(Method method, @
491506
*/
492507
protected void adviceChanged() {
493508
this.methodCache.clear();
509+
this.proxyMetadataCache = null;
494510
}
495511

496512
/**
@@ -551,18 +567,6 @@ Object getAdvisorKey() {
551567
}
552568

553569

554-
//---------------------------------------------------------------------
555-
// Serialization support
556-
//---------------------------------------------------------------------
557-
558-
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
559-
// Rely on default serialization; just initialize state after deserialization.
560-
ois.defaultReadObject();
561-
562-
// Initialize transient fields.
563-
this.methodCache = new ConcurrentHashMap<>(32);
564-
}
565-
566570
@Override
567571
public String toProxyConfigString() {
568572
return toString();
@@ -584,6 +588,19 @@ public String toString() {
584588
}
585589

586590

591+
//---------------------------------------------------------------------
592+
// Serialization support
593+
//---------------------------------------------------------------------
594+
595+
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
596+
// Rely on default serialization; just initialize state after deserialization.
597+
ois.defaultReadObject();
598+
599+
// Initialize transient fields.
600+
this.methodCache = new ConcurrentHashMap<>(32);
601+
}
602+
603+
587604
/**
588605
* Simple wrapper class around a Method. Used as the key when
589606
* caching methods, for efficient equals and hashCode comparisons.
@@ -633,7 +650,7 @@ public int compareTo(MethodCacheKey other) {
633650
* @see #getConfigurationOnlyCopy()
634651
* @see #getAdvisorKey()
635652
*/
636-
private static class AdvisorKeyEntry implements Advisor {
653+
private static final class AdvisorKeyEntry implements Advisor {
637654

638655
private final Class<?> adviceType;
639656

@@ -643,7 +660,6 @@ private static class AdvisorKeyEntry implements Advisor {
643660
@Nullable
644661
private final String methodMatcherKey;
645662

646-
647663
public AdvisorKeyEntry(Advisor advisor) {
648664
this.adviceType = advisor.getAdvice().getClass();
649665
if (advisor instanceof PointcutAdvisor pointcutAdvisor) {

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

+69-49
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616

1717
package org.springframework.aop.framework;
1818

19+
import java.io.IOException;
20+
import java.io.ObjectInputStream;
1921
import java.io.Serializable;
2022
import java.lang.reflect.InvocationHandler;
2123
import java.lang.reflect.Method;
@@ -71,34 +73,16 @@ final class JdkDynamicAopProxy implements AopProxy, InvocationHandler, Serializa
7173
private static final long serialVersionUID = 5531744639992436476L;
7274

7375

74-
/*
75-
* NOTE: We could avoid the code duplication between this class and the CGLIB
76-
* proxies by refactoring "invoke" into a template method. However, this approach
77-
* adds at least 10% performance overhead versus a copy-paste solution, so we sacrifice
78-
* elegance for performance (we have a good test suite to ensure that the different
79-
* proxies behave the same :-)).
80-
* This way, we can also more easily take advantage of minor optimizations in each class.
81-
*/
76+
private static final String COROUTINES_FLOW_CLASS_NAME = "kotlinx.coroutines.flow.Flow";
8277

8378
/** We use a static Log to avoid serialization issues. */
8479
private static final Log logger = LogFactory.getLog(JdkDynamicAopProxy.class);
8580

86-
private static final String COROUTINES_FLOW_CLASS_NAME = "kotlinx.coroutines.flow.Flow";
87-
8881
/** Config used to configure this proxy. */
8982
private final AdvisedSupport advised;
9083

91-
private final Class<?>[] proxiedInterfaces;
92-
93-
/**
94-
* Is the {@link #equals} method defined on the proxied interfaces?
95-
*/
96-
private boolean equalsDefined;
97-
98-
/**
99-
* Is the {@link #hashCode} method defined on the proxied interfaces?
100-
*/
101-
private boolean hashCodeDefined;
84+
/** Cached in {@link AdvisedSupport#proxyMetadataCache}. */
85+
private transient ProxiedInterfacesCache cache;
10286

10387

10488
/**
@@ -110,8 +94,17 @@ final class JdkDynamicAopProxy implements AopProxy, InvocationHandler, Serializa
11094
public JdkDynamicAopProxy(AdvisedSupport config) throws AopConfigException {
11195
Assert.notNull(config, "AdvisedSupport must not be null");
11296
this.advised = config;
113-
this.proxiedInterfaces = AopProxyUtils.completeProxiedInterfaces(this.advised, true);
114-
findDefinedEqualsAndHashCodeMethods(this.proxiedInterfaces);
97+
98+
// Initialize ProxiedInterfacesCache if not cached already
99+
ProxiedInterfacesCache cache;
100+
if (config.proxyMetadataCache instanceof ProxiedInterfacesCache proxiedInterfacesCache) {
101+
cache = proxiedInterfacesCache;
102+
}
103+
else {
104+
cache = new ProxiedInterfacesCache(config);
105+
config.proxyMetadataCache = cache;
106+
}
107+
this.cache = cache;
115108
}
116109

117110

@@ -125,13 +118,13 @@ public Object getProxy(@Nullable ClassLoader classLoader) {
125118
if (logger.isTraceEnabled()) {
126119
logger.trace("Creating JDK dynamic proxy: " + this.advised.getTargetSource());
127120
}
128-
return Proxy.newProxyInstance(determineClassLoader(classLoader), this.proxiedInterfaces, this);
121+
return Proxy.newProxyInstance(determineClassLoader(classLoader), this.cache.proxiedInterfaces, this);
129122
}
130123

131124
@SuppressWarnings("deprecation")
132125
@Override
133126
public Class<?> getProxyClass(@Nullable ClassLoader classLoader) {
134-
return Proxy.getProxyClass(determineClassLoader(classLoader), this.proxiedInterfaces);
127+
return Proxy.getProxyClass(determineClassLoader(classLoader), this.cache.proxiedInterfaces);
135128
}
136129

137130
/**
@@ -160,28 +153,6 @@ private ClassLoader determineClassLoader(@Nullable ClassLoader classLoader) {
160153
return classLoader;
161154
}
162155

163-
/**
164-
* Finds any {@link #equals} or {@link #hashCode} method that may be defined
165-
* on the supplied set of interfaces.
166-
* @param proxiedInterfaces the interfaces to introspect
167-
*/
168-
private void findDefinedEqualsAndHashCodeMethods(Class<?>[] proxiedInterfaces) {
169-
for (Class<?> proxiedInterface : proxiedInterfaces) {
170-
Method[] methods = proxiedInterface.getDeclaredMethods();
171-
for (Method method : methods) {
172-
if (AopUtils.isEqualsMethod(method)) {
173-
this.equalsDefined = true;
174-
}
175-
if (AopUtils.isHashCodeMethod(method)) {
176-
this.hashCodeDefined = true;
177-
}
178-
if (this.equalsDefined && this.hashCodeDefined) {
179-
return;
180-
}
181-
}
182-
}
183-
}
184-
185156

186157
/**
187158
* Implementation of {@code InvocationHandler.invoke}.
@@ -198,11 +169,11 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Throwabl
198169
Object target = null;
199170

200171
try {
201-
if (!this.equalsDefined && AopUtils.isEqualsMethod(method)) {
172+
if (!this.cache.equalsDefined && AopUtils.isEqualsMethod(method)) {
202173
// The target does not implement the equals(Object) method itself.
203174
return equals(args[0]);
204175
}
205-
else if (!this.hashCodeDefined && AopUtils.isHashCodeMethod(method)) {
176+
else if (!this.cache.hashCodeDefined && AopUtils.isHashCodeMethod(method)) {
206177
// The target does not implement the hashCode() method itself.
207178
return hashCode();
208179
}
@@ -324,4 +295,53 @@ public int hashCode() {
324295
return JdkDynamicAopProxy.class.hashCode() * 13 + this.advised.getTargetSource().hashCode();
325296
}
326297

298+
299+
//---------------------------------------------------------------------
300+
// Serialization support
301+
//---------------------------------------------------------------------
302+
303+
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
304+
// Rely on default serialization; just initialize state after deserialization.
305+
ois.defaultReadObject();
306+
307+
// Initialize transient fields.
308+
this.cache = new ProxiedInterfacesCache(this.advised);
309+
}
310+
311+
312+
/**
313+
* Holder for the complete proxied interfaces and derived metadata,
314+
* to be cached in {@link AdvisedSupport#proxyMetadataCache}.
315+
* @since 6.1.3
316+
*/
317+
static final class ProxiedInterfacesCache {
318+
319+
Class<?>[] proxiedInterfaces;
320+
321+
boolean equalsDefined;
322+
323+
boolean hashCodeDefined;
324+
325+
ProxiedInterfacesCache(AdvisedSupport config) {
326+
this.proxiedInterfaces = AopProxyUtils.completeProxiedInterfaces(config, true);
327+
328+
// Find any {@link #equals} or {@link #hashCode} method that may be defined
329+
//on the supplied set of interfaces.
330+
for (Class<?> proxiedInterface : this.proxiedInterfaces) {
331+
Method[] methods = proxiedInterface.getDeclaredMethods();
332+
for (Method method : methods) {
333+
if (AopUtils.isEqualsMethod(method)) {
334+
this.equalsDefined = true;
335+
}
336+
if (AopUtils.isHashCodeMethod(method)) {
337+
this.hashCodeDefined = true;
338+
}
339+
if (this.equalsDefined && this.hashCodeDefined) {
340+
return;
341+
}
342+
}
343+
}
344+
}
345+
}
346+
327347
}

spring-context/src/test/java/org/springframework/aop/framework/JdkDynamicProxyTests.java

+11-10
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,7 @@ protected AopProxy createAopProxy(AdvisedSupport as) {
5656

5757
@Test
5858
void testNullConfig() {
59-
assertThatIllegalArgumentException().isThrownBy(() ->
60-
new JdkDynamicAopProxy(null));
59+
assertThatIllegalArgumentException().isThrownBy(() -> new JdkDynamicAopProxy(null));
6160
}
6261

6362
@Test
@@ -69,10 +68,8 @@ void testProxyIsJustInterface() {
6968
JdkDynamicAopProxy aop = new JdkDynamicAopProxy(pc);
7069

7170
Object proxy = aop.getProxy();
72-
boolean condition = proxy instanceof ITestBean;
73-
assertThat(condition).isTrue();
74-
boolean condition1 = proxy instanceof TestBean;
75-
assertThat(condition1).isFalse();
71+
assertThat(proxy instanceof ITestBean).isTrue();
72+
assertThat(proxy instanceof TestBean).isFalse();
7673
}
7774

7875
@Test
@@ -131,11 +128,15 @@ void testProxyNotWrappedIfIncompatible() {
131128

132129
@Test
133130
void testEqualsAndHashCodeDefined() {
134-
AdvisedSupport as = new AdvisedSupport(Named.class);
135-
as.setTarget(new Person());
136-
JdkDynamicAopProxy aopProxy = new JdkDynamicAopProxy(as);
137-
Named proxy = (Named) aopProxy.getProxy();
138131
Named named = new Person();
132+
AdvisedSupport as = new AdvisedSupport(Named.class);
133+
as.setTarget(named);
134+
135+
Named proxy = (Named) new JdkDynamicAopProxy(as).getProxy();
136+
assertThat(proxy).isEqualTo(named);
137+
assertThat(named.hashCode()).isEqualTo(proxy.hashCode());
138+
139+
proxy = (Named) new JdkDynamicAopProxy(as).getProxy();
139140
assertThat(proxy).isEqualTo(named);
140141
assertThat(named.hashCode()).isEqualTo(proxy.hashCode());
141142
}

0 commit comments

Comments
 (0)