Виклик методу Spring @Transaction методом у межах одного класу не працює?


109

Я новачок у весняній транзакції. Щось, що мені здалося дивним, напевно, я це правильно зрозумів.

Я хотів мати транзакційний рівень на рівні методу, і у мене є метод виклику в межах одного класу, і здається, що це не подобається, його потрібно викликати з окремого класу. Я не розумію, як це можливо.

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

Ось код:

public class UserService {

    @Transactional
    public boolean addUser(String userName, String password) {
        try {
            // call DAO layer and adds to database.
        } catch (Throwable e) {
            TransactionAspectSupport.currentTransactionStatus()
                    .setRollbackOnly();

        }
    }

    public boolean addUsers(List<User> users) {
        for (User user : users) {
            addUser(user.getUserName, user.getPassword);
        }
    } 
}

Подивіться на TransactionTemplateпідході: stackoverflow.com/a/52989925/355438
Lu55 від

Про те, чому самостійне виклик не працює, див. 8.6 Механізми передачі інформації .
Закон Джейсона

Відповіді:


99

Це обмеження Spring AOP (динамічних об'єктів та підключень ).

Якщо ви налаштуєте Spring для використання AspectJ для обробки транзакцій, ваш код буде працювати.

Проста і, мабуть, найкраща альтернатива - це рефакторинг коду. Наприклад, один клас, який обробляє користувачів, і клас, який обробляє кожного користувача. Тоді обробка транзакцій за замовчуванням з Spring AOP буде працювати.


Поради щодо конфігурації для обробки транзакцій з AspectJ

Щоб увімкнути Spring для використання AspectJ для транзакцій, потрібно встановити режим AspectJ:

<tx:annotation-driven mode="aspectj"/>

Якщо ви використовуєте Spring із старішою версією 3.0, ви також повинні додати це до своєї конфігурації Spring:

<bean class="org.springframework.transaction.aspectj
        .AnnotationTransactionAspect" factory-method="aspectOf">
    <property name="transactionManager" ref="transactionManager" />
</bean>

Спасибі за інформацію. Наразі я відновив код, але ви можете, будь ласка, надіслати мені приклад за допомогою AspectJ або надати мені корисні посилання. Заздалегідь спасибі. Майк.
Майк

У мою відповідь додана специфічна для транзакцій конфігурація AspectJ. Я сподіваюся, що це допомагає.
Еспен

10
Добре! Btw: Було б добре, якщо ви можете позначити моє запитання як найкращу відповідь, щоб дати мені деякі моменти. (зелена галочка)
Espen

2
Конфігурація весняного завантаження: @EnableTransactionManagement (режим = AdviceMode.ASPECTJ)
VinyJones

64

Проблема тут полягає в тому, що проксі-сервери AOP Spring не поширюються, а просто обертають ваш примірник послуги для перехоплення викликів. Це призводить до того, що будь-який виклик "цього" з вашого екземпляра служби безпосередньо викликається в цьому екземплярі і не може бути перехоплений прокси-сервером (проксі навіть не знає про такий виклик). Одне рішення вже згадується. Ще одним елегантним було б просто запросити Spring ввести екземпляр служби в саму службу і викликати ваш метод на ін'єкційному екземплярі, який буде проксі-сервером, який обробляє ваші транзакції. Але майте на увазі, що це може мати і погані побічні ефекти, якщо ваша послуга не одиночна:

<bean id="userService" class="your.package.UserService">
  <property name="self" ref="userService" />
    ...
</bean>

public class UserService {
    private UserService self;

    public void setSelf(UserService self) {
        this.self = self;
    }

    @Transactional
    public boolean addUser(String userName, String password) {
        try {
        // call DAO layer and adds to database.
        } catch (Throwable e) {
            TransactionAspectSupport.currentTransactionStatus()
                .setRollbackOnly();

        }
    }

    public boolean addUsers(List<User> users) {
        for (User user : users) {
            self.addUser(user.getUserName, user.getPassword);
        }
    } 
}

3
Якщо ви вирішите піти цим маршрутом (чи це хороший дизайн чи ні - це інша справа) і не використовуєте конструкторські інжекції, переконайтесь, що ви також бачите це запитання
Jeshurun

Що робити, якщо UserServiceмає одиночну область застосування? Що робити, якщо це один і той же об’єкт?
Ян Хонський

26

З весною 4 можна самостійно провести електропроводку

@Service
@Transactional
public class UserServiceImpl implements UserService{
    @Autowired
    private  UserRepository repository;

    @Autowired
    private UserService userService;

    @Override
    public void update(int id){
       repository.findOne(id).setName("ddd");
    }

    @Override
    public void save(Users user) {
        repository.save(user);
        userService.update(1);
    }
}

2
НАЙКРАЩИЙ ВІДПОВІДЬ !! Thx
mjassani

2
Виправте мене, якщо я помиляюся, але така модель дійсно схильна до помилок, хоча працює. Це більше схоже на вітрину можливостей Spring, правда? Хтось, не знайомий з поведінкою "цього дзвінка", може випадково видалити самозарядний пристрій (методи доступні через "це". Зрештою), що може спричинити проблеми, які важко виявити на перший погляд. Він навіть міг потрапити в середовище prod, перш ніж його знайдуть).
pidabrow

