1.4 依赖注入
一个Web应用系统不可能只含有一个对象(或者说bean),即使最简单的系统也需要多个对象协同工作。接下来的内容将阐释如何定义一系列bean,并让它们一起协同工作。
1.4.1 基本依赖注入
● 依赖注入概念
依赖注入(Dependency Injection)和控制反转本质上表示同一个概念,其具体含义是:当某个角色(可能是一个Java实例,调用者)需要另一个角色(另一个Java实例,被调用者)的协助时,在传统的程序设计中,通常由调用者来创建被调用者的实例。但在Spring中,创建被调用者的工作不再由调用者来完成,而是由IOC容器来完成,所以称之为控制反转,然后注入调用者,因此也称为依赖注入。
再用一个通俗的例子来说明什么是依赖注入。在这个例子里面有一个调用者:一个人,一个被调用者:斧子。原始社会里,几乎没有社会分工,需要斧子的人(调用者)只能自己去造一把斧子(被调用者)。对应的为Java程序里的调用者自己创建被调用者。
进入工业社会,工厂出现了,斧子不再由需要斧子的人生产,而是在工厂里被生产出来了。此时需要斧子的人(调用者)找到工厂,购买斧子,并且无须关心斧子的制造过程,对应Java程序的简单工厂设计模式。
进入按需分配的社会,需要斧子的人不需要找到工厂,只要发出一个需要斧子的简单指令,斧子就会出现在他面前,对应的情形为Spring框架的依赖注入。
针对这个场景,我们进行抽象,并完成如图1.3所示的设计。
图1.3 人与斧子类图
程序具体代码如下:
//斧子 public interface Axe { //砍 public String chop(); } //石斧 public class StoneAxe implements Axe { @Override public String chop() { return "石斧砍柴好慢"; } } //钢斧 public class SteelAxe implements Axe { @Override public String chop() { return "钢斧砍柴真快"; } } //人 public interface Person { //使用斧子 public void useAxe(); } //中国人 public class Chinese implements Person{ private Axe axe; public Chinese() { super(); this.axe = new StoneAxe(); } @Override public void useAxe() { System.out.println(axe.chop()); } public Axe getAxe() { return axe; } public void setAxe(Axe axe) { this.axe = axe; } }
分析上述代码,关键点在Chinese类的构造方法中。这里的实现就像原始社会的场景一样,调用者手工打造了一把石斧。如果某一天,要将石斧替换为钢斧,或者其他类型的斧头,我们必须进入Chinese类内部来修改源代码。进入工业社会之后,相关代码替换为通过工厂获得斧子实例,这样虽然解除了调用者和斧子的耦合,但是调用者又和特定的工厂耦合在了一起。进入按需分配的社会,IOC容器会将被调用者实例(一把斧子)注入到调用者(一个人),实现了解耦。IOC容器主要有两种注入方式,即构造器注入和Setter注入。
● 构造器注入
基于构造器的依赖注入通过调用带参数的构造器来实现,每个参数代表着一个依赖。
(1)首先改写Chinese类,具体代码如下:
public class Chinese implements Person{ private Axe axe; public Chinese() { super(); } public Chinese(Axe axe) { super(); this.axe = axe; } @Override public void useAxe() { System.out.println(axe.chop()); }
在该代码中,调用者(Chinese)不需要创建Axe实例,使用一个构造器准备接收Axe实例,虚位以待。
(2)使用配置文件描述依赖关系
<bean id="stoneAxe" class="org.bd.spring.ch01.di.StoneAxe"></bean> <bean id="steelAxe" class="org.bd.spring.ch01.di.SteelAxe"></bean> <bean id="chinese" class="org.bd.spring.ch01.di.Chinese"> <constructor-arg index="0" ref="stoneAxe"></constructor-arg> </bean>
上面的配置信息中,第3行定义了之前提到的调用者,第4行采用构造器注入的方式将标识为stoneAxe的bean实例注入给标识为Chinese调用者实例。
constructor-arg元素代表着一个构造参数,index属性指定参数的位置,0代表第一个参数,在本例中实际上可以省略,ref属性指定了被注入bean的id。
(3)运行测试
下面可以写一个简单的main()方法进行测试。
public class Main { public static void main(String[] args) { ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml"); Person person = context.getBean("chinese", Person.class); person.useAxe(); } }
输出结果为“石斧砍柴好慢”。如果想要给调用者注入不同类型的实例,只需修改配置文件,例如:
<bean id="chinese" class="org.bd.spring.ch01.di.Chinese"> <constructor-arg index="0" ref="steelAxe"></constructor-arg> </bean>
源代码不做任何修改,输出结果为“钢斧砍柴真快”。
● Setter注入
构造器注入是在实例化对象时,将主对象依赖的对象作为参数传递给构造器。而Setter注入是在实例化完成之后调用Setter方法,将主对象依赖的对象作为参数传递给Setter方法。后者要求主对象有无参构造器,且提供对应的Setter方法。现在的Chinese类已满足这样的要求,那么需要变动的只是配置文件中的内容。
<bean id="chinese" class="org.bd.spring.ch01.di.Chinese"> <property name="axe" ref="stoneAxe"></property> </bean>
该配置文件的含义是,使用id为stoneAxe的bean给Chinese实例的axe属性赋值。在理解了注入的本质和构造器注入之后,理解Setter注入就非常简单,大家可变换被注入的bean的id,自行测试运行结果。
1.4.2 更多依赖注入的细节
上面的例子是通过构造器注入或Setter注入将一个被调用者bean实例注入到一个调用者bean实例。依赖注入不仅仅支持将一个bean实例注入到另一个bean实例,还可以将基本数据类型、String类型、集合等类型注入到调用者bean实例中,下面对这些不同的注入类型逐一进行介绍。
● 基本数据类型和String类型
在构造器注入或Setter注入中,可以通过value属性给property和constructor-arg元素指定字符串值,将字符串注入到调用者bean实例中。如果调用者bean中的数据类型为基本数据类型,则Spring会帮我们将字符串转换为基本数据类型。下面是配置DBCP(一个数据库连接池框架)的dataSource的实例。
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource"> <property name="driverClassName" value="oracle.jdbc.OracleDriver"></property> <property name="url" value="jdbc:oracle:thin:@localhost:1521:XE"></property> <property name="username" value="oa"></property> <property name="password" value="oa123"></property> </bean>
测试代码如下:
//加载beans.xml,实例化容器 ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml"); DataSource ds = context.getBean("dataSource", DataSource.class); try { System.out.println(ds.getConnection()); } catch (SQLException e) { e.printStackTrace(); }
注意:该段代码的运行依赖commons-dbcp-x.x.jar、commons-pool-x.x.jar和数据库驱动包。
● 集合类型
在配置文件中通过使用list、set、map及props元素,可以向调用者bean中注入List、Set、Map及Properties类型的对象。例如有这样一个类,它依赖各种集合类型的对象,类定义如下:
public class ExampleBean3 { private Properties emails; private List<String> stringList; private List<ExampleBean> beanList; private Map<String, String> someMap; private Set<String> someSet; //省略getter&setter方法 @Override public String toString() { return "ExampleBean3 :\n[emails=" + emails + "\n stringList=" + stringList + "\n beanList=" + beanList + "\n someMap=" + someMap + "\n someSet=" + someSet + "]"; } }
在配置文件中为ExampleBean3类对象注入其依赖的集合对象。
<bean id="expBean3" class="org.bd.spring.ch01.collectios.ExampleBean3"> <property name="emails"><!--emails属性是Properties类型 --> <props> <prop key="administrator">administrator@example.org</prop> <prop key="support">support@example.org</prop> <prop key="development">development@example.org</prop> </props> </property> <property name="stringList"><!--stringList属性是List<String>类型 --> <list> <value>IOC</value> <value>DI</value> <value>Spring</value> </list> </property> <property name="beanList"><!--beanList属性是List<ExampleBean>类型 --> <list> <ref bean="expBean"/><!-- 第一个元素,引用bean作为List的元素 --> <bean class="org.bd.spring.ch01.ExampleBean"></bean><!-- 第二个元素,匿名bean--> </list> </property> <property name="someMap"><!--someMap属性是Map<String,String>类型 --> <map> <entry><!-- 第一个元素 --> <key><value>admin</value></key><!-- 键 --> <value>administrator@example.org</value><!-- 值 --> </entry> <entry><!-- 第二个元素 --> <key><value>support</value></key> <value>support@example.org</value> </entry> </map> </property> <property name="someSet"><!--someSet属性是Set<String>类型 --> <set> <value>JavaSE</value> <value>JavaEE</value> <value>JavaME</value> </set> </property> </bean>
向调用者对象中注入集合类型对象时,实际上是先往集合里添加元素,再将集合对象注入给调用者。添加元素时,应当注意集合上的泛型,除Properties较为特殊(键和值都是字符串类型的Map)外,对于元素类型是基本类型或者字符串类型的,可以用<value>标签来表示一个元素,对于元素类型是引用类型的,可以使用<ref bean="…"/>引入一个已经定义好的bean,或者使用<bean>标签创建一个bean。
测试代码如下:
public static void main(String[] args) { ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml"); ExampleBean3 bean = context.getBean("expBean3",ExampleBean3.class); System.out.println(bean); }
运行结果为:
ExampleBean3 : [emails={support=support@example.org, administrator=administrator@example.org, development=development@example.org} stringList=[IOC, DI, Spring] beanList=[org.bd.spring.ch01.ExampleBean@10a3b24, org.bd.spring.ch01.ExampleBean@b0ce8f] someMap={admin=administrator@example.org, support=support@example.org} someSet=[JavaSE, JavaEE, JavaME]]
● 空字符串和Null
也可以为bean的属性赋空字符串和null,具体配置如下:
<bean id="exampleBean"class="ExampleBean"> <property name="email"><value/></property> </bean>
功能相同的Java代码为:exampleBean.setEmail("")。
<bean id="exampleBean"class="ExampleBean"> <property name="email"><null/></property> </bean>
功能相同的Java代码为exampleBean.setEmail(null)。
1.4.3 自动装配
Spring容器可以自动装配协作者之间的关系。我们可以让Spring通过检查整个ApplicationContext的内容,自动为bean解析协作者(另外的bean)。自动装配有以下好处:(1)自动装配能显著减少通过属性或者构造参数进行注入的配置数量。
(2)自动装配可以使配置与Java代码同步更新。例如,如果需要给一个Java类增加一个依赖,那么该依赖将被自动实现而不需要修改配置。
因此在开发过程中采用自动装配将会非常有用,在系统趋于稳定的时候可以改为显式装配的方式。
在基于XML的配置中,为bean元素指定autowire属性可以用来控制自动装配的模式。自动装配功能有5种模式,表1.1列出了这5种模式及对应的描述。
表1.1 自动装配模式
byName配置的示例如下:
<bean id="axe" class="org.bd.spring.ch01.di.StoneAxe"></bean> <bean id="chinese" class="org.bd.spring.ch01.di.Chinese" autowire="byName"></bean>
自动装配可以减少对依赖注入的显式配置,减少了配置文件的行数,但其也存在一些缺点,需要大家有所了解。
(1)尽管自动装配比显式装配显得更加神奇,但使用Spring应尽量避免在装配不明确的时候进行猜测,因为装配不明确可能出现难以预料的结果。而且使用自动装配,Spring所管理的对象之间的关联关系也不能清晰地进行文档化。
(2)对于那些根据Spring配置文件生成文档的工具来说,自动装配将会使这些工具没法生成依赖信息。