What is a scoped proxy in Spring?

2020-02-08 01:40发布

问题:

As we know Spring uses proxies to add functionality (@Transactional and @Scheduled for example). There are two options - using a JDK dynamic proxy (the class has to implement non-empty interfaces), or generating a child class using the CGLIB code generator. I always thought that proxyMode allows me to choose between a JDK dynamic proxy and CGLIB.

But I was able to create an example which shows that my assumption is wrong:

Case 1:

Singleton:

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

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

    public MyBeanB getMyBeanB() {
        return myBeanB;
    }
}

Prototype:

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

Main:

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

Output:

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

Here we can see two things:

  1. MyBeanB was instantiated only once.
  2. To add the @Transactional functionality for MyBeanB, Spring used CGLIB.

Case 2:

Let me correct the MyBeanB definition:

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

In this case the output is:

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

Here we can see two things:

  1. MyBeanB was instantiated 3 times.
  2. To add the @Transactional functionality for MyBeanB, Spring used CGLIB.

Could you explain what is going on? How does proxy mode really work?

P.S.

I've read the documentation:

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

but it is not clear for me.

Update

Case 3:

I investigated one more case, in which I extracted the interface from 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 {

and in this case the output is:

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

Here we can see two things:

  1. MyBeanB was instantiated 3 times.
  2. To add the @Transactional functionality for MyBeanB, Spring used a JDK dynamic proxy.

回答1:

The proxy generated for @Transactional behavior serves a different purpose than the scoped proxies.

The @Transactional proxy is one that wraps the specific bean to add session management behavior. All method invocations will perform the transaction management before and after delegating to the actual bean.

If you illustrate it, it would look like

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

For our purposes, you can essentially ignore its behavior (remove @Transactional and you should see the same behavior, except you won't have the cglib proxy).

The @Scope proxy behaves differently. The documentation states:

[...] you need to inject a proxy object that exposes the same public interface as the scoped object but that can also retrieve the real target object from the relevant scope (such as an HTTP request) and delegate method calls onto the real object.

What Spring is really doing is creating a singleton bean definition for a type of factory representing the proxy. The corresponding proxy object, however, queries the context for the actual bean for every invocation.

If you illustrate it, it would look like

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

Since MyBeanB is a prototype bean, the context will always return a new instance.

For the purposes of this answer, assume you retrieved the MyBeanB directly with

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

which is essentially what Spring does to satisfy an @Autowired injection target.


In your first example,

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

You declare a prototype bean definition (through the annotations). @Scope has a proxyMode element which

Specifies whether a component should be configured as a scoped proxy and if so, whether the proxy should be interface-based or subclass-based.

Defaults to 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.

So Spring is not creating a scoped proxy for the resulting bean. You retrieve that bean with

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

You now have a reference to a new MyBeanB object created by Spring. This is like any other Java object, method invocations will go directly to the referenced instance.

If you used getBean(MyBeanB.class) again, Spring would return a new instance, since the bean definition is for a prototype bean. You're not doing that, so all your method invocations go to the same object.


In your second example,

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

you declare a scoped proxy that is implemented through cglib. When requesting a bean of this type from Spring with

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

Spring knows that MyBeanB is a scoped proxy and therefore returns a proxy object that satisfies the API of MyBeanB (ie. implements all its public methods) that internally knows how to retrieve an actual bean of type MyBeanB for each method invocation.

Try running

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

This will return true hinting to the fact that Spring is returning a singleton proxy object (not a prototype bean).

On a method invocation, inside the proxy implementation, Spring will use a special getBean version that knows how to distinguish between the proxy definition and the actual MyBeanB bean definition. That'll return a new MyBeanB instance (since it's a prototype) and Spring will delegate the method invocation to it through reflection (classic Method.invoke).


Your third example is essentially the same as your second.