> Streaming Master: > > 1. When a Service calls the private method of another Service, will @Transactional take effect? > > 2. The normal process cannot take effect. > > 3. After some operations, it can theoretically be achieved.
> This article is based on Spring Boot 2.3.3.RELEASE, JDK1.8 version, using Lombok plugin
doubt
One day my friend asked me,
"A Service calls the private method of another Service, will the @Transactional transaction take effect?"
I replied directly on the spot: "I still need to think about it, it definitely won't work!". So he asked, "Why can't it work?"
"It's not very obvious, how do you call a private method of another Service from one Service?". He went on to say: "You can use reflection".
"Even with reflection, the principle of @Transactional is implemented based on AOP's dynamic proxy, and dynamic proxy will not proxy private methods!".
He went on to ask: "Does it really not proxy private methods?".
"Um...probably not..."
Now I am hesitant to answer. Because I usually only know that the dynamic proxy will generate java classes at the bytecode level, but how to implement it in it, and whether it will handle private methods, I am really not sure.
verify
Although I know the result in my heart, I still have to practice it. If the service calls the private method of other services, @Transactional
whether the transaction can take effect or not, and see if it will be slapped in the face.
Because @Transactional
the transaction effect test is not convenient to see directly, but its transaction is realized through the AOP aspect, so here is a custom aspect to represent the transaction effect, which is convenient for testing. As long as this aspect takes effect, the transaction will definitely take effect. Not a thing.
@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;
}
}
Then write the test class and Test method, and HelloServiceImpl
the private method called by reflection in the Test method 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);
}
}
From the results, it can be seen that the public method is hello()
successfully proxied, but the private method is not only not proxied, but also cannot be called through reflection.
This is actually not difficult to understand, and you can also see from the exception information thrown:
java.lang.NoSuchMethodException: cn.zzzzbw.primary.proxy.service.impl.HelloServiceImpl$$EnhancerBySpringCGLIB$$679d418b.privateHello(java.lang.Integer)
helloService
What is injected is not the implementation class HelloServiceImpl
, but the proxy class HelloServiceImpl$$EnhancerBySpringCGLIB$$6f6c17b4
. If the private method is not written when the proxy class is generated, it will naturally not be called.
一个Service调用其他Service的private方法, @Transactional的事务是不会生效的
This result can be obtained from the above verification results. But this is just a phenomenon, and you need to finally look at the specific code to determine whether the private method is really lost during the proxy, and how.
Spring Boot proxy generation process
Spring Boot
The general process of generating a proxy class is as follows:
[Generate Bean instance] -> [Bean post-processor (eg BeanPostProcessor
)] -> [Call ProxyFactory.getProxy
method (if it needs to be proxied)] -> [Call DefaultAopProxyFactory.createAopProxy.getProxy
method to get the proxy object]
The focus is on the DefaultAopProxyFactory.createAopProxy
method.
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);
}
}
}
This code is the Spring Boot
classic selection process of two dynamic proxy methods. If the target class has an implementation interface ( targetClass.isInterface() || Proxy.isProxyClass(targetClass)
), use JDK proxy ( JdkDynamicAopProxy
), otherwise use CGlib proxy ( ObjenesisCglibAopProxy
).
> However, after the Spring Boot 2.x version, the CGlib proxy mode will be used by default, but in fact, the AOP default proxy mode in Spring 5.x is still JDK, which was specially modified by Spring Boot. The specific reasons are not explained in detail here. Interested If you want to force the use of JDK proxy mode, you can set the configurationspring.aop.proxy-target-class=false
The above HelloServiceImpl
implements the HelloService
interface, which is used JdkDynamicAopProxy
(in order to prevent Spring Boot2.x
the impact of modification, the configuration is set here to force the JDK proxy to be turned on). So let's take a look at the JdkDynamicAopProxy.getProxy
method
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);
}
}
It can be seen JdkDynamicAopProxy
that the InvocationHandler
interface is implemented, and then getProxy
a series of operations are first performed in the method (AOP execution expression analysis, proxy chain call, etc., the logic inside is complex and has little relationship with our proxy main process, so I won't study it), The final return is the result of the method provided by the JDK to generate the proxy class Proxy.newProxyInstance
.
JDK proxy class generation process
Since Spring
the proxy process is entrusted to JDK, let's follow the process to see how JDK generates proxy classes.
Let's take a look at the Proxy.newProxyInstance()
method
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()
The method actually does 3 things, and the process code is annotated above. The most important step is to generate the Class of the proxy class, Class<!--?--> cl = getProxyClass0(loader, intfs);
, which is the core method of generating the dynamic proxy class.
Then look at the getProxyClass0()
method again
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()
The method proxyClassCache
obtains the corresponding proxy class from the cache. proxyClassCache
It is an WeakCache
object, which is a cache similar to the Map form, and the logic inside is more complicated, so we won't take a closer look. But we only need to know that if there is a value in the cache, it will return The value, if not present, the ProxyClassFactory
method to call apply()
.
So now let's see ProxyClassFactory.apply()
how
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()
The method is mainly to do two things: 1. Call ProxyGenerator.generateProxyClass()
the method to generate the bytecode data of the proxy class 2. Load the data into the JVM to generate the Class.
Proxy class bytecode generation process
After a series of source code inspections, we finally reach the most critical part of generating bytecodes. Now let's see how the proxy class bytecodes are generated and how to deal with private methods.
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();
}
This sun.misc.ProxyGenerator.generateClassFile()
method is the place where the real implementation of generating proxy class bytecode data is performed, mainly in three steps:
-
Add all the methods that need to be proxied, and record some information about the methods that need to be proxied (equal, hashcode, toString methods and methods declared in the interface ).
-
Assemble the field information and method information of the proxy class that needs to be generated. Here, according to the method added in step 1, the implementation of the actual proxy class method will be generated. For example:
If the target proxy class implements an
HelloService
interface and implements its methodshello
, the generated proxy class will generate the following formal methods:public Object hello(Object... args){ try{ return (InvocationHandler)h.invoke(this, this.getMethod("hello"), args); } catch ... }
-
The information added and assembled above is spliced into the final java class bytecode data through the stream
After reading this code, now we can really be sure that the proxy class will not proxy the private method. In step 1, we know that the proxy class will only proxy the equal, hashcode, toString methods and the methods declared in the interface, so the private method of the target class It will not be proxied. But think about it, private methods cannot be called externally under normal circumstances, and even if they are proxied, they cannot be used, so there is no need to proxy.
in conclusion
By reading the Spring Boot
dynamic proxy process and the source code implemented by the JDK dynamic proxy function above, it is concluded that the dynamic proxy will not proxy the private method, so the @Transactional
annotated transaction will not take effect on it.
But after looking at the whole proxy process, I feel that dynamic proxy is just the same. The dynamic proxy function provided by JDKtoo dishy, we can fully implement the function of dynamic proxy by ourselves, so that @Transactional
the private method of the annotation can also take effect, so can I!
According to the above process of looking at the source code, if you want to achieve the effect of proxying the private method and making the @Transactional
annotation take effect, just flashback the process of looking at the source code, as follows:
- Reimplement a
ProxyGenerator.generateClassFile()
method to output proxy class bytecode data with private method - Load bytecode data into JVM and generate Class
- Replace
Spring Boot
the default dynamic proxy function in the replacement with our own dynamic proxy.
This part of the content calls the private method of other services in the service, will @Transactional take effect (below) , welcome to read
Original address: Service calls private methods of other Services, will @Transactional take effect (Part 1)
</proxymethod></proxymethod></void>