Сервис вызывает частные методы других Сервисов, вступит ли в силу @Transactional (Часть 1)

Spring Boot

Ведущий провинциального стрима:

  1. Служба вызывает частный метод другой службы, вступит ли в силу @Transactional?

  2. Нормальный процесс не работает

  3. После некоторых операций теоретически возможно

Эта статья основана на версии Spring Boot 2.3.3.RELEASE, JDK1.8, с использованием плагина Lombok.

сомневаться

Однажды мой друг спросил меня,

«Служба вызывает частный метод другой службы, вступит ли в силу транзакция @Transactional?»

Я ответил непосредственно на месте: «Мне все еще нужно думать об этом, это определенно не будет работать!». Так он спросил: «Почему это не может работать?»

«Это не очень очевидно, как вы вызываете частный метод другого Сервиса из одного Сервиса?» Он продолжил: «Вы можете использовать отражение».

«Даже с отражением принцип @Transactional реализован на основе динамического прокси АОП, а динамический прокси не будет проксировать частные методы!».

Далее он спросил: «Разве это действительно не проксирование частных методов?».

— Эм… наверное, нет…

Сейчас затрудняюсь ответить, потому что обычно я знаю только то, что динамический прокси будет генерировать java-классы на уровне байткода, а вот как это реализовать в нем, и будет ли он обрабатывать приватные методы, я действительно не уверен.

проверять

Хотя я знаю результат, но мне все еще нужно практиковаться, сервис вызывает другие частные методы сервиса,@TransactionalСможет ли сделка вступить в силу в конце концов, посмотрим, получит ли она пощечину.

из-за@TransactionalНеудобно непосредственно видеть тест на влияние сделки . вещь.

@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частный метод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

Как видно из результатов, публичный методhello()Он успешно проксируется, но приватный метод не только не проксируется, но и не может быть вызван через рефлексию.

На самом деле это нетрудно понять, и вы также можете увидеть это из выброшенной информации об исключении:

java.lang.NoSuchMethodException: cn.zzzzbw.primary.proxy.service.impl.HelloServiceImpl$$EnhancerBySpringCGLIB$$679d418b.privateHello(java.lang.Integer)

helloServiceInjected не является реализующим классомHelloServiceImpl, но сгенерированный прокси-классомHelloServiceImpl$$EnhancerBySpringCGLIB$$6f6c17b4Если приватный метод не написан при генерации прокси-класса, то, естественно, его нельзя вызвать.

一个Service调用其他Service的private方法, @Transactional的事务是不会生效的

Такой результат можно получить из приведенных выше результатов проверки, но это всего лишь явление, и нужно наконец посмотреть на конкретный код, чтобы определить, действительно ли приватный метод теряется при проксировании, и каким образом.

Процесс генерации прокси Spring Boot

Spring BootОбщий процесс создания прокси-класса выглядит следующим образом:

[Создать экземпляр компонента] -> [Постпроцессор компонента (например,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 будет использоваться по умолчанию, но на самом деле режим прокси-сервера AOP по умолчанию в Spring 5.x по-прежнему является 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В методе сначала выполняется ряд операций (анализ выражения исполнения АОП, вызов цепочки прокси и т.д., логика внутри сложная и имеет мало отношения к нашему основному прокси-процессу, поэтому мы ее изучать не будем), Окончательный результат — это метод, предоставляемый 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<?> 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ЯвляетсяWeakCacheObject, он же кеш, аналогичный форме Map, логика внутри сложнее, поэтому подробно рассматривать не буду. Но нам нужно только знать, что если в кеше есть значение при получении, то он вернет это значение, если его нет, то вызов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 для создания класса.

Процесс генерации байт-кода прокси-класса

После серии проверок исходного кода мы, наконец, подошли к самой важной части генерации байт-кодов.Теперь давайте посмотрим, как генерируются байт-коды прокси-класса и как работать с закрытыми методами.

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. Соберите информацию о поле и информацию о методе прокси-класса, который будет сгенерирован.Здесь, в соответствии с методом, добавленным на шаге 1, будет сгенерирована реализация фактического метода прокси-класса.Например:

    Если целевой прокси-класс реализуетHelloServiceинтерфейс и реализовать его методыhello, то сгенерированный прокси-класс будет генерировать следующие формальные методы:

    public Object hello(Object... args){
        try{
            return (InvocationHandler)h.invoke(this, this.getMethod("hello"), args);
        } catch ...  
    }
    
  3. Информация, добавленная и собранная выше, встраивается в окончательные данные байт-кода класса java через поток

Прочитав этот код, теперь мы действительно можем быть уверены, что прокси-класс не будет проксировать частный метод.На шаге 1 мы знаем, что прокси-класс будет проксировать только методы equal, hashcode, toString и методы, объявленные в интерфейсе, поэтому частный метод целевого класса не будет проксироваться. Но подумайте об этом и знайте, что частные методы нельзя вызывать извне при нормальных обстоятельствах, и даже если они проксируются, их нельзя использовать, поэтому проксировать не нужно.

В заключение

выше, прочитавSpring BootДинамический прокси-процесс и исходный код реализации динамической прокси-функции JDK делают вывод, что динамический прокси-сервер не будет проксировать частный метод, поэтому@TransactionalАннотированные транзакции также не повлияют на них.

Но, взглянув на весь процесс прокси, я чувствую, что динамический прокси — это то же самое Функция динамического прокси, предоставляемая JDK.слишком неряшливый, мы можем полностью реализовать функцию динамического прокси самостоятельно, пусть@TransactionalЧастный метод аннотации также может действовать, и я тоже!

В соответствии с приведенным выше процессом исходного кода, если вы хотите реализовать частный метод прокси и сделать@TransactionalЭффект аннотации вступает в силу, затем просто вспомните процесс просмотра исходного кода следующим образом:

  1. повторно внедритьProxyGenerator.generateClassFile()метод, вывод данных байт-кода прокси-класса с помощью закрытого метода
  2. Загрузите данные байт-кода в JVM и сгенерируйте класс
  3. заменятьSpring BootВ функции динамического прокси по умолчанию замените ее нашим собственным динамическим прокси.

Эта часть находится вСлужба вызывает частные методы других служб, будет ли @Transactional вступать в силу (ниже), добро пожаловать на чтение


Оригинальный адрес:Сервис вызывает частные методы других Сервисов, вступит ли в силу @Transactional (Часть 1)