关于Spring的两三事:为什么说代理对象不持有对象依赖关系?

人生苦短,不如养狗

作者:闲宇

公众号:Brucebat的伪技术鱼塘

一、前言

哈喽,大家好,我是闲宇。在之前的一期关于Spring解决循环依赖分析的视频中我曾经提出过这样一个观点:代理对象不能持有目标对象的对象依赖关系。有些朋友对于这样一个观点存在疑问,以至于无法很好地去理解存在代理对象的Bean创建流程。为了帮助大家更好地去理解Spring Bean的创建流程,今天这篇文章我会通过分析代理模式来解释一下:在Spring中,为什么代理对象不能持有目标对象的对象依赖关系。

二、什么是代理模式?

老规矩,在分析“为什么”之前,先来分析一下“是什么”。那么什么是代理模式呢?这里我借助一个生活中的例子来说明一下:

作为万千背井离乡、外出务工的打工人之一,要想快速地在一个陌生的城市找到一个合适的住房,找中介可以说是我们的首选方案。不过今天的例子并不是从租客的角度来分析,而是要从房东的角度来进行分析。从房东的角度来看,每次有租客来看房时,会先由中介带着看房,并介绍对应的房屋情况。如果符合租客的心理预期,中介就会通知房东进行谈价和签订合同。如果不符合心理预期,中介则不会通知房东进行后续的流程。

从这个例子当中可以看到,代理模式实际上就是通过代理对象来替代被代理的目标对象接受外部的访问,然后再由代理对象去访问目标对象。在接受外部访问时,代理对象会先去完成一些与实际业务不是强相关的、可以剥离开来的逻辑处理,也就是我们常说的“行为增强”。在完成这些逻辑的处理之后,再去调用目标对象的对应方法执行实际的业务处理。

在实现代理模式时,有以下两种方案:

  • 静态代理

  • 动态代理

这两种实现方式最大的不同就在于,前者在编译期(也就是编码的时候)就已经确定好代理类,不需要运行时进行动态生成。而后者则需要在运行时动态生成对应的代理类。这两种不同的实现方案也导致了在实际编码过程中编写的代码量和灵活性有显著的不同,使用静态代理方案,每一个被代理的接口或抽象类都需要编写一个对应的代理类,即使代理类的逻辑是一样的,也需要进行重复的编写。而使用动态代理则只需要编写一次代理类的执行逻辑,不需要实际去编写对应的代理类,后续根据使用的需要,动态地为目标对象生成对应的代理对象即可,编码上更加简洁,使用上也会更加灵活。

那么下面,我们来具体看一下两种代理方式的编码实现。

静态代理

代理对象是为了替代目标对象接受外部访问而存在的,秉持最小改动原则,代理对象就必须提供和目标对象完全一样的方法。要想实现这个目标,我们就必须通过派生的方式,让目标类和代理类实现同一个接口或者继承同一个类,以此来保证两者内部提供的方法是一样的。以下就是静态代理的两种不同实现方式。

需要注意,由于代理类是面向所有实现该接口或者继承该抽象类的类进行代理,所以在编写构造器时,我们面向的也是接口或者抽象类,而不是某一个特定的类。

可以看到,在这两种实现中,代理对象都必须持有目标对象的对象引用。在执行实际的业务逻辑时,仍然需要依赖目标对象来完成对应的业务逻辑。在这种情况下,假设目标对象存在依赖关系,如果想要将这个对象依赖关系转移到代理对象中会发生什么呢?下面,我们分别来看一下,在接口实现方案和类继承方案中会发生什么。

首先我们来看一下接口实现方案。在接口实现方案中,为了能够在代理对象中设置目标对象的依赖关系,我们需要在代理类中额外增加一个成员变量。相信一些熟悉设计模式的朋友在看到这一步操作时就已经发现了不妥,因为这一步就已经违反了设计模式的多个原则,最直接的就是开闭原则。简单来说,目标对象发生什么,关我代理对象什么事情,这完全是你内部的事情。不过为了完成我们的分析,让我们先忽略这个问题。

在完成了代理对象的属性填充之后,运行一下代码,我们会发现,代码在执行到目标对象调用所依赖对象的方法时报了空指针错误。好吧,这其实不难理解,毕竟这个所依赖的对象被设置进了代理对象中,而不是目标对象中,当然会发生空指针错误。

那么,使用类继承的方法会有什么不同呢?这里确实有一个看起来像是的优化的变动,即我们可以把成员变量提升到抽象类中。这样就可以让代理类和目标类拥有相同的行为。但是,这样真的好吗?在上面,我提到过:代理类是面向所有实现该接口或者继承该抽象类的类进行代理,此时我们编写的Demo类内部依赖的是A,如果出现一个DemoB类内部依赖的是B,那么我们是否又要在抽象类中继续添加一个新的成员变量来维护对应的依赖关系呢?说到这里,相信大家应该能发觉这当中的不合理。不过,依然让我们先忽略这个问题,先完成我们的分析。

