Service calls private methods of other Services, will @Transactional take effect (Part 1)

> 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, @Transactionalwhether the transaction can take effect or not, and see if it will be slapped in the face.

Because @Transactionalthe 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 HelloServiceImplthe 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);
    }
}

Private method proxy failed _IMG

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)

helloServiceWhat 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 BootThe general process of generating a proxy class is as follows:

[Generate Bean instance] -> [Bean post-processor (eg BeanPostProcessor)] -> [Call ProxyFactory.getProxymethod (if it needs to be proxied)] -> [Call DefaultAopProxyFactory.createAopProxy.getProxymethod to get the proxy object]

The focus is on the DefaultAopProxyFactory.createAopProxymethod.

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 Bootclassic 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 HelloServiceImplimplements the HelloServiceinterface, which is used JdkDynamicAopProxy(in order to prevent Spring Boot2.xthe 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.getProxymethod

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 JdkDynamicAopProxythat the InvocationHandlerinterface is implemented, and then getProxya 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 Springthe 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 &gt; 65535) {
        throw new IllegalArgumentException("interface limit exceeded");
    }
    /*
     * 如果代理类已经生成则直接返回, 否则通过ProxyClassFactory创建新的代理类
     */
    return proxyClassCache.get(loader, interfaces);
}

getProxyClass0()The method proxyClassCacheobtains the corresponding proxy class from the cache. proxyClassCacheIt is an WeakCacheobject, 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 ProxyClassFactorymethod 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 &lt; interfaces.length; i++) {
       Method[] methods = interfaces[i].getMethods();
       for (int j = 0; j &lt; 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() &gt; 65535) {
       throw new IllegalArgumentException("method limit exceeded");
   }
   if (fields.size() &gt; 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:

  1. 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 ).

  2. 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 HelloServiceinterface and implements its methods hello, 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 ...  
    }
    
  3. 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 Bootdynamic 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 @Transactionalannotated 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 @Transactionalthe 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 @Transactionalannotation take effect, just flashback the process of looking at the source code, as follows:

  1. Reimplement a ProxyGenerator.generateClassFile()method to output proxy class bytecode data with private method
  2. Load bytecode data into JVM and generate Class
  3. Replace Spring Bootthe 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>

{{o.name}}
{{m.name}}

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=324198328&siteId=291194637