Apa yang dimaksud dengan proxy yang dicakup dalam Spring?

21

Seperti yang kita ketahui Spring menggunakan proksi untuk menambah fungsionalitas ( @Transactionaldan@Scheduled misalnya). Ada dua opsi - menggunakan proxy dinamis JDK (kelas harus mengimplementasikan antarmuka yang tidak kosong), atau menghasilkan kelas anak menggunakan generator kode CGLIB. Saya selalu berpikir bahwa proxyMode memungkinkan saya untuk memilih antara proxy dinamis JDK dan CGLIB.

Tetapi saya dapat membuat contoh yang menunjukkan bahwa asumsi saya salah:

Kasus 1:

Singleton:

@Service
public class MyBeanA {
    @Autowired
    private MyBeanB myBeanB;

    public void foo() {
        System.out.println(myBeanB.getCounter());
    }

    public MyBeanB getMyBeanB() {
        return myBeanB;
    }
}

Prototipe:

@Service
@Scope(value = "prototype")
public class MyBeanB {
    private static final AtomicLong COUNTER = new AtomicLong(0);

    private Long index;

    public MyBeanB() {
        index = COUNTER.getAndIncrement();
        System.out.println("constructor invocation:" + index);
    }

    @Transactional // just to force Spring to create a proxy
    public long getCounter() {
        return index;
    }
}

Utama:

MyBeanA beanA = context.getBean(MyBeanA.class);
beanA.foo();
beanA.foo();
MyBeanB myBeanB = beanA.getMyBeanB();
System.out.println("counter: " + myBeanB.getCounter() + ", class=" + myBeanB.getClass());

Keluaran:

constructor invocation:0
0
0
counter: 0, class=class test.pack.MyBeanB$$EnhancerBySpringCGLIB$$2f3d648e

Di sini kita dapat melihat dua hal:

  1. MyBeanBhanya dipakai satu kali .
  2. Untuk menambahkan @Transactionalfungsi MyBeanB, Spring menggunakan CGLIB.

Kasus 2:

Biarkan saya memperbaiki MyBeanBdefinisi:

