Introduce ProxiedInterfacesCache for JdkDynamicAopProxy

Closes gh-30499
This commit is contained in:
Juergen Hoeller 2023-12-22 11:55:59 +01:00
parent cd8bc2f82a
commit 44c652ec98
3 changed files with 110 additions and 73 deletions

View File

@ -102,8 +102,23 @@ public class AdvisedSupport extends ProxyConfig implements Advised {
*/
private List<Advisor> advisors = new ArrayList<>();
/**
* List of minimal {@link AdvisorKeyEntry} instances,
* to be assigned to the {@link #advisors} field on reduction.
* @since 6.0.10
* @see #reduceToAdvisorKey
*/
private List<Advisor> advisorKey = this.advisors;
/**
* Optional field for {@link AopProxy} implementations to store metadata in.
* Used for {@link JdkDynamicAopProxy.ProxiedInterfacesCache}.
* @since 6.1.3
* @see JdkDynamicAopProxy#JdkDynamicAopProxy(AdvisedSupport)
*/
@Nullable
transient Object proxyMetadataCache;
/**
* No-arg constructor for use as a JavaBean.
@ -491,6 +506,7 @@ public class AdvisedSupport extends ProxyConfig implements Advised {
*/
protected void adviceChanged() {
this.methodCache.clear();
this.proxyMetadataCache = null;
}
/**
@ -551,18 +567,6 @@ public class AdvisedSupport extends ProxyConfig implements Advised {
}
//---------------------------------------------------------------------
// Serialization support
//---------------------------------------------------------------------
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
// Rely on default serialization; just initialize state after deserialization.
ois.defaultReadObject();
// Initialize transient fields.
this.methodCache = new ConcurrentHashMap<>(32);
}
@Override
public String toProxyConfigString() {
return toString();
@ -584,6 +588,19 @@ public class AdvisedSupport extends ProxyConfig implements Advised {
}
//---------------------------------------------------------------------
// Serialization support
//---------------------------------------------------------------------
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
// Rely on default serialization; just initialize state after deserialization.
ois.defaultReadObject();
// Initialize transient fields.
this.methodCache = new ConcurrentHashMap<>(32);
}
/**
* Simple wrapper class around a Method. Used as the key when
* caching methods, for efficient equals and hashCode comparisons.
@ -633,7 +650,7 @@ public class AdvisedSupport extends ProxyConfig implements Advised {
* @see #getConfigurationOnlyCopy()
* @see #getAdvisorKey()
*/
private static class AdvisorKeyEntry implements Advisor {
private static final class AdvisorKeyEntry implements Advisor {
private final Class<?> adviceType;
@ -643,7 +660,6 @@ public class AdvisedSupport extends ProxyConfig implements Advised {
@Nullable
private final String methodMatcherKey;
public AdvisorKeyEntry(Advisor advisor) {
this.adviceType = advisor.getAdvice().getClass();
if (advisor instanceof PointcutAdvisor pointcutAdvisor) {

View File

@ -16,6 +16,8 @@
package org.springframework.aop.framework;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
@ -71,34 +73,16 @@ final class JdkDynamicAopProxy implements AopProxy, InvocationHandler, Serializa
private static final long serialVersionUID = 5531744639992436476L;
/*
* NOTE: We could avoid the code duplication between this class and the CGLIB
* proxies by refactoring "invoke" into a template method. However, this approach
* adds at least 10% performance overhead versus a copy-paste solution, so we sacrifice
* elegance for performance (we have a good test suite to ensure that the different
* proxies behave the same :-)).
* This way, we can also more easily take advantage of minor optimizations in each class.
*/
private static final String COROUTINES_FLOW_CLASS_NAME = "kotlinx.coroutines.flow.Flow";
/** We use a static Log to avoid serialization issues. */
private static final Log logger = LogFactory.getLog(JdkDynamicAopProxy.class);
private static final String COROUTINES_FLOW_CLASS_NAME = "kotlinx.coroutines.flow.Flow";
/** Config used to configure this proxy. */
private final AdvisedSupport advised;
private final Class<?>[] proxiedInterfaces;
/**
* Is the {@link #equals} method defined on the proxied interfaces?
*/
private boolean equalsDefined;
/**
* Is the {@link #hashCode} method defined on the proxied interfaces?
*/
private boolean hashCodeDefined;
/** Cached in {@link AdvisedSupport#proxyMetadataCache}. */
private transient ProxiedInterfacesCache cache;
/**
@ -110,8 +94,17 @@ final class JdkDynamicAopProxy implements AopProxy, InvocationHandler, Serializa
public JdkDynamicAopProxy(AdvisedSupport config) throws AopConfigException {
Assert.notNull(config, "AdvisedSupport must not be null");
this.advised = config;
this.proxiedInterfaces = AopProxyUtils.completeProxiedInterfaces(this.advised, true);
findDefinedEqualsAndHashCodeMethods(this.proxiedInterfaces);
// Initialize ProxiedInterfacesCache if not cached already
ProxiedInterfacesCache cache;
if (config.proxyMetadataCache instanceof ProxiedInterfacesCache proxiedInterfacesCache) {
cache = proxiedInterfacesCache;
}
else {
cache = new ProxiedInterfacesCache(config);
config.proxyMetadataCache = cache;
}
this.cache = cache;
}
@ -125,13 +118,13 @@ final class JdkDynamicAopProxy implements AopProxy, InvocationHandler, Serializa
if (logger.isTraceEnabled()) {
logger.trace("Creating JDK dynamic proxy: " + this.advised.getTargetSource());
}
return Proxy.newProxyInstance(determineClassLoader(classLoader), this.proxiedInterfaces, this);
return Proxy.newProxyInstance(determineClassLoader(classLoader), this.cache.proxiedInterfaces, this);
}
@SuppressWarnings("deprecation")
@Override
public Class<?> getProxyClass(@Nullable ClassLoader classLoader) {
return Proxy.getProxyClass(determineClassLoader(classLoader), this.proxiedInterfaces);
return Proxy.getProxyClass(determineClassLoader(classLoader), this.cache.proxiedInterfaces);
}
/**
@ -160,28 +153,6 @@ final class JdkDynamicAopProxy implements AopProxy, InvocationHandler, Serializa
return classLoader;
}
/**
* Finds any {@link #equals} or {@link #hashCode} method that may be defined
* on the supplied set of interfaces.
* @param proxiedInterfaces the interfaces to introspect
*/
private void findDefinedEqualsAndHashCodeMethods(Class<?>[] proxiedInterfaces) {
for (Class<?> proxiedInterface : proxiedInterfaces) {
Method[] methods = proxiedInterface.getDeclaredMethods();
for (Method method : methods) {
if (AopUtils.isEqualsMethod(method)) {
this.equalsDefined = true;
}
if (AopUtils.isHashCodeMethod(method)) {
this.hashCodeDefined = true;
}
if (this.equalsDefined && this.hashCodeDefined) {
return;
}
}
}
}
/**
* Implementation of {@code InvocationHandler.invoke}.
@ -198,11 +169,11 @@ final class JdkDynamicAopProxy implements AopProxy, InvocationHandler, Serializa
Object target = null;
try {
if (!this.equalsDefined && AopUtils.isEqualsMethod(method)) {
if (!this.cache.equalsDefined && AopUtils.isEqualsMethod(method)) {
// The target does not implement the equals(Object) method itself.
return equals(args[0]);
}
else if (!this.hashCodeDefined && AopUtils.isHashCodeMethod(method)) {
else if (!this.cache.hashCodeDefined && AopUtils.isHashCodeMethod(method)) {
// The target does not implement the hashCode() method itself.
return hashCode();
}
@ -324,4 +295,53 @@ final class JdkDynamicAopProxy implements AopProxy, InvocationHandler, Serializa
return JdkDynamicAopProxy.class.hashCode() * 13 + this.advised.getTargetSource().hashCode();
}
//---------------------------------------------------------------------
// Serialization support
//---------------------------------------------------------------------
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
// Rely on default serialization; just initialize state after deserialization.
ois.defaultReadObject();
// Initialize transient fields.
this.cache = new ProxiedInterfacesCache(this.advised);
}
/**
* Holder for the complete proxied interfaces and derived metadata,
* to be cached in {@link AdvisedSupport#proxyMetadataCache}.
* @since 6.1.3
*/
static final class ProxiedInterfacesCache {
Class<?>[] proxiedInterfaces;
boolean equalsDefined;
boolean hashCodeDefined;
ProxiedInterfacesCache(AdvisedSupport config) {
this.proxiedInterfaces = AopProxyUtils.completeProxiedInterfaces(config, true);
// Find any {@link #equals} or {@link #hashCode} method that may be defined
//on the supplied set of interfaces.
for (Class<?> proxiedInterface : this.proxiedInterfaces) {
Method[] methods = proxiedInterface.getDeclaredMethods();
for (Method method : methods) {
if (AopUtils.isEqualsMethod(method)) {
this.equalsDefined = true;
}
if (AopUtils.isHashCodeMethod(method)) {
this.hashCodeDefined = true;
}
if (this.equalsDefined && this.hashCodeDefined) {
return;
}
}
}
}
}
}

View File

@ -56,8 +56,7 @@ class JdkDynamicProxyTests extends AbstractAopProxyTests implements Serializable
@Test
void testNullConfig() {
assertThatIllegalArgumentException().isThrownBy(() ->
new JdkDynamicAopProxy(null));
assertThatIllegalArgumentException().isThrownBy(() -> new JdkDynamicAopProxy(null));
}
@Test
@ -69,10 +68,8 @@ class JdkDynamicProxyTests extends AbstractAopProxyTests implements Serializable
JdkDynamicAopProxy aop = new JdkDynamicAopProxy(pc);
Object proxy = aop.getProxy();
boolean condition = proxy instanceof ITestBean;
assertThat(condition).isTrue();
boolean condition1 = proxy instanceof TestBean;
assertThat(condition1).isFalse();
assertThat(proxy instanceof ITestBean).isTrue();
assertThat(proxy instanceof TestBean).isFalse();
}
@Test
@ -131,11 +128,15 @@ class JdkDynamicProxyTests extends AbstractAopProxyTests implements Serializable
@Test
void testEqualsAndHashCodeDefined() {
AdvisedSupport as = new AdvisedSupport(Named.class);
as.setTarget(new Person());
JdkDynamicAopProxy aopProxy = new JdkDynamicAopProxy(as);
Named proxy = (Named) aopProxy.getProxy();
Named named = new Person();
AdvisedSupport as = new AdvisedSupport(Named.class);
as.setTarget(named);
Named proxy = (Named) new JdkDynamicAopProxy(as).getProxy();
assertThat(proxy).isEqualTo(named);
assertThat(named.hashCode()).isEqualTo(proxy.hashCode());
proxy = (Named) new JdkDynamicAopProxy(as).getProxy();
assertThat(proxy).isEqualTo(named);
assertThat(named.hashCode()).isEqualTo(proxy.hashCode());
}