La chiamata al metodo Spring @Transaction dal metodo all'interno della stessa classe, non funziona?


109

Sono nuovo in Spring Transaction. Qualcosa che ho trovato davvero strano, probabilmente l'ho capito bene.

Volevo avere un livello di metodo transazionale e ho un metodo chiamante all'interno della stessa classe e sembra che non piaccia, deve essere chiamato dalla classe separata. Non capisco come sia possibile.

Se qualcuno ha un'idea di come risolvere questo problema, lo apprezzerei molto. Vorrei utilizzare la stessa classe per chiamare il metodo transazionale annotato.

Ecco il codice:

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

Date un'occhiata al TransactionTemplatemetodo: stackoverflow.com/a/52989925/355438
Lu55

Sul motivo per cui l'auto invocazione non funziona, vedere 8.6 Meccanismi di proxy .
Jason Law

Risposte:


99

È una limitazione di Spring AOP (oggetti dinamici e cglib ).

Se configuri Spring per utilizzare AspectJ per gestire le transazioni, il tuo codice funzionerà.

L'alternativa semplice e probabilmente migliore è il refactoring del codice. Ad esempio una classe che gestisce gli utenti e una che elabora ogni utente. Quindi la gestione delle transazioni predefinita con Spring AOP funzionerà.


Suggerimenti per la configurazione per la gestione delle transazioni con AspectJ

Per consentire a Spring di utilizzare AspectJ per le transazioni, è necessario impostare la modalità su AspectJ:

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

Se stai usando Spring con una versione precedente alla 3.0, devi aggiungerla anche alla tua configurazione Spring:

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

Grazie per l'informazione. Ho modificato il codice per ora, ma potresti inviarmi un esempio utilizzando AspectJ o fornirmi alcuni link utili. Grazie in anticipo. Mike.
Mike

Aggiunta la configurazione AspectJ specifica della transazione nella mia risposta. Spero possa essere d'aiuto.
Espen

10
Quello è buono! Btw: Sarebbe bello se potesse contrassegnare la mia domanda come la migliore risposta per darmi alcuni punti. (segno di spunta verde)
Espen

2
Configurazione dell'avvio di primavera: @EnableTransactionManagement (mode = AdviceMode.ASPECTJ)
VinyJones,

64

Il problema qui è che i proxy AOP di Spring non si estendono ma piuttosto avvolgono l'istanza del servizio per intercettare le chiamate. Ciò ha l'effetto che qualsiasi chiamata a "this" dall'interno dell'istanza del servizio viene invocata direttamente su quell'istanza e non può essere intercettata dal proxy di wrapping (il proxy non è nemmeno a conoscenza di tale chiamata). Una delle soluzioni è già menzionata. Un altro interessante sarebbe semplicemente fare in modo che Spring inietti un'istanza del servizio nel servizio stesso e chiami il tuo metodo sull'istanza iniettata, che sarà il proxy che gestisce le tue transazioni. Ma tieni presente che questo potrebbe avere anche effetti collaterali negativi, se il tuo bean di servizio non è un singleton:

<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
Se scegli di seguire questa strada (che sia un buon design o meno è un'altra questione) e non usi l'iniezione del costruttore, assicurati di vedere anche questa domanda
Jeshurun

E se UserServiceavesse l'ambito singleton? E se fosse lo stesso oggetto?
Yan Khonski

26

Con Spring 4 è possibile eseguire l'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);
    }
}

2
MIGLIORE RISPOSTA !! Thx
mjassani

2
Correggimi se sbaglio, ma un tale schema è davvero soggetto a errori, sebbene funzioni. È più come una vetrina delle capacità di Spring, giusto? Qualcuno che non ha familiarità con il comportamento di "this bean call" potrebbe rimuovere accidentalmente il bean auto-autowired (i metodi sono disponibili tramite "this." Dopotutto), il che potrebbe causare problemi difficili da rilevare a prima vista. Potrebbe persino arrivare all'ambiente di produzione prima di essere trovato).
pidabrow

2
@pidabrow hai ragione, è un enorme anti pattern e non è ovvio in primo luogo. Quindi, se puoi, dovresti evitarlo. Se devi utilizzare il metodo della stessa classe, prova a utilizzare librerie AOP più potenti come AspectJ
Almas Abdrazak

21

A partire da Java 8 c'è un'altra possibilità, che preferisco per i motivi indicati di seguito:

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

Questo approccio presenta i seguenti vantaggi:

1) Può essere applicato a metodi privati . Quindi non devi rompere l'incapsulamento rendendo pubblico un metodo solo per soddisfare i limiti di Spring.

2) Lo stesso metodo può essere chiamato all'interno di diverse propagazioni di transazioni e spetta al chiamante scegliere quello adatto. Confronta queste 2 linee:

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

3) È esplicito, quindi più leggibile.


È fantastico! Evita tutte le insidie ​​che la primavera introduce diversamente con la sua annotazione. Lo adoro!
Frank Hopkins

Se estendo TransactionHandlercome sottoclasse e la sottoclasse richiama questi due metodi nella TransactionHandlersuper classe, sarò comunque in grado di ottenere i vantaggi di @Transactionalcome previsto?
tom_mai78101

6

Questa è la mia soluzione per l' auto invocazione :

public class SBMWSBL {
    private SBMWSBL self;

    @Autowired
    private ApplicationContext applicationContext;

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

    // ...
}

0

Puoi autowired BeanFactory all'interno della stessa classe e fare un file

getBean(YourClazz.class)

Invierà automaticamente la tua classe e terrà conto della tua annotazione @Transactional o di altre annotazioni aop.


2
È considerata una cattiva pratica. Anche l'iniezione ricorsiva del bean in se stessa è migliore. L'uso di getBean (clazz) è un accoppiamento stretto e una forte dipendenza dalle classi ApplicationContext primaverili all'interno del codice. Inoltre, ottenere il bean per classe potrebbe non funzionare in caso di avvolgimento primaverile del bean (la classe potrebbe essere modificata).
Vadim Kirilchuk,

0

Il problema è legato al modo in cui le classi di carico della molla e i proxy. Non funzionerà, finché non scriverai il tuo metodo / transazione interiore in un'altra classe o non andrai in un'altra classe e poi tornerai di nuovo nella tua classe e poi scriverai il metodo di trascrizione annidato interno.

Riassumendo, i proxy primaverili non consentono gli scenari che stai affrontando. devi scrivere il secondo metodo di transazione in un'altra classe


0

Ecco cosa faccio per piccoli progetti con un utilizzo solo marginale delle chiamate di metodo all'interno della stessa classe. La documentazione in-code è vivamente consigliata, poiché potrebbe sembrare strana ai colleghi. Ma funziona con i singleton , è facile da testare, semplice, veloce da ottenere e mi risparmia la strumentazione AspectJ in piena regola. Tuttavia, per un utilizzo più intenso, consiglierei la soluzione AspectJ come descritto nella risposta di 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);
        }
    }
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.