如何在spring代理中实现自我调用(self-invocation)

Posted 小杨Vita

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了如何在spring代理中实现自我调用(self-invocation)相关的知识,希望对你有一定的参考价值。

问题

在spring中如果在方法上添加了诸如@Transactional@Cacheable或是切面之类的注解,那么这个类就将由spring生成其代理对象,对指定方法进行相关的包装。当该方法在其他对象中被调用时是可以正常触发代理方法的,然而在本类的方法中进行内部调用时却不会,最终调用的还是原始方法。

class Service 
    public void methodA()
        methodB();
    
    @Transactional
    public void methodB()
        // do something
    

也就是说通过methodA调用methodB不会走代理方法,这是为什么呢?

原因分析

我们知道spring生成代理对象常见的有两种方式,一种是基于接口的JDK动态代理,一种是基于子类的CGLIB风格代理,可通过proxyTargetClass属性来控制。JDK文档中的一段话:

Users can control the type of proxy that gets created for FooService using the proxyTargetClass() attribute. The following enables CGLIB-style ‘subclass’ proxies as opposed to the default interface-based JDK proxy approach.

当为其配置了false时强制使用JDK动态代理,代理对象必须实现接口;当配置为true时强制使用CGLIB风格代理,代理方法不可使用final修饰;当没有配置该项时spring将自动选择有效的代理方式来实现。

JDK动态代理情况

JDK动态代理大家应该都比较熟悉了,这儿列出大致实现步骤,进而说明为什么无法触发代理方法。

public interface Subject

    public void methodA();
    public void methodB();


public class RealSubject implements Subject

    public void methodA()
    
        // do something
    
    public void methodB()
    
        // do something
    


public class InvocationHandlerImpl implements InvocationHandler

 
    /**
     * real proxied object
     */
    private Object subject;
 
    public InvocationHandlerImpl(Object subject)
    
        this.subject = subject;
    
 
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable
    
        // do something before invocation
 
        Object returnValue = method.invoke(subject, args);
 
        // do something after invocation
 
        return returnValue;
    

// generate proxy step
Subject realSubject = new RealSubject();

InvocationHandler handler = new InvocationHandlerImpl(realSubject);

ClassLoader loader = realSubject.getClass().getClassLoader();
Class[] interfaces = realSubject.getClass().getInterfaces();

Subject subject = (Subject) Proxy.newProxyInstance(loader, interfaces, handler);

subject.methodA();

我们可以看到真实对象realSubject其实是放入的handler中,然后传给Proxy来生成代理对象的。我们用反编译工具看下最终生成的代理对象类:

public final class ProxySubject extends Proxy implements Subject

    private static Method m1;
	private static Method m2;

    public ProxySubject(InvocationHandler paramInvocationHandler)
    
        super(paramInvocationHandler);
    

    public final void methodA()
    
        try
        
            this.h.invoke(this, m1, null);
        
        catch (Error|RuntimeException localError)
        
            throw localError;
        
    
    
    public final void methodB()
    
        try
        
            this.h.invoke(this, m2, null);
        
        catch (Error|RuntimeException localError)
        
            throw localError;
        
    
    
    static
    
        m1 = Class.forName("xxx.Subject").getMethod("methodA", new Class[0]);
		m2 = Class.forName("xxx.Subject").getMethod("methodB", new Class[0]);
    

为了方便理解上面删除了一些其他不影响的代码。我们可以看到针对代理方法methodA的调用,本质调用的是h.invoke(this, m1, null),也就是handler中的method.invoke(subject, args)语句。这儿的subject为传入的真实对象,因此如果在methodA方法中调用methodB,那就直接调用了内部的真实对象的methodB方法。也就是说代理对象ProxySubject所包装的方法仅适用于外部调用者直接访问,内部调用是无法再走代理过的方法的。

CGLIB风格代理情况

注意spring中使用的是CGLIB风格的代理,而不是正宗的CGLIB代理,我们先看下正宗的CGLIB代理实现方式

public class Service   
    public void methodA()   
        methodB();
      
    public void methodA()   
        // do something  
      


