Spring @Transaction panggilan metode dengan metode dalam kelas yang sama, tidak bekerja?

109

Saya baru mengenal Transaksi Musim Semi. Sesuatu yang menurut saya sangat aneh, mungkin saya memahami ini dengan benar.

Saya ingin melakukan transaksi di sekitar level metode dan saya memiliki metode pemanggil dalam kelas yang sama dan sepertinya tidak seperti itu, itu harus dipanggil dari kelas yang terpisah. Saya tidak mengerti bagaimana itu mungkin.

Jika ada yang punya ide bagaimana menyelesaikan masalah ini, saya akan sangat menghargai. Saya ingin menggunakan kelas yang sama untuk memanggil metode transaksional beranotasi.

Ini kodenya:

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
sumber
Lihatlah TransactionTemplatependekatannya: stackoverflow.com/a/52989925/355438
Lu55
Tentang mengapa self- invocation tidak berfungsi, lihat 8.6 Mekanisme proxy .
Jason Law

Jawaban:

99

Ini adalah batasan Spring AOP (objek dinamis dan cglib ).

Jika Anda mengonfigurasi Spring untuk menggunakan AspectJ untuk menangani transaksi, kode Anda akan berfungsi.

Alternatif sederhana dan mungkin terbaik adalah dengan memfaktor ulang kode Anda. Misalnya satu kelas yang menangani pengguna dan satu lagi yang memproses setiap pengguna. Kemudian penanganan transaksi default dengan Spring AOP akan berfungsi.


Tip konfigurasi untuk menangani transaksi dengan AspectJ

Untuk mengaktifkan Spring menggunakan AspectJ untuk transaksi, Anda harus mengatur mode ke AspectJ:

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

Jika Anda menggunakan Spring dengan versi yang lebih lama dari 3.0, Anda juga harus menambahkan ini ke konfigurasi Spring Anda:

<bean class="org.springframework.transaction.aspectj
        .AnnotationTransactionAspect" factory-method="aspectOf">
    <property name="transactionManager" ref="transactionManager" />
</bean>
Espen
sumber
Terima kasih untuk informasinya. Saya memfaktorkan ulang kodenya untuk saat ini, tetapi bisakah Anda mengirimkan saya contoh menggunakan AspectJ atau berikan saya beberapa tautan yang berguna. Terima kasih sebelumnya. Mike.
Mike
Menambahkan konfigurasi AspectJ spesifik transaksi di jawaban saya. Saya harap ini membantu.
Espen
10
Itu bagus! Btw: Alangkah baiknya jika Anda dapat menandai pertanyaan saya sebagai jawaban terbaik untuk memberi saya beberapa poin. (tanda centang hijau)
Espen
2
Konfigurasi boot musim semi: @EnableTransactionManagement (mode = AdviceMode.ASPECTJ)
VinyJones
64

Masalahnya di sini adalah, bahwa proxy AOP Spring tidak memperluas melainkan membungkus instance layanan Anda untuk mencegat panggilan. Ini berdampak, bahwa setiap panggilan ke "ini" dari dalam instance layanan Anda secara langsung dipanggil pada instance itu dan tidak dapat dicegat oleh proxy pembungkus (proxy bahkan tidak mengetahui panggilan semacam itu). Salah satu solusi telah disebutkan. Cara bagus lainnya adalah dengan meminta Spring menyuntikkan instance layanan ke dalam layanan itu sendiri, dan memanggil metode Anda pada instance yang diinjeksi, yang akan menjadi proxy yang menangani transaksi Anda. Tetapi ketahuilah, bahwa ini mungkin memiliki efek samping yang buruk juga, jika kacang layanan Anda bukan tunggal:

<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
sumber
3
Jika Anda memilih untuk pergi ke rute ini (apakah ini desain yang baik atau tidak adalah masalah lain) dan tidak menggunakan injeksi konstruktor, pastikan Anda juga melihat pertanyaan ini
Jeshurun
Bagaimana jika UserServicememiliki cakupan tunggal? Bagaimana jika itu adalah objek yang sama?
Yan Khonski
26

