春季的作用域代理是什么?

2022-09-01 13:24:34

众所周知,Spring使用代理来添加功能(例如)。有两个选项 - 使用JDK动态代理(类必须实现非空接口),或使用CGLIB代码生成器生成子类。我一直认为proxyMode允许我在JDK动态代理和CGLIB之间进行选择。@Transactional@Scheduled

但是我能够创建一个例子,表明我的假设是错误的:

案例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. 为了添加 的功能,Spring使用了CGLIM。@TransactionalMyBeanB

案例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. 为了添加 的功能,Spring使用了CGLIM。@TransactionalMyBeanB

你能解释一下这是怎么回事吗?代理模式如何真正工作?

附言

我已经阅读了文档:

/**
 * 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. 为了添加 的功能,Spring使用了一个JDK动态代理。@TransactionalMyBeanB

答案 1

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

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

如果你举例说明它,它看起来像

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

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

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

[...]您需要注入一个代理对象,该对象公开与作用域内对象相同的公共接口,但也可以从相关作用域(例如 HTTP 请求)中检索实际目标对象,并将方法调用委托给真实对象。

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

如果你举例说明它,它看起来像

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

由于 是 原型 Bean,因此上下文将始终返回新实例。MyBeanB

出于此答案的目的,假设您直接检索了MyBeanB

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

这基本上就是Spring为满足注射目标所做的。@Autowired


在第一个示例中,

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

声明原型 Bean 定义(通过注释)。@Scope有一个代理模式元素,它

指定是否应将组件配置为作用域内代理,如果是,则指定代理是基于接口还是基于子类的代理。

默认值为 ScopedProxyMode.DEFAULT,这通常表示不应创建作用域内代理,除非在组件扫描指令级别配置了不同的默认值。

因此,Spring 不会为生成的 Bean 创建作用域内代理。你检索那颗豆子

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

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

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


在第二个示例中,

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

声明通过 cglib 实现的作用域代理。当从春天请求这种类型的豆子时

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

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

尝试跑步

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

这将返回提示Spring返回单例代理对象(而不是原型bean)的事实。true

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


你的第三个例子基本上和你的第二个例子是一样的。


答案 2

推荐