public class MyMethodInterceptor implements MethodInterceptor 
    public Object intercept(Object obj, Method method, Object[] arg, MethodProxy proxy) throws Throwable 
        // do something before invocation
        Object object = proxy.invokeSuper(obj, arg);
        // do something after invocation
        return object;
    

// generate proxy step
Enhancer enhancer = new Enhancer();  
enhancer.setSuperclass(Service.class);  
enhancer.setCallback(new MyMethodInterceptor());  
Service service = (Service)enhancer.create();
service.methodA()

CGLib使用字节码增强器Enhancer生成代理类的字节码,对应的反编译内容为:

public class Service$$EnhancerByCGLIB$$123aabb extends Service implements Factory

    private boolean CGLIB$BOUND;
    private static final ThreadLocal CGLIB$THREAD_CALLBACKS;
    private static final Callback[] CGLIB$STATIC_CALLBACKS;
    private MethodInterceptor CGLIB$CALLBACK_0;
    private static final Method CGLIB$g$0$Method;
    private static final MethodProxy CGLIB$g$0$Proxy;
    private static final Object[] CGLIB$emptyArgs;
    private static final Method CGLIB$f$1$Method;
    private static final MethodProxy CGLIB$f$1$Proxy;

    static void CGLIB$STATICHOOK1()
    
        CGLIB$THREAD_CALLBACKS = new ThreadLocal();
        CGLIB$emptyArgs = new Object[0];
        Class localClass1 = Class.forName("Service$$EnhancerByCGLIB$$123aabb");
        Class localClass2;
        Method[] tmp60_57 = ReflectUtils.findMethods(new String[]  "methodA", "()V", "methodB", "()V" , (localClass2 = Class.forName("xxx.Service")).getDeclaredMethods());
        CGLIB$g$0$Method = tmp60_57[0];
        CGLIB$g$0$Proxy = MethodProxy.create(localClass2, localClass1, "()V", "methodA", "CGLIB$g$0");
        CGLIB$f$1$Method = tmp60_57[1];
        CGLIB$f$1$Proxy = MethodProxy.create(localClass2, localClass1, "()V", "methodB", "CGLIB$f$1");
    

    final void CGLIB$g$0()
    
        super.methodA();
    

    public final void methodA()
    
        MethodInterceptor tmp4_1 = this.CGLIB$CALLBACK_0;
        if (tmp4_1 == null)
        
            CGLIB$BIND_CALLBACKS(this);
            tmp4_1 = this.CGLIB$CALLBACK_0;
        
        if (this.CGLIB$CALLBACK_0 != null) 
            tmp4_1.intercept(this, CGLIB$g$0$Method, CGLIB$emptyArgs, CGLIB$g$0$Proxy);
        
        else
            super.methodA();
        
    
    
    final void CGLIB$g$1()
    
        super.methodB();
    

    public final void methodB()
    
        MethodInterceptor tmp4_1 = this.CGLIB$CALLBACK_0;
        if (tmp4_1 == null)
        
            CGLIB$BIND_CALLBACKS(this);
            tmp4_1 = this.CGLIB$CALLBACK_0;
        
        if (this.CGLIB$CALLBACK_0 != null) 
            tmp4_1.intercept(this, CGLIB$g$0$Method, CGLIB$emptyArgs, CGLIB$g$0$Proxy);
        
        else
            super.methodB();
        
    

我们重点看下代理方法methodA调用的是super.methodA(),而该父类方法执行的是methodB(),由于子类继承重写了methodB方法,所以此时将调用子类(代理类)中的methodB方法。也就是说使用CGLIB方式是支持代理中自我调用的,那为什么spring中不可以呢?原因刚才也提到过,因为spring使用的是CGLIB风格的代理,生成的代理类大致是如下形式:

class MyService extends Service 
    private final Service delegate;

    @Override
    public void methodA() 
        // do some proxy thing
        delegate.methodA();
        // do some proxy thing
    

    @Override
    public void methodB() 
        // do some proxy thing
        delegate.methodB();
        // do some proxy thing
    

