Ведущий провинциального стрима:
Служба вызывает частный метод другой службы, вступит ли в силу @Transactional?
Нормальный процесс не работает
После некоторых операций теоретически возможно
Эта статья основана на версии 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);
}
}
Как видно из результатов, публичный методhello()
Он успешно проксируется, но приватный метод не только не проксируется, но и не может быть вызван через рефлексию.
На самом деле это нетрудно понять, и вы также можете увидеть это из выброшенной информации об исключении:
java.lang.NoSuchMethodException: cn.zzzzbw.primary.proxy.service.impl.HelloServiceImpl$$EnhancerBySpringCGLIB$$679d418b.privateHello(java.lang.Integer)
helloService
Injected не является реализующим классом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
ЯвляетсяWeakCache
Object, он же кеш, аналогичный форме 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()
Метод — это место, где реализуется реальная генерация данных байт-кода прокси-класса, в основном в три этапа:
-
Добавьте все методы, которые необходимо проксировать, поместите методы, которые необходимо проксировать (equal, hashcode, методы toString иобъявлено в интерфейсеметод) для записи некоторой важной информации.
-
Соберите информацию о поле и информацию о методе прокси-класса, который будет сгенерирован.Здесь, в соответствии с методом, добавленным на шаге 1, будет сгенерирована реализация фактического метода прокси-класса.Например:
Если целевой прокси-класс реализует
HelloService
интерфейс и реализовать его методыhello
, то сгенерированный прокси-класс будет генерировать следующие формальные методы:public Object hello(Object... args){ try{ return (InvocationHandler)h.invoke(this, this.getMethod("hello"), args); } catch ... }
-
Информация, добавленная и собранная выше, встраивается в окончательные данные байт-кода класса java через поток
Прочитав этот код, теперь мы действительно можем быть уверены, что прокси-класс не будет проксировать частный метод.На шаге 1 мы знаем, что прокси-класс будет проксировать только методы equal, hashcode, toString и методы, объявленные в интерфейсе, поэтому частный метод целевого класса не будет проксироваться. Но подумайте об этом и знайте, что частные методы нельзя вызывать извне при нормальных обстоятельствах, и даже если они проксируются, их нельзя использовать, поэтому проксировать не нужно.
В заключение
выше, прочитавSpring Boot
Динамический прокси-процесс и исходный код реализации динамической прокси-функции JDK делают вывод, что динамический прокси-сервер не будет проксировать частный метод, поэтому@Transactional
Аннотированные транзакции также не повлияют на них.
Но, взглянув на весь процесс прокси, я чувствую, что динамический прокси — это то же самое Функция динамического прокси, предоставляемая JDK.слишком неряшливый, мы можем полностью реализовать функцию динамического прокси самостоятельно, пусть@Transactional
Частный метод аннотации также может действовать, и я тоже!
В соответствии с приведенным выше процессом исходного кода, если вы хотите реализовать частный метод прокси и сделать@Transactional
Эффект аннотации вступает в силу, затем просто вспомните процесс просмотра исходного кода следующим образом:
- повторно внедрить
ProxyGenerator.generateClassFile()
метод, вывод данных байт-кода прокси-класса с помощью закрытого метода - Загрузите данные байт-кода в JVM и сгенерируйте класс
- заменять
Spring Boot
В функции динамического прокси по умолчанию замените ее нашим собственным динамическим прокси.
Эта часть находится вСлужба вызывает частные методы других служб, будет ли @Transactional вступать в силу (ниже), добро пожаловать на чтение
Оригинальный адрес:Сервис вызывает частные методы других Сервисов, вступит ли в силу @Transactional (Часть 1)