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


110

Я новичок в 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);
        }
    } 
}

Взгляните на TransactionTemplateподход: stackoverflow.com/a/52989925/355438
— Lu55

О том, почему самовызов не работает, см. 8.6 Механизмы проксирования .
— Джейсон Лоу

Ответы:


99

Это ограничение 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>

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

В мой ответ добавлена ​​конфигурация AspectJ для конкретной транзакции. Я надеюсь, что это помогает.
— Espen

10
Это хорошо! Кстати: Было бы неплохо, если бы вы отметили мой вопрос как лучший ответ, чтобы дать мне несколько баллов. (зеленая галочка)
— Espen

2
Конфигурация загрузки Spring: @EnableTransactionManagement (mode = AdviceMode.ASPECTJ)
— VinyJones

64

Проблема здесь в том, что прокси 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);
        }
    } 
}

3
Если вы все же решите пойти по этому пути (хороший ли это дизайн или нет - другой вопрос) и не используете инъекцию конструктора, убедитесь, что вы также видите этот вопрос
— Джешурун

Что делать, если UserServiceесть синглтон? Что, если это один и тот же объект?
— Ян Хонски

26

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

2
ЛУЧШИЙ ОТВЕТ !! Thx
— mjassani

2
Поправьте меня, если я ошибаюсь, но такой шаблон действительно подвержен ошибкам, хотя он работает. Это больше похоже на демонстрацию возможностей Spring, верно? Кто-то, не знакомый с поведением «this bean call», может случайно удалить самонастраиваемый bean-компонент (в конце концов, методы доступны через this.), Что может вызвать проблему, которую трудно обнаружить с первого взгляда. Он мог даже попасть в среду prod до того, как был найден).
— pidabrow

2
@pidabrow, вы правы, это огромный антипаттерн, и в первую очередь это неочевидно. Так что, если можете, вам следует избегать этого. Если вам нужно использовать метод того же класса, попробуйте использовать более мощные библиотеки АОП, такие как AspectJ
— Алмас Абдразак,

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

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

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

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


Это круто! Он избегает всех подводных камней, которые в противном случае вводит Spring с его аннотацией. Любить это!
— Фрэнк Хопкинс,

Если я расширяюсь 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
Это считается плохой практикой. Даже рекурсивное внедрение bean-компонента в себя лучше. Использование getBean (clazz) - это тесная связь и сильная зависимость от классов Spring ApplicationContext внутри вашего кода. Также получение bean-компонента по классу может не работать в случае весеннего обертывания bean-компонента (класс может быть изменен).
— Вадим Кирильчук

0

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

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


0

Вот что я делаю для небольших проектов с минимальным использованием вызовов методов в одном классе. Настоятельно рекомендуется внутренняя документация, поскольку она может показаться коллегам странной. Но он работает с синглтонами , легко тестируется, прост, быстро реализуется и избавляет меня от полноценного инструментария 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);
        }
    }
}
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.