Вызов метода 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) Он явный, поэтому более читаемый.

Bunarro
источник
Это круто! Он избегает всех подводных камней, которые в противном случае вводит 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);
    }

    // ...
}
Hlex
источник
0

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

getBean(YourClazz.class)

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

LionH
источник
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);
        }
    }
}
Марио Эйс
источник