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

34

正如我们所知,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. 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 动态代理。

请展示您的事务配置。 - Sotirios Delimanolis
1
@SotiriosDelimanolis 我没有任何特殊的配置。 - gstackoverflow
我不了解Spring或JEE中包含的作用域bean或其他企业框架魔法。@SotiriosDelimanolis写了一个关于那些东西的精彩答案,我只想评论一下JDK与CGLIB代理:在情况1和2中,您的MyBeanB类没有扩展任何接口,因此您的控制台日志显示CGLIB代理实例并不奇怪。在情况3中,您引入并实现了一个接口,因此您会得到一个JDK代理。您甚至在介绍性文本中描述了这一点。 - kriegaex
@kriegaex,你能详细解释一下你的意思吗?AOP帮助我们在某个方法/...之前/之后/代替插入一些代码。Spring使用它来进行Transactional、Scheduled、Async等注释。但是带有插入AOP的bean是一个代理。 - gstackoverflow
将文档链接添加到代码中:ScopedProxy - Stephan
显示剩余4条评论
1个回答

23

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

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

如果进行说明,它看起来像:

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

对于我们的目的,您可以基本忽略其行为(删除@Transactional,您应该会看到相同的行为,除了您将没有cglib代理)。 @Scope代理的行为不同。文档说明如下:
[...] 您需要注入一个代理对象,该代理对象公开与作用域对象相同的公共接口,但还可以从相关范围(例如HTTP请求)中检索真实目标对象并将方法调用委托给真实对象。
Spring实际上正在为代表代理的工厂类型创建单例bean定义。然而,相应的代理对象在每次调用时查询上下文以获取实际的bean。如果您进行说明,它看起来像
main -> getCounter -> (cglib-scoped-proxy -> context/bean-factory -> new MyBeanB)

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

为了回答这个问题,假设您直接使用以下方式检索到MyBeanB

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

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


在你的第一个例子中,{{请补充翻译内容}}。
@Service
@Scope(value = "prototype")
public class MyBeanB { 

你通过注释声明了一个原型bean定义。@Scope具有proxyMode元素,用于指定组件是否应配置为作用域代理,如果是,则代理应基于接口还是子类。默认值为ScopedProxyMode.DEFAULT,通常表示不应创建作用域代理,除非在组件扫描指令级别上配置了不同的默认值。因此,Spring不会为结果bean创建作用域代理。你可以使用以下代码检索该bean:
MyBeanB beanB = context.getBean(MyBeanB.class);

你现在有一个由Spring创建的新的MyBeanB对象的引用。它就像任何其他的Java对象,方法调用将直接传递到引用实例。如果你再次使用getBean(MyBeanB.class),Spring会返回一个新的实例,因为bean定义是原型模式。但你没有这样做,所以所有的方法调用都指向同一个对象。
在你的第二个例子中,{{具体内容需要提供上下文}}。
@Service
@Scope(value = "prototype", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class MyBeanB {

您可以声明一个通过Cglib实现的作用域代理。当从Spring请求此类型的bean时,

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

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

尝试运行

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

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

你的第三个例子本质上与第二个相同。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接