Spring @Transaction Methodenaufruf von der Methode innerhalb derselben Klasse, funktioniert nicht?

108

Ich bin neu bei Spring Transaction. Etwas, das ich wirklich seltsam fand, wahrscheinlich habe ich das richtig verstanden.

Ich wollte eine Transaktion auf Methodenebene haben und ich habe eine Aufrufermethode innerhalb derselben Klasse und es scheint, dass es nicht so ist, es muss von der separaten Klasse aufgerufen werden. Ich verstehe nicht, wie das möglich ist.

Wenn jemand eine Idee hat, wie dieses Problem behoben werden kann, würde ich mich sehr freuen. Ich möchte dieselbe Klasse verwenden, um die mit Anmerkungen versehene Transaktionsmethode aufzurufen.

Hier ist der 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
quelle
Schauen Sie sich den TransactionTemplateAnsatz an: stackoverflow.com/a/52989925/355438
Lu55
Informationen dazu, warum Selbstaufruf nicht funktioniert, finden Sie unter 8.6 Proxy-Mechanismen .
Jason Law

Antworten:

99

Dies ist eine Einschränkung von Spring AOP (dynamische Objekte und cglib ).

Wenn Sie Spring so konfigurieren, dass AspectJ zur Abwicklung der Transaktionen verwendet wird, funktioniert Ihr Code.

Die einfache und wahrscheinlich beste Alternative besteht darin, Ihren Code umzugestalten. Zum Beispiel eine Klasse, die Benutzer behandelt, und eine, die jeden Benutzer verarbeitet. Dann funktioniert die Standardtransaktionsbehandlung mit Spring AOP.


Konfigurationstipps für die Abwicklung von Transaktionen mit AspectJ

Damit Spring AspectJ für Transaktionen verwenden kann, müssen Sie den Modus auf AspectJ einstellen:

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

Wenn Sie Spring mit einer älteren Version als 3.0 verwenden, müssen Sie dies auch zu Ihrer Spring-Konfiguration hinzufügen:

<bean class="org.springframework.transaction.aspectj
        .AnnotationTransactionAspect" factory-method="aspectOf">
    <property name="transactionManager" ref="transactionManager" />
</bean>
Espen
quelle
Danke für die Information. Ich habe den Code vorerst überarbeitet, aber können Sie mir bitte ein Beispiel mit AspectJ senden oder mir einige hilfreiche Links geben? Danke im Voraus. Mike.
Mike
Transaktionsspezifische AspectJ-Konfiguration in meiner Antwort hinzugefügt. Ich hoffe, es hilft.
Espen
10
Das ist gut! Übrigens: Es wäre schön, wenn Sie meine Frage als die beste Antwort markieren könnten, um mir einige Punkte zu geben. (grünes Häkchen)
Espen
2
Spring Boot-Konfiguration: @EnableTransactionManagement (mode = AdviceMode.ASPECTJ)
VinyJones
64

Das Problem hierbei ist, dass die AOP-Proxys von Spring nicht erweitert werden, sondern Ihre Dienstinstanz umschließen, um Anrufe abzufangen. Dies hat zur Folge, dass jeder Aufruf von "this" innerhalb Ihrer Dienstinstanz direkt auf dieser Instanz aufgerufen wird und vom Wrapping-Proxy nicht abgefangen werden kann (der Proxy ist sich eines solchen Aufrufs nicht einmal bewusst). Eine Lösung ist bereits erwähnt. Eine weitere gute Möglichkeit wäre, Spring einfach eine Instanz des Dienstes in den Dienst selbst einfügen zu lassen und Ihre Methode für die injizierte Instanz aufzurufen, die der Proxy ist, der Ihre Transaktionen verarbeitet. Beachten Sie jedoch, dass dies auch schlimme Nebenwirkungen haben kann, wenn Ihre Service-Bean kein Singleton ist:

<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
quelle
3
Wenn Sie sich für diesen Weg entscheiden (ob dies ein gutes Design ist oder nicht, ist eine andere Sache) und keine Konstruktorinjektion verwenden, stellen Sie sicher, dass Sie auch diese Frage sehen
Jeshurun
Was ist, wenn UserServiceSingleton-Bereich hat? Was ist, wenn es dasselbe Objekt ist?
Yan Khonski
26

Mit Spring 4 ist es möglich, sich selbst zu verdrahten

