Service呼叫其他Service的private方法, @Transactional會生效嗎(上)

zzzzbw發表於2020-10-27

省流大師:

  1. 一個Service呼叫其他Service的private方法, @Transactional會生效嗎
  2. 正常流程不能生效
  3. 經過一番操作, 達到理論上可以

本文基於Spring Boot 2.3.3.RELEASE、JDK1.8 版本, 使用Lombok外掛

疑問

有一天, 我的小夥伴問我,

"一個Service呼叫其他Service的private方法, @Transactional的事務會生效嗎?"

我當場直接就回答: "這還用想, 那肯定不能生效啊!". 於是他問, "為什麼不能生效?"

"這不是很明顯的事情, 你怎麼在一個Service呼叫另一個Service的私有方法?". 他接著說到: "可以用反射啊".

"就算用反射, @Transactional的原理是基於AOP的動態代理實現的, 動態代理不會代理private方法的!".

他接著問道: "真的不會代理private方法嗎?".

"額...應該不會吧..."

這下我回答的比較遲疑了. 因為平時只是大概知道動態代理會在位元組碼的層面生成java類, 但是裡面具體怎麼實現, 會不會處理private方法, 還真的不確定

驗證

雖然心裡知道了結果, 但還是要實踐一下, Service呼叫其他Service的private方法, @Transactional的事務到底能不能生效, 看看會不會被打臉.

由於@Transactional的事務效果測試的時候不方便直白的看到, 不過其事務是通過AOP的切面實現的, 所以這裡自定義一個切面來表示事務效果, 方便測試, 只要這個切面生效, 那事務生效肯定也不是事.

@Slf4j
@Aspect
@Component
public class TransactionalAop {
    @Around("@within(org.springframework.transaction.annotation.Transactional)")
    public Object recordLog(ProceedingJoinPoint p) throws Throwable {
        log.info("Transaction start!");
        Object result;
        try {
            result = p.proceed();
        } catch (Exception e) {
            log.info("Transaction rollback!");
            throw new Throwable(e);
        }
        log.info("Transaction commit!");
        return result;
    }
}

然後寫測試的類和Test方法, Test方法中通過反射呼叫HelloServiceImpl的private方法primaryHello().

public interface HelloService {
    void hello(String name);
}

@Slf4j
@Transactional
@Service
public class HelloServiceImpl implements HelloService {
    @Override
    public void hello(String name) {
        log.info("hello {}!", name);
    }

    private long privateHello(Integer time) {
        log.info("private hello! time: {}", time);
        return System.currentTimeMillis();
    }
}

@Slf4j
@SpringBootTest
public class HelloTests {

    @Autowired
    private HelloService helloService;

    @Test
    public void helloService() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        helloService.hello("hello");

        Method privateHello = helloService.getClass().getDeclaredMethod("privateHello", Integer.class);
        privateHello.setAccessible(true);
        Object invoke = privateHello.invoke(helloService, 10);
        log.info("privateHello result: {}", invoke);
    }
}

私有方法代理失敗_IMG

從結果看到, public方法hello()成功被代理了, 但是private方法不僅沒有被代理到, 甚至也無法通過反射呼叫.

這其實也不難理解, 從丟擲的異常資訊中也可以看到:

java.lang.NoSuchMethodException: cn.zzzzbw.primary.proxy.service.impl.HelloServiceImpl$$EnhancerBySpringCGLIB$$679d418b.privateHello(java.lang.Integer)

helloService注入的不是實現類HelloServiceImpl, 而是代理類生成的HelloServiceImpl$$EnhancerBySpringCGLIB$$6f6c17b4. 假如生成代理類的時候沒有把private方法也寫上, 那麼自然是沒法呼叫的.

一個Service呼叫其他Service的private方法, @Transactional的事務是不會生效的

從上面的驗證結果可以得到這個結果. 但是這只是現象, 還需要最終看具體的程式碼來確定一下, 是不是真的在代理的時候把private方法丟掉了, 是怎麼丟掉的.

Spring Boot代理生成流程

Spring Boot生成代理類的大致流程如下:

[生成Bean例項] -> [Bean後置處理器(如BeanPostProcessor)] -> [呼叫ProxyFactory.getProxy方法(如果需要被代理)] -> [呼叫DefaultAopProxyFactory.createAopProxy.getProxy方法獲取代理後的物件]

其中重點關注一下DefaultAopProxyFactory.createAopProxy方法.

public class DefaultAopProxyFactory implements AopProxyFactory, Serializable {

    @Override
    public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {
        if (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.");
            }
            // 被代理類有介面, 使用JDK代理
            if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) {
                return new JdkDynamicAopProxy(config);
            }
            // 被代理類沒有實現介面, 使用Cglib代理
            return new ObjenesisCglibAopProxy(config);
        }
        else {
            // 預設JDK代理
            return new JdkDynamicAopProxy(config);
        }
    }
}

這段程式碼就是Spring Boot經典的兩種動態代理方式選擇過程, 如果目標類有實現介面(targetClass.isInterface() || Proxy.isProxyClass(targetClass)),
則用JDK代理(JdkDynamicAopProxy), 否則用CGlib代理(ObjenesisCglibAopProxy).