2
@pidabrow Ви маєте рацію, це величезна анти-модель, і це не очевидно в першу чергу. Тож, якщо зможете, слід уникати цього. Якщо вам доведеться використовувати метод того ж класу, тоді спробуйте використовувати більш потужні бібліотеки AOP, такі як AspectJ
Almas Abdrazak

21

Починаючи з Java 8, є ще одна можливість, яку я віддаю перевагу з наведених нижче причин:

@Service
public class UserService {

    @Autowired
    private TransactionHandler transactionHandler;

    public boolean addUsers(List<User> users) {
        for (User user : users) {
            transactionHandler.runInTransaction(() -> addUser(user.getUsername, user.getPassword));
        }
    }

    private boolean addUser(String username, String password) {
        // TODO
    }
}

@Service
public class TransactionHandler {

    @Transactional(propagation = Propagation.REQUIRED)
    public <T> T runInTransaction(Supplier<T> supplier) {
        return supplier.get();
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public <T> T runInNewTransaction(Supplier<T> supplier) {
        return supplier.get();
    }
}

Цей підхід має такі переваги:

1) Це може застосовуватися до приватних методів. Таким чином, вам не доведеться порушувати інкапсуляцію шляхом оприлюднення методу просто для задоволення обмежень весни.

2) Один і той же метод може бути застосований під час розповсюдження різних транзакцій, і той, хто телефонує, повинен вибрати відповідний. Порівняйте ці 2 рядки:

transactionHandler.runInTransaction(() -> userService.addUser(user.getUserName, user.getPassword));
transactionHandler.runInNewTransaction(() -> userService.addUser(user.getUserName, user.getPassword));

3) Він явний, таким чином, більш читабельний.


Це чудово! Це дозволяє уникнути всіх підводних каменів, які весна вводить своїм поясненням інакше. Любіть це!
Френк Хопкінс

Якщо я розширююсь TransactionHandlerяк підклас, і підклас вимагає цих двох методів у TransactionHandlerсуперкласі, чи зможу я отримати переваги за @Transactionalпризначенням?
tom_mai78101

6

Це моє рішення для самостійного виклику :

public class SBMWSBL {
    private SBMWSBL self;

    @Autowired
    private ApplicationContext applicationContext;

    @PostConstruct
    public void postContruct(){
        self = applicationContext.getBean(SBMWSBL.class);
    }

    // ...
}

0

Ви можете встановити автоматичний BeanFactory всередині одного класу і зробити це

getBean(YourClazz.class)

Він автоматично проксифікує ваш клас та врахує вашу анотацію @Transactional або іншу aop.


2
Це вважається поганою практикою. Навіть вводити квасолю рекурсивно в себе краще. Використання getBean (clazz) - це тісне з'єднання і сильна залежність від весняних класів ApplicationContext всередині вашого коду. Крім того, отримання квасолі за класом може не працювати в разі пружинного загортання квасолі (клас може бути змінено).
Вадим Кирильчук

0

Питання пов’язане з тим, як весняні класи навантажують та проксі. Це не спрацює, доки ви не вкажете свій внутрішній метод / транзакцію в інший клас або перейдете до іншого класу, а потім знову прийшли до свого класу, а потім напишіть внутрішній спосіб вкладеного переходу.

Підводячи підсумок, проксі-проксі не дозволяє використовувати сценарії, з якими ви стикаєтесь. вам потрібно записати другий метод транзакції в інший клас


0

Ось що я роблю для малих проектів із лише граничним використанням викликів методів у межах одного класу. Навчальна документація настійно рекомендується, оскільки це може виглядати дивно колегам. Але це працює з одинарними кнопками , легко перевірити, простий, швидкий для досягнення та пощадить мене повноцінно продуваним інструментом AspectJ. Однак для більш важкого використання я б порадив рішення AspectJ, як описано у відповіді Espens.

@Service
@Scope(proxyMode = ScopedProxyMode.TARGET_CLASS)
class PersonDao {

    private final PersonDao _personDao;

    @Autowired
    public PersonDao(PersonDao personDao) {
        _personDao = personDao;
    }

    @Transactional
    public void addUser(String username, String password) {
        // call database layer
    }

    public void addUsers(List<User> users) {
        for (User user : users) {
            _personDao.addUser(user.getUserName, user.getPassword);
        }
    }
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.