Spring学习第一章、第二节:依赖注入(包括自动装配,对象的注入)
依赖注入
前言
其实在第一章中,我们多次提到了关于依赖注入(DI)的内容,而上一章中讲的Spring容器为什么在Spring中叫做Ioc呢?
我们知道了Ioc叫做控制反转,也就是说我们将控制权交给了Spring容器,我们回顾一下Spring对Spring Bean进行控制的过程:
- 首先,我们定义了Bean的配置文件,和相应的实现类
- Spring会对Bean配置信息进行读取,将Bean信息加载到Spring容器中的Bean定义注册表中
- 根据Bean注册表和对应的实现类实例化Bean对象,放到Spring容器中,供应用程序调用
如图:![]()
嗯,有没有想过为什么要进行控制反转呢?
因为大多数应用程序都是由两个或是更多的类通过彼此的合作来实现业务逻辑,这使得每个对象都需要获取与其合作的对象(也就是它所依赖的对象)的引用。如果这个获取过程要靠自身实现,那么这将导致代码高度耦合并且难以维护和调试。例如:
Class A中用到了Class B的对象b,一般情况下,需要在A的代码中显式的new一个B的对象。
此时,A类中将new出B对象。
那么有没有办法缓解这个问题呢?开发者们发现了一个好办法:接口驱动(Interface Driven Design)
classA
{
AInterface a;
A(){}
AMethod(){
a = new AInterfaceImp();
}
}
这时,我们还可以写一个Factory来应对需要AInterface另一个或多个实现的需求(举一个简单工厂的栗子)
InterfaceImplFactory
{
AInterface create(Object condition)
{
if(condition == condA)
{
return new AInterfaceImpA();
}
else if(condition == condB)
{
return new AInterfaceImpB();
}
else
{
return new AInterfaceImp();
}
}
}
这种方式虽然让代码更灵活,健壮性更高,但是本质上,最后还是会让类本身实现了接口产生依赖,也就是说:AInterface a = new AInterfaceImp(); 这样的代码迟早要执行,耦合关系的产生无法避免。
而Ioc的提出可以彻底解决这种耦合,它把耦合从代码中移出去,放到统一的XML 文件中,通过一个容器在需要的时候把这个依赖关系形成,即把需要的接口实现注入到需要它的类中,这可能就是“依赖注入”说法的来源了。
下面,我们就来学习Spring如何通过DI实现Ioc
在此之前,我们补充一个知识点
Spring Bean 定义继承
bean 定义可以包含很多的配置信息,包括构造函数的参数,属性值,容器的具体信息例如初始化方法,静态工厂方法名,等等。
子 bean 的定义继承父定义的配置数据。子定义可以根据需要重写一些值,或者添加其他值。
Spring Bean 定义的继承与 Java 类的继承无关,但是继承的概念是一样的。你可以定义一个父 bean 的定义作为模板和其他子 bean 就可以从父 bean 中继承所需的配置。
当你使用基于 XML 的配置元数据时,通过使用父属性,指定父 bean 作为该属性的值来表明子 bean 的定义。
下面是配置文件 Beans.xml,在该配置文件中我们定义有两个属性 message1 和 message2 的 “helloWorld” bean。然后,使用 parent 属性把 “helloIndia” bean 定义为 “helloWorld” bean 的孩子。这个子 bean 继承 message2 的属性,重写 message1 的属性,并且引入一个属性 message3。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">
<bean id="helloWorld" class="com.tutorialspoint.HelloWorld">
<property name="message1" value="Hello World!"/>
<property name="message2" value="Hello Second World!"/>
</bean>
<bean id="helloIndia" class="com.tutorialspoint.HelloIndia" parent="helloWorld">
<property name="message1" value="Hello India!"/>
<property name="message3" value="Namaste India!"/>
</bean>
</beans>
这里是 HelloWorld.java 文件的内容:
public class HelloWorld {
private String message1;
private String message2;
public void setMessage1(String message){
this.message1 = message;
}
public void setMessage2(String message){
this.message2 = message;
}
public void getMessage1(){
System.out.println("World Message1 : " + message1);
}
public void getMessage2(){
System.out.println("World Message2 : " + message2);
}
}
这里是 HelloIndia.java 文件的内容:
public class HelloIndia {
private String message1;
private String message2;
private String message3;
public void setMessage1(String message){
this.message1 = message;
}
public void setMessage2(String message){
this.message2 = message;
}
public void setMessage3(String message){
this.message3 = message;
}
public void getMessage1(){
System.out.println("India Message1 : " + message1);
}
public void getMessage2(){
System.out.println("India Message2 : " + message2);
}
public void getMessage3(){
System.out.println("India Message3 : " + message3);
}
}
Bean 定义模板
你可以创建一个 Bean 定义模板,不需要花太多功夫它就可以被其他子 bean 定义使用。在定义一个 Bean 定义模板时,你不应该指定类的属性,而应该指定带 true 值的抽象属性,如下所示:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">
<bean id="beanTeamplate" abstract="true">
<property name="message1" value="Hello World!"/>
<property name="message2" value="Hello Second World!"/>
<property name="message3" value="Namaste India!"/>
</bean>
<bean id="helloIndia" class="com.tutorialspoint.HelloIndia" parent="beanTeamplate">
<property name="message1" value="Hello India!"/>
<property name="message3" value="Namaste India!"/>
</bean>
</beans>
父 bean 自身不能被实例化,因为它是不完整的,而且它也被明确地标记为抽象的。当一个定义是抽象的,它仅仅作为一个纯粹的模板 bean 定义来使用的,充当子定义的父定义使用。
正文
Spring依赖注入
每个基于应用程序的 java 都有几个对象,这些对象一起工作来呈现出终端用户所看到的工作的应用程序。当编写一个复杂的 Java 应用程序时,应用程序类应该尽可能独立于其他 Java 类来增加这些类重用的可能性,并且在做单元测试时,测试独立于其他类的独立性。依赖注入(或有时称为布线)有助于把这些类粘合在一起,同时保持他们独立。
就像之前提到的,我们在使用一个类中可能会依赖另一个类的实例:假设你有一个包含文本编辑器组件的应用程序,并且你想要提供拼写检查。标准代码看起来是这样的
public class TextEditor {
private SpellChecker spellChecker;
public TextEditor() {
spellChecker = new SpellChecker();
}
}
而在控制反转的场景中,我们会做类似这样的事:
public class TextEditor {
private SpellChecker spellChecker;
public TextEditor(SpellChecker spellChecker) {
this.spellChecker = spellChecker;
}
}
我们发现,TextEditor 不应该担心 SpellChecker 的实现。SpellChecker 将会独立实现,并且在 TextEditor 实例化的时候将提供给 TextEditor,整个过程是由 Spring 框架的控制。(包括实例化的过程,嗯,这里应该就只有它的引用),实现这个过程就得需要依赖注入出场了!
而Spring中依赖注入主要有两种方式:
序号 | 依赖注入类型 & 描述 |
---|---|
1 | Constructor-based dependency injection 当容器调用带有多个参数的构造函数类时,实现基于构造函数的 DI,每个代表在其他类中的一个依赖关系。 |
2 | Setter-based dependency injection基于 setter 方法的 DI 是通过在调用无参数的构造函数或无参数的静态工厂方法实例化 bean 之后容器调用 beans 的 setter 方法来实现的 |
你可以混合这两种方法,基于构造函数和基于 setter 方法的 DI,不过更推荐使用Setter方法。代码是 DI 原理的清洗机,当对象与它们的依赖关系被提供时,解耦效果更明显。对象不查找它的依赖关系,也不知道依赖关系的位置或类,而这一切都由 Spring 框架控制的。
如下图:
基于构造器的依赖注入
public class B {
public B() {
System.out.println("B()");
}
public void f1() {
System.out.println("B's f1()");
}
}
public class A {
private B b;
public A(B b) {//配置构造器
System.out.println("A(B)");
this.b = b;
}
public A() {
System.out.println("A()");
}
public void execute() {
System.out.println(" execute()");
b.f1();
}
}
<bean id="b1" class="ioc2.B"/>
<bean id="a1" class="ioc2.A">
<constructor-arg index="0" ref="b1"></constructor-arg>
</bean>
构造器方法注入:constructor-arg
index:选择构造器中第n个参数
其他情况:
public class Foo {
public Foo(int year, String name) {
// ...
}
}
<beans>
<bean id="exampleBean" class="examples.ExampleBean">
<constructor-arg type="int" value="2001"/>
<constructor-arg type="java.lang.String" value="Zara"/>
</bean>
</beans>
最后并且也是最好的传递构造函数参数的方式,使用 index 属性来显式的指定构造函数参数的索引。下面是基于索引为 0 的例
<beans>
<bean id="exampleBean" class="examples.ExampleBean">
<constructor-arg index="0" value="2001"/>
<constructor-arg index="1" value="Zara"/>
</bean>
</beans>
最后,如果你想要向一个对象传递一个引用,你需要使用 标签的 ref 属性,如果你想要直接传递值,那么你应该使用如上所示的 value 属性。
基于Setter方法的依赖注入
Spring会把配置文件中属性名的第一个字母大写来解析Setter方法。
public class B {
public B() {
System.out.println("B()");
}
public void f1() {
System.out.println("B's f1()");
}
}
public class A {
private B b;
public void setB(B b) {
System.out.println("setB()");
this.b = b;
}
public A() {
System.out.println("A()");
}
public void execute() {
System.out.println("execute()");
b.f1();
}
}
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">
<bean id="b1" class="ioc.B"/>
<bean id="a1" class="ioc.A">
<property name="b" ref="b1"></property>
</bean>
</beans>
property元素:表示使用set方法注入依赖关系
name:指定属性名
ref:指定属性值
main(){//表示main函数
AbstractApplicationContext ac =
new ClassPathXmlApplicationContext(
"ioc.xml");
A a1 = ac.getBean("a1",A.class);
a1.execute();
}
测试下结果就发现B已经被容器注入了!
扩展:使用 p-namespace 实现 XML 配置:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">
<bean id="john-classic" class="com.example.Person">
<property name="name" value="John Doe"/>
<property name="spouse" ref="jane"/>
</bean>
<bean name="jane" class="com.example.Person">
<property name="name" value="John Doe"/>
</bean>
</beans>
上述 XML 配置文件可以使用 p-namespace 以一种更简洁的方式重写,如下所示:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">
<bean id="john-classic" class="com.example.Person"
p:name="John Doe"
p:spouse-ref="jane"/>
</bean>
<bean name="jane" class="com.example.Person"
p:name="John Doe"/>
</bean>
</beans>
在这里,你不应该区别指定原始值和带有 p-namespace 的对象引用。-ref 部分表明这不是一个直接的值,而是对另一个 bean 的引用。
自动装配
你已经学会如何使用元素来声明 bean 和通过使用 XML 配置文件中的和元素来注入 。
Spring 容器可以在不使用和 元素的情况下自动装配相互协作的 bean 之间的关系,这有助于减少编写一个大的基于 Spring 的应用程序的 XML 配置的数量。
自动装配模式 (较官方的版本)
下列自动装配模式,它们可用于指示 Spring 容器为来使用自动装配进行依赖注入。你可以使用元素的 autowire 属性为一个 bean 定义指定自动装配模式:
模式 | 描述 |
---|---|
no | 这是默认的设置,它意味着没有自动装配,你应该使用显式的bean引用来连线。你不用为了连线做特殊的事。在依赖注入章节你已经看到这个了。 |
byName | 由属性名自动装配。Spring 容器看到在 XML 配置文件中 bean 的自动装配的属性设置为 byName。然后尝试匹配,并且将它的属性与在配置文件中被定义为相同名称的 beans 的属性进行连接。 |
byType | 由属性数据类型自动装配。Spring 容器看到在 XML 配置文件中 bean 的自动装配的属性设置为 byType。然后如果它的类型匹配配置文件中的一个确切的 bean 名称,它将尝试匹配和连接属性的类型。如果存在不止一个这样的 bean,则一个致命的异常将会被抛出。 |
constructor | 类似于 byType,但该类型适用于构造函数参数类型。如果在容器中没有一个构造函数参数类型的 bean,则一个致命错误将会发生。 |
autodetect | Spring首先尝试通过 constructor 使用自动装配来连接,如果它不执行,Spring 尝试通过 byType 来自动装配 |
呃,简单点说吧:
- 自动装配:spring容器依据某种规则自动建立对象之间的依赖关系
- 默认情况下,容器不会自动装配
- 可以通过指定autowire属性告诉容器进行自动装配(本质上,容器仍然是set或是构造器注入)
我们举较常用的栗子说明下:
我们想将Waiter注入到Restrurant中
public class Waiter {
public Waiter() {
System.out.println("Waiter()");
}
}
<bean id="waiter" class="ioc2.Waiter"></bean>
<bean id="rest" class="ioc2.Restaurant" autowire="byName"></bean>
<bean id="rest" class="ioc2.Restaurant" autowire="byType"></bean>
<bean id="rest" class="ioc2.Restaurant" autowire="constructor"></bean>
把autowire属性的三个值进行对比:
- byName:容器依次查找属性名(配置文件中bean节点的id或第一个name属性)对应的bean,然后调用对应的set方法来完成注入
- 若找不到对应的bean,注入空值。
- 不可能找到多个符合条件的bean(Bean中 name或id是唯一的)
- byType:容器次查找(配置文件中对应类中类型)属性类型对应的bean,然后调用对应的set方法来完成注入
- 若找不到对应的bean,注入空值。
- 多个符合条件的bean会报错(一个变量只能有一个值)
- constructor:类似于byType容器次查找属性类型对应的bean,然后调用对应的构造器来完成注入
推荐优先使用byName方法。
注入基本类型的值
基本数据类型我们通过value进行注入,依旧是set方法或构造器方法注入
为set方法写一个示例:(当然在类中要有set方法),构造器方法请自己举一反三☺
<bean id="vb1" class="value.ValueBean">
<property name="name" value="张三"></property>
<property name="age"value="30"></property>
</bean>
注入内部beans
Java 内部类是在其他类的范围内被定义的,同理,inner beans 是在其他 bean 的范围内定义的 bean。因此在 或 元素内 元素被称为内部bean,如下所示。
<bean id="outerBean" class="ioc.B">
<property name="target">
<bean id="innerBean" class="ioc.A"/>
</property>
</bean>
注入集合
你已经看到了如何使用 value 属性来配置基本数据类型和在你的 bean 配置文件中使用标签的 ref 属性来配置对象引用。这两种情况下处理奇异值传递给一个 bean。
现在如果你想传递多个值,如 Java Collection 类型 List、Set、Map 和 Properties,应该怎么做呢。为了处理这种情况,Spring 提供了四种类型的集合的配置元素
import java.util.*;
public class ValueBean {
public ValueBean() {
System.out.println("ValueBean()");
}
private String name;
private int age;
private List<String> list1 ;
private Set<String> interest;
private Map<String,Double> mapa;
private Properties db;
public void setDb(Properties db) {
this.db = db;
}
public void setMapa(Map<String, Double> mapa) {
this.mapa = mapa;
}
public void setInterest(Set<String> interest) {
this.interest = interest;
}
public void setName(String name) {
this.name = name;
}
public void setAge(int age) {
this.age = age;
}
public void setList1(List<String> list1) {
this.list1 = list1;
}
@Override
public String toString() {
return "ValueBean [name=" + name + ", age=" + age + ", list1=" + list1 + ", interest=" + interest + ", mapa="
+ mapa + ", db=" + db + "]";
}
}
<bean id="vb1" class="value.ValueBean">
<property name="name" value="张三"></property>
<property name="age" value="30"></property>
<!--注入集合 -->
<property name="list1">
<list>
<value>北京</value>
<value>上海</value>
<value>广州</value>
<value>呼和浩特</value>
</list>
</property>
<property name="interest">
<!-- 注入Set -->
<set>
<value>篮球</value>
<value>足球</value>
<value>排球</value>
<value>游泳</value>
</set>
</property>
<!-- 注入Map -->
<property name="mapa">
<map>
<entry key="english" value="60" />
<entry key="math" value="66" />
<entry key="历史" value="60" />
</map>
</property>
<!-- 注入properties -->
<property name="db">
<props>
<prop key="username">Tiger</prop>
<prop key="password">12345</prop>
</props>
</property>
</bean>
<!-- 引用方式注入集合 -->
<!-- 这样写,可以使集合进行复用 -->
<util:list id="cityBean" >
<value>上海</value>
<value>北京</value>
<value>呼和浩特</value>
</util:list>
<bean id="vb2" class="value.ValueBean">
<property name="list1" ref="cityBean"></property>
</bean>