Service調用其餘Service的private方法, @Transactional會生效嗎(上)

省流大師:java

  1. 一個Service調用其餘Service的private方法, @Transactional會生效嗎
  2. 正常流程不能生效
  3. 通過一番操做, 達到理論上能夠

本文基於Spring Boot 2.3.3.RELEASE、JDK1.8 版本, 使用Lombok插件git

疑問

有一天, 個人小夥伴問我, github

"一個Service調用其餘Service的private方法, @Transactional的事務會生效嗎?" spring

我當場直接就回答: "這還用想, 那確定不能生效啊!". 因而他問, "爲何不能生效?"緩存

"這不是很明顯的事情, 你怎麼在一個Service調用另外一個Service的私有方法?". 他接着說到: "能夠用反射啊". app

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

他接着問道: "真的不會代理private方法嗎?". spring-boot

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

這下我回答的比較遲疑了. 由於平時只是大概知道動態代理會在字節碼的層面生成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會生效嗎(上)

相關文章
相關標籤/搜索