Nie działa wywołanie metody Spring @Transaction przez metodę z tej samej klasy?

110

Jestem nowy w Spring Transaction. Coś, co wydało mi się naprawdę dziwne, prawdopodobnie dobrze to zrozumiałem.

Chciałem mieć transakcję na poziomie metody i mam metodę wywołującą w tej samej klasie i wygląda na to, że nie podoba mi się to, musi być wywoływana z oddzielnej klasy. Nie rozumiem, jak to możliwe.

Jeśli ktoś ma pomysł, jak rozwiązać ten problem, byłbym bardzo wdzięczny. Chciałbym użyć tej samej klasy do wywołania metody transakcyjnej z adnotacjami.

Oto kod:

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);
        }
    } 
}
Mikrofon
źródło
Spójrz na TransactionTemplatepodejście: stackoverflow.com/a/52989925/355438
Lu55
O tym, dlaczego samo wywołanie nie działa, zobacz 8.6 Mechanizmy proxy .
Jason Law

Odpowiedzi:

99

Jest to ograniczenie Spring AOP (dynamiczne obiekty i cglib ).

Jeśli skonfigurujesz Springa, aby używał AspectJ do obsługi transakcji, twój kod będzie działał.

Prostą i prawdopodobnie najlepszą alternatywą jest refaktoryzacja kodu. Na przykład jedna klasa obsługująca użytkowników i jedna przetwarzająca każdego użytkownika. Wtedy domyślna obsługa transakcji za pomocą Spring AOP będzie działać.


Wskazówki dotyczące konfiguracji obsługi transakcji z AspectJ

Aby umożliwić Springowi używanie AspectJ do transakcji, musisz ustawić tryb na AspectJ:

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

Jeśli używasz Springa ze starszą wersją niż 3.0, musisz również dodać to do swojej konfiguracji Springa:

<bean class="org.springframework.transaction.aspectj
        .AnnotationTransactionAspect" factory-method="aspectOf">
    <property name="transactionManager" ref="transactionManager" />
</bean>
Espen
źródło
Dziękuję za informację. Na razie zrefaktoryzowałem kod, ale czy mógłbyś przesłać mi przykład za pomocą AspectJ lub podać mi przydatne linki. Z góry dziękuję. Mikrofon.
Mike
Dodano konfigurację AspectJ specyficzną dla transakcji w mojej odpowiedzi. Mam nadzieję, że to pomoże.
Espen
10
Dobre! Btw: Byłoby miło, gdybyś mógł oznaczyć moje pytanie jako najlepszą odpowiedź, aby dać mi kilka punktów. (zielony znacznik wyboru)
Espen
2
Konfiguracja wiosennego rozruchu: @EnableTransactionManagement (mode = AdviceMode.ASPECTJ)
VinyJones
64

Problem polega na tym, że serwery proxy AOP Springa nie rozszerzają się, ale raczej opakowują instancję usługi w celu przechwytywania połączeń. W efekcie każde wywołanie „this” z Twojej instancji usługi jest bezpośrednio wywoływane w tej instancji i nie może być przechwycone przez opakowujący serwer proxy (proxy nie wie nawet o takim wywołaniu). Wspomniano już o jednym rozwiązaniu. Innym fajnym rozwiązaniem byłoby po prostu wstrzyknięcie przez Spring instancji usługi do samej usługi i wywołanie metody na wstrzykniętej instancji, która będzie serwerem proxy obsługującym transakcje. Ale pamiętaj, że może to mieć również złe skutki uboczne, jeśli Twój komponent bean nie jest singletonem:

<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);
        }
    } 
}
Kai
źródło
3
Jeśli zdecydujesz się pójść tą drogą (czy to dobry projekt, czy nie, to inna sprawa) i nie używasz iniekcji konstruktora, upewnij się, że widzisz również to pytanie
Jeshurun
A jeśli UserServicema zasięg pojedynczy? A jeśli to ten sam obiekt?
Yan Khonski
26

Dzięki Spring 4 możliwe jest Self autowired

