依赖注入
按照传统的做法,每个对象负责管理与自己相互协作的对象的引用,这将会导致高度耦合和难以测试的代码。例如:
package sia.knights;
public class DamselRescuingKnight implements Knight {
private RescueDamselQuest quest;
public DamselRescuingKnight() {
this.quest = new RescueDamselQuest();
}
public void embarkOnQuest() {
quest.embark();
}
}
DamselRescuingKnight在其构造函数中自行创建了Rescue DamselQuest。这使DamselRescuingKnight和Rescue DamselQuest紧密地耦合到了一起。
通过DI,对象的依赖关系将由系统中负责协调各对象的第三方组件在创建对象的时候进行设定。对象无需自行创建或管理它们的依赖关系。这样代码将会变得更加灵活:
package sia.knights;
public class BraveKnight implements Knight {
private Quest quest;
public BraveKnight(Quest quest) {
this.quest = quest;
}
public void embarkOnQuest() {
quest.embark();
}
}
BraveKnight足够灵活可以接受任何赋予他的Quest。可以看到不同于之前的DamselRescuingKnight,BraveKnight没有自行创建探险任务,而是在构造的时候把探险任务作为构造器参数传入。这是依赖注入的方式之一,即构造器注入。
由于传入的探险类型是Quest,BraveKnight没有与任何特定的Quest实现相耦合。而Quest是所有探险任务都必须实现的一个接口。所以BraveKnight能够响应任意的Quest实现类。这就是DI带来的收益——松耦合。如果一个应用只通过接口而不是具体实现或初始化过程来表明依赖关系,那么这种依赖就能够在对象本身毫不知情的情况下,用不同的具体实现进行替换。
在测试的时候,对于紧耦合的DamselRescuingKnight无法进行充分测试,但是BraveKnight却可以,因为只需要给它一个Quest的mock实现即可。
package sia.knights;
import static org.mockito.Mockito.*;
import org.junit.Test;
import sia.knights.BraveKnight;
import sia.knights.Quest;
public class BraveKnightTest {
@Test
public void knightShouldEmbarkOnQuest() {
Quest mockQuest = mock(Quest.class);
BraveKnight knight = new BraveKnight(mockQuest);
knight.embarkOnQuest();
verify(mockQuest, times(1)).embark();
}
}
上面的代码使用Mock框架Mockito创建了一个Quest的mock实现。通过这个mock对象,就可以创建一个新的BraveKnight实例,并通过构造器注入这个mock Quest。通过使用verify方法来验证mock实现的embark()方法被调用了多少次。
将Quest注入到Knight中
BraveKnight类可以接受传递的任意一种Quest实现。问题是怎样把特定的Query实现传给它?例如下面的SlayDragonQuest:
package sia.knights;
import java.io.PrintStream;
public class SlayDragonQuest implements Quest {
private PrintStream stream;
public SlayDragonQuest(PrintStream stream) {
this.stream = stream;
}
public void embark() {
stream.println("Embarking on quest to slay the dragon!");
}
}
上面代码没有使用System.out.println(),而是在构造方法中请求一个更为通用的PrintStream。
现在最大的问题在于如何将SlayDragonQuest传递给BraveKnight,又如何把PrintStream交给SlayDragonQuest?
创建应用组件之间的协作行为通常被称为装配(wiring)。Spring提供了多种装配bean的方式, 常用的装配方式是采用XML。
下面是一个简单的Spring配置文件 knights.xml,该配置文件将BraveKnight、SlayDragonQuest和PrintStream装配到了一起:
<?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.xsd">
<bean id="knight" class="sia.knights.BraveKnight">
<constructor-arg ref="quest" />
</bean>
<bean id="quest" class="sia.knights.SlayDragonQuest">
<constructor-arg value="#{T(System).out}" />
</bean>
</beans>
BraveKnight和SlayDragon被声明为Spring中的Bean。BraveKnight bean在构造时传入了对SlayDragonQuest的引用,将其作为构造器参数。同时,SlayDragonQuest使用Spring表达式语言将System.out(这是一个PrintStream)传入到SlayDragonQuest的构造器中。
同时SPring还支持使用Java来描述配置:
package sia.knights.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import sia.knights.BraveKnight;
import sia.knights.Knight;
import sia.knights.Quest;
import sia.knights.SlayDragonQuest;
@Configuration
public class KnightConfig {
@Bean
public Knight knight() {
return new BraveKnight(quest());
}
@Bean
public Quest quest() {
return new SlayDragonQuest(System.out);
}
}
尽管BraveKnight依赖于Quest,但是它并不需要知道传递的是什么Quest,SlayDragonQuest 也不需要知道PrintStream是什么样子。只需要让Spring通过配置文件了解这些组成部分要如何装配起来。这样就可以在不改变所依赖的类的情况下,修改依赖关系。接下来只需要装载XMl配置并把应用启动起来就可以了。
Spring装配的过程:
Spring通过应用上下文装载bean并把它们组装起来。Spring应用上下文全权负责对象的创建和组装。Spring提供了多种应用上下文的实现,这些应用上下文的区别仅仅在于如何加载配置。
对于使用XML文件配置的knights.xml,选择ClassPathXmlApplicationContext
作为应用上下文。该类会加载位于应用程序路径下的一个或多个XML配置文件。
下面的程序中的main()方法调用ClassPathXmlApplicationContext加载knights.xml并获得Knight对象的引用:
package sia.knights;
import org.springframework.context.support.
ClassPathXmlApplicationContext;
public class KnightMain {
public static void main(String[] args) throws Exception {
ClassPathXmlApplicationContext context =
new ClassPathXmlApplicationContext(
"META-INF/spring/knight.xml");
Knight knight = context.getBean(Knight.class);
knight.embarkOnQuest();
context.close();
}
}
这里的main()方法基于knights.xml文件创建了Spring应用上下文。随后调用应用上下文的getBean方法来获得ID为knight的bean。得到Knight对象的引用后,只需要调用embarkOnQuest()方法就可以执行所赋予的探险任务。
这个类完全不知道是哪个骑士接受哪种探险任务,只有knights.xml文件知道是哪个骑士执行哪种探险任务。
应用切面
DI使相互协作的软件组件保持松散耦合,而面向切面编程AOP实现了把遍布应用各处的功能分离出来形成可重用的组件。
面向切面编程促使软件系统实现关注点的分离。系统由许多不同的组件组成,每一个组件负责特定的服务。日志、事务管理和安全之类的系统服务经常融入到自身具有核心业务逻辑的组件中去,这些系统服务通常被称为横切关注点。因为它会跨越系统的多个组件。
这些关注点如果被分散到多个组件里,会带来双重的复杂性:
实现系统关注点功能的代码会重复出现在多个组件中。意味着如果要改变这些关注点的逻辑,必须同时修改各个模块中的相关实现。即使把这些关注点抽象为一个独立的模块,其他模块调用它的方法,但方法的调用还是会重复出现在各个模块中。
组件会因为那些与自己核心业务无关的代码变得混乱。
下图展示了这种复杂性,左边的业务对象与系统级服务结合得过于紧密。每个对象要记日记、安全控制、参与事务,还要亲自执行这些服务:
AOP能够使这些服务模块化,并以声明的方式将它们应用到它们需要影响的组件中去。这些组件会具有更高的内聚性,并且更加关乎自身的业务而不需要去了解设计系统服所带来的复杂性。
可以把切面想象成覆盖在很多组件上的一个外壳。实现各自业务功能的模块组成整个应用。借助AOP,可以使用各种功能层去包裹核心业务层。这些层以声明的方式灵活地应用到系统中。核心应用甚至不需要知道其它功能层的存在。这样可以将安全、事务和日志关注点与核心业务逻辑分离。
现在回到骑士的例子上,给它添加一个切面。
AOP应用
现在创建一个吟游诗人Minstrel类来记载骑士所做的事迹:
package sia.knights;
import java.io.PrintStream;
public class Minstrel {
private PrintStream stream;
public Minstrel(PrintStream stream) {
this.stream = stream;
}
public void singBeforeQuest() {
stream.println("Fa la la, the knight is so brave!");
}
public void singAfterQuest() {
stream.println("Tee hee hee, the brave knight " +
"did embark on a quest!");
}
}
MineStrel是一个只有两个方法的简单类,在骑士执行每一个任务之前,singBeforeQuest()会被调用;在骑士完成探险任务之后,singAfterQuest()方法会被调用。在这两种情况,Minestrel都会通过一个PrintStream类来歌颂骑士的事迹,这个类是通过构造器注入的。
现在尝试做一下调整让BraveKnight可以使用Minstrel:
package sia.knights;
public class BraveKnight implements Knight {
private Quest quest;
private Minstrel minstrel;
public BraveKnight(Quest quest) {
this.quest = quest;
}
public void embarkOnQuest() {
minestrel.singBeforeQuest();
quest.embark();
minestrel.singAfterQuest();
}
}
这样有个问题,管理吟游诗人的行为是骑士的工作吗?此外因为其实需要知道吟游诗人,这样必须要把吟游诗人注入到BraveKnight类中。但如果一个骑士不需要吟游诗人,这就会发生错误。
简单的Knight类变得更复杂了。利用AOP可以声明吟游诗人必须歌颂骑士的探险事迹,而骑士本身不用自己去访问Minestrel的方法。
要将Minestrel抽象为一个切面,则需要在Spring配置文件中声明它:
<?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:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="knight" class="sia.knights.BraveKnight">
<constructor-arg ref="quest" />
</bean>
<bean id="quest" class="sia.knights.SlayDragonQuest">
<constructor-arg ref="fakePrintStream" />
</bean>
<bean id="minstrel" class="sia.knights.Minstrel">
<constructor-arg ref="fakePrintStream" />
</bean>
<bean id="fakePrintStream" class="sia.knights.FakePrintStream" />
<aop:config>
<aop:aspect ref="minstrel">
<aop:pointcut id="embark"
expression="execution(* *.embarkOnQuest(..))"/>
<aop:before pointcut-ref="embark"
method="singBeforeQuest"/>
<aop:after pointcut-ref="embark"
method="singAfterQuest"/>
</aop:aspect>
</aop:config>
</beans>
上面使用了Spring的AOP配置命名空间把Minestrel bean声明为一个切面。
- 首先,需要把Minestrel声明为一个bean,然后在<aop:aspect>元素中引用该bean。
- 然后进一步定义切面,使用<aop:before>在embarkOnQuest()方法执行前调用Minestrel的singBeforeQuest()方法。这种方式被称为前置通知。
- 使用<aop:after>在embarkOnQuest()执行后调用singAfterQuest()方法。这种方式被称为后置通知。
在这两种方式中,pointcut-ref属性都引用了一个名字为embark的切入点。该切入点是在<aop:pointcut>元素中定义的,并配置的expression属性来选择所引用的通知。表达式的语法采用的是AspectJ的切点表达式语言。
详细的在后面的章节会讨论,现在只需要知道Spring在骑士执行任务前后会调用Minestrel的singBeforeQuest()和singAfterQuest()方法就足够了。
通过少量的XML配置就可以把Minestrel声明为一个Spring切面。Minestrel仍然是一个POJO,没有任何代码表明它要被作为一个切面使用。Minestrel可以被应用到BraveKnight中,而BraveKnight不需要显示地调用它。甚至完全不知道Minestrel的存在。
使用模板消除样板式代码
有时候为了实现通用的和简单的任务,你不得不一遍遍地重复编写样板式代码。
一个常见范例是使用JDBC访问数据库查询数据,少量的查询员工的代码被淹没在一堆的JDBC样板式代码中。首先需要创建一个数据库连接,然后创建一个语句对象,最后才能进行查询。还要捕捉SQLException。
最后还要关闭数据库连接、语句和结果集。同样要捕捉SQLException。
在许多编程场景中往往都会导致类似的样板式代码。Sping旨在通过模板封装来消除样板式代码。Spring的JdbcTemplate使执行数据库操作时,避免传统的JDBC样板代码成为了可能。
XML配置文件是如何加载的呢?它们被加载到了哪里去了?接下来了解一下Spring容器,这是应用中的所有bean驻留的地方。
容纳你的Bean
在基于Spring的引用中,应用对象生存于Spring容器(container)中。如下图所示:
Spring容器负责创建对象,装配它们,配置它们并管理它们的整个生命周期。
容器是Spring框架的核心,Spring容器使用DI管理构成应用的组件,它会创建相互协作的组件之间的关联。
Spring自带了多个容器实现,可以归为两种不同的类型:
- bean工厂
- 应用上下文
bean工厂是最简单的容器,提供基本的DI支持。应用上下文基于BeanFactory构建,并提供应用框架级别的服务。
使用应用上下文
Spring自带了多种类型的应用上下文,下面是最常见的几种:
- AnnotationConfigApplicationContext:从一个或多个基于Java的配置类中加载Spring应用上下文。
- AnnotationConfigWebApplicationContext:从一个或多个基于Java的配置类中加载Spring Web应用上下文。
- ClassPathXmlApplicationContext:从类路径下的 一个或多个XML配置文件中加载上下文定义,把应用上下文的定义文件作为类资源。
- FileSystemXmlApplicationContext:从文件系统下的一个或多个XML配置文件中加载上下文定义。
- XmlWebApplicationContext:从Web应用下的一个或多个XML配置文件中加载上下文定义。
先简单地使用FileSystemXmlApplicationContext从文件系统中加载应用上下文或者使用ClassPathXmlApplicationContext从类路径下加载应用上下文。无论是从文件系统还是从类路径下装载应用上下文,将bean加载到bean工厂的过程都是类似的。
如下代码展示了如何加载一个FileSystemXmlApplicationContext:
ApplicationContext context = new
FileSystemXmlApplicationContext("c:/knight.xml");
类似地,可以使用ClassPathXmlApplicationContext从类路径下加载应用上下文:
ApplicationContext context = new
ClassPathXmlApplicationContext("knight.xml");
如果想从Java配置中加载应用上下文,可以使用AnnotationConfigApplicationContext:
ApplicationContext context = new AnnotationConfigApplicationContext(com.springinaciton.knights.config.KnightConfig.class);
应用上下文准备就绪之后,就可以调用上下文的getBean()方法从Spring容器中获取bean。
bean的生命周期
在传统的Java应用中,bean的生命周期很简单。使用Java关键字new进行实例化,然后该bean就可以使用了。一旦该bean不再被使用,则由Java自动进行垃圾回收。
Spring容器中的bean的生命周期要相对复杂的多。下图展示了bean装载到Spring应用上下文中的一个典型的生命周期过程。
在bean准备就绪之前,bean工厂执行了若干启动步骤:
Spring对Bean进行实例化;
Spring将值和bean引用注入到bean对应的属性中;
如果bean实现了BeanNameAware接口,Spring将bean的ID传递给setBeanName()方法;
如果bean实现了BeanFactoryAware接口,Spring将调用setBeanFactory()方法,将BeanFactory容器实例传入;
如果bean实现了ApplicationContextAware接口,Spring将调用setApplicationContext()方法,将bean所在的应用上下文引用传入进来;
如果bean实现了BeanPostProcessor接口,Spring将调用它们的postProcessAfterInitialization()方法;
如果bean实现了InitializingBean接口,Spring将调用它们的afterPropertiesSet()方法。类似地,如果bean使用initmethod声明了初始化方法,该方法也会被调用。
如果bean实现了BeanPostProcessor接口,Spring将调用它们的postProcessAfterInitialization()方法;
此时,bean已经准备就绪,可以被应用程序使用,它们将一直驻留在应用上下文中,知道该应用上下文被销毁;
如果bean实现了DisposableBean接口,Spring将调用它的destroy()接口方法。同样,如果bean使用destroymethod声明了销毁方法,该方法也会被调用。
俯瞰Spring
Spring模块
Spring模块为开发企业级应用提供了所需的一切。这些模块根据所属的功能可以划分为6类不同的功能。
也可以不将应用建立在整个Spring框架之上,可以自由地选择适合自身应用需求的Spring模块;Spring甚至提供了与其他第三方框架和类库的集成点。
现在我们逐一浏览Spring的模块,看看它们是如何构建起整个Spring蓝图的。
Spring核心容器
Spring框架最核心的部分,它管理者Spring应用中bean的创建、应用和管理。该模块中包括了Spring bean工厂,它为Spring提供了DI的功能。还有多种基于bean工厂的Spring应用上下文的实现,每一种提供了配置Spring的不同方式。
Spring核心容器除了bean工厂和应用上下文,也提供了许多企业服务,例如Email、JNDI访问、EJB集成和调度。
所有的Spring模块都构建于核心容器之上。
Spring的AOP模块
在AOP模块中,Spring对面向切面编程提供了丰富的支持。这个模块是Spring应用系统中开发切面的基础。与DI一样,AOP可以帮助应用对象解耦。将遍布系统的关注点(例如事务和安全)从它们所应用的对象中解耦出来。
数据访问与集成
使用JDBC编写代码通常会导致大量的样板式代码,Spring的JDBC和DAO模块抽象了这些样板式代码,使数据库代码变得简单明了,还可以避免因为关闭数据库资源失败而引发的问题。该模块在多种数据库服务的错误信息之上构建了一个语义丰富的异常层。
对于更喜欢ORM工具而不愿意直接使用JDBC的开发者,Spring提供了ORM模块。Spring的ORM模块建立在对DAO的支持之上,并为多个ORM框架提供了一种构建DAO的简便方式。Spring对许多流行的ORM框架进行了继承。。Spring的事务管理支持所有的ORM框架以及JDBC。
本模块同样包含了在JMS之上构建的Spring抽象层,使消息以异步的方式与其他应用集成。
除此之外,本模块会使用Spring AOP模块为Spring应用中的对象提供事务管理服务。
Web与远程调用
Spring虽然能够与多种流行的MVC框架集成,但它的Web和远程调用模块自带了一个强大的MVC框架,有助于在Web层提升应用的松耦合水平。
除了面向用户的Web应用,该模块还提供了多种构建与其他应用交互的远程调用方案。
Instrumentation
Spring的Instrumentation模块提供了为JVM添加代理的功能。
为Tomcat提供了一个织入代理,能够为Tomcat传递类文件,就像是这些文件被类加载器加载一样。
Instrumentation的使用场景非常有限,不会具体介绍。
测试
Spring提供了测试模块以致力于Spring应用的测试。
Spring为使用JNDI、Sevlet和Portlet编写单元测试提供了一系列的mock对象实现。对于集成测试,测试模块为加载Spring应用上下文的bean集合以及与Spring上下文中的bean进行交互提供了支持。
依赖注入和AOP是Spring框架最核心的部分,只有理解了如何应用Spring最关键的功能,才有能力使用Spring框架的其他功能。