在完成了代理对象的属性填充之后,运行一下代码,我们会发现,竟然又出现了空指针错误!这其中的原因其实和之前使用接口实现方案中的一样,有兴趣的朋友可以本地实验一下,这里就不再赘述。

至此,我们可以确定,在静态代理的实现方案中,代理对象是不能持有目标对象的对象依赖关系的。那么动态代理中会有什么不同吗?

动态代理

在动态代理当中,同样也存在接口实现和类继承两个流派,分别是:jdk动态代理和cglib动态代理。和最原始的代理模式思路略有不同的是,在cglib的实现方案中,并不是让目标类和代理类去继承同一个类,而是让代理类去继承目标类,以此来实现方法的派生。以下就是两种动态代理方案的编码实现:

可以看到,相比静态代理当中需要显式地实现某个接口或者继承某个抽象类而言,在动态代理的实现方案中,我们只要关注代理类需要执行的逻辑即可。

那么在存在依赖关系的情况下,动态代理会有什么不同吗?

对于jdk动态代理来说,和之前在静态代理中的分析并不无不同,这里就不多加赘述,直接给出结论:依然会出现空指针错误。

但是在cglib动态代理的实现方案中,这里出现了完全不同的结果。我们竟然成功执行了代码!其实稍微分析一下,就可以得出原因:在静态代理当中,代理类和目标类都是派生自同一个抽象类,两者同属子类,所以两者的依赖关系并不能共享。而在cglib动态代理的实现方案中,代理类派生自目标类,也就是目标类的子类,子类和父类是可以实现依赖关系共享的。基于这样一个原因,在使用cglib进行代理逻辑编写时,我们可以不持有被代理的目标对象,直接通过子类调用父类的方法就可以完成对应方法的调用,最终正确地执行了对应的逻辑。

三、为什么说在Spring中代理对象不能持有对象依赖关系?

从上面的编码实现当中我们可以发现,在动态代理中,如果使用cglib提供的子类派生父类方法的方式,代理对象也是可以持有目标对象的对象依赖关系的。那么在Spring框架中,为什么说代理对象不能持有对象依赖关系呢?很简单,有两个原因:

  • 需要行为统一

  • 需要符合设计模式的设计原则

行为统一

首先说一说行为统一。在Spring框架中,代理对象的生成是通过jdk动态代理和cglib动态代理两种方式来完成的。在之前的分析中,我们已经得出了使用jdk动态代理方式是无法实现代理对象持有对象依赖关系这样一个结论。虽然在cglib的实现方案是可以实现代理对象持有对象依赖关系的,但是为了减少两种代理方式在代码编写上的差异性,提高代码的复用能力,在使用cglib时并没有选择子类调用父类方法的方式来进行方法增强,而是依然使用了持有目标对象的对象引用的方案,借助目标对象来完成对应方法的调用。通过这种方式,尽可能地保证了代理对象在初始化和调用阶段的行为统一。

符合设计模式的设计原则

接着来谈一谈符合设计模式的设计原则。对于设计模式的设计原则,相信大家应该已经背得滚瓜烂熟了。这里我简单概括一下,那就是写最少的代码,做最少的修改,完成最多的功能。

在Spring框架中,对象的依赖关系都是维护在原始对象当中的,代理对象的工作只是对目标对象的某些行为进行增强,并不负责依赖关系的维护,两者所肩负的职责单一且固定。如果为了使用cglib提供的子类调用父类方法的能力,而去强行给代理对象添加依赖关系维护的职责,明显是不符合单一职责这样一个设计原则的。再者说,在Spring Bean生成流程中,在目标对象已经完成了一次对象填充的情况下,再让代理对象进行一次属性填充,并且只能是cglib方式创建的代理对象进行这个操作,明显是多余且不合理的。

所以,综合以上两个原因,在Spring框架的设计中,我们并没有看到代理对象持有对象依赖关系的情况。

说到这里,我们还能重新解释一下在讨论Spring循环依赖解决方案时另一个解释得并不是那么清楚的问题:代理对象的生成时机和延迟创建理念无关这样一个说法。从上面的分析可以看到,代理对象需要通过目标对象的引用来调用被代理的方法,那么要想要获得目标对象的引用不就得先去完成目标对象的创建吗?也就是说,在目标对象初始化之后再创建代理对象,完全是正常的编码思路,和延迟创建理念无关。除此以外,这也解释了,为什么在处理存在代理对象的循环依赖时,在属性填充阶段就可以进行代理对象的生成,因为在那个时候目标对象的引用已经创建,代理对象可以直接使用这引用,不需要等到目标对象的初始化完成。

以上就是本文的全部内容,希望对你有所帮助,如果不喜欢阅读文字的朋友还可以观看下方的视频。