第一章-Spring之旅
本章内容:
- Spring的bean容器
- 介绍Spring的核心模块
- 更为强大的Spring生态系统
- Spring的新功能
1.1 简化Java开发
Spring是一个开源框架,最早由Rod Johnson创建,并在《Expert One-on-One:J2EE Design and Development》这本著作中进行了介绍。
为了降低Java开发的复杂性,Spring采取了以下四种关键策略:
- 基于POJO的轻量级和最小侵入性编程
- 通过依赖注入和面向接口实现松耦合
- 基于切面和惯例进行声明式编程
- 通过切面和模版减少样板式代码
1.1.1 激发POJO的潜能
很多框架通过强迫应用继承它们的类或实现它们的接口从而导致应用与框架绑死。Spring竭力避免因自身的API而弄乱你的应用代码。Spring不会强迫你实现Spring规范的接口或继承Spring规范的类,相反,在基于Spring构建的应用中,它的类通常没有任何痕迹表明你使用了Spring。
例:Spring不会在HelloWorldBean上有任何不合理的要求:
package com.habuma.spring
public class HelloWorldBean{
public String sayHello(){
return "Hello World";
}
}
上述代码只是一个简单的Java类-POJO。Spring的非侵入性编程模型意味着这个类在Spring应用和非Spring应用中都可以发挥同样的作用。
Spring赋予POJO魔力的方式之一就是通过DI来装配它们。
1.1.2 依赖注入
在项目中应用DI,你会发现你的代码会变得异常简单并且更容易理解和测试。
DI功能是如何实现的:
任何一个有实际意义的应用都会由两个或者更多的类组成,这些类相互之间进行协作来完成特定的业务逻辑。按照传统的做法,每个对象负责管理与自己互相协作的对象(即它所依赖的对象)的引用,这将会导致高度耦合和难以测试的代码。
例: DamselRescuingKnight只能执行 只能执行RescueDamselQuest探险任务:
package com.spring.knights;
public class DamselRescuingKnight implements Knight{
private RescueDamselQuest quest;
public DamselRescuingKnight () {
// 与RescueDamselQuest禁耦合
this.quest = new RescueDamselQuest();
}
public void embarkOnQuest(){
quest.embark();
}
}
上述代码中,DamselRescuingKnight在它的构造函数中自行创建了RescueDamselQuest。这使得DamselRescuingKnight紧密地和RescueDamselQuest耦合到了一起,因此极大地限制了这个骑士执行探险的能力。
更糟糕的是,为这个DamselRescuingKnight编写单元测试将出奇地困难。在这样的一个测试中,你必须保证当骑士的embarkOnQuest()方法被调用的时候,探险的embark()方法也要被调用。但是没有一个简单明了的方式能够实现这一点。很遗憾,DamselRescuingKnight将无法进行测试。
耦合具有两面性。一方面,紧密耦合的代码难以测试、难以复用、难以理解,并且典型地表现出“打地鼠”式的bug特性(修复一个bug,将会出现一个或者更多新的bug)。另一方面,一定程度的耦合又是必须的——完全没有耦合的代码什么也做不了。为了完成有实际意义的功能,不同的类必须以适当的方式进行交互。总而言之,耦合是必须的,但应当被小心谨慎地管理。
通过DI,对象的依赖关系将由系统中负责协调各对象的第三方组件在创建对象的时候进行设定。对象无需自行创建或管理它们的依赖关系,如下图所示,依赖关系将被自动注入到需要它们的对象中去。
为了展示这一点,请看下面这个例子:
例:BraveKnight足够灵活可以接受任何赋予他的探险任务:
package com.spring.knights;
public class BraveKnight implements Knight {
private Quest quest;
// Quest被注入进来
public BraveKnight (Quest quest) {
this.quest = quest;
}
public void embarkOnQuest() {
quest.embark();
}
}
不同于之前的DamselRescuingKnight,BraveKnight没有自行创建探险任务,而是在构造的时候把探险任务作为构造器参数传入。这是依赖注入的方式之一,即构造器注入(constructor injection)。
更重要的是,传入的探险类型是Quest,也就是所有探险任务都必须实现的一个接口。所以,BraveKnight能够响应RescueDamselQuest、 SlayDragonQuest、 MakeRound TableRounderQuest等任意的Quest实现。
这里的要点是BraveKnight没有与任何特定的Quest实现发生耦合。对它来说,被要求挑战的探险任务只要实现了Quest接口,那么具体是哪种类型的探险就无关紧要了。这就是DI所带来的最大收益——松耦合。如果一个对象只通过接口(而不是具体实现或初始化过程)来表明依赖关系,那么这种依赖就能够在对象本身毫不知情的情况下,用不同的具体实现进行替换。
将Quest注入到Knight中:
现在BraveKnight类可以接受你传递给它的任意一种Quest的实现,但该怎样把特定的Quest实现传给它呢?
例:SlayDragonQuest是要注入到BraveKnight中的Quest实现:
package com.spring.knights;
import java.io.PrintStream;
public class SlayDrgonQuest implements Quest {
private PrintStream stream;
public SlayDragonQuest(PrintStream stream) {
this.stream = stream;
}
public void embark() {
stream.println("Emabarking on quest to slay the dragon!");
}
}
SlayDragonQuest实现了Quest接口,这样它就是和注入到BraveKnight中去了。与其他的Java入门样例有所不同,SlayDragonQuest没有使用System.out.println(),而是在构造方法中请求一个更为通用的PrintStream。这里最大的问题在于,我们该如何将SlayDragonQuest交给BraveKnight呢?又如何将PrintStream交给SlayDragonQuest呢?
创建应用组件之间协作的行为通常称为装配(wiring)。Spring有多种装配bean的方式,采用XML是很常见的一种装配方式。
例:使用Spring将SlayDragonQuest注入到BraveKnight中:
<?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">
<!-- 注入Quest bean -->
<constructor-arg ref="quest" />
</bean>
<bean id="quest" class="sia.knights.SlayDragonQuest">
<!-- 创建SlayDragonQuest -->
<constructor-arg value="#{T(System).out}" />
</bean>
</beans>
上述代码中,BraveKnight和SlayDragonQuest被声明为Spring中的bean。就BraveKnight来讲,它在构造时传入了对SlayDragonQuest bean的引用。同时SlayDragonQuest bean的声明使用了 Spring表达式语言(Spring Expression Language) 将System.out(这是一个PrintStream)传入到了SlayDragonQuest的构造器中。
同时,Spring还支持使用Java来描述配置,
例:Spring提供了基于Java的配置,可作为XML的替代方案:
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);
}
}
不管你使用的是XML的配置还是基于Java的配置,DI所带来的收益都是相同的。
现在已经声明了BraveKnight和Quest的关系,接下来我们只需要装载XML配置文件,并把应用启动起来。
观察它如何工作:
Spring通过 应用上下文(Application Context) 装载bean的定义并把它们组装起来。Spring应用上下文全权负责对象的创建和组装。Spring自带了多种应用上下文的实现,它们之间的主要区别仅仅在于如何加载配置。
例:KnightMain.java加载包含Knight的Spring上下文:
package sia.knights;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class KnightMain {
public static void main(String[] args) throws Exception {
// 加载Spring上下文
ClassPathXmlApplicationContext context =
new ClassPathXmlApplicationContext("META-INF/spring/knight.xml");
// 获取knight bean
Knight knight = context.getBean(Knight.class);
// 使用knight
knight.embarkOnQuest();
context.close();
}
}
main()方法基于knights.xml文件创建了Spring应用上下文,随后它调用该应用上下文获取一个ID为knight的bean。得到Knight对象的引用后,只需简单调用embarkOnQuest()方法就可以执行所赋予的探险任务了。注意这个类完全不知道我们的英雄骑士接受哪种探险任 务,而且完全没有意识到这是由BraveKnight来执行的。只有knights.xml文件知道哪个骑士执行哪种探险任务。
1.1.3 应用切面
DI能够让相互协作的软件组件保持松散耦合,而面向切面编程(aspect-oriented programming,AOP) 允许你把遍布应用各处的功能分离出来形成可重用的组件。
面向切面编程往往被定义为促使软件系统实现关注点的分离一项技术。系统由许多不同的组件组成,每一个组件各负责一块特定功能。除了实现自身核心的功能之外,这些组件还经常承担着额外的职责。诸如日志、事务管理和安全这样的系统服务经常融入到自身具有核心业务逻辑的组件中去,这些系统服务通常被称为横切关注点,因为它们会跨越系统的多个组件。
这将给你的代码带来双重的复杂性:
- 实现系统关注点功能的代码将会重复出现在多个组件中。这意味着如果你要改变这些关注点的逻辑,必须修改各个模块中的相关实现。即使你把这些关注点抽象为一个独立的模块,其他模块只是调用它的方法,但方法的调用还是会重复出现在各个模块中。
- 组件会因为那些与自身核心业务无关的代码而变得混乱。
AOP能使这些服务模块化,并以声明的方式将它们应用到它们需要影响的组件中去。所造成的结果就是这些组件会具有更高的内聚性并且会更加关注自身的业务,完全不需要了解涉及系统服务所带来的复杂性。
如图所示,借助AOP,可以使用各种功能层去包裹核心业务层,这些层以声明的方式灵活地应用到系统中,你的核心应用甚至根本不知道它们的存在,这是一个非常强大的理念,可以将安全、事务和日志关注点与核心业务逻辑相分离。
例:吟游诗人是中世纪的音乐记录器:
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!");
}
}
Minstrel是只有两个方法的简单类,在骑士执行每一次探险任务之前,singBeforeQuest()方法会被调用;在骑士完成探险任务之后,singAfterQuest()方法会被调用。
现在,将Minstrel加入代码中,
例:BraveKnight必须要调用Minstrel的方法:
package sia.knights;
public class BraveKnight implements Knight {
private Quest quest;
public BraveKnight(Quest quest,Minstrel minstrel) {
this.quest = quest;
this.minstrel=minstrel;
}
public void embarkOnQuest() throws QuestException {
// Knight应该管理Minstrel吗?
minstrel.singBeforeQuest();
quest.embark();
minstrel.singAfterQuest();
}
}
这应该可以达到预期效果。现在,你所需要做的就是回到Spring配置中,声明Minstrel bean并将其注入到BraveKnight的构造器之中。
但是,我们似乎感觉有些东西不太对。管理他的吟游诗人真的是骑士职责范围内的工作吗?在我看来,吟游诗人应该做他份内的事,根本不需要骑士命令他这么做。毕竟,用诗歌记载骑士的探险事迹,这是吟游诗人的职责。为什么骑士还需要提醒吟游诗人去做他份内的事情呢?
此外,因为骑士需要知道吟游诗人,所以就必须把吟游诗人注入到BarveKnight类中。这不仅使BraveKnight的代码复杂化了,而且还让我疑惑是否还需要一个不需要吟游诗人的骑士呢?如果Minstrel为null会发生什么呢?我是否应该引入一个空值校验逻辑来覆盖该场景?
简单的BraveKnight类开始变得复杂,如果你还需要应对没有吟游诗人时的场景,那代码会变得更复杂。但利用AOP,你可以声明吟游诗人必须歌颂骑士的探险事迹,而骑士本身并不用直接访问Minstrel的方法。
要将Minstrel抽象为一个切面,你所需要做的事情就是在一个Spring配置文件中声明它。
例:将Minstrel声明为一个切面:
<?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 value="#{T(System).out}" />
</bean>
<bean id="minstrel" class="sia.knights.Minstrel">
<constructor-arg value="#{T(System).out}" />
</bean>
<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配置命名空间把Minstrel bean声明为一个切面。首先,需要把Minstrel声明为一个bean,然后在 <aop:aspect> 元素中引用该bean。为了进一步定义切面,声明(使用 <aop:before> )在embarkOnQuest()方法执行前调用Minstrel的singBeforeQuest()方法。这种方法被称为前置通知,另一种则是后置通知。
在这两种方式中,pointcut-ref属性都引用了embank的切入点。该切入点是在前边的\ 元素中定义的,并配置expression属性来选择所应用的通知。
当我们按照上面那样进行配置后,在Spring上下文中Minstrel实际上已经变成一个切面了。其次,Minstrel可以被应用到BraveKnight中,而BraveKnight不需要显式地调用它,实际上,BraveKnight完全不知道Minstrel的存在。
必须还要指出的是,尽管我们使用Spring魔法把Minstrel转变为一个切面,但首先要把它声明为以Spring bean,能够为其他Spring bean做到的事情都可以同样应用到Spring切面中。
1.1.4 使用模版消除样板式代码
样板式代码(boilerplate code),通常为了实现通用的和简单的任务,你不得不一遍遍地重复编写这样的代码。它们中的很多是因为使用Java API而导致的样板式代码,常见的是使用JDBC访问数据库查询数据。
Spring旨在通过模版封装来消除样板式代码。例如:Spring的JdbcTemplate使得执行数据库操作时,避免传统的JDBC样板代码成为了可能。
上面已经展示了Spring通过面向POJO编程、DI、切面和模版技术来简化Java开发中的复杂性。在这个过程中,展示了在基于XML的配置文件中如何配置bean和切面,但这些文件是如何加载的呢?它们被加载到哪里去了?让我们再了解下Spring容器,这是应用中的所有bean所驻留的地方。
1.2 容纳你的bean
在基于Spring的应用中,你的应用对象生存与Spring容器(container)。如图所示,Spring容器负责创建对象,装配它们,配置它们并管理它们的整个生命周期,从生存到死亡。
容器是Spring框架的核心。Spring容器使用DI管理构成应用的组件,它会创建相互协作的组件之间的关联。
Spring容器并不是只有一个,Spring自带了多个容器实现,可以归为两种不同的类型。bean工厂(由org.springframework. beans. factory.eanFactory接口定义)是最简单的容器,提供基本的DI支持。应用上下文 (由org.springframework.context.ApplicationContext接口定义)基于BeanFactory构建,并提供应用框架级别的服务。
虽然我们可以在bean工厂和应用上下文之间任选一种,但bean工厂对大多数应用来说往往太低级了,因此,应用上下文要比bean工厂更受欢迎。
1.2.1 使用应用上下文
Spring自带了多种类型的应用上下文:
- AnnotationConfigApplicationContext:从一个或多个基于Java的配置类中加载Spring应用上下文
- AnnotationConfigWebApplicationContext:从一个或多个基于Java的配置类中加载Spring Web应用上下文
- ClassPathXmlApplicationContext:从类路径下的一个或多个XML配置文件中加载上下文定义,把应用上下文的定义文件作为类资源
- FileSystemXmlApplicationContext:从文件系统下的一个或多个XML配置文件中加载上下文定义
- XmlWebApplicationContext:从Web应用下的一个或多个XML配置文件加载上下文定义
现在我们先简单地使用FileSystemXmlApplicationContext从文件系统中加载应用上下文或者使用ClassPathXmlApplicationContext从类路径中加载应用上下文。
例:如何加载一个FileSystemXmlApplicationContext:
ApplicationContext context = new FileSystemXmlApplicationContext("c:/knight.xml");
例:使用ClassPathXmlApplicationContext从应用的类路径下加载应用上下文:
ApplicationContext context = new ClassPathXmlApplicationContext("knight.xml");
使用FileSystemXmlApplicationContext和使用ClassPathXmlApp-licationContext的区别在 于:FileSystemXmlApplicationContext在指定的文件系统路径下查找knight.xml文件; 而ClassPathXmlApplicationContext是在所有的类路径(包含JAR文件)下查找 knight.xml文件。
1.2.2 bean的生命周期
在传统的Java应用中,bean的生命周期为:使用Java关键字new进行bean实例化,然后该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将调用它们的postProcessBeforeInitialization()方法;
- 如果bean实现了InitializingBean接口,Spring将调用它们的afterpropertiesSet()方法。如果bean使用initmethod声明了初始化方法,该方法也会被调用;
- 如果bean实现了BeanPostProcessor接口,Spring将调用它们的postProcessAfterInitiallization()方法;
- 此时,bean已经准备就绪,可以被应用程序使用了,它们将一直驻留在应用上下文中,直到该应用上下文被销毁;
- 如果bean实现了DisposableBean接口,Spring将调用它们的destroy()接口方法。如果bean使用destroy-method声明了销毁方法,该方法也会被调用;
现在你已经了解了如何创建和加载一个Spring容器。但是一个空的容器并没有太大的价值,在你把东西放进去之前, 它里面什么都没有。
小结
Spring致力于简化企业级Java开发,促进代码的松散耦合,成功的关键在于依赖注入和AOP。
DI是组装应用对象的一种方式,借助这种方式对象无需知道依赖来自何处或者依赖的实现方式,不同于自己获取依赖对象,对象会在运行期赋予它们所依赖的对象,依赖对象通常会通过接口了解所注入的对象,这样的话就能确保低耦合。
AOP可以帮助应用将散落在各处的逻辑汇集于一处——切面,当Spring装配bean的时候,这些切面能够在运行期编织起来,这样就能非常有效地赋予bean新的行为。