@Service
@Scope(value = "prototype", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class MyBeanB {

Dalam hal ini outputnya adalah:

constructor invocation:0
0
constructor invocation:1
1
constructor invocation:2
counter: 2, class=class test.pack.MyBeanB$$EnhancerBySpringCGLIB$$b06d71f2

Di sini kita dapat melihat dua hal:

  1. MyBeanBdipakai 3 kali.
  2. Untuk menambahkan @Transactionalfungsi MyBeanB, Spring menggunakan CGLIB.

Bisakah Anda menjelaskan apa yang sedang terjadi? Bagaimana cara proxy benar-benar berfungsi?

PS

Saya sudah membaca dokumentasinya:

/**
 * Specifies whether a component should be configured as a scoped proxy
 * and if so, whether the proxy should be interface-based or subclass-based.
 * <p>Defaults to {@link ScopedProxyMode#DEFAULT}, which typically indicates
 * that no scoped proxy should be created unless a different default
 * has been configured at the component-scan instruction level.
 * <p>Analogous to {@code <aop:scoped-proxy/>} support in Spring XML.
 * @see ScopedProxyMode
 */

tetapi tidak jelas bagi saya.

Memperbarui

Kasus 3:

Saya menyelidiki satu lagi kasus, di mana saya mengekstrak antarmuka dari MyBeanB:

public interface MyBeanBInterface {
    long getCounter();
}



@Service
public class MyBeanA {
    @Autowired
    private MyBeanBInterface myBeanB;


@Service
@Scope(value = "prototype", proxyMode = ScopedProxyMode.INTERFACES)
public class MyBeanB implements MyBeanBInterface {

dan dalam hal ini hasilnya adalah:

constructor invocation:0
0
constructor invocation:1
1
constructor invocation:2
counter: 2, class=class com.sun.proxy.$Proxy92

Di sini kita dapat melihat dua hal:

  1. MyBeanBdipakai 3 kali.
  2. Untuk menambahkan @Transactionalfungsi MyBeanB, Spring menggunakan proxy dinamis JDK.
gstackoverflow
sumber
Tolong tunjukkan kepada kami konfigurasi transaksional Anda.
Sotirios Delimanolis
@ SotiriosDelimanolis Saya tidak punya konfigurasi khusus
gstackoverflow
Saya tidak tahu tentang scoped bean atau apa pun jenis sihir kerangka kerja perusahaan yang terkandung dalam Spring atau JEE. @ SotiriosDelimanolis menulis jawaban yang bagus tentang hal-hal itu, saya hanya ingin mengomentari proksi JDK vs. CGLIB: Dalam kasus 1 dan 2 MyBeanBkelas Anda tidak memperluas antarmuka apa pun, sehingga tidak mengejutkan bahwa log konsol Anda menunjukkan contoh proxy CGLIB. Dalam kasus 3 Anda memperkenalkan dan mengimplementasikan antarmuka, akibatnya Anda mendapatkan proxy JDK. Anda bahkan menggambarkan ini dalam teks pengantar Anda.
kriegaex
Jadi untuk jenis non-antarmuka Anda benar-benar tidak punya pilihan, mereka harus menjadi proxy CGLIB karena proxy JDK hanya bekerja untuk jenis antarmuka. Namun Anda dapat menerapkan proxy CGLIB bahkan untuk tipe antarmuka saat menggunakan Spring AOP. Ini dikonfigurasi melalui <aop:config proxy-target-class="true">atau @EnableAspectJAutoProxy(proxyTargetClass = true), masing-masing.
kriegaex
@ kriegaex Apakah Anda ingin mengatakan bahwa Aspectj menggunakan CGlib untuk pembuatan proxy?
gstackoverflow

Jawaban:

10

Proxy yang dihasilkan untuk @Transactionalperilaku memiliki tujuan yang berbeda dari proxy yang dicakup.

The @Transactionalproxy yang membungkus kacang khusus untuk menambahkan perilaku manajemen sesi. Semua doa metode akan melakukan manajemen transaksi sebelum dan setelah mendelegasikan ke kacang yang sebenarnya.

Jika Anda menggambarkannya, akan terlihat seperti

main -> getCounter -> (cglib-proxy -> MyBeanB)

Untuk tujuan kami, Anda pada dasarnya dapat mengabaikan perilakunya (menghapus @Transactionaldan Anda akan melihat perilaku yang sama, kecuali Anda tidak memiliki proxy cglib).

The @Scopeproxy yang berperilaku berbeda. Dokumentasi menyatakan:

[...] Anda perlu menyuntikkan objek proxy yang mengekspos antarmuka publik yang sama dengan objek yang dicakup tetapi itu juga dapat mengambil objek target nyata dari cakupan yang relevan (seperti permintaan HTTP) dan mendelegasikan panggilan metode ke objek nyata .

Apa yang sebenarnya dilakukan Spring adalah menciptakan definisi kacang tunggal untuk jenis pabrik yang mewakili proxy. Objek proxy yang sesuai, bagaimanapun, menanyakan konteks untuk kacang yang sebenarnya untuk setiap doa.

Jika Anda menggambarkannya, akan terlihat seperti

main -> getCounter -> (cglib-scoped-proxy -> context/bean-factory -> new MyBeanB)

Karena MyBeanBini adalah prototipe kacang, konteksnya akan selalu mengembalikan contoh baru.

Untuk keperluan jawaban ini, anggap Anda telah mengambil MyBeanB langsung dengan

MyBeanB beanB = context.getBean(MyBeanB.class);

yang pada dasarnya adalah apa yang dilakukan Spring untuk memuaskan seorang @Autowired target injeksi.


Dalam contoh pertama Anda,

@Service
@Scope(value = "prototype")
public class MyBeanB { 

Anda mendeklarasikan definisi kacang prototipe (melalui anotasi). @Scopemempunyai sebuahproxyMode elemen yang

Menentukan apakah suatu komponen harus dikonfigurasikan sebagai proxy yang tercakup dan jika demikian, apakah proxy tersebut harus berbasis antarmuka atau berbasis subkelas.

Default ke ScopedProxyMode.DEFAULT, yang biasanya menunjukkan bahwa tidak ada proxy yang tercakup harus dibuat kecuali standar berbeda telah dikonfigurasi pada tingkat instruksi pemindaian komponen.

Jadi Spring tidak membuat proksi cakupan untuk kacang yang dihasilkan. Anda mengambil kacang itu dengan

MyBeanB beanB = context.getBean(MyBeanB.class);

Anda sekarang memiliki referensi ke MyBeanBobjek baru yang dibuat oleh Spring. Ini seperti objek Java lainnya, pemanggilan metode akan langsung ke contoh yang direferensikan.

Jika Anda menggunakan getBean(MyBeanB.class)lagi, Spring akan mengembalikan contoh baru, karena definisi bean adalah untuk a kacang prototipe . Anda tidak melakukan itu, jadi semua pemanggilan metode Anda pergi ke objek yang sama.


Dalam contoh kedua Anda,

@Service
@Scope(value = "prototype", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class MyBeanB {

Anda mendeklarasikan proksi cakupan yang diimplementasikan melalui cglib. Saat meminta kacang jenis ini dari Spring with

MyBeanB beanB = context.getBean(MyBeanB.class);

Spring tahu itu MyBeanBadalah proksi scoped dan karenanya mengembalikan objek proksi yang memenuhi APIMyBeanB (mis. Mengimplementasikan semua metode publiknya) yang secara internal tahu cara mengambil jenis kacang yang sebenarnya MyBeanBuntuk setiap doa metode.

Coba jalankan

System.out.println("singleton?: " + (context.getBean(MyBeanB.class) == context.getBean(MyBeanB.class)));

Ini akan kembali true mengisyaratkan fakta bahwa Spring mengembalikan objek proxy tunggal (bukan kacang prototipe).

Pada permohonan metode, di dalam implementasi proxy, Spring akan menggunakan getBeanversi khusus yang tahu bagaimana membedakan antara definisi proxy dan MyBeanBdefinisi kacang yang sebenarnya . Itu akan mengembalikan MyBeanBcontoh baru (karena itu prototipe) dan Spring akan mendelegasikan pemanggilan metode melalui refleksi (klasik Method.invoke).


Contoh ketiga Anda pada dasarnya sama dengan yang kedua.

Sotirios Delimanolis
sumber
Jadi untuk kasus kedua saya memiliki 2 proxy: scoped_proxy yang membungkus transactional_proxy yang membungkus MyBeanB_bean alami ? scoped_proxy -> transactional_proxy -> MyBeanB_bean
gstackoverflow
Apakah mungkin untuk memiliki proxy CGLIB untuk scoped_proxy dan JDK_Dynamic_proxy untuk transactiona_proxy?
gstackoverflow
1
@ gstackoverflow Ketika Anda melakukannya context.getBean(MyBeanB.class), Anda tidak benar-benar mendapatkan proxy, Anda mendapatkan kacang yang sebenarnya. @Autowiredmendapatkan proxy (sebenarnya itu akan gagal jika Anda menyuntikkan MyBeanBbukan tipe antarmuka). Saya tidak tahu mengapa Spring memungkinkan Anda melakukan getBean(MyBeanB.class)dengan INTERFACES.
Sotirios Delimanolis
1
@ gstackoverflow Lupakan @Transactional. Dengan @Autowired MyBeanBInterfacedan cakupan proxy, Spring akan menyuntikkan objek proxy. Jika Anda hanya melakukannya getBean(MyBeanB.class), Spring tidak akan mengembalikan proxy, itu akan mengembalikan kacang target.
Sotirios Delimanolis
1
Perlu dicatat bahwa ini adalah implementasi pola delegasi sehubungan dengan Beans dalam Spring
Stephan