Reduce ProxyCallbackFilter to key-only role after class generation

Avoids memory leaks from ProxyCallbackFilter-contained Advisors.
Includes consistent ProxyCallbackFilter#equals/hashCode methods.

Closes gh-26266
Closes gh-30615
This commit is contained in:
Juergen Hoeller 2023-06-08 17:27:09 +02:00
parent e210f08dce
commit 045df81f14
3 changed files with 140 additions and 71 deletions

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2022 the original author or authors. * Copyright 2002-2023 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -22,6 +22,7 @@ import java.lang.reflect.Method;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
@ -32,6 +33,8 @@ import org.springframework.aop.Advisor;
import org.springframework.aop.DynamicIntroductionAdvice; import org.springframework.aop.DynamicIntroductionAdvice;
import org.springframework.aop.IntroductionAdvisor; import org.springframework.aop.IntroductionAdvisor;
import org.springframework.aop.IntroductionInfo; import org.springframework.aop.IntroductionInfo;
import org.springframework.aop.Pointcut;
import org.springframework.aop.PointcutAdvisor;
import org.springframework.aop.TargetSource; import org.springframework.aop.TargetSource;
import org.springframework.aop.support.DefaultIntroductionAdvisor; import org.springframework.aop.support.DefaultIntroductionAdvisor;
import org.springframework.aop.support.DefaultPointcutAdvisor; import org.springframework.aop.support.DefaultPointcutAdvisor;
@ -41,6 +44,7 @@ import org.springframework.lang.Nullable;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.util.ClassUtils; import org.springframework.util.ClassUtils;
import org.springframework.util.CollectionUtils; import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
/** /**
* Base class for AOP proxy configuration managers. * Base class for AOP proxy configuration managers.
@ -72,15 +76,13 @@ public class AdvisedSupport extends ProxyConfig implements Advised {
/** Package-protected to allow direct access for efficiency. */ /** Package-protected to allow direct access for efficiency. */
@SuppressWarnings("serial")
TargetSource targetSource = EMPTY_TARGET_SOURCE; TargetSource targetSource = EMPTY_TARGET_SOURCE;
/** Whether the Advisors are already filtered for the specific target class. */ /** Whether the Advisors are already filtered for the specific target class. */
private boolean preFiltered = false; private boolean preFiltered = false;
/** The AdvisorChainFactory to use. */ /** The AdvisorChainFactory to use. */
@SuppressWarnings("serial") private AdvisorChainFactory advisorChainFactory;
AdvisorChainFactory advisorChainFactory = new DefaultAdvisorChainFactory();
/** Cache with Method as key and advisor chain List as value. */ /** Cache with Method as key and advisor chain List as value. */
private transient Map<MethodCacheKey, List<Object>> methodCache; private transient Map<MethodCacheKey, List<Object>> methodCache;
@ -89,21 +91,22 @@ public class AdvisedSupport extends ProxyConfig implements Advised {
* Interfaces to be implemented by the proxy. Held in List to keep the order * Interfaces to be implemented by the proxy. Held in List to keep the order
* of registration, to create JDK proxy with specified order of interfaces. * of registration, to create JDK proxy with specified order of interfaces.
*/ */
@SuppressWarnings("serial")
private List<Class<?>> interfaces = new ArrayList<>(); private List<Class<?>> interfaces = new ArrayList<>();
/** /**
* List of Advisors. If an Advice is added, it will be wrapped * List of Advisors. If an Advice is added, it will be wrapped
* in an Advisor before being added to this List. * in an Advisor before being added to this List.
*/ */
@SuppressWarnings("serial")
private List<Advisor> advisors = new ArrayList<>(); private List<Advisor> advisors = new ArrayList<>();
private List<Advisor> advisorKey = this.advisors;
/** /**
* No-arg constructor for use as a JavaBean. * No-arg constructor for use as a JavaBean.
*/ */
public AdvisedSupport() { public AdvisedSupport() {
this.advisorChainFactory = DefaultAdvisorChainFactory.INSTANCE;
this.methodCache = new ConcurrentHashMap<>(32); this.methodCache = new ConcurrentHashMap<>(32);
} }
@ -116,6 +119,15 @@ public class AdvisedSupport extends ProxyConfig implements Advised {
setInterfaces(interfaces); setInterfaces(interfaces);
} }
/**
* Internal constructor for {@link #getConfigurationOnlyCopy()}.
* @since 6.0.10
*/
private AdvisedSupport(AdvisorChainFactory advisorChainFactory, Map<MethodCacheKey, List<Object>> methodCache) {
this.advisorChainFactory = advisorChainFactory;
this.methodCache = methodCache;
}
/** /**
* Set the given object as target. * Set the given object as target.
@ -520,15 +532,27 @@ public class AdvisedSupport extends ProxyConfig implements Advised {
* replacing the TargetSource. * replacing the TargetSource.
*/ */
AdvisedSupport getConfigurationOnlyCopy() { AdvisedSupport getConfigurationOnlyCopy() {
AdvisedSupport copy = new AdvisedSupport(); AdvisedSupport copy = new AdvisedSupport(this.advisorChainFactory, this.methodCache);
copy.copyFrom(this); copy.copyFrom(this);
copy.targetSource = EmptyTargetSource.forClass(getTargetClass(), getTargetSource().isStatic()); copy.targetSource = EmptyTargetSource.forClass(getTargetClass(), getTargetSource().isStatic());
copy.advisorChainFactory = this.advisorChainFactory;
copy.interfaces = new ArrayList<>(this.interfaces); copy.interfaces = new ArrayList<>(this.interfaces);
copy.advisors = new ArrayList<>(this.advisors); copy.advisors = new ArrayList<>(this.advisors);
copy.advisorKey = new ArrayList<>(this.advisors.size());
for (Advisor advisor : this.advisors) {
copy.advisorKey.add(new AdvisorKeyEntry(advisor));
}
return copy; return copy;
} }
void reduceToAdvisorKey() {
this.advisors = this.advisorKey;
this.methodCache = Collections.emptyMap();
}
Object getAdvisorKey() {
return this.advisorKey;
}
//--------------------------------------------------------------------- //---------------------------------------------------------------------
// Serialization support // Serialization support
@ -604,4 +628,51 @@ public class AdvisedSupport extends ProxyConfig implements Advised {
} }
} }
/**
* Stub for an Advisor instance that is just needed for key purposes,
* allowing for efficient equals and hashCode comparisons against the
* advice class and the pointcut.
* @since 6.0.10
* @see #getConfigurationOnlyCopy()
* @see #getAdvisorKey()
*/
private static class AdvisorKeyEntry implements Advisor {
private final Class<?> adviceType;
@Nullable
private String classFilterKey;
@Nullable
private String methodMatcherKey;
public AdvisorKeyEntry(Advisor advisor) {
this.adviceType = advisor.getAdvice().getClass();
if (advisor instanceof PointcutAdvisor pointcutAdvisor) {
Pointcut pointcut = pointcutAdvisor.getPointcut();
this.classFilterKey = ObjectUtils.identityToString(pointcut.getClassFilter());
this.methodMatcherKey = ObjectUtils.identityToString(pointcut.getMethodMatcher());
}
}
@Override
public Advice getAdvice() {
throw new UnsupportedOperationException();
}
@Override
public boolean equals(Object other) {
return (this == other || (other instanceof AdvisorKeyEntry otherEntry &&
this.adviceType == otherEntry.adviceType &&
ObjectUtils.nullSafeEquals(this.classFilterKey, otherEntry.classFilterKey) &&
ObjectUtils.nullSafeEquals(this.methodMatcherKey, otherEntry.methodMatcherKey)));
}
@Override
public int hashCode() {
return this.adviceType.hashCode();
}
}
} }

