[java] Spring에서 범위 프록시는 무엇입니까?

우리가 알고 있듯이 봄이 기능을 추가하는 프록시를 사용하는 ( @Transactional그리고 @Scheduled예를 들어). JDK 동적 프록시 (클래스가 비어 있지 않은 인터페이스를 구현해야 함)를 사용하거나 CGLIB 코드 생성기를 사용하여 하위 클래스를 생성하는 두 가지 옵션이 있습니다. 나는 항상 proxyMode를 사용하여 JDK 동적 프록시와 CGLIB 중에서 선택할 수 있다고 생각했습니다.

그러나 나는 내 가정이 잘못되었음을 보여주는 예를 만들 수있었습니다.

사례 1 :

하나씩 일어나는 것:

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

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

    public MyBeanB getMyBeanB() {
        return myBeanB;
    }
}

원기:

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

본관:

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

산출:

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

여기서 우리는 두 가지를 볼 수 있습니다.

  1. MyBeanB 오직 인스턴스화되었다 한 번만 .
  2. @Transactional기능 을 추가하려면MyBeanBSpring은 CGLIB를 사용했습니다.

사례 2 :

MyBeanB정의를 수정하겠습니다 :

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

이 경우 출력은 다음과 같습니다.

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

여기서 우리는 두 가지를 볼 수 있습니다.

  1. MyBeanB3 인스턴스화되었습니다 번 되었습니다.
  2. 에 대한 @Transactional기능 을 추가하기 위해 MyBeanBSpring은 CGLIB를 사용했습니다.

무슨 일인지 설명해 주시겠습니까? 프록시 모드는 실제로 어떻게 작동합니까?

추신

설명서를 읽었습니다.

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

그러나 그것은 분명하지 않습니다.

최신 정보

사례 3 :

인터페이스를 추출한 사례를 한 번 더 조사했습니다 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 {

이 경우 출력은 다음과 같습니다.

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

여기서 우리는 두 가지를 볼 수 있습니다.

  1. MyBeanB3 인스턴스화되었습니다 번 되었습니다.
  2. @Transactional기능 을 추가하기 위해 MyBeanBSpring은 JDK 동적 프록시를 사용했습니다.


답변

@Transactional동작을 위해 생성 된 프록시 는 범위가 지정된 프록시와 다른 용도로 사용됩니다.

@Transactional프록시는 세션 관리 동작을 추가 할 수있는 특정 빈을 감싸는 것입니다. 모든 메소드 호출은 실제 Bean에 위임하기 전후에 트랜잭션 관리를 수행합니다.

설명하면 다음과 같습니다

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

우리의 목적을 위해, 당신은 본질적으로 그 행동을 무시할 수 있습니다 (제거 @Transactional하고 cglib 프록시가 없다는 것을 제외하고는 동일한 행동을 보게됩니다).

@Scope프록시는 다르게 동작합니다. 설명서에는 다음이 명시되어 있습니다.

[…] 범위가 지정된 객체와 동일한 공용 인터페이스를 노출 하지만 관련 범위 (예 : HTTP 요청) 에서 실제 대상 객체를 검색하고 메소드 호출을 실제 객체에 위임 할 수 있는 프록시 객체를 주입해야 합니다. .

Spring이 실제로하고있는 일은 프록시를 나타내는 팩토리 유형에 대한 싱글 톤 Bean 정의를 만드는 것입니다. 그러나 해당 프록시 오브젝트는 모든 호출에 대한 실제 Bean의 컨텍스트를 조회합니다.

설명하면 다음과 같습니다

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

때문에 MyBeanB프로토 타입 콩은, 문맥은 항상 새로운 인스턴스를 반환합니다.

이 답변의 목적을 위해, 당신은을 검색 가정 MyBeanB에 직접

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

이것은 본질적으로 Spring이 @Autowired주입 목표 를 만족시키기 위해 수행하는 것입니다.


첫 번째 예에서

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

어노테이션을 통해 프로토 타입 Bean 정의를 선언합니다 . @Scope갖는 proxyMode요소를 어느

구성 요소를 범위 프록시로 구성해야하는지 여부와 프록시가 인터페이스 기반인지 서브 클래스 기반인지를 지정합니다.

기본값은 ScopedProxyMode.DEFAULT이며, 이는 구성 요소 스캔 명령 레벨에서 다른 기본값을 구성하지 않으면 범위 프록시를 작성 하지 않아야 함을 나타냅니다 .

따라서 Spring은 결과 Bean에 대한 범위 프록시를 작성하지 않습니다. 당신은 그 콩을 검색

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

MyBeanBSpring에 의해 생성 된 새로운 객체에 대한 참조가 생겼습니다. 이것은 다른 Java 객체와 마찬가지로 메소드 호출이 참조 된 인스턴스로 직접 이동합니다.

getBean(MyBeanB.class)다시 사용 하면 Bean 정의는 프로토 타입 Bean 이므로 Spring은 새 인스턴스를 리턴합니다 . 그렇게하지 않으므로 모든 메소드 호출이 동일한 객체로 이동합니다.


두 번째 예에서

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

cglib를 통해 구현 된 범위 프록시를 선언합니다. Spring 에서이 유형의 Bean을 요청할 때

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

Spring MyBeanB은 이것이 범위가 지정된 프록시 임을 알고 있으므로 각 메소드 호출에 대한 MyBeanB실제 유형의 Bean을 검색하는 방법을 내부적으로 알고 있는 API를 만족하는 프록시 오브젝트 (즉, 모든 공용 메소드를 구현 함)를 리턴합니다 MyBeanB.

달리기

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

이것은 trueSpring이 프로토 타입 Bean이 아닌 싱글 톤 프록시 객체를 반환한다는 사실을 암시합니다.

프록시 구현 내에서 메소드 호출에서 Spring은 getBean프록시 정의와 실제 MyBeanBBean 정의 를 구별하는 방법을 알고 있는 특수 버전을 사용합니다 . MyBeanB프로토 타입이므로 새 인스턴스를 반환 하고 Spring은 리플렉션 (classic Method.invoke)을 통해 메소드 호출을 위임합니다 .


세 번째 예는 기본적으로 두 번째 예와 동일합니다.


답변