Service调用其他Service的private方法, @Transactional会生效吗(上)
Posted zzzzbw
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Service调用其他Service的private方法, @Transactional会生效吗(上)相关的知识,希望对你有一定的参考价值。
省流大师:
- 一个Service调用其他Service的private方法, @Transactional会生效吗
- 正常流程不能生效
- 经过一番操作, 达到理论上可以
本文基于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);
}
}
从结果看到, 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时如果存在值, 则返回这个值, 如果不存在, 则调用ProxyClassFactory
的apply()
方法.
所以现在看一下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()
方法就是真正的实现生成代理类字节码数据的地方, 主要为三个步骤:
- 添加所有需要代理的方法, 把需要代理的方法(equal、hashcode、toString方法和接口中声明的方法)的一些相关信息记录下来.
组装需要生成的代理类的字段信息和方法信息. 这里会根据步骤一添加的方法, 生成实际的代理类的方法的实现. 比如:
如果目标代理类实现了一个
HelloService
接口, 且实现其中的方法hello
, 那么生成的代理类就会生成如下形式方法:public Object hello(Object... args){ try{ return (InvocationHandler)h.invoke(this, this.getMethod("hello"), args); } catch ... }
- 把上面添加和组装的信息通过流拼接出最终的java class字节码数据
**看了这段代码, 现在我们可以真正确定代理类是不会代理private方法了. 在步骤一中知道代理类只会代理equal、hashcode、toString方法和接口中声明的方法, 所以目标类的private方法是不会被代理到的.
不过想一下也知道, 私有方法在正常情况下外部也无法调用, 即使代理了也没法使用, 所以也没必要去代理.**
结论
上文通过阅读Spring Boot
动态代理流程以及JDK动态代理功能实现的源码, 得出结论动态代理不会代理private方法, 所以@Transactional
注解的事务也不会对其生效.
但是看完成整个代理流程之后感觉动态代理也不过如此嘛, JDK提供的动态代理功能太菜了, 我们完全可以自己来实现动态代理的功能, 让@Transactional
注解的private方法也能生效, 我上我也行!
根据上面看源码流程, 如果要实现代理private方法并使@Transactional
注解生效的效果, 那么只要倒叙刚才看源码的流程, 如下:
- 重新实现一个
ProxyGenerator.generateClassFile()
方法, 输出带有private方法的代理类字节码数据 - 把字节码数据加载到JVM中, 生成Class
- 替代
Spring Boot
中默认的动态代理功能, 换成我们自己的动态代理.
这部分内容在Service调用其他Service的private方法, @Transactional会生效吗(下), 欢迎阅读
原文地址:Service调用其他Service的private方法, @Transactional会生效吗(上)
以上是关于Service调用其他Service的private方法, @Transactional会生效吗(上)的主要内容,如果未能解决你的问题,请参考以下文章
Android如何启用Service,如何停用Service。
java中dao层和service层的区别,为啥要用service
关于 php 调用 其他语言写的Web Service SOAP 接口的参数传递问题
使用多个键调用 Service Fabric Reliable Dictionary