关于Spring IoC的那些事
荒腔走板今天把自己家里的键盘卸下来洗了一下 。 键盘是去年买的 , 国产静电容 , 宁芝的 。 整体上来说使用体验挺好的 , 写代码和写文章非常舒服 , 用很长时间手也不累 。 静电容键盘用了就回不去了 。 关键是还便宜 , 只要七百来块 , 差不多只有国外静电容牌子的1/3价格 , 感兴趣的朋友可以考虑入手一波 。
我买的是82键的 , 感觉如果是windows的电脑 , 还是应该买87的好一点 , 不然有些IDEA的快捷键用起来比较麻烦 , 还得多按一个Fn键配合 。
键盘还是要经常洗一洗的 , 代码也是要经常敲一敲的 , 文章也是要经常发一发的 , 对吧 。
打个比方 , 如果员工小明上班需要乘坐公交车 , 从家里到公司 , 那小明就依赖了公交车 。 抽象成代码大概是这样:
publicclassWorker(){privateStringname;privateStringhome;privateStringoffice;//这里依赖了Bus类privateBusbus=newBus();publicvoidgoToWork(){bus.take(name,home,office);}}复制代码我们知道 , 依赖是一种耦合 , 而过多的耦合对程序是有害的 , 代码架构的本质 , 就是尽量去降低耦合 。 试想一下 , 如果有一天员工小明升职加薪了 , 自己买了一辆小轿车代步 , 那凡是用到公交车的地方(比如上班、下班、接孩子、去商场、回家等等)岂不是需要修改代码 , 把Bus换成Car?如果某一天又想步行或者骑自行车呢?
如果A依赖B , 我们称A为“上层” , B为“下层” , 依赖倒置原则强调上层模块不应该依赖下层模块 , 两者应依赖其抽象 。
别急 , 这就是我们下面会讲到的控制反转要解决的问题 。
控制反转控制反转(InversionofControl)也就是我们说的IoC了 。 要理解IoC , 需要弄清楚到底什么被反转了?如何反转的?
上面的示例代码我们可以看到 , 即使我们引入了一个抽象层 , 但当一个Worker对象实际要使用Vehicle的时候 , 它还是必须得创建一个具体的对象 , 它可能是一个Bus , 也可能是一个Car等 。 但这样造成的问题是 , 依赖没有被彻底分离 , 两者还是存在耦合 。
那如何把它们彻底分离呢?答案就是把创建具体的Vehicle对象交给第三方去做 。 这样Worker不用管如何创建的交通工具 , 而Bus也不用管自己是如何被创建的 。
想想我们生活中就有这样的例子 , 员工小明要坐公交车 , 他不用每次都自己去造一辆公交车吧 , 只需要去公交车站 , 等公交车公司的调度就行了 。 而公交车工厂也跟小明没有任何关系 , 它的职责就是生产好公交车 , 交付给公交车公司 。 通过引入了“公交车公司”这个第三方 , 小明和公交车工厂就完全解耦了 。
【关于Spring IoC的那些事】反转的是什么?对象如何获取它的依赖对象这件事情上 , 控制权反转了 。 从自己创建 , 反转成了第三方管理 。
控制反转的进一步含义 , 不仅仅是获取 , 还有整个要依赖对象的生命周期(包括创建、维护、销毁等) , 控制权都被反转了 。
从代码设计来看 , 一个简单的解决方式是 , 把具体的对象通过方法参数传进来 , 这样就不强依赖了:
publicclassWorker(){privateStringname;privateStringhome;privateStringoffice;//通过方法传进来publicvoidgoToWork(Vehiclevehicle){vehicle.take(name,home,office);}}复制代码但这样会带来一个问题 , 就是给调用端带来了麻烦 , 相当于把对Bus的依赖 , 从Worker类转移到了它的调用端 , 那它的调用端也会强依赖Bus , 这本不属于调用端的职责 , 所以没有从根本上解决问题 。 而且每次调用都要传一个Vehicle对象进来 , 很不合理 , 管理对象也比较麻烦 。
那你可能会想 , 我搞个第三方容器不就行了吗 , 这样每次去第三方容器里面拿:
publicclassWorker(){privateStringname;privateStringhome;privateStringoffice;//第三方容器privateVehicleContainercontainer;//通过容器取publicvoidgoToWork(){container.getTodayVehicle().take(name,home,office);}}复制代码
构造器注入顾名思义 , 就是通过构造器的方式 , 把依赖的对象注入进来 。 这样在new一个对象的时候 , 就完成了它依赖的对象的装配 。
publicclassWorker(){privateStringname;privateStringhome;privateStringoffice;privateVehiclevehicle;//通过构造器把要依赖的对象传进来publicWorker(Vehiclevehicle){this.vehicle=vehicle;}//直接用publicvoidgoToWork(){vehicle.take(name,home,office);}}复制代码setter方法注入另一种方式是使用方法注入 , 一般是使用要依赖的对象对应的属性的setter方法来注入 。 比如:
publicclassWorker(){privateStringname;privateStringhome;privateStringoffice;privateVehiclevehicle;//通过setter方法注入publicvoidsetVehicle(Vehiclevehicle){this.vehicle=vehicle;}//直接用publicvoidgoToWork(){vehicle.take(name,home,office);}}复制代码属性注入构造器和setter方法都有些麻烦 , 需要写额外的代码 。 要是容器可以通过反射直接注入进来就好了 , 这样代码看起来比较干净 。 比如:
publicclassWorker(){privateStringname;privateStringhome;privateStringoffice;//容器直接通过反射把相应的对象注入进来privateVehiclevehicle;//直接用publicvoidgoToWork(){vehicle.take(name,home,office);}}复制代码控制反转容器前面反复提到的一个词 , 叫“第三方容器” , 其实就是IoC容器 。 所谓IoC容器 , 就是可以生产和管理要依赖的对象 , 然后通过合适的时机注入进来 。
IoC容器并不等于Spring 。 还有其它IoC容器框架 , 比如Google开发的Guice等 , 甚至我们可以自己开发一个轻量级的IoC容器 。 其实IoC容器实现起来并不难 。
只是我们平常用Spring比较多 , 它又提供了非常好用的IoC功能 , 所以大多数项目 , 我们都是用Spring的IoC了 。 Spring作为IoC容器还是非常成熟和稳定的 。
依赖注入和控制反转是什么关系?2004年 , MartinFowler探讨了同一个问题 , 既然IOC是控制反转 , 那么到底是“哪些方面的控制被反转了呢?” , 经过详细地分析和论证后 , 他得出了答案:“获得依赖对象的过程被反转了” 。 控制被反转之后 , 获得依赖对象的过程由自身管理变为了由IOC容器主动注入 。 于是 , 他给“控制反转”取了一个更合适的名字叫做“依赖注入(DependencyInjection)” 。 他的这个答案 , 实际上给出了实现IOC的方法:注入 。 所谓依赖注入 , 就是由IOC容器在运行期间 , 动态地将某种依赖关系注入到对象之中 。
所以 , 依赖注入(DI)和控制反转(IOC)是从不同的角度的描述的同一件事情 , 就是指通过引入IOC容器 , 利用依赖关系注入的方式 , 实现对象之间的解耦 。 控制反转是解决问题的一种思路和方法论 , 依赖注入是它的具体实现方式 。
在Spring中使用IoC首先要明确Bean的概念 , Spring把需要纳入IoC容器观察的对象称为Bean 。
一些对象是不用交给Spring管理的 , 比如POJO对象 , 类似DO、DTO等对象(包括DDD中的领域模型) , 它们都是可以在程序里面通过new或者builder来创建的 , 因为创建的时候要给它们的一些属性赋值 , 而且在使用这些类时 , 没法使用“依赖倒置原则” 。
声明Bean首先第一步要声明Bean , 这样Bean才能被Spring的IoC容器管理 。 声明Bean有很多种方式 , 在一开始 , Spring是使用XML的方式来声明一个Bean:
复制代码这样以后如果要依赖的Bean变了 , 只需要修改XML文件就行了 。
后来由于XML文件难以阅读和维护 , Spring开始支持用注解的方式定义Bean 。 我们在定义具体实现类的时候 , 可以在class上面加上@Component注解 , 然后配置好Spring的自动扫描路径 , 这样Spring就能够自己去扫描相应的类 , 纳入IoC容器中进行管理了 。
@ComponentpublicclassA{}复制代码@Component的语义其实不是很明确 , 因为“万物皆可为组件” 。 它其实是一个元注解 , 也就是说 , 可以注解其它注解 。 Spring提供了@Controller、@Service、@Repository、@Aspect等注解来供特定功能的Bean上面使用 。
我们自己也可以声明一些类似的注解 , 如果我们使用DDD , 也可以用@Component声明一些诸如@ApplicationService、@DomainService之类的注解 。
SpringBoot默认的扫描路径是启动类当前的包和子包 。 我们可以通过@ComponentScan和@ComponentScans来配置包的扫描路径 。
另一种方式是通过在方法上声明@Bean注解来声明一个Bean 。 这个时候一般是会与@Configuration一起来配合使用的 。
@ConfigurationpublicclassMyConfig{@BeanpublicBgetB(){returnnewB();}}复制代码一般只有在对框架提供的Bean有一些特殊配置的时候 , 才会使用@Bean注解 。 比如数据库配置等 。
使用Bean使用Bean也有很多种方式 。 XML就不说了 , 上面例子也展示了如何在XML里配置Bean的注入 。
Spring比较推荐的是使用构造器注入 , 因为构造器注入能够在启动的时候就检查要依赖的对象是否存在 , 如果不存在 , 会启动失败并且抛出以下异常:
Parameter0ofconstructorincom.example.springbase.bean.Arequiredabeanoftype'com.example.springbase.config.B'thatcouldnotbefound.Thefollowingcandidateswerefoundbutcouldnotbeinjected:-User-definedbeanmethod'getB'in'MyConfig'ignoredasthebeanvalueisnullAction:Considerrevisitingtheentriesaboveordefiningabeanoftype'com.example.springbase.config.B'inyourconfiguration.复制代码这样我们就可以更早地发现依赖问题 , 而不用在运行时才发现要依赖的对象没有被注入进来 , 发生一些空指针异常 。
另一种方式是注解注入 , 注解注入的好处是代码简洁 , 不用专门写构造器 。 Spring支持三个注解:
Resource-JSR250定义Inject-JSR330定义Autowired-Spring提供其中@Resource和@Inject都是在JSR中定义的规范 , 主流的IoC框架都已经支持了这两个规范 。 这两个规范的区别在于 , 查找Bean的方式不同 。
@Resource是先通过名称匹配 , 找不到再通过类型匹配 , 找不到再通过结合@Qualifier来匹配 。
而@Inject是先通过类型匹配 , 找不到再通过Qualifier来匹配 , 找不到再通过名称匹配 。 如果要使用@Inject , 需要引入额外的包:
javax.injectjavax.inject1复制代码@Autowired和@Inject的用法一致 , 唯一区别就是@Autowired属于Spring框架提供的注解 。
其实最推荐的是使用JSR-330的规范 , 这样可以做到与框架无关 。 但是笔者发现大多数项目还是使用@Autowired居多 , 而且很难真正做到与Spring框架无关 , 因为@Component就是Spring提供的注解 。 我们平时经常使用的@Controller、@Service、@Repository、@Aspect等注解也都是Spring提供的 。
所以如果要说推荐一个注解的话 , 笔者更推荐Spring的@Autowired 。
还有一种方式 , 可以从Spring的上下文中直接拿Bean 。 这种方式一般用于:从一个不受Spring管理的对象中获取一个Bean 。 比如说二方包里面的代码 , 就有可能会有这种情况 。
//定义一个aware , 持有一个static的context对象@ComponentpublicclassMySpringContextAwareimplementsApplicationContextAware{publicstaticApplicationContextapplicationContext;@OverridepublicvoidsetApplicationContext(ApplicationContextapplicationContext)throwsBeansException{MySpringContextAware.applicationContext=applicationContext;}}//A是不受Spring管理的publicclassA{//B是受Spring管理的privateBb;publicA(){System.out.println("ainit");//这样就可以在不受Spring管理的对象里面 , 获取到Bean了this.b=(B)MySpringContextAware.applicationContext.getBean(B.class);;System.out.println(b.hashCode());}}复制代码常见问题单例和多例Spring默认Bean是单例的 。 因为绝大多数Bean其实是“无状态的” , 比如Controller、Service、Repository 。 所以多个线程去使用同一个Bean不会造成什么问题 。 本着节约成本的理念 , 使用单例Bean比较好 。
但是有时候我们可能会需要一个“有状态”的类 , 它内部又依赖其它Bean 。 比如一个Context或者一个Processor之类的 。 对于这种有状态有依赖其它Bean的类 , 有两种设计思路:
不给Spring管理 , 如果要用到其它Bean , 使用上面的applicationContext来直接获取Bean 。 给Spring管理 , 做成多例Bean , 每次都新建一个第二种使用起来会更优雅一些 , 也比较好测试一点 。 这里有一个小问题 , 我们来考虑以下这种情况:如果我们使用了一个多例Bean , 它可能会依赖一些单例Bean , 这个很好解决 , 在多例Bean中正常地注入单例Bean就行了 。 但是 , 如果我们要在一个单例Bean中使用一个多例Bean , 我们知道无论是构造器注入 , 还是方法注入 , 还是属性注入 , 都只会在Bean初始化的时候注入一次 , 那怎么能保证多个线程得到的是不同的多例Bean呢?
所以要在单例Bean中使用多例Bean , 不能使用一般的自动注入 。 Spring提供了@Lookup注解来帮我们做这个事 。 它是方法级别的注解 。
//定义一个多例Bean@Component@Scope("prototype")publicclassPrototypeBean{publicvoidsay(){System.out.println("saysomething...");}}@ComponentpublicclassSingletonBean{publicvoidprint(){//单例Bean中用多例BeanPrototypeBeanbean=methodInject();System.out.println("BeanSingletonBean'sHashCode"+bean.hashCode());}@LookuppublicPrototypeBeanmethodInject(){returnnull;}}复制代码需要注意的是 , 用@Lookup修饰的方法 , 不能是private的 。 可以是包访问权限、protected或public的 。 这里推荐写成public的 , 这样在单元测试的时候比较方便mock 。
循环依赖循环依赖其实很好理解 , 就是A依赖B , 而B又依赖A 。 这样就形成了循环依赖 。 那Spring是如何解决循环依赖的呢?
聪明你的肯定能够马上想到 , 如果两个Bean都是使用构造器注入 , 那是不能解决循环依赖的 , 一旦有循环依赖只能报错 。 而如果是属性注入或者方法注入 , 那可以先初始化两个Bean , 然后分别延迟注入进去 。 这样就可以解决循环依赖的问题 。
这也是为什么我们推荐使用构造器注入 。 循环依赖不是一个好设计 , 构造器注入可以提早发现这种循环依赖 。
Spring使用了一个叫做三级缓存的东西来解决循环依赖 , 具体的实现细节本文不做讨论 , 感兴趣的读者可以自己去找找相关的文章 。
给不给Spring管理?又回到上面那个单例和多例的问题 。 如果一个类是多例的 , 那它一般是有状态的 , 我们有必要把它交给Spring管理吗?或者说 , 有必要交给IoC容器管理吗?
在回答这个问题之前 , 我们先假设一下 , 如果不给IoC容器管理 , 会怎样?我们从三个角度来考虑:
这个类依不依赖其它Bean这个类是不是单例的如果这个类不依赖其它Bean , 那其实不太需要交给IoC容器管理 , POJO类就是一个很典型的例子 。 但如果这个类是一个单例的 , 那其实推荐交给IoC容器管理 , 因为要自己保证单例是比较麻烦的 , 而且不优雅 。 不信去看看单例模式的各种实现 。
如果这个类依赖其它Bean , 那推荐交给IoC容器管理 , 不然还得使用上面的那种applicationContext的getBean方法来获取依赖的Bean , 这就与IoC框架耦合了 , 不太划算 。
关于作者我是Yasin , 一个有颜有料又有趣的程序员 。
微信公众号:编了个程
个人网站:yasinshaw.com
关注我的公众号 , 和我一起成长~
推荐阅读
- 辣椒|关于信仰、哲学与科学的迷思|東西堂主
- 高血脂|聚焦2021CROI大会,发布多项关于日常服药引发血脂问题的研究
- 新冠疫苗|这些关于新冠疫苗的健康知识,建议收藏!
- 糖尿病|糖吃多了就会得糖尿病?关于糖尿病的疑惑,这篇文章一次性解答!
- 低血糖|关于低血糖的那些事儿
- 疫苗接种|重磅!关于疫苗接种,天津最新消息来了!
- 疫苗接种|关于疫苗接种,最新通报
- 甲亢|王克珍:关于甲亢疾病你了解多少?
- 治疗高血压|Cell Physiol Biochem:喝茶可以治疗高血压
- 大豆|关于大豆运输安全,此文很实用