View File

@ -26,14 +26,11 @@ import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.WeakHashMap; import java.util.WeakHashMap;
import org.aopalliance.aop.Advice;
import org.aopalliance.intercept.MethodInvocation; import org.aopalliance.intercept.MethodInvocation;
import org.apache.commons.logging.Log; import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory; import org.apache.commons.logging.LogFactory;
import org.springframework.aop.Advisor;
import org.springframework.aop.AopInvocationException; import org.springframework.aop.AopInvocationException;
import org.springframework.aop.PointcutAdvisor;
import org.springframework.aop.RawTargetAccess; import org.springframework.aop.RawTargetAccess;
import org.springframework.aop.TargetSource; import org.springframework.aop.TargetSource;
import org.springframework.aop.support.AopUtils; import org.springframework.aop.support.AopUtils;
@ -205,12 +202,21 @@ class CglibAopProxy implements AopProxy, Serializable {
types[x] = callbacks[x].getClass(); types[x] = callbacks[x].getClass();
} }
// fixedInterceptorMap only populated at this point, after getCallbacks call above // fixedInterceptorMap only populated at this point, after getCallbacks call above
enhancer.setCallbackFilter(new ProxyCallbackFilter( ProxyCallbackFilter filter = new ProxyCallbackFilter(
this.advised.getConfigurationOnlyCopy(), this.fixedInterceptorMap, this.fixedInterceptorOffset)); this.advised.getConfigurationOnlyCopy(), this.fixedInterceptorMap, this.fixedInterceptorOffset);
enhancer.setCallbackFilter(filter);
enhancer.setCallbackTypes(types); enhancer.setCallbackTypes(types);
// Generate the proxy class and create a proxy instance. // Generate the proxy class and create a proxy instance.
return (classOnly ? createProxyClass(enhancer) : createProxyClassAndInstance(enhancer, callbacks)); // ProxyCallbackFilter has method introspection capability with Advisor access.
try {
return (classOnly ? createProxyClass(enhancer) : createProxyClassAndInstance(enhancer, callbacks));
}
finally {
// Reduce ProxyCallbackFilter to key-only state for its class cache role
// in the CGLIB$CALLBACK_FILTER field, not leaking any Advisor state...
filter.advised.reduceToAdvisorKey();
}
} }
catch (CodeGenerationException | IllegalArgumentException ex) { catch (CodeGenerationException | IllegalArgumentException ex) {
throw new AopConfigException("Could not generate CGLIB subclass of " + this.advised.getTargetClass() + throw new AopConfigException("Could not generate CGLIB subclass of " + this.advised.getTargetClass() +
@ -294,9 +300,9 @@ class CglibAopProxy implements AopProxy, Serializable {
private Callback[] getCallbacks(Class<?> rootClass) throws Exception { private Callback[] getCallbacks(Class<?> rootClass) throws Exception {
// Parameters used for optimization choices... // Parameters used for optimization choices...
boolean exposeProxy = this.advised.isExposeProxy();
boolean isFrozen = this.advised.isFrozen();
boolean isStatic = this.advised.getTargetSource().isStatic(); boolean isStatic = this.advised.getTargetSource().isStatic();
boolean isFrozen = this.advised.isFrozen();
boolean exposeProxy = this.advised.isExposeProxy();
// Choose an "aop" interceptor (used for AOP calls). // Choose an "aop" interceptor (used for AOP calls).
Callback aopInterceptor = new DynamicAdvisedInterceptor(this.advised); Callback aopInterceptor = new DynamicAdvisedInterceptor(this.advised);
@ -776,7 +782,7 @@ class CglibAopProxy implements AopProxy, Serializable {
*/ */
private static class ProxyCallbackFilter implements CallbackFilter { private static class ProxyCallbackFilter implements CallbackFilter {
private final AdvisedSupport advised; final AdvisedSupport advised;
private final Map<Method, Integer> fixedInterceptorMap; private final Map<Method, Integer> fixedInterceptorMap;
@ -857,9 +863,9 @@ class CglibAopProxy implements AopProxy, Serializable {
// Proxy is not yet available, but that shouldn't matter. // Proxy is not yet available, but that shouldn't matter.
List<?> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass); List<?> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);
boolean haveAdvice = !chain.isEmpty(); boolean haveAdvice = !chain.isEmpty();
boolean exposeProxy = this.advised.isExposeProxy();
boolean isStatic = this.advised.getTargetSource().isStatic(); boolean isStatic = this.advised.getTargetSource().isStatic();
boolean isFrozen = this.advised.isFrozen(); boolean isFrozen = this.advised.isFrozen();
boolean exposeProxy = this.advised.isExposeProxy();
if (haveAdvice || !isFrozen) { if (haveAdvice || !isFrozen) {
// If exposing the proxy, then AOP_PROXY must be used. // If exposing the proxy, then AOP_PROXY must be used.
if (exposeProxy) { if (exposeProxy) {
@ -921,63 +927,18 @@ class CglibAopProxy implements AopProxy, Serializable {
return false; return false;
} }
AdvisedSupport otherAdvised = otherCallbackFilter.advised; AdvisedSupport otherAdvised = otherCallbackFilter.advised;
if (this.advised.isFrozen() != otherAdvised.isFrozen()) { return (this.advised.getAdvisorKey().equals(otherAdvised.getAdvisorKey()) &&
return false; AopProxyUtils.equalsProxiedInterfaces(this.advised, otherAdvised) &&
} ObjectUtils.nullSafeEquals(this.advised.getTargetClass(), otherAdvised.getTargetClass()) &&
if (this.advised.isExposeProxy() != otherAdvised.isExposeProxy()) { this.advised.getTargetSource().isStatic() == otherAdvised.getTargetSource().isStatic() &&
return false; this.advised.isFrozen() == otherAdvised.isFrozen() &&
} this.advised.isExposeProxy() == otherAdvised.isExposeProxy() &&
if (this.advised.getTargetSource().isStatic() != otherAdvised.getTargetSource().isStatic()) { this.advised.isOpaque() == otherAdvised.isOpaque());
return false;
}
if (!AopProxyUtils.equalsProxiedInterfaces(this.advised, otherAdvised)) {
return false;
}
// Advice instance identity is unimportant to the proxy class:
// All that matters is type and ordering.
if (this.advised.getAdvisorCount() != otherAdvised.getAdvisorCount()) {
return false;
}
Advisor[] thisAdvisors = this.advised.getAdvisors();
Advisor[] thatAdvisors = otherAdvised.getAdvisors();
for (int i = 0; i < thisAdvisors.length; i++) {
Advisor thisAdvisor = thisAdvisors[i];
Advisor thatAdvisor = thatAdvisors[i];
if (!equalsAdviceClasses(thisAdvisor, thatAdvisor)) {
return false;
}
if (!equalsPointcuts(thisAdvisor, thatAdvisor)) {
return false;
}
}
return true;
}
private static boolean equalsAdviceClasses(Advisor a, Advisor b) {
return (a.getAdvice().getClass() == b.getAdvice().getClass());
}
private static boolean equalsPointcuts(Advisor a, Advisor b) {
// If only one of the advisor (but not both) is PointcutAdvisor, then it is a mismatch.
// Takes care of the situations where an IntroductionAdvisor is used (see SPR-3959).
return (!(a instanceof PointcutAdvisor pointcutAdvisor1) ||
(b instanceof PointcutAdvisor pointcutAdvisor2 &&
ObjectUtils.nullSafeEquals(pointcutAdvisor1.getPointcut(), pointcutAdvisor2.getPointcut())));
} }
@Override @Override
public int hashCode() { public int hashCode() {
int hashCode = 0; return this.advised.getAdvisorKey().hashCode();
Advisor[] advisors = this.advised.getAdvisors();
for (Advisor advisor : advisors) {
Advice advice = advisor.getAdvice();
hashCode = 13 * hashCode + advice.getClass().hashCode();
}
hashCode = 13 * hashCode + (this.advised.isFrozen() ? 1 : 0);
hashCode = 13 * hashCode + (this.advised.isExposeProxy() ? 1 : 0);
hashCode = 13 * hashCode + (this.advised.isOptimize() ? 1 : 0);
hashCode = 13 * hashCode + (this.advised.isOpaque() ? 1 : 0);
return hashCode;
} }
} }

View File

@ -28,6 +28,7 @@ import org.springframework.aop.MethodMatcher;
import org.springframework.aop.Pointcut; import org.springframework.aop.Pointcut;
import org.springframework.aop.support.AopUtils; import org.springframework.aop.support.AopUtils;
import org.springframework.aop.support.DefaultPointcutAdvisor; import org.springframework.aop.support.DefaultPointcutAdvisor;
import org.springframework.aop.support.annotation.AnnotationMatchingPointcut;
import org.springframework.aop.testfixture.advice.CountingBeforeAdvice; import org.springframework.aop.testfixture.advice.CountingBeforeAdvice;
import org.springframework.aop.testfixture.interceptor.NopInterceptor; import org.springframework.aop.testfixture.interceptor.NopInterceptor;
import org.springframework.beans.testfixture.beans.ITestBean; import org.springframework.beans.testfixture.beans.ITestBean;
@ -35,6 +36,7 @@ import org.springframework.beans.testfixture.beans.TestBean;
import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextException; import org.springframework.context.ApplicationContextException;
import org.springframework.context.support.ClassPathXmlApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable; import org.springframework.lang.Nullable;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
@ -304,6 +306,8 @@ public class CglibProxyTests extends AbstractAopProxyTests implements Serializab
CglibAopProxy cglib = new CglibAopProxy(as); CglibAopProxy cglib = new CglibAopProxy(as);
ITestBean proxy1 = (ITestBean) cglib.getProxy(); ITestBean proxy1 = (ITestBean) cglib.getProxy();
ITestBean proxy1a = (ITestBean) cglib.getProxy();
assertThat(proxy1a.getClass()).isSameAs(proxy1.getClass());
mockTargetSource.setTarget(proxy1); mockTargetSource.setTarget(proxy1);
as = new AdvisedSupport(new Class<?>[]{}); as = new AdvisedSupport(new Class<?>[]{});
@ -313,6 +317,39 @@ public class CglibProxyTests extends AbstractAopProxyTests implements Serializab
ITestBean proxy2 = (ITestBean) cglib.getProxy(); ITestBean proxy2 = (ITestBean) cglib.getProxy();
assertThat(proxy2).isInstanceOf(Serializable.class); assertThat(proxy2).isInstanceOf(Serializable.class);
assertThat(proxy2.getClass()).isNotSameAs(proxy1.getClass());
ITestBean proxy2a = (ITestBean) cglib.getProxy();
assertThat(proxy2a).isInstanceOf(Serializable.class);
assertThat(proxy2a.getClass()).isSameAs(proxy2.getClass());
mockTargetSource.setTarget(proxy1);
as = new AdvisedSupport(new Class<?>[]{});
as.setTargetSource(mockTargetSource);
as.addAdvisor(new DefaultPointcutAdvisor(new AnnotationMatchingPointcut(Nullable.class), new NopInterceptor()));
cglib = new CglibAopProxy(as);
ITestBean proxy3 = (ITestBean) cglib.getProxy();
assertThat(proxy3).isInstanceOf(Serializable.class);
assertThat(proxy3.getClass()).isNotSameAs(proxy2.getClass());
ITestBean proxy3a = (ITestBean) cglib.getProxy();
assertThat(proxy3a).isInstanceOf(Serializable.class);
assertThat(proxy3a.getClass()).isSameAs(proxy3.getClass());
mockTargetSource.setTarget(proxy1);
as = new AdvisedSupport(new Class<?>[]{});
as.setTargetSource(mockTargetSource);
as.addAdvisor(new DefaultPointcutAdvisor(new AnnotationMatchingPointcut(NonNull.class), new NopInterceptor()));
cglib = new CglibAopProxy(as);
ITestBean proxy4 = (ITestBean) cglib.getProxy();
assertThat(proxy4).isInstanceOf(Serializable.class);
assertThat(proxy4.getClass()).isNotSameAs(proxy3.getClass());
ITestBean proxy4a = (ITestBean) cglib.getProxy();
assertThat(proxy4a).isInstanceOf(Serializable.class);
assertThat(proxy4a.getClass()).isSameAs(proxy4.getClass());
} }
@Test @Test