Spring: Изучение причины, по которой онлайн-аннотация транзакции @Transational не вступает в силу

Java задняя часть Spring API

Симптом

На прошлой неделе мой коллега обнаружил, что есть проблема с онлайн-кодом распределенной блокировки, реализованной mySql.Код упрощен следующим образом:

@Controller
class XService {
    @Autowired
    private YService yService;
    public void doOutside(){
        this.doInside(); //或者直接doInside();效果是一样的
    }
    @Transactional
    private void doInside(){
        //do sql statement
    }
}
@Controller
class Test {
    @Autowired
    private XService xService;
    public void test(){
        xService.doOutside();
    }
}

фактическое исполнениеtest()позже узналdoInside()Процесс выполнения Sql не выполняетсяSpring Transaction ManagerУправляй этим.

Найдены две проблемы

  1. вызывается в методе экземпляра@TransactionalПри аннотировании другого метода, помеченного аннотацией, и оба метода принадлежат одному и тому же классу, транзакция не вступит в силу.
  2. вызванный@TransactionalДля непубличных методов, помеченных аннотациями, транзакции не вступят в силу.

Сначала просмотрите соответствующие знания: Spring AOP, динамический прокси JDK, CGLIB, AspectJ, @Aspect.

@TransactionalПринцип реализации заключается в том, чтобы обернуть код уровня диспетчера транзакций (то есть вставить аспект) через Spring AOP вне бизнес-метода Это обычная практика в шаблоне проектирования Java для улучшения прокси-класса через прокси.

В основе Spring AOP есть две реализации: динамический прокси JDK и CGLIB. Принцип первого заключается в отражении JDK и поддерживает только прокси интерфейса Java; принцип второго — в наследовании (extend) и переопределение (override) и, таким образом, может поддерживать прокси для обычных классов Java. Оба метода являются динамическими прокси, то есть прокси генерируются в режиме реального времени во время выполнения.

Из-за ограничений JVM CGLIB не может заменить уже загруженный байт-код проксируемого класса, он может только сгенерировать и загрузить новый подкласс в качестве прокси-класса, а байт-код проксируемого класса все еще существует в JVM.

В отличие от первых двух, AspectJ является реализацией статического прокси, то есть он напрямую изменяет байт-код файла прокси-класса во время компиляции или при загрузке класса вместо создания прокси в реальном времени во время выполнения. Следовательно, этот метод требует дополнительной поддержки компилятора или агента JVM, а Spring и AspectJ также можно использовать вместе с некоторой конфигурацией.

@Aspect изначально был формой аннотации Java, представленной AspectJ. Позже Spring AOP также поддерживал использование этой формы для выражения аспектов, но на самом деле базовая реализация не имеет ничего общего с AspectJ. В конце концов, Spring AOP — это динамический прокси, который несовместим со статическим прокси.

дальнейший анализ

Поскольку менеджер транзакций не действует, нам сначала нужно определить проблему:thisНа какой объект он указывает, на нерасширенный XService или на расширенный XService? А также возможно ли, что расширенный экземпляр и метод были вызваны, но диспетчер транзакций не действует по другим причинам?

Вспомним основы Java,thisПредставляет текущий экземпляр класса, затем ключом является определение того, что экземпляр класса является нерасширенной службой XService (далее именуемойXService), или XService, расширенный CGLIB (далее именуемыйXService?Cglib).

В Test переменная экземпляра класса XService — это bean-компонент, управляемый средой Spring.test()когда, согласно@AutowiredАннотация вводится соответствующим образом, поэтому экземпляр XService фактическиXService?CglibвместоXService. Код расширенного класса можно упростить следующим образом:

class XService?Cglib extend XService {
    @Override
    public doInside(){
        //开始事务的增强代码
        super.doInside();
        //结束事务的增强代码
    }
}

при исполненииXService?Cglib.doOutside()Когда подкласс не переопределяет одноименный метод суперкласса, суперкласс фактически выполняется.XServiceизdoOutside()метод, поэтому при его выполненииthis.doInside()не будет выполнять нерасширенный родительский классdoInside(), поэтому диспетчер транзакций дает сбой.

Эта проблема широко распространена в Spring AOP, т.е.вызывающий сам себя, что по сути является слепым пятном, которое не может быть решено динамическими прокси-серверами и может быть решено только статическими прокси-серверами, такими как AspectJ.

Вторая проблема — Spring AOPРасширения непубличных методов не поддерживаются, подобно самовызову, это также слепое пятно, которое динамический прокси не может решить.

Хотя CGLIB может поддерживать расширение общедоступных, защищенных методов и методов уровня пакета посредством наследования, поскольку динамический прокси-сервер JDK должен передавать интерфейс Java и может поддерживать только методы общедоступного уровня, Spring AOP должен отменить поддержку непубличных методов.

Решение «самовызов»

1. Лучше всего вызывать его методы вне проксируемого класса

2. Самостоятельная инъекция, начиная с Spring 4.3

@Controller
class XService {
    @Autowired
    private YService yService;
    @Autowired
    private XService xService;
    public void doOutside(){
        xService.doInside();//从this换成了xService
    }
    @Transactional
    private void doInside(){
        //do sql statement
    }
}
@Controller
class Test {
    @Autowired
    private XService xService;
    public void test(){
        xService.doOutside();
    }
}

Поскольку переменная xService вводится Spring, она фактически указывает наXService?Cglibобъект,xService.doInside()Следовательно, он также может правильно указывать на усиленный метод.

Неверное решение: преобразовать в форму интерфейса Java

@Controller
class XService implements IXService {
    @Autowired
    private YService yService;
    @Override
    public void doOutside(){
        this.doInside();
    }
    @Transactional
    private void doInside(){
        //do sql statement
    }
}
@Controller
class Test {
    @Autowired
    private IXService iXService;
    public test(){
        iXService.doOutside();
    }
}

Причина в том, что я неправильно понял принцип, что транзакция раньше не вступала в силу: если нет настройки в xml, используется только CGLIB,@TransactionalМожно использовать только динамический прокси-сервер JDK, поэтому он не вступит в силу, если прокси-сервер не используется в интерфейсе Java.

На самом деле это все еще не может избежать проблемы самовызова, потому что это обычная проблема с динамическими прокси, будь то динамический прокси JDK или динамический прокси CGLIB.

Суммировать

Будьте осторожны при использовании Spring AOP.Если вы объявляете AOP в виде аннотаций, убедитесь, что расширенный метод вызывается вне прокси-класса.

Reference

  1. Принцип реализации Spring AOP и приложение CGLIB
  2. Вопрос о проблеме прокси-сервера защищенного метода перехвата Spring
  3. Тщательное понимание использования @transactional в Spring
  4. Spring @Transactional принцип и использование