Вызов метода Spring @Transaction методом в том же классе не работает?

Я новичок в Spring Transaction. Что-то, что я нашел действительно странным, наверное, я правильно понял.

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

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

Вот код:

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);
        }
    } 
}

person Mike    schedule 06.08.2010    source источник
comment
Взгляните на подход TransactionTemplate: stackoverflow.com/a/52989925/355438   -  person Lu55    schedule 06.05.2019
comment
О том, почему самостоятельный вызов не работает, см. 8.6 Механизмы прокси.   -  person Jason Law    schedule 08.11.2019


Ответы (9)


Это ограничение Spring AOP (динамические объекты и cglib).

Если вы настроите 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>
person Espen    schedule 07.08.2010
comment
Спасибо за информацию. На данный момент я реорганизовал код, но не могли бы вы прислать мне пример с использованием AspectJ или предоставить мне несколько полезных ссылок. Заранее спасибо. Майк. - person Mike; 09.08.2010
comment
В мой ответ добавлена ​​конфигурация AspectJ для конкретной транзакции. Я надеюсь, что это помогает. - person Espen; 10.08.2010
comment
Это хорошо! Кстати: Было бы неплохо, если бы вы отметили мой вопрос как лучший ответ, чтобы дать мне несколько баллов. (зеленая галочка) - person Espen; 16.08.2010
comment
Конфигурация загрузки Spring: @EnableTransactionManagement (mode = AdviceMode.ASPECTJ) - person VinyJones; 11.05.2020

Проблема здесь в том, что прокси-серверы Spring AOP не расширяют, а скорее обертывают ваш экземпляр службы для перехвата вызовов. Это приводит к тому, что любой вызов this из вашего экземпляра службы напрямую вызывается в этом экземпляре и не может быть перехвачен прокси-сервером-оболочкой (прокси даже не знает ни о каком таком вызове). Одно из решений уже упоминалось. Еще один интересный вариант - просто заставить 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);
        }
    } 
}
person Kai    schedule 05.10.2010
comment
Если вы все же решите пойти по этому пути (другой вопрос, хороший ли это дизайн) и не используете инъекцию конструктора, убедитесь, что вы также видите этот вопрос - person Jeshurun; 12.04.2012
comment
Что, если UserService имеет одноэлементную область видимости? Что, если это один и тот же объект? - person Yan Khonski; 30.05.2019

В 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 call userRepository
    }
}

@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. Его можно применять к частным методам. Таким образом, вам не нужно нарушать инкапсуляцию, делая метод общедоступным, чтобы удовлетворить ограничения Spring.

  2. Один и тот же метод может быть вызван в рамках разных транзакций, и вызывающий абонент должен выбрать подходящий. Сравните эти 2 строки:

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

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

  3. Он явный, поэтому более читаемый.

person Bunarro    schedule 27.05.2019
comment
Отлично! Он избегает всех подводных камней, которые в противном случае вводит Spring с его аннотацией. Любить это! - person Frank Hopkins; 24.07.2020
comment
Если я расширю TransactionHandler как подкласс, а подкласс вызовет эти два метода в суперклассе TransactionHandler, смогу ли я получить преимущества @Transactional, как задумано? - person tom_mai78101; 28.07.2020
comment
Звучит замечательно! Интересно, есть ли какие-то оговорки? - person ch271828n; 04.09.2020
comment
очень элегантная и умная идея ... спасибо. - person Muhammad Hewedy; 10.02.2021
comment
Превосходно. Я тоже использовал это решение, с небольшой разницей: я назвал методы в TransactionHandler runInTransactionSupplier и runInNewTransactionSupplier. Это оставляет открытой возможность для добавления в дальнейшем аналогичных, но недействительных методов возврата в TransactionHandler. - person burebista; 11.03.2021
comment
@burebista, вы делаете это неправильно, можно определить два метода с одинаковым именем, один из которых принимает поставщика и возвращает T, а другой принимает runnable и возвращает void. - person roma2341; 12.07.2021

С Spring 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);
    }
}
person Almas Abdrazak    schedule 19.03.2018
comment
ЛУЧШИЙ ОТВЕТ !! Спасибо - person mjassani; 28.02.2019
comment
Поправьте меня, если я ошибаюсь, но такой шаблон действительно подвержен ошибкам, хотя он работает. Это больше похоже на демонстрацию возможностей Spring, верно? Кто-то, не знакомый с этим поведением вызова bean-компонента, может случайно удалить самонастраиваемый bean-компонент (в конце концов, методы доступны через this.), Что может вызвать проблему, которую трудно обнаружить на первый взгляд. Он мог даже попасть в среду prod до того, как был найден). - person pidabrow; 16.06.2020
comment
@pidabrow, вы правы, это огромный антипаттерн, и в первую очередь это неочевидно. Так что, если вы можете, вам следует избегать этого. Если вам нужно использовать метод того же класса, попробуйте использовать более мощные библиотеки АОП, такие как AspectJ. - person Almas Abdrazak; 16.06.2020

Это мое решение для самостоятельного вызова:

public class SBMWSBL {
    private SBMWSBL self;

    @Autowired
    private ApplicationContext applicationContext;

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

    // ...
}
person Hlex    schedule 04.12.2016

Вы можете автоматически подключить BeanFactory внутри того же класса и выполнить

getBean(YourClazz.class)

Он автоматически проксифицирует ваш класс и примет во внимание вашу @Transactional или другую аннотацию aop.

person LionH    schedule 04.11.2014
comment
Это считается плохой практикой. Лучше даже рекурсивное внедрение bean-компонента в самого себя. Использование getBean (clazz) - это тесная связь и сильная зависимость от классов Spring ApplicationContext внутри вашего кода. Также получение bean-компонента по классу может не работать в случае весеннего обертывания bean-компонента (класс может быть изменен). - person Vadim Kirilchuk; 28.09.2015

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

@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);
        }
    }
}
person Mario Eis    schedule 22.04.2020

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

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

person Ujjwal Choudhari    schedule 06.07.2018

Нет смысла использовать AspectJ или другие способы. Достаточно просто использовать АОП. Итак, мы можем добавить @Transactional к addUsers(List<User> users), чтобы решить текущую проблему.

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

        }
    }

    @Transactional
    public boolean addUsers(List<User> users) {
        for (User user : users) {
            addUser(user.getUserName, user.getPassword);
        }
    } 
}
person Junyeong Yu    schedule 23.06.2021