-
Notifications
You must be signed in to change notification settings - Fork 38.6k
Description
public class TestMain {
public static void main(String[] args) {
// 1. Prepare the target object to be proxied.
People peo = new People();
// 2. Prepare the proxy factory.
ProxyFactory pf = new ProxyFactory();
// 3. Prepare the introduction advice. The advice holds the additional interface Developer and its implementation.
DelegatingIntroductionInterceptor dii = new DelegatingIntroductionInterceptor((Developer) () -> System.out.println("Coding"));
// 4. Add the advice and the interfaces that the proxy object should inherit.
pf.addAdvice(dii);
// 5. Set the target object to be proxied.
pf.setTarget(peo);
// 6. Type casting here will fail because the proxy object uses JDK dynamic proxying, which only implements the Developer interface and Spring AOP internal interfaces.
// It should ideally use CGLIB proxying instead!!!
peo = (People) pf.getProxy();
peo.drink();
peo.eat();
// 7. Forced casting to the Developer interface. The actual method calls will be intercepted by the introduction advice, and the request will be forwarded to the Developer interface implementation held by the advice.
Developer developer = (Developer) peo;
developer.code();
}
public static class People {
void eat() {
System.out.println("eat");
}
void drink() {
System.out.println("drink");
}
}
public interface Developer {
void code();
}
}
The result of running the code is:
Exception in thread "main" java.lang.ClassCastException: class com.sun.proxy.$Proxy0 cannot be cast to class com.spring.TestMain$People (com.sun.proxy.$Proxy0 and com.spring.TestMain$People are in unnamed module of loader 'app')
at com.spring.TestMain.main(TestMain.java:20)
The original expectation here was for the proxy object to be able to use Cglib for proxying because the target object did not implement any interfaces. However, this situation occurred due to a special handling of IntroductionAdvisor within the ProxyFactory. It added all the interfaces provided by IntroductionAdvisor to the AdvisedSupport's interface collection. Consequently, when DefaultAopProxyFactory eventually executed the proxying process, it chose to use JDK dynamic proxy instead of Cglib.
As a result, the proxy object we obtain is actually implemented using JDK dynamic proxy. It implements internal Spring AOP module-related interfaces and the Developer interface. When we forcefully attempt to cast the proxy object to the People type, a type casting exception is thrown.
The Spring AOP module version is: 5.3.9
Reason:
When AdvisedSupport adds advice, it specifically handles Advice of the IntroductionInfo type and adds the interfaces implemented by it to the interfaces collection.
@Override
public void addAdvice(Advice advice) throws AopConfigException {
int pos = this.advisors.size();
addAdvice(pos, advice);
}
@Override
public void addAdvice(int pos, Advice advice) throws AopConfigException {
Assert.notNull(advice, "Advice must not be null");
if (advice instanceof IntroductionInfo) {
// We don't need an IntroductionAdvisor for this kind of introduction:
// It's fully self-describing.
addAdvisor(pos, new DefaultIntroductionAdvisor(advice, (IntroductionInfo) advice));
}
...
}
@Override
public void addAdvisor(int pos, Advisor advisor) throws AopConfigException {
if (advisor instanceof IntroductionAdvisor) {
validateIntroductionAdvisor((IntroductionAdvisor) advisor);
}
addAdvisorInternal(pos, advisor);
}
private void validateIntroductionAdvisor(IntroductionAdvisor advisor) {
advisor.validateInterfaces();
// If the advisor passed validation, we can make the change.
Class<?>[] ifcs = advisor.getInterfaces();
for (Class<?> ifc : ifcs) {
addInterface(ifc);
}
}
At this point, even if the target object does not implement any interfaces, the interfaces collection will not be empty :
private List<Class<?>> interfaces = new ArrayList<>();
This results in DefaultAopProxyFactory making an incorrect choice between using JDK or CGLIB for dynamic proxying. It chooses JDK instead of CGLIB for dynamic proxying, even when the target object does not implement any interfaces. Consequently, the resulting proxy object cannot be cast to the target object type, which is not in line with our expected behavior.
@Override
public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {
if (!NativeDetector.inNativeImage() &&
(config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config))) {
Class<?> targetClass = config.getTargetClass();
if (targetClass == null) {
throw new AopConfigException("TargetSource cannot determine target class: " +
"Either an interface or a target is required for proxy creation.");
}
if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) {
return new JdkDynamicAopProxy(config);
}
return new ObjenesisCglibAopProxy(config);
}
else {
return new JdkDynamicAopProxy(config);
}
}
// The `interfaces` collection is not empty at this point, so JDK dynamic proxying is used.
private boolean hasNoUserSuppliedProxyInterfaces(AdvisedSupport config) {
Class<?>[] ifcs = config.getProxiedInterfaces();
return (ifcs.length == 0 || (ifcs.length == 1 && SpringProxy.class.isAssignableFrom(ifcs[0])));
}
I'm not sure if this can be considered a bug. If possible, I would prefer that in this case, the IntroductionAdvisor's additional interface list be handled separately to avoid choosing JDK dynamic proxying when the target object does not implement interfaces.