Spring核心——Bean的依赖注入
依赖注入
在设计模式与IoC这篇文章中,介绍了Spring基础的三大支柱的两项内容——IoC、Bean。本篇将继续围绕着Bean的创建时的注入方式来介绍Spring的核心思想与设计模式。
天底下所有面向对象的语言都不可能只用一个类来解决问题,即使是最简单的应用程序都存在类与类之间的依存关系。如下面这个人人都理解的组合例子:
class Foo{ private Other other; public Foo(){ other = new Other(); } } class Other{}
在设计模式上关于类的组合与继承的适用性不属于本篇的讨论范围,但是从Spring框架非侵入式的设计思路来看,组合才是使用Spring的正确姿势。
官方将这种组合的关系叫做“依赖注入(DI——Dependency injection)”。从名字上来看这也是一种依托Ioc容器很自然的实现方式——所有的Bean都放置在容器中,然后通过一些配置来告诉容器bean与bean之间的依存关系。一个类除了在内部块中通过new关键字实现一个组合关系,也可以通过构造方法传参或接口方法设置。
由于IoC容器不可能去修改一个类内部的代码,所以类与类的组合方式通过构造方法(Constructor)和set方法(Setter)来实现。此外,Ioc可以根据接口(interface)来注入对应的实现类(class extands interface),所以从设计模式的角度来说,依赖注入的方式很好的规避了标准组合模式中new关键字违反依赖倒置原则的问题。
构造方法注入
直接通过构造方法注入组合数据。
class:
package x.y; public class A { private B b; private C c; public Foo(B b, C c) { this.b = b; this.c = c; } } public class B {} public class C {}
xml:
<beans> <bean id="a" class="x.y.A"> <constructor-arg ref="b"/> <constructor-arg ref="c"/> </bean> <bean id="b" class="x.y.B"/> <bean id="c" class="x.y.C"/> </beans>
如果是源生类型的参数,可以通过指定类型来注入数据:
package x.y; public class A { private int b; private String c; public Foo(int b, String c) { this.b = b; this.c = c; } }
<bean id="a" class="x.y.A"> <constructor-arg type="int" value="1"/> <constructor-arg type="java.lang.String" value="42"/> </bean>
也可以通过索引的方式:
<bean id="a" class="x.y.A"> <constructor-arg index="0" value="1"/> <constructor-arg index="1" value="42"/> </bean>
配合@ConstructorProperties注解,还可以直接使用名称来注入:
package x.y; public class A { private int b; private String c; ({"b", "c"}) public Foo(int b, String c) { this.b = b; this.c = c; } }
<bean id="exampleBean" class="examples.ExampleBean"> <constructor-arg name="b" value="1"/> <constructor-arg name="c" value="42"/> </bean>
在Debug模式下不用这个注解也可以实现按名字注入,但是千万别这样做。
Set方法注入
package x.y; public class A { private B b; private C c; private String value; public void setA(A a){this.a = a;} public void setB(B b){this.b = b;} public void setB(String value){this.value = value;} } public class B {} public class C {}
<bean id="a" class="x.y.A"> <property name="b" ref="b"/> <property name="c" ref="c"/> <property name="value" value="1"/> </bean> <bean id="b" class="x.y.B"/> <bean id="c" class="x.y.C"/>
使用 Constructor还是Setter?
2种注入方法在使用的过程中我们应该如何选取呢?Spring官方给出的答案是如果注入的数据或bean是一个“必要依赖”那么使用构造方法注入,如果属于配置性的非必须数据,使用Set方法注入。但是在实际应用时,会发现绝大部分注入方式都是通过Setter实现的,包括一些很流行的开源工具,例如下面的druid:
<bean id="ds" class="com.mchange.v2.c3p0.ComboPooledDataSource"> <property name="driverClass" value="com.mysql.jdbc.Driver"/> <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/c3p0jdbctemplate"/> <property name="user" value="admin"/> <property name="password" value="123456"/> </bean>
话说你不提供账户和密码能链接到数据库吗?这算必要依赖还是配置性依赖?所以也不用死守这些规则。下面是一些关键性的建议:
- 数据配置类使用constructor注入的方法来实现,因为这样可以将bean设置为一个不可变对象(immutable objects)。这样结合单例模式能够很好实现享元模式共享数据,结合原型模式可以创建“浅对比”对象(变更则替换)。
- 如果构造函数要传入的参数太多,证明你的类要完成的责任太多,这个时候用Setter当然比较合理,但是建议回头去看看类当中是不是有可以拆分的功能。
- Setter注入主要用于可选的依赖关系,如果没有设置值,类应该提供默认值。所以Setter方法应该检查传入值的有效性(not null、not blank等)。
- 如果出现了循环依赖,其实可以通过一个bean使用setter注入另外一个bean使用constructor注入来解决,不过最好检查一下代码为什么会循环,这是设计模式上的大忌。
- 最有一个建议最重要。如果用第三方类,别人给什么你只能用什么,没得选。
注入参数
在XML配置中,用来设定注入方式和注入数据的XML标签很多,详细内容就不一一复述了,常规用法可以到官网 Dependencies and configuration in detail 一节了解。这里仅仅说明一些要点:
- 父子Bean。Ioc容器提供Bean的父子关系配置。父子关系Bean可以进行数据合并,但是很少看见什么地方有实际应用。
- <idref>标签和<ref>标签的差异:1)前者只能通过id引入,后者可以通过id或name引入;2)前者可以直接用value属性替换,但是value属性的效率会差很多;3)前者只能适用与当前配置文件或当前容器,后者可以引入任何位置的内容。
- 当需要设置一个null值时,用<null>标签代替value=""。在执行代码时直接传入一个null。
自动装配
这里所说的自动装配是通过<bean>上的autowire属性实现的功能,与@Autowired注解并不是一回事,但是他的一些参数会影像@Autowired注解的行为。
在有@Autowired注解的情况下,autowire属性现在用得很少。基本上他实现的结果和@Autowired差不多,就是让Ioc容器根据bean的类型或者bean名称等自动将容器中其他能对应得上的bean注入到对于的构造方法或者set方法中。详情了解 Autowiring collaborators。
方法注入
如果每一个Bean都是单例模式,那么我们通过常规的XML配置引用的手段就可以实现所有的依赖组合关系。但是每个bean都有不同的生命周期,常规配置方法很难实现某些应用不同生命周期bean的依赖关系。
第一种方式是通过继承 ApplicationContextAware 类,继承后可以直接使用 applicationContext 的 getBean 接口来获取任何一个 bean。
package x.y; import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; public class BeanManagerFoo implements ApplicationContextAware { private ApplicationContext applicationContext; public <T> T getBean(Class<T> type){ return applicationContext.getBean(type); } public Object getBean(String id){ return springContext.getBean(id); } public void setApplicationContext( ApplicationContext applicationContext) throws BeansException { this.applicationContext = applicationContext; } }
第二种方法是使用Lookup Method。
Lookup Method的实现思路是使用CGLIB生成了动态代理类并放置到Ioc中代替源生的类。看下面的例子。
首先实现我们的抽象类,抽象的要求至少有一个抽象方法:
package x.y; public abstract class A { public String getName() { B b = this.createB(); return b.getName(); } protected abstract B createB(); } public class B { private String name = "B class"; public String getName(){ return this.name; } }
然后通过<lookup-method>标签来指定获取bean的方式:
<bean id="b" class="x.y.B" scope="prototype" /> <bean id="a" class="x.y.A"> <lookup-method name="createB" bean="b"/> </bean>
现在,在调用A.getName方法时都会创建一个新的B类实例。需要注意scope属性,如果修改为singleton则每次都获取同一个B实例。
使用动态代理由于是字节码级别的变换,所有有很多限制需要注意:方法和类都不能用fina关键字;测试用例需要自己实现代理模式,否则抽象类没有实现;
第三种方法是使用委派模式,即我们执行A.compute方法时,实际执行的是被委派的B.reimplement方法。
先定义2个基础类——Origin、Replace:
package x.y public class Origin { public int compute(int in1, int in2) { return in1+in2; } } public class Replace { public int reimplement(Object o, Method m, Object[] args) { int in1 = (int)args[0]; int in2 = (int)args[1]; return in1+in2; } }
然后定义Spring配置:
<bean id="origin" class="x.y.Origin"> <replaced-method name="compute" replacer="replace"> <arg-type>int</arg-type> <arg-type>int</arg-type> </replaced-method> </bean> <bean id="replace" class="x.y.Replace"/>
这个时候,在任何时候执行“origin”这个bean的compute方法,实际上都是执行的Replace::reimplement方法。
上面<arg-type/>的参数用全称或简写都可以,例如java.lang.String,使用String,Str都是指向这个类型。
使用委派模式的好处是限制少、灵活,并且不会用到CGLIB这种重量级工具。但是委派之后委派方法的真实参数和被委派方法的参数完全不一样,开发时需要时时刻刻紧跟委派类的结构来修改代码。一旦委派类发生任何修改而没有相应的调整被委派类,可能会出现意想不到的问题。
低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
2018 年最值得关注的 JavaScript 趋势
JavaScript 渗透的范围越来越广,它能做的事情已经远不止前端开发而已。不久前stateofjs.com刚刚发布了 2017 JavaScript 现状报告 ,现在Ryan Chartrand非常应景地推出了2018年的JavaScript发展趋势,把这两份文章一起结合来看,相信作为JS开发者的你一定不再迷茫。 去年,有50000人对JavaScript的上升趋势感到好奇。。那么好吧,我的开发者同胞们,现在我们再来看看2018年怎样。 如果你2017年一整年都与世隔绝或者忙于项目而自顾不暇的话,这篇文章就是给你准备的。 2017年发生的很多事情正在为2018年的许多行动和创新做好准备。 你还可以把本文用作规划个人成长的指南,来推出更具创新性的项目。 React vs.Vue.js 我们开门见山,直接上好东西吧:认为Vue可能会成为React的一大竞争敌手的人不是很多,但是今年想要无视Vue是不可能的,在开发者的炒作方面甚至令Angular黯然失色。 展望2018年的时候,我们即将迎来2年的激烈竞争,而对Vue的炒作会非常多。 React有着全球最富有公司之一的财政支持,更不用说他...
- 下一篇
React JSX语法与组件
JSX基础介绍 先看看一个最简单的例子: const element = <h1>Hello, world!</h1>; 上面这段有趣的例子既不是标准的JavaScript也不是HTML,它就是我们接下来要介绍的JSX的语法,是一种JavaScript的扩展。在React中使用JSX描述一个UI是什么样子的,就好像HTML告诉浏览器我们看到的页面是什么样子。最开始接触JSX时会感觉它很像一种模板语言,但是除了提供模板能力之外,他拥有JavaScript所有的能力。 JSX用于产生React的组件,JSX最大的特色就是就是在JavaScript中嵌入和HTML表达式。我们先看下面这个例子: function formatName(user) { //将参数合并成一个srting return user.firstName + ' ' + user.lastName; } //创建user对象 const user = { firstName: 'Harper', lastName: 'Perez' }; //创建element对象,并用JSX语法标识为一个html...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- Red5直播服务器,属于Java语言的直播服务器
- Docker使用Oracle官方镜像安装(12C,18C,19C)
- Jdk安装(Linux,MacOS,Windows),包含三大操作系统的最全安装
- Springboot2将连接池hikari替换为druid,体验最强大的数据库连接池
- Linux系统CentOS6、CentOS7手动修改IP地址
- CentOS7,CentOS8安装Elasticsearch6.8.6
- CentOS8编译安装MySQL8.0.19
- CentOS关闭SELinux安全模块
- SpringBoot2整合MyBatis,连接MySql数据库做增删改查操作
- SpringBoot2整合Redis,开启缓存,提高访问速度