This website requires JavaScript.

Service 调用其他 Service 的 private 方法,@Transactional 会生效吗(上)

省流大师:

  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 ...  
}
  1. 把上面添加和组装的信息通过流拼接出最终的 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 会生效吗(上) /22)

0条评论
avatar