不過在Spring Boot 2.x版本以後, 預設會用CGlib代理模式, 但實際上Spring 5.x中AOP預設代理模式還是JDK, 是Spring Boot特意修改的, 具體原因這裡不詳細講解了, 感興趣的可以去看一下issue #5423
假如想要強制使用JDK代理模式, 可以設定配置spring.aop.proxy-target-class=false

上面的HelloServiceImpl實現了HelloService介面, 用的就是JdkDynamicAopProxy(為了防止Spring Boot2.x修改的影響, 這裡設定配置強制開啟JDK代理). 於是看一下JdkDynamicAopProxy.getProxy方法

final class JdkDynamicAopProxy implements AopProxy, InvocationHandler, Serializable {
    @Override
    public Object getProxy(@Nullable ClassLoader classLoader) {
        if (logger.isTraceEnabled()) {
            logger.trace("Creating JDK dynamic proxy: " + this.advised.getTargetSource());
        }
        Class<?>[] proxiedInterfaces = AopProxyUtils.completeProxiedInterfaces(this.advised, true);
        findDefinedEqualsAndHashCodeMethods(proxiedInterfaces);
        return Proxy.newProxyInstance(classLoader, proxiedInterfaces, this);
    }
}

可以看到JdkDynamicAopProxy實現了InvocationHandler介面, 然後在getProxy方法中先是做了一系列操作(AOP的execution表示式解析、代理鏈式呼叫等, 裡面邏輯複雜且和我們代理主流程關係不大, 就不研究了),
最後返回的是由JDK提供的生成代理類的方法Proxy.newProxyInstance的結果.

JDK代理類生成流程

既然Spring把代理的流程託付給JDK了, 那我們也跟著流程看看JDK到底是怎麼生成代理類的.

先來看一下Proxy.newProxyInstance()方法

public class Proxy implements java.io.Serializable {
    public static Object newProxyInstance(ClassLoader loader,
                                          Class<?>[] interfaces,
                                          InvocationHandler h)
        throws IllegalArgumentException
    {

        /*
         * 1. 各種校驗
         */
        Objects.requireNonNull(h);

        final Class<?>[] intfs = interfaces.clone();
        final SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            checkProxyAccess(Reflection.getCallerClass(), loader, intfs);
        }

        /*
         * 2. 獲取生成的代理類Class
         */
        Class<?> cl = getProxyClass0(loader, intfs);

        /*
         * 3. 反射獲取構造方法生成代理物件例項
         */
        try {
            if (sm != null) {
                checkNewProxyPermission(Reflection.getCallerClass(), cl);
            }

            final Constructor<?> cons = cl.getConstructor(constructorParams);
            final InvocationHandler ih = h;
            if (!Modifier.isPublic(cl.getModifiers())) {
                AccessController.doPrivileged(new PrivilegedAction<Void>() {
                    public Void run() {
                        cons.setAccessible(true);
                        return null;
                    }
                });
            }
            return cons.newInstance(new Object[]{h});
        } catch ...
    }
}

Proxy.newProxyInstance()方法實際上做了3件事, 在上面流程程式碼註釋了. 最重要的就是步驟2, 生成代理類的Class, Class<?> cl = getProxyClass0(loader, intfs);, 這就是生成動態代理類的核心方法.

那就再看一下getProxyClass0()方法

private static Class<?> getProxyClass0(ClassLoader loader,
                                       Class<?>... interfaces) {
    if (interfaces.length > 65535) {
        throw new IllegalArgumentException("interface limit exceeded");
    }
    /*
     * 如果代理類已經生成則直接返回, 否則通過ProxyClassFactory建立新的代理類
     */
    return proxyClassCache.get(loader, interfaces);
}

getProxyClass0()方法從快取proxyClassCache中獲取對應的代理類. proxyClassCache是一個WeakCache物件, 他是一個類似於Map形式的快取, 裡面邏輯比較複雜就不細看了.
不過我們只要知道, 這個快取在get時如果存在值, 則返回這個值, 如果不存在, 則呼叫ProxyClassFactoryapply()方法.

所以現在看一下ProxyClassFactory.apply()方法

public Class<?> apply(ClassLoader loader, Class<?>[] interfaces) {
    ...
    // 上面是很多校驗, 這裡先不看

    /*
     * 為新生成的代理類起名:proxyPkg(包名) + proxyClassNamePrefix(固定字串"$Proxy") + num(當前代理類生成量)
     */
    long num = nextUniqueNumber.getAndIncrement();
    String proxyName = proxyPkg + proxyClassNamePrefix + num;

    /*
     * 生成定義的代理類的位元組碼 byte資料
     */
    byte[] proxyClassFile = ProxyGenerator.generateProxyClass(
        proxyName, interfaces, accessFlags);
    try {
        /*
         * 把生成的位元組碼資料載入到JVM中, 返回對應的Class
         */
        return defineClass0(loader, proxyName,
                            proxyClassFile, 0, proxyClassFile.length);
    } catch ...
}

