Một proxy phạm vi trong mùa xuân là gì?


21

Như chúng ta đã biết Spring sử dụng proxy để thêm chức năng ( @Transactional@Scheduledví dụ). Có hai tùy chọn - sử dụng proxy động JDK (lớp phải triển khai các giao diện không trống) hoặc tạo một lớp con bằng cách sử dụng trình tạo mã CGLIB. Tôi luôn nghĩ rằng proxyMode cho phép tôi chọn giữa proxy động JDK và CGLIB.

Nhưng tôi đã có thể tạo một ví dụ cho thấy giả định của tôi là sai:

Trường hợp 1:

Người độc thân:

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

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

    public MyBeanB getMyBeanB() {
        return myBeanB;
    }
}

Nguyên mẫu:

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

Chủ yếu:

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

Đầu ra:

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

Ở đây chúng ta có thể thấy hai điều:

  1. MyBeanBđã được khởi tạo chỉ một lần .
  2. Để thêm @Transactionalchức năng cho MyBeanB, Spring đã sử dụng CGLIB.

Trường hợp 2:

Hãy để tôi sửa MyBeanBđịnh nghĩa:

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

Trong trường hợp này, đầu ra là:

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

Ở đây chúng ta có thể thấy hai điều:

  1. MyBeanBđã được khởi tạo 3 lần.
  2. Để thêm @Transactionalchức năng cho MyBeanB, Spring đã sử dụng CGLIB.

Bạn có thể giải thích những gì đang xảy ra? Làm thế nào để chế độ proxy thực sự hoạt động?

PS

Tôi đã đọc tài liệu:

/**
 * 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
 */

nhưng nó không rõ ràng cho tôi

Cập nhật

Trường hợp 3:

