1.依赖注入
依赖注入(DI)用来管理Bean之间的依赖关系,将几个独立的、不同的类组合在一起来完成工作。
当一个对象被创建时,IOC 容器会注入它的依赖,这个过程与传统创建对象的方式是相反的故称为控制反转(IOC)。
传统创建对象的方式:
public TextEditor()
{
spellChecker = new SpellChecker();
}
控制反转(IOC):
public TextEditor(SpellChecker spellChecker)
{
this.spellChecker = spellChecker;
}
在这里,TextEditor 不应该担心 SpellChecker 的实现。SpellChecker 将会独立实现,并且在 TextEditor 实例化的时候将提供给 TextEditor,整个过程是由 Spring 框架的控制。
在这里,我们已经从 TextEditor 中删除了全面控制,并且把它保存到其他地方(即 XML 配置文件),且依赖关系(即 SpellChecker 类)通过类构造函数被注入到 TextEditor 类中。因此,控制流通过依赖注入(DI)已经“反转”,因为你已经有效地委托依赖关系到一些外部系统。
使用DI后代码十分的简洁,并且在定义对象依赖时也能有效的解耦。对象不需要过多的去关注它依赖的对象,所以类更容易测试,特别是依赖于接口或者是抽象类时。可以在单元测试中使用stub或者mock实现。
DI存在两种主要的形式:基于构造器注入和Setter方法注入。
1.1.基于构造器注入
构造注入通过容器提供指定的参数调用构造方法来完成,这和通过提供给静态的工厂方法的参数来创建对象几乎是等价的。
下面的样例展示了一个类通过构造方法来完成依赖注入的过程。注意:这个类是普通的Java对象,不依赖于容器相关的接口、基类或者注解。
public class SimpleMovieLister
{
// SimpleMovieLister依赖于MovieFinder
private MovieFinder movieFinder;
// Spring通过构造器注入MovieFinder实例
public SimpleMovieLister(MovieFinder movieFinder)
{
this.movieFinder = movieFinder;
}
//使用被注入MovieFinder实例来完成的业务逻辑已被忽略
}
1.1.1.构造参数的识别
构造参数的识别是通过类型匹配来完成的,如果在Bean的定义中没有模糊不清的构造参数,那么在Bean定义中指定的构造参数的顺序 也就是 Bean被实例化时构造器加载参数的顺序。如下:
package x.y;
public class Foo {
public Foo(Bar bar, Baz baz) {
// ...
}
}
现在,参数都是明确的。假定Bar和Baz没有继承关系,因此下面的配置没有任何问题,你也不需要在元素中指定参数的索引和类型。
<beans>
<bean id="foo" class="x.y.Foo">
<constructor-arg ref="bar"/>
<constructor-arg ref="baz"/>
</bean>
<bean id="bar" class="x.y.Bar"/>
<bean id="baz" class="x.y.Baz"/>
</beans>
当引用的Bean的类型是确定的,如上一个Bar 类型, Baz 类型,这能很好的进行参数匹配。如果使用的是简单类型(基本类型、字符串等等),Spring就不能判断其类型了,所以不能独自完成类型匹配。参考如下代码:
package examples;
public class ExampleBean {
//计算最终答案的年份
private int years;
//生活、宇宙、万物的答案
private String ultimateAnswer;
public ExampleBean(int years, String ultimateAnswer) {
this.years = years;
this.ultimateAnswer = ultimateAnswer;
}
}
<bean id="exampleBean" class="examples.ExampleBean">
<constructor-arg value="ultimateAnswer"/>
<constructor-arg value="7500000"/>
</bean>
形参实参就不好识别,因为在Bean定义中不知道构造器参数是什么类型的,所以
1.构造参数类型匹配
在上面的例子中,如果你通过type
属性指定参数的类型,那么容器可以很好的进行简单参数类型的类型匹配。参考如下代码:
<bean id="exampleBean" class="examples.ExampleBean">
<constructor-arg type="int" value="7500000"/>
<constructor-arg type="java.lang.String" value="42"/>
</bean>
2.构造参数索引 //推荐
使用index属性指定参数在构造器中的索引,参考如下代码:
<bean id="exampleBean" class="examples.ExampleBean">
<constructor-arg index="0" value="7500000"/>
<constructor-arg index="1" value="42"/>
</bean>
7500000是第一个参数,42是第二个
如果不指定index的值,则默认以出现的顺序为参数索引。
为了解决多个简单类型间的歧义性,有多个相同类型的时候,可以通过指定值在构造参数中的索引。注意:索引从0开始。
3.构造参数名 //不推荐
你也可以指定值所对应构造器中的参数名来消除歧义:
<bean id="exampleBean" class="examples.ExampleBean">
<constructor-arg name="years" value="7500000"/>
<constructor-arg name="ultimateAnswer" value="42"/>
</bean>
注意:要使上述样例正常工作,必须启用调试标记来编译代码,这样Spring才能从构造器中查找到参数名。否则你必须使用@ConstructorProperties JDK注解来显式指定Bean的参数名,注解中的参数名与XML文件里一样,参考如下代码:
package examples;
public class ExampleBean
{
// 忽略属性
@ConstructorProperties({"years", "ultimateAnswer"})
public ExampleBean(int years, String ultimateAnswer) {
this.years = years;
this.ultimateAnswer = ultimateAnswer;
}
}
最后,如果你想要向一个对象传递一个引用,你需要使用 标签的 ref 属性,如果你想要直接传递值,那么你应该使用如上所示的 value 属性。
1.1.2.例子
这是 TextEditor.java 文件的内容:
package com.tutorialspoint;
public class TextEditor
{
private SpellChecker spellChecker;
public TextEditor(SpellChecker spellChecker)
{
System.out.println("Inside TextEditor constructor." );
this.spellChecker = spellChecker;
}
public void spellCheck()
{
spellChecker.checkSpelling();
}
}
下面是另一个依赖类文件 SpellChecker.java 的内容:
package com.tutorialspoint;
public class SpellChecker
{
public SpellChecker(){
System.out.println("Inside SpellChecker constructor." );
}
public void checkSpelling() {
System.out.println("Inside checkSpelling." );
}
}
以下是 MainApp.java 文件的内容:
package com.tutorialspoint;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class MainApp
{
public static void main(String[] args)
{
ApplicationContext context =
new ClassPathXmlApplicationContext("Beans.xml");
TextEditor te = (TextEditor) context.getBean("textEditor");
te.spellCheck();
}
}
下面是配置文件 Beans.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">
<!-- Definition for textEditor bean -->
<bean id="textEditor" class="com.tutorialspoint.TextEditor">
<constructor-arg ref="spellChecker"/>
</bean>
<!-- Definition for spellChecker bean -->
<bean id="spellChecker" class="com.tutorialspoint.SpellChecker">
</bean>
</beans>
当你完成了创建源和 bean 配置文件后,让我们开始运行应用程序。如果你的应用程序运行顺利的话,那么将会输出下述所示消息:
Inside SpellChecker constructor.
Inside TextEditor constructor.
Inside checkSpelling.
先init一个SpellChecker对象,再创到TextEditor构造方法中init一个TextEditor对象,再调用SpellChecker对象的checkSpelling方法
1.2.基于Setter方法注入
容器通过无参构造方法或者无参静态工厂方法实例化Bean之后,再通过调用Setter方法来完成依赖注入。
1.2.1.例子
下面是 TextEditor.java 文件的内容:
package com.tutorialspoint;
/*与构造注入不同的是,没有带参数的构造方法,使用一个setSpellChecker来设置属性spellChecker
这TMD区别很大吗?不就是Java里的set参数、get参数方法吗?*/
public class TextEditor
{
private SpellChecker spellChecker;
// a setter method to inject the dependency.
public void setSpellChecker(SpellChecker spellChecker)
{
System.out.println("Inside setSpellChecker." );
this.spellChecker = spellChecker;
}
// a getter method to return spellChecker
public SpellChecker getSpellChecker() {
return spellChecker;
}
public void spellCheck() {
spellChecker.checkSpelling();
}
}
在这里,你需要检查设值函数方法的名称转换。要设置一个变量 spellChecker,我们使用 setSpellChecker() 方法,该方法与 Java POJO 类非常相似。让我们创建另一个依赖类文件 SpellChecker.java 的内容:
//与构造注入一样
package com.tutorialspoint;
public class SpellChecker
{
public SpellChecker(){
System.out.println("Inside SpellChecker constructor." );
}
public void checkSpelling() {
System.out.println("Inside checkSpelling." );
}
}
以下是 MainApp.java 文件的内容:
package com.tutorialspoint;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class MainApp
{
public static void main(String[] args)
{
ApplicationContext context =
new ClassPathXmlApplicationContext("Beans.xml");
TextEditor te = (TextEditor) context.getBean("textEditor");
te.spellCheck();
}
}
下面是配置文件 Beans.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">
<!-- Definition for textEditor bean -->
<bean id="textEditor" class="com.tutorialspoint.TextEditor">
<property name="spellChecker" ref="spellChecker"/>
</bean>
<!-- Definition for spellChecker bean -->
<bean id="spellChecker" class="com.tutorialspoint.SpellChecker">
</bean>
</beans>
你应该注意定义在基于构造函数注入和基于设值函数注入中的 Beans.xml 文件的区别。唯一的区别就是在基于构造函数注入中,我们使用的是〈bean〉
标签中的〈constructor-arg〉
元素,而在基于设值函数的注入中,我们使用的是〈bean〉
标签中的〈property〉
元素。
第二个你需要注意的点是,如果你要把一个引用传递给一个对象,那么你需要使用 标签的 ref 属性,而如果你要直接传递一个值,那么你应该使用 value 属性。
当你完成了创建源和 bean 配置文件后,让我们开始运行应用程序。如果你的应用程序运行顺利的话,那么将会输出下述所示消息:
Inside SpellChecker constructor.
Inside setSpellChecker.
Inside checkSpelling.
1.3.使用构造注入还是Setter方法注入?
当你使用混合注入时通过构造方法完成必要依赖的注入以及通过Setter方法完成可选依赖注入是一个好的编程方式。注意:在Setter方法上标注@Required(这个后面再说)注解使得属性为必需依赖。
Spring团队提倡使用构造注入,该方式可以使得依赖为不可变对象并且不会为空;此外,通过构造注入的依赖被客户端调用时都是完全初始化好的;它还可以作为启示:过多的构造参数是一种不好的编程方式,意味着此类拥有过多的职责,最好重构代码,将多余的职责分离开来。
Setter注入主要用于可选依赖,但是在类中也应该分配合理的默认值,否则在客户端使用此依赖时需要做非空检查。使用Setter注入有一个好处:可以重新配置或者重新注入依赖。通过JMX mbean进行管理是Setter注入的一个引人注目的用例。
DI只在大多场景下是有效的,当处理没有源码的第三方类时,可以选择其它注入方式。比如:第三方类没有暴露任何Setter方法,那只能通过构造注入了。
2.依赖配置
2.1.使用 p-namespace 简化 XML 配置:
如果你有许多的设值函数方法,那么在 XML 配置文件中使用 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:<name><-ref>=<value>
-ref 部分表明这不是一个直接的值,而是对另一个 bean 的引用。
Spring团队更喜欢<value/>
元素而不是value
属性
Spring容器通过PropertyEditor将<value/>
元素内的文本转换为java.util.Properties实例。这种方式简单快捷。
2.2.< idref/ >元素
此元素是一种通过其他Bean的id值来引用Bean的方式,可以使用在或者元素中,并且拥有错误校验的功能。
<bean id="theTargetBean" class="..."/>
<bean id="theClientBean" class="...">
<property name="targetName">
<idref bean="theTargetBean"/>
</property>
</bean>
上面的配置与下面的配置完全等价(运行时):
<bean id="theTargetBean" class="..." />
<bean id="client" class="...">
<property name="targetName" value="theTargetBean"/>
</bean>
第一种形式的配置要优于第二种,因为<idref/>
元素使得容器会在部署时就验证引用的Bean是否真实存在。在第二例子中,targetName的值不会被验证,拼写错误只能在client Bean被实例化时才出现(很有可能出现严重错误)。如果client Bean是原型模式的,拼写错误和异常结果可能会在容器发布很久之后才被发现。