L'attribut Spring @Transactional fonctionne-t-il sur une méthode privée?

196

Si j'ai une annotation @Transactional sur une méthode privée dans un bean Spring, l'annotation a-t-elle un effet?

Si l' @Transactionalannotation est sur une méthode publique, elle fonctionne et ouvre une transaction.

public class Bean {
  public void doStuff() {
     doPrivateStuff();
  }
  @Transactional
  private void doPrivateStuff() {

  }
}

...

Bean bean = (Bean)appContext.getBean("bean");
bean.doStuff();
Juha Syrjälä
la source

Réponses:

163

La question n'est pas privée ou publique, la question est: comment est-elle invoquée et quelle implémentation AOP vous utilisez!

Si vous utilisez (par défaut) Spring Proxy AOP, toutes les fonctionnalités AOP fournies par Spring (comme @Transational) ne seront prises en compte que si l'appel passe par le proxy. - C'est normalement le cas si la méthode annotée est invoquée depuis un autre bean.

Cela a deux implications:

  • Étant donné que les méthodes privées ne doivent pas être appelées à partir d'un autre bean (l'exception est la réflexion), leur @Transactionalannotation n'est pas prise en compte.
  • Si la méthode est publique, mais qu'elle est invoquée à partir du même bean, elle ne sera pas prise en compte non plus (cette instruction n'est correcte que si (par défaut) Spring Proxy AOP est utilisé).

@See Spring Reference: Chapitre 9.6 9.6 Mécanismes de proxy

À mon humble avis, vous devriez utiliser le mode aspectJ, au lieu des Spring Proxies, qui résoudra le problème. Et les aspects transactionnels AspectJ sont tissés même dans des méthodes privées (vérifié pour Spring 3.0).

Ralph
la source
4
Les deux points ne sont pas nécessairement vrais. La première est incorrecte - les méthodes privées peuvent être invoquées de manière réfléchie, mais la logique de découverte du proxy choisit de ne pas le faire. Le deuxième point n'est vrai que pour les proxys JDK basés sur l'interface, mais pas pour les proxys basés sur la sous-classe CGLIB.
skaffman
@skaffman: 1 - je rend ma déclaration plus précise, 2. Mais le proxy par défaut est basé sur l'interface - n'est-ce pas?
Ralph
2
Cela dépend si la cible utilise ou non des interfaces. Si ce n'est pas le cas, CGLIB est utilisé.
skaffman
canu me dire la reson ou une référence pourquoi cglib ne peut pas mais aspectj peut?
phil
1
Référence à partir du lien dans le bloc de réponses, si vous souhaitez utiliser Spring Proxies [environnement par défaut], mettez une annotation sur doStuff () et appelez doPrivateStuff () en utilisant ((Bean) AopContext.currentProxy ()). DoPrivateStuff (); Il exécutera les deux méthodes dans une même transaction si la propagation est réutilisée [environnement par défaut].
Michael Ouyang
219

La réponse à votre question est non - @Transactionaln'aura aucun effet si elle est utilisée pour annoter des méthodes privées. Le générateur de proxy les ignorera.

Ceci est documenté dans le chapitre 10.5.6 du manuel Spring :

Visibilité de la méthode et @Transactional

Lorsque vous utilisez des proxys, vous devez appliquer l' @Transactionalannotation uniquement aux méthodes avec visibilité publique. Si vous annotez des méthodes protégées, privées ou visibles par package avec l' @Transactionalannotation, aucune erreur n'est générée, mais la méthode annotée ne présente pas les paramètres transactionnels configurés. Envisagez l'utilisation d'AspectJ (voir ci-dessous) si vous devez annoter des méthodes non publiques.

skaffman
la source
Es-tu sûr de ça? Je ne m'attendrais pas à ce que cela fasse une différence.
willcodejavaforfood
que diriez-vous si le style de proxy est Cglib?
lily
32

Par défaut, l' @Transactionalattribut ne fonctionne que lors de l'appel d'une méthode annotée sur une référence obtenue à partir de applicationContext.