@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
quelle
2
BESTE ANTWORT !! Thx
mjassani
2
Korrigieren Sie mich, wenn ich falsch liege, aber ein solches Muster ist wirklich fehleranfällig, obwohl es funktioniert. Es ist eher ein Schaufenster der Frühlingsfähigkeiten, oder? Jemand, der mit dem Verhalten "Dieser Bean-Aufruf" nicht vertraut ist, entfernt möglicherweise versehentlich die selbstautowire-Bean (die Methoden sind schließlich über "this" verfügbar), was zu Problemen führen kann, die auf den ersten Blick schwer zu erkennen sind. Es könnte sogar in die Produktumgebung gelangen, bevor es gefunden wurde.
Pidabrow
2
@pidabrow du hast recht, es ist ein riesiges Anti-Muster und es ist überhaupt nicht offensichtlich. Wenn du kannst, solltest du es vermeiden. Wenn Sie eine Methode derselben Klasse verwenden müssen, versuchen Sie, leistungsfähigere AOP-Bibliotheken wie AspectJ
Almas Abdrazak
20

Ab Java 8 gibt es eine andere Möglichkeit, die ich aus den folgenden Gründen bevorzuge:

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

Dieser Ansatz hat folgende Vorteile:

1) Es kann auf private Methoden angewendet werden. Sie müssen die Kapselung also nicht unterbrechen, indem Sie eine Methode veröffentlichen, um die Spring-Einschränkungen zu erfüllen.

2) Dieselbe Methode kann innerhalb unterschiedlicher Transaktionsausbreitung aufgerufen werden, und es ist Sache des Aufrufers , die geeignete auszuwählen. Vergleichen Sie diese 2 Zeilen:

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

3) Es ist explizit und somit besser lesbar.

Bunarro
quelle
Das ist toll! Es vermeidet alle Fallstricke, die Spring sonst mit seiner Anmerkung einführt. Liebe es!
Frank Hopkins
Wenn ich TransactionHandlerals Unterklasse erweitere und die Unterklasse diese beiden Methoden in der Superklasse aufruft TransactionHandler, kann ich dann trotzdem die @Transactionalbeabsichtigten Vorteile nutzen ?
tom_mai78101
6

Dies ist meine Lösung für den Selbstaufruf :

public class SBMWSBL {
    private SBMWSBL self;

    @Autowired
    private ApplicationContext applicationContext;

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

    // ...
}
Hlex
quelle
0

Sie können BeanFactory innerhalb derselben Klasse automatisch verdrahten und a

getBean(YourClazz.class)

Es wird Ihre Klasse automatisch proximieren und Ihre @ Transactional- oder andere aop-Annotation berücksichtigen.

LionH
quelle
2
Es wird als schlechte Praxis angesehen. Sogar das rekursive Injizieren der Bohne in sich selbst ist besser. Die Verwendung von getBean (clazz) ist eine enge Kopplung und eine starke Abhängigkeit von den ApplicationContext-Klassen im Frühjahr in Ihrem Code. Auch das Abrufen von Bohnen nach Klassen funktioniert möglicherweise nicht, wenn die Bohnen im Frühjahr umwickelt werden (die Klasse kann geändert werden).
Vadim Kirilchuk
0

Das Problem hängt damit zusammen, wie Federbelastungsklassen und Proxys. Es wird nicht funktionieren, bis Sie Ihre innere Methode / Transaktion in eine andere Klasse schreiben oder zu einer anderen Klasse gehen und dann wieder zu Ihrer Klasse kommen und dann die innere verschachtelte Transkationsmethode schreiben.

Zusammenfassend lässt sich sagen, dass Spring Proxys die Szenarien, mit denen Sie konfrontiert sind, nicht zulassen. Sie müssen die 2. Transaktionsmethode in eine andere Klasse schreiben

Ujjwal Choudhari
quelle
0

Folgendes mache ich für kleine Projekte mit nur geringfügiger Verwendung von Methodenaufrufen innerhalb derselben Klasse. In-Code-Dokumentation wird dringend empfohlen, da sie für Kollegen seltsam aussehen kann. Aber es funktioniert mit Singletons , ist leicht zu testen, einfach, schnell zu erreichen und erspart mir die volle AspectJ-Instrumentierung. Für eine stärkere Nutzung würde ich jedoch die AspectJ-Lösung empfehlen, wie in der Antwort von Espens beschrieben.

@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
quelle