Spring 中的作用域代理是什么?

What is a scoped proxy in Spring?

我们知道 Spring 使用代理来添加功能(例如 @Transactional@Scheduled)。有两种选择 - 使用 JDK 动态代理(class 必须实现非空接口),或使用 CGLIB 代码生成器生成子 class。我一直认为 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$f3d648e

这里我们可以看到两件事:

  1. MyBeanB 仅被实例化 一次
  2. 要为 MyBeanB 添加 @Transactional 功能,Spring 使用了 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. MyBeanB 被实例化 3 次。
  2. 要为 MyBeanB 添加 @Transactional 功能,Spring 使用了 CGLIB。

你能解释一下这是怎么回事吗?代理模式究竟如何运作?

P.S.

我已阅读文档:

/**
 * 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. MyBeanB 被实例化 3 次。
  2. 要为 MyBeanB 添加 @Transactional 功能,Spring 使用了 JDK 动态代理。

@Transactional 行为生成的代理与作用域代理的用途不同。

@Transactional 代理是一种包装特定 bean 以添加会话管理行为的代理。所有方法调用将在委托给实际 bean 之前和之后执行事务管理。

如果你把它画出来,它看起来像

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

出于我们的目的,您基本上可以忽略它的行为(删除 @Transactional 并且您应该看到相同的行为,只是您没有 cglib 代理)。

@Scope proxy 的行为不同。文档指出:

[...] 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.

Spring 真正做的是为代表代理的工厂类型创建单例 bean 定义。但是,相应的代理对象会为每次调用查询实际 bean 的上下文。

如果你把它画出来,它看起来像

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

由于 MyBeanB 是原型 bean,上下文将始终 return 一个新实例。

出于此答案的目的,假设您直接使用

检索了 MyBeanB
MyBeanB beanB = context.getBean(MyBeanB.class);

这本质上是 Spring 为满足 @Autowired 注入目标所做的。


在你的第一个例子中,

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

您声明了一个 prototype bean definition (through the annotations). @Scope has a proxyMode 元素,其中

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.

所以 Spring 没有为生成的 bean 创建作用域代理。您使用

检索该 bean
MyBeanB beanB = context.getBean(MyBeanB.class);

您现在拥有对 Spring 创建的新 MyBeanB 对象的引用。这与任何其他 Java 对象一样,方法调用将直接转到引用的实例。

如果您再次使用 getBean(MyBeanB.class),Spring 将 return 一个新实例,因为 bean 定义是针对 prototype bean。你没有这样做,所以你所有的方法调用都转到同一个对象。


在你的第二个例子中,

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

您声明了一个通过 cglib 实现的作用域代理。当使用

从 Spring 请求这种类型的 bean 时
MyBeanB beanB = context.getBean(MyBeanB.class);

Spring 知道 MyBeanB 是作用域代理,因此 return 是满足 MyBeanB 的 API 的代理对象(即实现所有它的 public 方法)在内部知道如何为每个方法调用检索 MyBeanB 类型的实际 bean。

尝试运行

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

这将 return true 暗示 Spring 是 return 单例代理对象(不是原型 bean)。

在方法调用中,在代理实现内部,Spring 将使用一个特殊的 getBean 版本,该版本知道如何区分代理定义和实际的 MyBeanB bean 定义。这将 return 一个新的 MyBeanB 实例(因为它是原型)并且 Spring 将通过反射将方法调用委托给它(经典 Method.invoke)。


您的第三个示例与第二个示例基本相同。