public class Bean {
  public void doStuff() {
    doTransactionStuff();
  }
  @Transactional
  public void doTransactionStuff() {

  }
}

Cela ouvrira une transaction:

Bean bean = (Bean)appContext.getBean("bean");
bean.doTransactionStuff();

Cela ne va pas:

Bean bean = (Bean)appContext.getBean("bean");
bean.doStuff();

Référence Spring: utilisation de @Transactional

Remarque: En mode proxy (qui est la valeur par défaut), seuls les appels de méthode «externes» entrant via le proxy seront interceptés. Cela signifie que «l'auto-invocation», c'est-à-dire une méthode dans l'objet cible appelant une autre méthode de l'objet cible, ne conduira pas à une transaction réelle au moment de l'exécution même si la méthode invoquée est marquée avec @Transactional!

Envisagez l'utilisation du mode AspectJ (voir ci-dessous) si vous vous attendez à ce que les auto-invocations soient également enveloppées de transactions. Dans ce cas, il n'y aura pas de proxy en premier lieu; à la place, la classe cible sera «tissée» (c'est-à-dire que son code d'octets sera modifié) afin de se transformer @Transactionalen comportement d'exécution sur n'importe quel type de méthode.

Juha Syrjälä
la source
Voulez-vous dire bean = new Bean () ;?
willcodejavaforfood
Nan. Si je crée des beans avec new Bean (), l'annotation ne fonctionnera jamais au moins sans utiliser Aspect-J.
Juha Syrjälä
2
Merci! Cela explique le comportement étrange que j'observais. Contre-intuitif cette restriction d'invocation de méthode interne ...
manuel aldana
J'ai appris que "seuls les appels de méthode externes arrivant via le proxy seront interceptés" à la dure
asgs
13

Oui, il est possible d'utiliser @Transactional sur des méthodes privées, mais comme d'autres l'ont mentionné, cela ne fonctionnera pas hors de la boîte. Vous devez utiliser AspectJ. Il m'a fallu un certain temps pour comprendre comment le faire fonctionner. Je partagerai mes résultats.

J'ai choisi d'utiliser le tissage au moment de la compilation au lieu du tissage au temps de chargement parce que je pense que c'est une meilleure option globale. En outre, j'utilise Java 8, vous devrez donc peut-être ajuster certains paramètres.

Tout d'abord, ajoutez la dépendance pour aspectjrt.

<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjrt</artifactId>
    <version>1.8.8</version>
</dependency>

