为什么我按类(而不是接口)查找由JDK动态代理包装的bean时没有遇到任何异常?

gst*_*low 3 java spring spring-aop dynamic-proxy spring-boot

让我们考虑以下 bean:

@Service
@Scope(value = "prototype", proxyMode = ScopedProxyMode.INTERFACES)
public class MyBeanB implements MyBeanBInterface {
    private static final AtomicLong COUNTER = new AtomicLong(0);

    private Long index;

    public MyBeanB() {
        index = COUNTER.getAndIncrement();
        System.out.println("constructor invocation:" + index);
    }

    @Transactional 
    @Override
    public long getCounter() {
        return index;
    }
}
Run Code Online (Sandbox Code Playgroud)

并考虑两种不同的用法:

用法 1:

@Service
public class MyBeanA {
    @Autowired
    private MyBeanB myBeanB;   
    ....
}
Run Code Online (Sandbox Code Playgroud)

在这种情况下,应用程序无法启动并打印:

***************************
APPLICATION FAILED TO START
***************************

Description:

The bean 'myBeanB' could not be injected as a 'my.pack.MyBeanB' because it is a JDK dynamic proxy that implements:
    my.pack.MyBeanBInterface


Action:

Consider injecting the bean as one of its interfaces or forcing the use of CGLib-based proxies by setting proxyTargetClass=true on @EnableAsync and/or @EnableCaching.
Run Code Online (Sandbox Code Playgroud)

我希望看到它,因为我要求 spring 为 bean 创建 JDK 动态代理,MyBeanB并且该代理不是 MyBeanB 的子类型。我们可以像这样轻松修复它:

@Service
public class MyBeanA {
    @Autowired
    private MyBeanBInterface myBeanB;   
    ....
}
Run Code Online (Sandbox Code Playgroud)

用法 2:

MyBeanB beanB = context.getBean(MyBeanB.class);
System.out.println(beanB.getCounter());
Run Code Online (Sandbox Code Playgroud)

对我来说令人惊讶的是它在没有任何运行时异常的情况下工作,但我希望NoSuchBeanDefinitionException在这种情况下看到,因为 int case 1 应用程序无法启动

感谢您的意见家伙-我查了之类的beanB,它是my.pack.MyBeanB$$EnhancerBySpringCGLIB$$b1346261如此使用Spring CGLIB创建代理,但它违背了bean定义(@Scope(value = "prototype", proxyMode = ScopedProxyMode.INTERFACES),看起来像一个bug。)

你能解释一下为什么它适用于案例 2 不适用于案例 1 吗?

kri*_*aex 5

正如我在对另一个问题的评论中向您解释的那样,Spring AOP 可以根据情况使用 CGLIB 和 JDK 代理。默认是实现接口的类的 JDK 代理,但您也可以为它们强制使用 CGLIB。对于没有实现接口的类,只剩下 CGLIB,因为 JDK 代理只能基于接口创建动态代理。

所以看看你的案例 1,你明确地说你想要接口代理,即 JDK 代理:

@Scope(value = "prototype", proxyMode = ScopedProxyMode.INTERFACES)
Run Code Online (Sandbox Code Playgroud)

MyBeanA不实现任何接口。因此,您会收到在这种情况下看到的错误消息。

但是,在第 2 种情况下,您使用ApplicationContext.getBean(..)它来创建代理。在这里,您依靠 Spring 来确定要选择的代理类型,而不是试图强制执行任何操作。因此,通过 CGLIB 代理成功。

这里没有惊喜。

如果您想避免第 1 种情况下的错误消息,也许您应该使用ScopedProxyMode.TARGET_CLASS.


更新:对不起,我对你类似和不起眼的类名MyBeanAMyBeanB. 下次使用更具描述性的、类似干净代码的类名是有意义的,最好是描述场景中类的角色,如MyService, MyInterface, MyScopedBean

无论如何,我再次阅读了您的问题和错误消息。错误消息表明,根据您的注释,正在生成基于接口的代理,但您正尝试将其注入到类类型中。你可以通过这样声明来解决这个问题:

@Autowired
private MyBeanBInterface myBeanB;
Run Code Online (Sandbox Code Playgroud)

在案例/用法 2 中,您再次为 bean 显式声明类而不是接口类型。正如我所说,Spring 试图通过唯一可能的方式满足您的要求,即为类创建 CGLIB 代理。您可以通过声明接口类型来解决此问题,您将获得预期的 JDK 代理:

MyBeanBInterface myBeanBInterface = appContext.getBean(MyBeanBInterface.class);
System.out.println(myBeanBInterface.getCounter());
System.out.println(myBeanBInterface.getClass());
Run Code Online (Sandbox Code Playgroud)

更新 2:根据您的评论,我认为您仍然不理解的是 OOP 的这个基本事实:如果您有

  • 班级Base和班级Sub extends Base
  • 接口Base和类Sub implements Base

你可以声明Base b = new Sub()但当然不是Sub s = new Base()因为 aSub也是 a Base,但不是每个Base都是 a Sub。例如,如果您也有OtherSub extends Base,则在尝试将Base对象分配给Sub变量时,它可能是一个OtherSub实例。这就是为什么 dot 甚至在不使用Sub s = (Sub) myBaseObject.

到现在为止还挺好。现在再次查看您的代码:

用法 1 中,您已@Autowired private MyBeanB myBeanB;配置MyBeanB为生成 JDK 代理,即将创建一个具有Proxy直接实现的父类的新代理类MyBeanBInterface。即你有两个不同的类,都直接实现相同的接口。由于我上面解释的原因,这些类彼此之间不兼容。关于接口,我们有类层次结构

MyBeanBInterface
  MyBeanB
  MyBeanB_JDKProxy
Run Code Online (Sandbox Code Playgroud)

因此,你不能注入MyBeanB_JDKProxy到一个MyBeanB领域,因为代理对象不是一个实例MyBeanB。你不明白吗?问题出在电脑前,没有神秘的Spring bug。您将其配置为失败。

这就是为什么我告诉你将代码更改为,@Autowired private MyBeanBInterface myBeanB;因为它当然可以工作,因为代理实现了接口并且一切正常。我还告诉过你,或者你可以保留,@Autowired private MyBeanB myBeanB;如果你使用proxyMode = ScopedProxyMode.TARGET_CLASS你的范围声明。

用法 2 中,问题是相同的:您说的是getBean(ClassB.class),即您明确指示 Spring 为该类创建代理。但是对于一个类你不能创建JDK代理,只能创建CGLIB代理,这就是Spring所做的。再次,我通过指导您使用getBean(MyBeanBInterface.class)来为您提供解决方案。然后您将获得预期的 JDK 代理。

Spring对两者都足够聪明

  • 使 JDK 代理在用法 1 中找到作用域服务 beanMyClassB并将方法调用委托给它(注意:委托,而不是继承!)和
  • 使 CGLIB 代理扩展MyClassB(注意:这里是继承,不需要委托)。