Master of provincial Flow:
Does @Transactional work when a Service calls a private method of another Service
Normal processes do not take effect
After some operation, it can be achieved theoretically
This article is based on Spring Boot 2.3.3.RELEASE, JDK1.8 version, using Lombok plug-in
doubt
One day, my friend asked me,
Does @Transactional transactions work when one Service calls a private method of another Service?
I answered directly on the spot: “This still use to think, that certainly can’t take effect!” So he asked, “Why doesn’t it work?”
“It’s not obvious, how do you call a private method from one Service to another?” And he said, “You can use reflection.”
Even with reflection, the @Transactional principle is implemented based on AOP dynamic proxies. Dynamic proxies do not proxy private methods! .
He then asks, “Are you sure you won’t proxy the private method?” .
“Er… Probably not…”
Now I hesitated to answer. Dynamic proxy generates Java classes at the bytecode level, but how to implement it, and whether to handle private methods, is still not sure
validation
See if @Transactional Transactional transactions work when a Service calls a private method of another Service.
Because of the @ Transactional affairs effect test is not convenient to blunt saw, but the transaction is through the cut surface of the AOP implementation, so the custom here a section to indicate affairs effect, convenient test, as long as the edge effect, the transaction takes effect certainly is not.
@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!");
returnresult; }}Copy the code
We then write the Test class and the Test method, where the private primaryHello() method of HelloServiceImpl is called by reflection.
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);
returnSystem.currentTimeMillis(); }}@Slf4j
@SpringBootTest
public class HelloTests {
@Autowired
private HelloService helloService;
@Test
public void helloService(a) 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); }}Copy the code
As a result, the public method Hello () is successfully proxied, while the private method not only isn’t proxied, but it can’t even be called by reflection.
This is not hard to understand, as you can see from the exception message thrown:
java.lang.NoSuchMethodException: cn.zzzzbw.primary.proxy.service.impl.HelloServiceImpl$$EnhancerBySpringCGLIB$$679d418b.privateHello(java.lang.Integer)
Instead of injecting the implementation class HelloServiceImpl, HelloServiceImpl$$EnhancerBySpringCGLIB$$6F6c17b4 is generated by the proxy class. If the private method is not included in the proxy class, it will not be called.
A @Transactional transaction does not take effect when a Service calls a private method of another Service
This result can be obtained from the verification results above. But this is just a phenomenon, and we need to finally look at the actual code to determine if and how the private method is actually dropped during proxying.
Spring Boot agent generation process
The general process for generating proxy classes in Spring Boot is as follows:
[Generate Bean instance] -> [Bean post-processor (such as BeanPostProcessor)] -> [invoke proxyFactory.getProxy method (if needed to be proxied)] -> [call DefaultAopProxyFactory. CreateAopProxy. GetProxy methods obtain the agent after the object]
With an emphasis 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.");
}
// The propped class has interfaces and uses JDK proxies
if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) {
return new JdkDynamicAopProxy(config);
}
// The proxied class does not implement an interface and uses the Cglib proxy
return new ObjenesisCglibAopProxy(config);
}
else {
// Default JDK proxy
return newJdkDynamicAopProxy(config); }}}Copy the code
This code is Spring Boot classic two dynamic Proxy mode selection process, if the target class implementing an interface (targetClass. IsInterface () | | Proxy. IsProxyClass (targetClass)), Use JDK proxy (JdkDynamicAopProxy), otherwise use CGlib proxy (ObjenesisCglibAopProxy).
The CGlib proxy mode is used by default in Spring Boot 2.x. However, the CGlib proxy mode is used by default in Spring 5.x. If you want to enforce JDK proxy mode, you can set the configuration to spring.aop.proxy-target-class=false
The HelloServiceImpl above implements the HelloService interface using JdkDynamicAopProxy(to prevent Spring Boot2.x changes). Then 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); }}Copy the code
The JdkDynamicAopProxy method implements the InvocationHandler interface, and the getProxy method performs a series of operations (AOP execution expression parsing, proxy chain invocation, etc.). The result is proxy.newProxyinstance, a method provided by the JDK that generates the Proxy class.
JDK proxy class generation process
Since Spring has entrusted the JDK with the process of proxy, let’s follow the process and see how the JDK actually generates proxy classes.
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);
finalClass<? >[] intfs = interfaces.clone();final SecurityManager sm = System.getSecurityManager();
if(sm ! =null) {
checkProxyAccess(Reflection.getCallerClass(), loader, intfs);
}
/* * 2. Obtain the generated proxy Class */Class<? > cl = getProxyClass0(loader, intfs);/* * 3. The reflection get constructor generates the proxy object instance */
try {
if(sm ! =null) {
checkNewProxyPermission(Reflection.getCallerClass(), cl);
}
finalConstructor<? > cons = cl.getConstructor(constructorParams);final InvocationHandler ih = h;
if(! Modifier.isPublic(cl.getModifiers())) { AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run(a) {
cons.setAccessible(true);
return null; }}); }return cons.newInstance(new Object[]{h});
} catch. }}Copy the code
The proxy.newProxyInstance () method actually does three things that the process code notes above. The most important step is step 2, which generates the Class of the proxy Class, Class
cl = getProxyClass0(loader, intfs); This is the core method for generating dynamic proxy classes.
Take another look at the getProxyClass0() method
private staticClass<? > getProxyClass0(ClassLoader loader, Class<? >... interfaces) {if (interfaces.length > 65535) {
throw new IllegalArgumentException("interface limit exceeded");
}
/* * If the proxy class has already been generated, create a new proxy class */ via ProxyClassFactory
return proxyClassCache.get(loader, interfaces);
}
Copy the code
The getProxyClass0() method obtains the corresponding proxy class from the cache proxyClassCache. ProxyClassCache is a WeakCache object, which is a cache similar to Map. But we just need to know that the cache returns a value if it exists in GET, or calls the apply() method of ProxyClassFactory if it doesn’t.
So now look at the proxyClassFactory.apply () method
publicClass<? > apply(ClassLoader loader, Class<? >[] interfaces) { ...// There are a lot of checksums
/* * Name the newly generated Proxy class: proxyPkg + proxyClassNamePrefix(fixed string "$Proxy") + num(current Proxy class generated) */
long num = nextUniqueNumber.getAndIncrement();
String proxyName = proxyPkg + proxyClassNamePrefix + num;
/* * Generate bytecode byte data for the defined proxy class */
byte[] proxyClassFile = ProxyGenerator.generateProxyClass(
proxyName, interfaces, accessFlags);
try {
/* * Loads the generated bytecode data into the JVM and returns the corresponding Class */
return defineClass0(loader, proxyName,
proxyClassFile, 0, proxyClassFile.length);
} catch. }Copy the code
The proxyClassFactory.apply () method does two things: 1. Call ProxyGenerator. GenerateProxyClass () method to generate the proxy class bytecode data 2. Classes are generated by loading the data into the JVM.
Proxy class bytecode generation process
After a series of source code review, finally to the most critical bytecode generation link. Now let’s take a look at how proxy class bytecode is actually generated and how private methods are treated.
public static byte[] generateProxyClass(final String name,
Class[] interfaces)
{
ProxyGenerator gen = new ProxyGenerator(name, interfaces);
// Actually generate bytecode
final byte[] classFile = gen.generateClassFile();
// Access permission operation, omitted here.return classFile;
}
private byte[] generateClassFile() {
/ * = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = * step one: add all the required * / agent
// Add the equal, hashCode, toString methods
addProxyMethod(hashCodeMethod, Object.class);
addProxyMethod(equalsMethod, Object.class);
addProxyMethod(toStringMethod, Object.class);
// Add all methods in all interfaces of the target proxy 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]); }}// Check whether there are duplicate methods
for (List<ProxyMethod> sigmethods : proxyMethods.values()) {
checkReturnTypes(sigmethods);
}
/ * = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = * step two: assembly need to generate the proxy class field information (FieldInfo) and method (MethodInfo) * /
try {
// Add constructor
methods.add(generateConstructor());
for (List<ProxyMethod> sigmethods : proxyMethods.values()) {
for (ProxyMethod pm : sigmethods) {
// Since the proxy class uses reflection internally to call the methods of the target class instance, there must be reflection dependency, so Method is fixed here
fields.add(new FieldInfo(pm.methodFieldName,
"Ljava/lang/reflect/Method;",
ACC_PRIVATE | ACC_STATIC));
// Add information about the proxy method
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");
}
/ * = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = * step three: output to generate a class file * / in the end
// This part is to write bytecode based on the information assembled above.return bout.toByteArray();
}
Copy the code
The sun. The misc. ProxyGenerator. GenerateClassFile () method is the realization of the real generated proxy class bytecode data, mainly for three steps:
-
Add all methods that need to be propped up, and write down some information about methods that need to be propped up (equal, HashCode, toString, and methods declared in the interface).
-
Assemble the field information and method information for the proxy class that needs to be generated. This generates an implementation of the methods of the actual proxy class, based on the methods added in Step 1. Such as:
If the target proxy class implements a HelloService interface and implements the method Hello, then the generated proxy class generates the following form methods:
public Object hello(Object... args){ try{ return (InvocationHandler)h.invoke(this.this.getMethod("hello"), args); } catch. }Copy the code
-
The information added and assembled above is streamed to concatenate the final Java Class bytecode data
After looking at this code, we can now be really sure that the proxy class does not proxy private methods. As you learned in Step 1, the proxy class only proxies equal, HashCode, toString methods, and methods declared in the interface, so the target class’s private methods are not proxied. However, if you think about it, private methods can’t be called externally under normal circumstances, and even if they are proxied, they can’t be used, so there’s no need to proxied.
conclusion
Dynamic proxies do not proxy private methods, so @Transactional annotated transactions do not apply to them.
The JDK provides a wide range of dynamic proxy functions. You can implement dynamic proxy functions yourself by using the @Transactional private method.
To implement the effect of proxising the private method and making the @Transactional annotation work, simply rewind the Transactional process as follows:
- Reimplement one
ProxyGenerator.generateClassFile()
Method to output proxy class bytecode data with private methods - The bytecode data is loaded into the JVM to generate the Class
- alternative
Spring Boot
The default dynamic proxy function, replaced by our own dynamic proxy.
Transactional: Does @Transactional work when a Service calls a private method of another Service
Does @Transactional work when a Service calls a private method of another Service?