Tôi đã điều tra thêm một trường hợp, trong đó tôi trích xuất giao diện từ 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 {

và trong trường hợp này, đầu ra là:

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

Ở đây chúng ta có thể thấy hai điều:

  1. MyBeanBđã được khởi tạo 3 lần.
  2. Để thêm @Transactionalchức năng cho MyBeanB, Spring đã sử dụng proxy động JDK.

Vui lòng cho chúng tôi xem cấu hình giao dịch của bạn.
Sotirios Delimanolis

@SotiriosDelimanolis Tôi không có cấu hình đặc biệt nào
gstackoverflow

Tôi không biết về đậu có phạm vi hoặc bất kỳ loại phép thuật khung doanh nghiệp nào có trong Spring hoặc JEE. @SotiriosDelimanolis đã viết một câu trả lời tuyệt vời về nội dung đó, tôi chỉ muốn nhận xét về proxy của JDK so với CGLIB: Trong trường hợp 1 và 2, MyBeanBlớp của bạn không mở rộng bất kỳ giao diện nào, vì vậy không có gì đáng ngạc nhiên khi nhật ký bảng điều khiển của bạn hiển thị các phiên bản proxy CGLIB. Trong trường hợp 3 bạn giới thiệu và triển khai một giao diện, do đó bạn nhận được proxy JDK. Bạn thậm chí mô tả điều này trong văn bản giới thiệu của bạn.
kriegaex

Vì vậy, đối với các loại không có giao diện mà bạn thực sự không có lựa chọn nào, chúng phải là các proxy CGLIB vì các proxy của JDK chỉ hoạt động cho các loại giao diện. Tuy nhiên, bạn có thể thực thi proxy CGLIB ngay cả đối với các loại giao diện khi sử dụng Spring AOP. Điều này được cấu hình thông qua <aop:config proxy-target-class="true">hoặc @EnableAspectJAutoProxy(proxyTargetClass = true), tương ứng.
kriegaex

@kriegaex Bạn có muốn nói rằng Aspectj sử dụng CGlib để tạo proxy không?
gstackoverflow

Câu trả lời:


10

Proxy được tạo cho @Transactionalhành vi phục vụ một mục đích khác với các proxy trong phạm vi.

Các @Transactionalproxy là một kết thúc tốt đẹp đậu cụ thể để thêm hành vi quản lý phiên làm việc. Tất cả các yêu cầu phương thức sẽ thực hiện quản lý giao dịch trước và sau khi ủy quyền cho bean thực tế.

Nếu bạn minh họa nó, nó sẽ trông giống như

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

Đối với mục đích của chúng tôi, về cơ bản bạn có thể bỏ qua hành vi của nó (loại bỏ @Transactionalvà bạn sẽ thấy hành vi tương tự, ngoại trừ bạn sẽ không có proxy cglib).

Các @Scopeproxy hành xử khác nhau. Các tài liệu nêu:

[...] bạn cần tiêm một đối tượng proxy hiển thị giao diện chung giống như đối tượng trong phạm vi nhưng cũng có thể truy xuất đối tượng đích thực từ phạm vi có liên quan (như yêu cầu HTTP) và ủy quyền các cuộc gọi phương thức vào đối tượng thực .

Những gì Spring thực sự đang làm là tạo ra một định nghĩa đậu đơn cho một loại nhà máy đại diện cho proxy. Tuy nhiên, đối tượng proxy tương ứng truy vấn bối cảnh cho bean thực tế cho mỗi lần gọi.

Nếu bạn minh họa nó, nó sẽ trông giống như

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

MyBeanBlà một nguyên mẫu bean, bối cảnh sẽ luôn trả về một thể hiện mới.

Đối với mục đích của câu trả lời này, giả sử bạn đã truy xuất MyBeanBtrực tiếp với

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

đó thực chất là những gì Spring làm để thỏa mãn @Autowiredmục tiêu tiêm.


Trong ví dụ đầu tiên của bạn,

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

Bạn khai báo một định nghĩa bean nguyên mẫu (thông qua các chú thích). @Scopecó một proxyModeyếu tố

Chỉ định xem một thành phần sẽ được cấu hình như một proxy phạm vi và nếu vậy, liệu proxy nên dựa trên giao diện hoặc dựa trên lớp con.

Mặc định ScopedProxyMode.DEFAULT, thường chỉ ra rằng không nên tạo proxy phạm vi trừ khi một cấu hình mặc định khác được cấu hình ở cấp lệnh quét thành phần.

Vì vậy, Spring không tạo ra một proxy phạm vi cho bean kết quả. Bạn lấy đậu đó với

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

Bây giờ bạn có một tham chiếu đến một MyBeanBđối tượng mới được tạo bởi Spring. Điều này giống như bất kỳ đối tượng Java nào khác, các yêu cầu phương thức sẽ đi trực tiếp đến cá thể được tham chiếu.

Nếu bạn đã sử dụng getBean(MyBeanB.class)lại, Spring sẽ trả về một thể hiện mới, vì định nghĩa bean là dành cho bean nguyên mẫu . Bạn không làm điều đó, vì vậy tất cả các yêu cầu phương thức của bạn đều vào cùng một đối tượng.


Trong ví dụ thứ hai của bạn,

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

bạn khai báo một proxy phạm vi được triển khai thông qua cglib. Khi yêu cầu một loại đậu từ mùa xuân với

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

Spring biết rằng đó MyBeanBlà một proxy có phạm vi và do đó trả về một đối tượng proxy thỏa mãn API của MyBeanB(nghĩa là thực hiện tất cả các phương thức công khai của nó) mà bên trong biết cách truy xuất một loại đậu thực tế MyBeanBcho mỗi lần gọi phương thức.

Hãy thử chạy

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

Điều này sẽ trả về truegợi ý cho thực tế rằng Spring đang trả về một đối tượng proxy đơn (không phải là một bean nguyên mẫu).

Về cách gọi phương thức, bên trong triển khai proxy, Spring sẽ sử dụng một getBeanphiên bản đặc biệt để biết cách phân biệt giữa định nghĩa proxy và MyBeanBđịnh nghĩa bean thực tế . Điều đó sẽ trả về một thể hiện mới MyBeanB(vì nó là một nguyên mẫu) và Spring sẽ ủy thác lời gọi phương thức cho nó thông qua sự phản chiếu (cổ điển Method.invoke).


Ví dụ thứ ba của bạn về cơ bản giống như ví dụ thứ hai của bạn.


Vì vậy, đối với trường hợp seсond, tôi có 2 proxy: scoped_proxy bao bọc giao dịch_proxy bao bọc MyBeanB_bean tự nhiên ? scoped_proxy -> giao dịch_proxy
gstackoverflow

Có thể có proxy CGLIB cho scoped_proxy và JDK_Docate_proxy cho giao dịcha_proxy không?
gstackoverflow

1
@gstackoverflow Khi bạn làm context.getBean(MyBeanB.class), bạn không thực sự nhận được proxy, bạn đang nhận được bean thực sự. @Autowirednhận được proxy (thực tế nó sẽ thất bại nếu bạn tiêm MyBeanBthay vì loại giao diện). Tôi không biết tại sao Spring cho phép bạn làm getBean(MyBeanB.class)với INTERFACES.
Sotirios Delimanolis

1
@gstackoverflow Quên đi @Transactional. Với @Autowired MyBeanBInterfacevà proxy phạm vi, Spring sẽ tiêm đối tượng proxy. getBean(MyBeanB.class)Tuy nhiên, nếu bạn chỉ làm , Spring sẽ không trả lại proxy, nó sẽ trả về bean đích.
Sotirios Delimanolis

1
Điều đáng chú ý rằng đây là một triển khai mô hình ủy nhiệm liên quan đến Đậu trong Mùa xuân
Stephan
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.