Dengan Spring 4 dimungkinkan untuk 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
sumber
2
JAWABAN TERBAIK !! Thx
mjassani
2
Koreksi saya jika saya salah, tetapi pola seperti itu benar-benar rawan kesalahan, meskipun berhasil. Ini lebih seperti karya kemampuan Spring, bukan? Seseorang yang tidak akrab dengan perilaku "panggilan kacang ini" mungkin secara tidak sengaja menghapus kacang yang diserap sendiri (bagaimanapun juga metode ini tersedia melalui "ini.") Yang mungkin menyebabkan masalah yang sulit dideteksi pada pandangan pertama. Ia bahkan bisa sampai ke lingkungan produksi sebelum ditemukan).
pidabrow
2
@pidabrow Anda benar, ini adalah pola anti yang sangat besar dan tidak jelas sejak awal. Jadi jika bisa sebaiknya Anda menghindarinya. Jika Anda harus menggunakan metode dari kelas yang sama maka cobalah untuk menggunakan perpustakaan AOP yang lebih kuat seperti AspectJ
Almas Abdrazak
21

Mulai dari Java 8 ada kemungkinan lain, yang saya lebih suka karena alasan yang diberikan di bawah ini:

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

Pendekatan ini memiliki keuntungan sebagai berikut:

1) Ini dapat diterapkan pada metode pribadi . Jadi Anda tidak perlu merusak enkapsulasi dengan membuat metode menjadi publik hanya untuk memenuhi batasan Spring.

2) Metode yang sama dapat dipanggil dalam propagasi transaksi yang berbeda dan terserah pemanggil untuk memilih yang sesuai. Bandingkan 2 baris ini:

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

3) Ini eksplisit, sehingga lebih mudah dibaca.

Bunarro
sumber
Ini bagus! Ini menghindari semua jebakan yang diperkenalkan Spring dengan anotasi sebaliknya. Suka!
Frank Hopkins
Jika saya memperluas TransactionHandlersebagai subkelas, dan subkelas tersebut memanggil dua metode ini di TransactionHandlerkelas super, apakah saya masih bisa mendapatkan manfaat @Transactionalseperti yang dimaksudkan?
tom_mai78101
6

Ini adalah solusi saya untuk doa sendiri :

public class SBMWSBL {
    private SBMWSBL self;

    @Autowired
    private ApplicationContext applicationContext;

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

    // ...
}
Hlex
sumber
0

Anda dapat membuat BeanFactory secara otomatis di dalam kelas yang sama dan melakukan

getBean(YourClazz.class)

Ini akan secara otomatis memproksifikasi kelas Anda dan memperhitungkan @Transactional atau anotasi aop Anda lainnya.

LionH
sumber
2
Itu dianggap sebagai praktik yang buruk. Bahkan menyuntikkan kacang secara rekursif ke dalam dirinya lebih baik. Menggunakan getBean (clazz) adalah penggandengan yang erat dan ketergantungan yang kuat pada kelas ApplicationContext pegas di dalam kode Anda. Juga mendapatkan kacang berdasarkan kelas mungkin tidak bekerja jika musim semi membungkus kacang (kelas dapat diubah).
Vadim Kirilchuk
0

Masalahnya terkait dengan bagaimana kelas dan proxy beban pegas. Ini tidak akan berfungsi, sampai Anda menulis metode / transaksi batin Anda di kelas lain atau pergi ke kelas lain dan kemudian kembali lagi ke kelas Anda dan kemudian menulis metode transkasi bersarang bagian dalam.

Untuk meringkas, proxy pegas tidak mengizinkan skenario yang Anda hadapi. Anda harus menulis metode transaksi ke-2 di kelas lain

Ujjwal Choudhari
sumber
0

Inilah yang saya lakukan untuk proyek kecil dengan hanya penggunaan marginal dari pemanggilan metode dalam kelas yang sama. Dokumentasi dalam kode sangat disarankan, karena mungkin terlihat aneh bagi rekan kerja. Namun, alat ini bekerja dengan lajang , mudah diuji, sederhana, cepat dicapai, dan menghemat instrumentasi AspectJ sepenuhnya. Namun, untuk penggunaan yang lebih berat, saya akan menyarankan solusi AspectJ seperti yang dijelaskan dalam jawaban 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
sumber