有没有发现这种方式灰常像动态代理的实现!通过使用delegate.methodA()代替super.methodA(),这样最终还是直接调用了真实对象delegatemethodB方法,坑爹啊有木有……至于spring为什么要这么做呢?我在StackOverflow上找到了几个猜测:

The behavior of the Cglib-proxies has nothing to do with the way of how cglib works but with how cglib is used by Spring. Cglib is capable of either delegating or subclassing a call. Have a look at Spring’s DynamicAdvisedInterceptor which implements this delegation. Using the MethodProxy, it could instead perform a super method call.
Spring defines delegation rather than subclassing in order to minimize the difference between using Cglib or Java proxies.

Maybe the reason was that CGLIB proxies should behave similarly to the default Java dynamic proxies so as to make switching between the two for interface types seamless.

为了能在java动态代理和cglib代理无缝切换,减小两者的差异,因此使用这种实现方式……

那么如果想要实现同一个对象中的自我调用,可以通过哪些方式呢?

解决方案

在spring官方文档中给出的建议是:

最好进行代码重构,以便不会发生自我调用,虽然需要你做一些额外工作,但这是最佳的侵入性最低的方式。

也就是说把方法分到不同的类中,当然他们也给出了其他的实现方式。

exposeProxy暴露代理方式

接下来介绍的方法我务必要谨慎地指出,它实在令人可怕。你可以使用该方法彻底将你的类中的逻辑绑定到Spring AOP中。

通过配置exposeProxy属性来获取代理类,进而在业务逻辑代码中获取代理类。开启方式如@EnableAspectJAutoProxy(exposeProxy = true),该属性的介绍为:

Indicate that the proxy should be exposed by the AOP framework as a ThreadLocal for retrieval via the org.springframework.aop.framework.AopContext class. Off by default, i.e. no guarantees that AopContext access will work.

也就是说开启后,在线程执行时spring会将当前对象的代理对象放入ThreadLocal中,通过AopContext提供的方法你可以在任何时候都能获取到代理对象。使用方式:

public class SimpleService implements Service 

   public void methodA() 
      // this works, but... gah!
      ((Service) AopContext.currentProxy()).methodB();
   
   
   public void methodB() 
      // some logic...
   

exposeProxy开启后如果依然无法获取到currentProxy可以参考这篇文章

当然文档中还提到了另一种解决方案:

使用AspectJ则不会有自我调用的问题,因为它并不是一种基于代理的AOP框架。

AspectJ

在开启事务@EnableTransactionManagement、缓存@EnableCaching或是异步请求@EnableAsync时,Spring会提供了两种实现模式:proxy 和 AspectJ。

AspectJ的开启方式如@EnableTransactionManagement(mode = AdviceMode.ASPECTJ) 。AspectJ有两种织入方式:CTW(Compile Time Weaving,编译期织入)LTW(Load Time Weaving,类加载期织入)

如果使用CTW,则需要编写 aspect 文件,然后使用 ajc 编译器结合 aspect 文件对源代码进行编译,通常很少使用。

而LTW类加载期织入是通过字节码编辑技术在类加载期将切面织入目标类中。其核心思想是在目标类的class文件被JVM加载前,通过自定义类加载器或者类文件转换器将横切逻辑织入到目标类的class文件中,然后将修改后class文件交给JVM加载。

使用AspectJ LTW有两个主要步骤:

  1. 通过JVM的-javaagent参数设置LTW的织入器类包,以代理JVM默认的类加载器
  2. LTW织入器需要一个 aop.xml文件,在该文件中指定切面类和需要进行切面织入的目标类

具体实现方式参考:
SpringBoot中使用LoadTimeWeaving技术实现AOP功能
使用AspectJ LTW(Load Time Weaving)

以上是关于如何在spring代理中实现自我调用(self-invocation)的主要内容,如果未能解决你的问题,请参考以下文章

在Spring应用调试中实现反向代理服务器

Solidity代理/实现模式中实现合约回调函数的使用

Solidity代理/实现模式中实现合约回调函数的使用

(转)Spring事务处理时自我调用的解决方案及一些实现方式的风险

Weka:如何在 J48 决策树中实现代理拆分?

如何在 Spring Boot 中实现刷新令牌