Ajoutez ensuite le plugin AspectJ pour effectuer le tissage de bytecode réel dans Maven (ce n'est peut-être pas un exemple minimal).

<plugin>
    <groupId>org.codehaus.mojo</groupId>
    <artifactId>aspectj-maven-plugin</artifactId>
    <version>1.8</version>
    <configuration>
        <complianceLevel>1.8</complianceLevel>
        <source>1.8</source>
        <target>1.8</target>
        <aspectLibraries>
            <aspectLibrary>
                <groupId>org.springframework</groupId>
                <artifactId>spring-aspects</artifactId>
            </aspectLibrary>
        </aspectLibraries>
    </configuration>
    <executions>
        <execution>
            <goals>
                <goal>compile</goal>
            </goals>
        </execution>
    </executions>
</plugin>

Enfin, ajoutez ceci à votre classe de configuration

@EnableTransactionManagement(mode = AdviceMode.ASPECTJ)

Vous devriez maintenant pouvoir utiliser @Transactional sur des méthodes privées.

Une mise en garde à cette approche: vous devrez configurer votre IDE pour être au courant d'AspectJ sinon si vous exécutez l'application via Eclipse par exemple, cela peut ne pas fonctionner. Assurez-vous de tester par rapport à une construction Maven directe comme test de santé mentale.

James Watkins
la source
si la méthode de proxy est cglib, il n'est pas nécessaire d'implémenter une interface dont la méthode devrait être publique, alors il est capable d'utiliser @Transactional sur des méthodes privées?
lily
Oui, cela fonctionne sur des méthodes privées, et sans interfaces! Tant qu'AspectJ est correctement configuré, il garantit essentiellement les décorateurs de méthodes de travail. Et user536161 a souligné dans sa réponse que cela fonctionnerait même sur les auto-invocations. C'est vraiment cool et juste un tout petit peu effrayant.
James Watkins
12

Si vous devez encapsuler une méthode privée dans une transaction et que vous ne souhaitez pas utiliser aspectj, vous pouvez utiliser TransactionTemplate .

@Service
public class MyService {

    @Autowired
    private TransactionTemplate transactionTemplate;

    private void process(){
        transactionTemplate.execute(new TransactionCallbackWithoutResult() {
            @Override
            protected void doInTransactionWithoutResult(TransactionStatus status) {
                processInTransaction();
            }
        });

    }

    private void processInTransaction(){
        //...
    }

}
loonis
la source
Bon pour montrer l' TransactionTemplateutilisation, mais veuillez appeler cette deuxième méthode ..RequiresTransactionplutôt que ..InTransaction. Nommez toujours les choses comme vous aimeriez les lire un an plus tard. Je dirais également que si cela nécessite vraiment une deuxième méthode privée: soit mettez son contenu directement dans l' executeimplémentation anonyme, soit si cela devient compliqué, cela pourrait être une indication de diviser l'implémentation en un autre service que vous pouvez ensuite annoter @Transactional.
Bloqué le
@Stuck, la 2ème méthode n'est certes pas nécessaire, mais elle répond à la question d'origine qui est de savoir comment appliquer une transaction printanière sur une méthode privée
loonis
oui, j'ai déjà voté pour la réponse, mais je voulais partager un certain contexte et des réflexions sur la façon de l'appliquer, car je pense que du point de vue de l'architecture, cette situation est une indication potentielle d'un défaut de conception.
Coincé
5

Spring Docs explique que

En mode proxy (qui est la valeur par défaut), seuls les appels de méthode externes entrant via le proxy sont interceptés. Cela signifie que l'auto-invocation, en effet, une méthode dans l'objet cible appelant une autre méthode de l'objet cible, ne conduira pas à une transaction réelle au moment de l'exécution même si la méthode invoquée est marquée avec @Transactional.

Envisagez l'utilisation du mode AspectJ (voir l'attribut mode dans le tableau ci-dessous) si vous vous attendez à ce que les auto-invocations soient également enveloppées de transactions. Dans ce cas, il n'y aura pas de mandataire en premier lieu; à la place, la classe cible sera tissée (c'est-à-dire que son code d'octets sera modifié) afin de transformer @Transactional en comportement d'exécution sur n'importe quel type de méthode.

Un autre moyen est l'utilisateur BeanSelfAware

user536161
la source
pourriez-vous ajouter une référence à BeanSelfAware? Cela ne ressemble pas à une classe de printemps
asgs
@asgs Supposons qu'il s'agit d'une auto-injection (fournissez un bean avec une instance de lui-même enveloppé dans un proxy). Vous pouvez voir des exemples dans stackoverflow.com/q/3423972/355438 .
Lu55
3

La réponse est non. Veuillez consulter Spring Reference: Using @Transactional :

L' @Transactionalannotation peut être placée avant une définition d'interface, une méthode sur une interface, une définition de classe ou une méthode publique sur une classe

lierre
la source
1

De la même manière que @loonis a suggéré d'utiliser TransactionTemplate, on peut utiliser ce composant d'assistance (Kotlin):

@Component
class TransactionalUtils {
    /**
     * Execute any [block] of code (even private methods)
     * as if it was effectively [Transactional]
     */
    @Transactional
    fun <R> executeAsTransactional(block: () -> R): R {
        return block()
    }
}

Usage:

@Service
class SomeService(private val transactionalUtils: TransactionalUtils) {

    fun foo() {
        transactionalUtils.executeAsTransactional { transactionalFoo() }
    }

    private fun transactionalFoo() {
        println("This method is executed within transaction")
    }
}

Je ne sais pas si TransactionTemplateréutiliser la transaction existante ou non, mais ce code le fait certainement.

Lu55
la source