L'appel de la méthode Spring @Transaction par la méthode dans la même classe ne fonctionne pas?

109

Je suis nouveau dans Spring Transaction. Quelque chose que j'ai trouvé vraiment étrange, je l'ai probablement bien compris.

Je voulais avoir une méthode transactionnelle autour du niveau de la méthode et j'ai une méthode d'appel dans la même classe et il semble que cela n'aime pas ça, elle doit être appelée depuis la classe séparée. Je ne comprends pas comment cela est possible.

Si quelqu'un a une idée de la façon de résoudre ce problème, je l'apprécierais grandement. Je voudrais utiliser la même classe pour appeler la méthode transactionnelle annotée.

Voici le code:

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);
        }
    } 
}
Mike
la source
Jetez un œil à l' TransactionTemplateapproche: stackoverflow.com/a/52989925/355438
Lu55
À propos des raisons pour lesquelles l'auto-invocation ne fonctionne pas, voir 8.6 Mécanismes de proxy .
Jason Law

Réponses:

99

C'est une limitation de Spring AOP (objets dynamiques et cglib ).

Si vous configurez Spring pour utiliser AspectJ pour gérer les transactions, votre code fonctionnera.

La solution la plus simple et probablement la meilleure consiste à refactoriser votre code. Par exemple, une classe qui gère les utilisateurs et une qui traite chaque utilisateur. Ensuite , la gestion des transactions par défaut avec Spring AOP fonctionnera.


Conseils de configuration pour gérer les transactions avec AspectJ

Pour permettre à Spring d'utiliser AspectJ pour les transactions, vous devez définir le mode sur AspectJ:

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

Si vous utilisez Spring avec une version antérieure à 3.0, vous devez également l'ajouter à votre configuration Spring:

<bean class="org.springframework.transaction.aspectj
        .AnnotationTransactionAspect" factory-method="aspectOf">
    <property name="transactionManager" ref="transactionManager" />
</bean>
Espen
la source
Merci pour l'information. J'ai refactoré le code pour l'instant, mais pourriez-vous s'il vous plaît m'envoyer un exemple en utilisant AspectJ ou me fournir des liens utiles. Merci d'avance. Mike.
Mike
Ajout de la configuration AspectJ spécifique à la transaction dans ma réponse. J'espère que cela aide.
Espen
10
C'est bon! Btw: Ce serait bien si vous pouviez marquer ma question comme la meilleure réponse pour me donner quelques points. (coche verte)
Espen
2
Configuration de démarrage de printemps: @EnableTransactionManagement (mode = AdviceMode.ASPECTJ)
VinyJones
64

Le problème ici est que les proxys AOP de Spring ne s'étendent pas, mais enveloppent plutôt votre instance de service pour intercepter les appels. Cela a pour effet que tout appel à "this" depuis votre instance de service est directement invoqué sur cette instance et ne peut pas être intercepté par le proxy d'encapsulation (le proxy n'est même pas au courant d'un tel appel). Une solution est déjà mentionnée. Une autre astuce serait simplement de demander à Spring d'injecter une instance du service dans le service lui-même et d'appeler votre méthode sur l'instance injectée, qui sera le proxy qui gère vos transactions. Mais sachez que cela peut également avoir de mauvais effets secondaires, si votre bean service n'est pas 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);
        }
    } 
}
Kai
la source
3
Si vous choisissez d'emprunter cette voie (que ce soit une bonne conception ou non, c'est une autre affaire) et que vous n'utilisez pas d'injection de constructeur, assurez-vous de voir également cette question
Jeshurun
Et si UserServicea une portée singleton? Et si c'était le même objet?
Yan Khonski
26

Avec Spring 4, il est possible d'auto-câblé

@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
la source
2
MEILLEURE RÉPONSE !! Thx
mjassani
2
Corrigez-moi si je me trompe, mais un tel modèle est vraiment sujet aux erreurs, bien que cela fonctionne. Cela ressemble plus à une vitrine des capacités de Spring, non? Quelqu'un qui n'est pas familier avec le comportement de "cet appel de bean" pourrait accidentellement supprimer le bean auto-câblé (les méthodes sont disponibles via "ceci" après tout), ce qui pourrait causer des problèmes difficiles à détecter à première vue. Il pourrait même atteindre l'environnement de production avant qu'il ne soit trouvé).
pidabrow le
2
@pidabrow vous avez raison, c'est un énorme anti-pattern et ce n'est pas évident en premier lieu. Donc, si vous pouvez, vous devriez l'éviter. Si vous devez utiliser une méthode de la même classe, essayez d'utiliser des bibliothèques AOP plus puissantes telles que AspectJ
Almas Abdrazak
21

À partir de Java 8, il existe une autre possibilité, que je préfère pour les raisons données ci-dessous:

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

Cette approche présente les avantages suivants:

1) Il peut être appliqué aux méthodes privées . Vous n'avez donc pas à interrompre l'encapsulation en rendant une méthode publique juste pour satisfaire les limitations de Spring.

2) La même méthode peut être appelée dans une propagation de transaction différente et il appartient à l'appelant de choisir celle qui convient. Comparez ces 2 lignes:

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

3) Il est explicite, donc plus lisible.

Bunarro
la source
C'est bien! Cela évite tous les pièges que Spring introduit avec son annotation autrement. Aimer!
Frank Hopkins
Si je m'étend en TransactionHandlertant que sous-classe et que la sous-classe fait appel à ces deux méthodes dans la TransactionHandlersuper classe, pourrai-je toujours bénéficier des avantages de @Transactionalcomme prévu?
tom_mai78101
6

Voici ma solution pour l' auto-invocation :

public class SBMWSBL {
    private SBMWSBL self;

    @Autowired
    private ApplicationContext applicationContext;

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

    // ...
}
Hlex
la source
0

Vous pouvez installer automatiquement BeanFactory dans la même classe et faire un

getBean(YourClazz.class)

Il proxifiera automatiquement votre classe et prendra en compte votre @Transactional ou autre annotation aop.

LionH
la source
2
C'est considéré comme une mauvaise pratique. Même l'injection récursive du grain en lui-même est préférable. L'utilisation de getBean (clazz) est un couplage étroit et une forte dépendance aux classes Spring ApplicationContext à l'intérieur de votre code. Obtenir bean par classe peut également ne pas fonctionner dans le cas où le haricot est enveloppé à ressort (la classe peut être modifiée).
Vadim Kirilchuk
0

Le problème est lié à la façon dont les classes de charge de ressort et les proxys. Cela ne fonctionnera pas tant que vous n'écrivez pas votre méthode / transaction interne dans une autre classe ou que vous n'allez pas dans une autre classe, puis que vous revenez à votre classe et que vous n'écrivez pas la méthode de transcation imbriquée interne.

Pour résumer, les proxys de printemps ne permettent pas les scénarios auxquels vous êtes confrontés. vous devez écrire la 2ème méthode de transaction dans une autre classe

Ujjwal Choudhari
la source
0

Voici ce que je fais pour les petits projets avec une utilisation marginale des appels de méthode dans la même classe. La documentation dans le code est fortement conseillée, car elle peut sembler étrange aux collègues. Mais cela fonctionne avec des singletons , est facile à tester, simple, rapide à réaliser et m'épargne l'instrumentation AspectJ à part entière. Cependant, pour une utilisation plus intensive, je conseillerais la solution AspectJ comme décrit dans la réponse d'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
la source