diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/AdvisedSupport.java b/spring-aop/src/main/java/org/springframework/aop/framework/AdvisedSupport.java index a93781f2655c..37d530989f36 100644 --- a/spring-aop/src/main/java/org/springframework/aop/framework/AdvisedSupport.java +++ b/spring-aop/src/main/java/org/springframework/aop/framework/AdvisedSupport.java @@ -102,8 +102,23 @@ public class AdvisedSupport extends ProxyConfig implements Advised { */ private List 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 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 List getInterceptorsAndDynamicInterceptionAdvice(Method method, @ */ protected void adviceChanged() { this.methodCache.clear(); + this.proxyMetadataCache = null; } /** @@ -551,18 +567,6 @@ Object getAdvisorKey() { } - //--------------------------------------------------------------------- - // 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 String toString() { } + //--------------------------------------------------------------------- + // 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 int compareTo(MethodCacheKey other) { * @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 @@ private static class AdvisorKeyEntry implements Advisor { @Nullable private final String methodMatcherKey; - public AdvisorKeyEntry(Advisor advisor) { this.adviceType = advisor.getAdvice().getClass(); if (advisor instanceof PointcutAdvisor pointcutAdvisor) { diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/JdkDynamicAopProxy.java b/spring-aop/src/main/java/org/springframework/aop/framework/JdkDynamicAopProxy.java index 4ab6a89b231a..3a1435a09c8a 100644 --- a/spring-aop/src/main/java/org/springframework/aop/framework/JdkDynamicAopProxy.java +++ b/spring-aop/src/main/java/org/springframework/aop/framework/JdkDynamicAopProxy.java @@ -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 @@ public Object getProxy(@Nullable ClassLoader classLoader) { 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 @@ private ClassLoader determineClassLoader(@Nullable ClassLoader classLoader) { 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 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Throwabl 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 @@ public int hashCode() { 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; + } + } + } + } + } + } diff --git a/spring-context/src/test/java/org/springframework/aop/framework/JdkDynamicProxyTests.java b/spring-context/src/test/java/org/springframework/aop/framework/JdkDynamicProxyTests.java index 30f81abeaf0b..253c46b227ec 100644 --- a/spring-context/src/test/java/org/springframework/aop/framework/JdkDynamicProxyTests.java +++ b/spring-context/src/test/java/org/springframework/aop/framework/JdkDynamicProxyTests.java @@ -56,8 +56,7 @@ protected AopProxy createAopProxy(AdvisedSupport as) { @Test void testNullConfig() { - assertThatIllegalArgumentException().isThrownBy(() -> - new JdkDynamicAopProxy(null)); + assertThatIllegalArgumentException().isThrownBy(() -> new JdkDynamicAopProxy(null)); } @Test @@ -69,10 +68,8 @@ void testProxyIsJustInterface() { 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 @@ void testProxyNotWrappedIfIncompatible() { @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()); }