@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);
    }
}
Almas Abdrazak
źródło
2
NAJLEPSZA ODPOWIEDŹ !! Dzięki
mjassani
2
Popraw mnie, jeśli się mylę, ale taki wzór jest naprawdę podatny na błędy, chociaż działa. To bardziej jak prezentacja możliwości Springa, prawda? Ktoś, kto nie jest zaznajomiony z zachowaniem „to wywołanie fasoli”, może przypadkowo usunąć samoukładający się ziaren (w końcu metody są dostępne przez „this”), co może spowodować problem, który na pierwszy rzut oka jest trudny do wykrycia. Może nawet przedostać się do środowiska produkcyjnego, zanim został znaleziony).
pidabrow
2
@pidabrow masz rację, to ogromny anty-wzór i nie jest to po pierwsze oczywiste. Więc jeśli możesz, powinieneś tego unikać. Jeśli musisz użyć metody tej samej klasy, spróbuj użyć bardziej wydajnych bibliotek AOP, takich jak AspectJ
Almas Abdrazak
21

Zaczynając od Java 8 istnieje inna możliwość, którą wolę z powodów podanych poniżej:

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

Takie podejście ma następujące zalety:

1) Może być stosowany do metod prywatnych . Nie musisz więc przerywać hermetyzacji, upubliczniając metodę, aby spełnić ograniczenia Spring.

2) Ta sama metoda może być wywołana w ramach różnych propagacji transakcji i od wywołującego zależy wybór odpowiedniej. Porównaj te 2 linie:

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

3) Jest wyraźny, a przez to bardziej czytelny.

Bunarro
źródło
To jest świetne! Unika wszystkich pułapek, które Spring wprowadza w swoich adnotacjach w inny sposób. Kocham to!
Frank Hopkins,
Jeśli rozszerzę TransactionHandlerjako podklasę, a podklasa TransactionHandlerwywoła te dwie metody w superklasie, czy nadal będę w stanie uzyskać korzyści @Transactionalzgodnie z zamierzeniami?
tom_mai78101
6

Oto moje rozwiązanie do samodzielnego wywoływania :

public class SBMWSBL {
    private SBMWSBL self;

    @Autowired
    private ApplicationContext applicationContext;

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

    // ...
}
Hlex
źródło
0

Możesz automatycznie przypisać BeanFactory w tej samej klasie i wykonać

getBean(YourClazz.class)

Automatycznie przekaże twoją klasę i weźmie pod uwagę twoją @Transactional lub inną adnotację aop.

LionH
źródło
2
Uważa się to za złą praktykę. Nawet rekurencyjne wstrzyknięcie fasoli do siebie jest lepsze. Używanie getBean (clazz) to ścisłe powiązanie i silna zależność od sprężynowych klas ApplicationContext w kodzie. Również zbieranie fasoli według klasy może nie działać w przypadku wiosennego zawijania fasoli (klasa może ulec zmianie).
Vadim Kirilchuk
0

Problem jest związany z klasami obciążenia sprężyn i proxy. To nie zadziała, dopóki nie napiszesz swojej wewnętrznej metody / transakcji w innej klasie lub nie przejdziesz do innej klasy, a następnie ponownie przejdziesz do swojej klasy i nie napiszesz wewnętrznej metody zagnieżdżonej transkacji.

Podsumowując, wiosenne proxy nie pozwalają na scenariusze, z którymi się mierzysz. musisz napisać drugą metodę transakcji w innej klasie

Ujjwal Choudhari
źródło
0

Oto, co robię dla małych projektów z tylko marginalnym wykorzystaniem wywołań metod w tej samej klasie. Zdecydowanie zaleca się, aby dokumentacja w kodzie była dziwna dla współpracowników. Ale działa z singletonami , jest łatwy do przetestowania, prosty, szybki do osiągnięcia i oszczędza mi pełnego oprzyrządowania AspectJ. Jednak w przypadku bardziej intensywnego użytkowania radziłbym rozwiązanie AspectJ, jak opisano w odpowiedzi 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);
        }
    }
}
Mario Eis
źródło