ProxyClassFactory.apply()方法中主要就是做兩件事:1. 呼叫ProxyGenerator.generateProxyClass()方法生成代理類的位元組碼資料 2. 把資料載入到JVM中生成Class.

代理類位元組碼生成流程

經過一連串的原始碼檢視, 終於到最關鍵的生成位元組碼環節了. 現在一起來看代理類位元組碼是到底怎麼生成的, 對待private方法是怎麼處理的.

public static byte[] generateProxyClass(final String name,
                                       Class[] interfaces)
{
   ProxyGenerator gen = new ProxyGenerator(name, interfaces);
   // 實際生成位元組碼
   final byte[] classFile = gen.generateClassFile();
    
    // 訪問許可權操作, 這裡省略
    ...

   return classFile;
}

private byte[] generateClassFile() {

   /* ============================================================
    * 步驟一: 新增所有需要代理的方法
    */

   // 新增equal、hashcode、toString方法
   addProxyMethod(hashCodeMethod, Object.class);
   addProxyMethod(equalsMethod, Object.class);
   addProxyMethod(toStringMethod, Object.class);

   // 新增目標代理類的所有介面中的所有方法
   for (int i = 0; i < interfaces.length; i++) {
       Method[] methods = interfaces[i].getMethods();
       for (int j = 0; j < methods.length; j++) {
           addProxyMethod(methods[j], interfaces[i]);
       }
   }

   // 校驗是否有重複的方法
   for (List<ProxyMethod> sigmethods : proxyMethods.values()) {
       checkReturnTypes(sigmethods);
   }

   /* ============================================================
    * 步驟二:組裝需要生成的代理類欄位資訊(FieldInfo)和方法資訊(MethodInfo)
    */
   try {
       // 新增構造方法
       methods.add(generateConstructor());

       for (List<ProxyMethod> sigmethods : proxyMethods.values()) {
           for (ProxyMethod pm : sigmethods) {

               // 由於代理類內部會用反射呼叫目標類例項的方法, 必須有反射依賴, 所以這裡固定引入Method方法
               fields.add(new FieldInfo(pm.methodFieldName,
                   "Ljava/lang/reflect/Method;",
                    ACC_PRIVATE | ACC_STATIC));

               // 新增代理方法的資訊
               methods.add(pm.generateMethod());
           }
       }

       methods.add(generateStaticInitializer());

   } catch (IOException e) {
       throw new InternalError("unexpected I/O Exception");
   }

   if (methods.size() > 65535) {
       throw new IllegalArgumentException("method limit exceeded");
   }
   if (fields.size() > 65535) {
       throw new IllegalArgumentException("field limit exceeded");
   }

   /* ============================================================
    * 步驟三: 輸出最終要生成的class檔案
    */

    // 這部分就是根據上面組裝的資訊編寫位元組碼
    ...

   return bout.toByteArray();
}

這個sun.misc.ProxyGenerator.generateClassFile()方法就是真正的實現生成代理類位元組碼資料的地方, 主要為三個步驟:

  1. 新增所有需要代理的方法, 把需要代理的方法(equal、hashcode、toString方法和介面中宣告的方法)的一些相關資訊記錄下來.
  2. 組裝需要生成的代理類的欄位資訊和方法資訊. 這裡會根據步驟一新增的方法, 生成實際的代理類的方法的實現. 比如:

    如果目標代理類實現了一個HelloService介面, 且實現其中的方法hello, 那麼生成的代理類就會生成如下形式方法:

    public Object hello(Object... args){
        try{
            return (InvocationHandler)h.invoke(this, this.getMethod("hello"), args);
        } catch ...  
    }
  3. 把上面新增和組裝的資訊通過流拼接出最終的java class位元組碼資料

**看了這段程式碼, 現在我們可以真正確定代理類是不會代理private方法了. 在步驟一中知道代理類只會代理equal、hashcode、toString方法和介面中宣告的方法, 所以目標類的private方法是不會被代理到的.
不過想一下也知道, 私有方法在正常情況下外部也無法呼叫, 即使代理了也沒法使用, 所以也沒必要去代理.**

結論

上文通過閱讀Spring Boot動態代理流程以及JDK動態代理功能實現的原始碼, 得出結論動態代理不會代理private方法, 所以@Transactional註解的事務也不會對其生效.

但是看完成整個代理流程之後感覺動態代理也不過如此嘛, JDK提供的動態代理功能太菜了, 我們完全可以自己來實現動態代理的功能, 讓@Transactional註解的private方法也能生效, 我上我也行!

根據上面看原始碼流程, 如果要實現代理private方法並使@Transactional註解生效的效果, 那麼只要倒敘剛才看原始碼的流程, 如下:

  1. 重新實現一個ProxyGenerator.generateClassFile()方法, 輸出帶有private方法的代理類位元組碼資料
  2. 把位元組碼資料載入到JVM中, 生成Class
  3. 替代Spring Boot中預設的動態代理功能, 換成我們自己的動態代理.

這部分內容在Service呼叫其他Service的private方法, @Transactional會生效嗎(下), 歡迎閱讀


原文地址:Service呼叫其他Service的private方法, @Transactional會生效嗎(上)

相關文章