简化Java开发

Spring是为了解决企业级应用开发的复杂性而创建的,使用Spring可以让简单的JavaBean实现之前只有EJB才能完成的事情。但Spring不仅仅局限于服务器端开发,任何Java应用都能在简单性、可测试性和松耦合等方面从Spring中获益。
虽然Spring用bean或者JavaBean来表示应用组件,但并不意味着Spring组件必须要遵循JavaBean规范。一个Spring组件可以是任何形式的POJO。Spring 可以做非常多的事情。但归根结底,支撑Spring的仅仅是少许的基本理念,所有的理念都可以追溯到Spring最根本的使命上:简化Java开发。

为了降低Java开发的复杂性,Spring采取了以下4种关键策略:

  • 基于POJO的轻量级和最小侵入性编程;
  • 通过依赖注入和面向接口实现松耦合;
  • 基于切面和惯例进行声明式编程;
  • 通过切面和模板减少样板式代码。

激发POJO的潜能

很多框架通过强迫应用继承它们的类或实现他们的接口从而导致应用与框架绑死,就如EJB2时的无转台会话bean。而Spring竭力避免因自身API弄乱你的应用代码,Spring不会强迫你实现Spring规范的接口或继承Spring规范的类,相反,在基于Spring构建的应用中,它的类通常没有任何痕迹表明你使用了Spring。如代码清单1 Spring不会再HelloWorldBean上有任何不合理的要求。
代码清单1 HelloWorldBean类

1
2
3
4
5
public class HelloWorldBean {
public String sayHello() {
return "Hello World";
}
}

这是一个简单普通的Java类——POJO。没有任何地方表明它是一个Spring组件。Spring的非侵入编程模型意味着这个类在Spring应用和非Spring应用中都可以发挥同样的作用。Spring赋予POJO魔力的方式之一就是通过DI来装配它们。让我们看看DI是如何帮助应用对象彼此之间保持松散耦合的。

依赖注入

依赖注入这个词让人望而生畏,现在已经演变成一项复杂的编程技巧或设计模式理念。但事实证明,依赖注入并不像它听上去那么复杂。在项目中应用DI,你会发现你的代码会变得异常简单并且更容易理解和测试。

DI功能如何实现

任何一个有实际意义的应用(肯定比Hello World示例更复杂)都会由两个或者更多的类组成,这些类相互之间进行协作来完成特定的业务逻辑。按照传统的做法,每个对象负责管理与自己相互协作的对象(即它所依赖的对象)的引用,这将会导致高度耦合和难以测试的代码,如代码清单2所示。
代码清单2 DamselRescuingKnight类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class DamselRescuingKnight implements Knight {
private RescueDamselQuest quest;
/*
构造函数中创建RescueDamselQuest,与RescueDamselQuest紧耦合,
极大限度限制DamselRescuingKnight执行能力,如果RescueDamselQuest相当复杂,
为DamselRescuingKnight编写单元测试将出奇困难,那么接下来embarhOnQuest的测试将没法进行。
*/
public DamselRescuingKnight() {
quest = new RescueDamselQuest();
}
@Override
public void embarhOnQuest() {
quest.embark();
}
}

耦合具有两面性(two-headed beast)。一方面,紧密耦合的代码难以测试、难以复用、难以理解,并且典型地表现出“打地鼠”式的bug特性(修复一个bug,将会出现一个或者更多新的bug)。另一方面,一定程度的耦合又是必须的——完全没有耦合的代码什么也做不了。为了完成有实际意义的功能,不同的类必须以适当的方式进行交互。总而言之,耦合是必须的,但应当被小心谨慎地管理。

通过DI,对象的依赖关系将由系统中负责协调各对象的第三方组件在创建对象的时候进行设定。对象无需自行创建或管理它们的依赖关系,如图1所示,依赖关系将被自动注入到需要它们的对象当中去,而不是让对象自己去获取依赖。



图1 依赖注入会将所依赖的关系自动交给目标对象

下面的代码将展示这一点,BraveKnight能很简单的接收赋予它的任务。
代码清单3 BraveKnight能很简单的接收赋予它的任务

1
2
3
4
5
6
7
8
9
10
11
12
13
public class BraveKnight implements Knight {
private Quest quest;
public BraveKnight(Quest quest) {
this.quest = quest;// quest被注入到对象中
}
@Override
public void embarhOnQuest() throws QuestException {
quest.embark();
}
}

不同于之前的DamselRescuingKnight,Braveknight没有自行创建RescueDamselQuest(),而是构造的时候将Quest的对象quest作为构造器参数传入。这是依赖注入的方式之一,即构造器注入(constructor injection)。

更重要的是,传入的任务类型是Quest,一个所有的任务都必须实现的接口。所以,Braveknight能够响应RescueDamselQuest、SlayDragonQuest、MakeRoundTableRounderQuest等任意的Query实现。

这里的要点是BraveKnight没有与任何特定的Quest实现发生耦合。对它来说,只要能使Quest接口实现,那么具体哪种类型实现就无关紧要了。这就是DI所带来的最大收益————松耦合。如果一个对象只通过接口(而不是具体实现或者初始过程)来表名依赖关系,那么这种依赖就能够在对象本身毫不知情的情况下,用不同的具体实现进行替换。
对依赖进行替换的一个最常用方法就是在测试的时候使用mock实现。我们无法充分地测试DamselRescuingKnight,因为它是紧耦合的;但是可以轻松地测试BraveKnight,只需给它一个Quest的mock实现即可,如程序4所示。
代码清单4 注入一个mock Quest,测试BraveKnight

1
2
3
4
5
6
7
8
9
10
11
public class BraveKnightTest {
@Test
public void KnightShouldEmbarkOnQuest() {
Quest mockQuest=mock(Quest.class); //创建mock Quest
BraveKnight knight=new BraveKnight(mockQuest); //注入mock Quest
knight.embarkOnQuest();
verify(mockQuest,times(1)).embark();
}
}

可以使用mock框架Mockito去创建一个Quest接口的mock实现。通过这个mock对象,就可以创建一个新的BraveKnight实例,并通过构造器注入这个mock Quest。当调用embarkOnQuest()方法时,你可以要求Mockito框架验证Quest的mock实现的embark()方法仅仅被调用了一次。

将Query注入到Knight中

现在BraveKnight类可以接受你传递给它的任一一种Quest的实现,但该怎样把特定的Query实现传递给它呢?如代码清单5所示,SlayDragonQuest是要注入到BraveKnight中的Quest实现。
代码清单5 SlayDragonQuest是要注入到BraveKnight中的Quest实现

1
2
3
4
5
6
7
8
9
10
11
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!");
}
}

我们可以看到,SlayDragonQuest实现了Quest接口,这样它就适合注入到BraveKnight中去了。与其它的Java入门样例有所不同,SlayDragonQuest没有使用System.out.println(),而是构造方法中请求一个更为通用的PrintStream。这里最大的问题在于,我们该如何将SlayDragonQuest交给BraveKnight呢?又如何将PrintStream交给SlayDragonQuest呢?

创建应用组件之间协作的行为通常称为装配(wiring)。Spring有多种装配bean的方式,采用XML是很常见的一种装配方式。程序清单6展现了一个简单的Spring配置文件:knights.xml,该配置文件将BraveKnight、SlayDragonQuest和PrintStream装配到了一起。
代码清单6 使用Spring将SlayDragonQuest注入到BraveKnight中

1
2
3
4
5
6
7
8
9
10
11
12
<?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="Springing.Simplifying.BraveKnight">
<constructor-arg ref="quest" />
</bean>
<bean id="quest" class="Springing.Simplifying.SlayDragonQuest">
<constructor-arg value="#{T(System).out}" />
</bean>
</beans>

在这里,BraveKnight和SlayDragonQuest被声明为Spring中的bean。就BraveKnight bean来讲,它在构造时传入了对SlayDragonQuest bean的引用,将其作为构造器参数同时,SlayDragonQuest bean的声明使用了Spring表达式语言(SpringExpression Language),将System.out(这是一个PrintStream)传入到SlayDragonQuest的构造器中。下面为基于Java的配置,他的功能与代码清单6相同。
代码清单7 Spring基于Java的配置

1
2
3
4
5
6
7
8
public class KnightMain {
public static void main(String[] args) throws Exception {
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("/Springing/Simplifying/config/knights.xml");
Knight knight = context.getBean(Knight.class);
knight.embarkOnQuest();
context.close();
}
}

不管是基于xml还是基于Java配置,DI所带来的受益都是相同的。尽管BraveKnight依赖于Quest,但是它并不知道传递给它的是什么类型的Quest,也不知道这个Quest来自哪里。与之类似,SlayDragonQuest依赖于PrintStream,但是在编码时它并不需要知道这个PrintStream是什么样子的。只有Spring通过它的配置,能够了解这些组成部分是如何装配起来的。这样的话,就可以在不改变所依赖的类的情况下,修改依赖关系。这个样例展现了在Spring中装配bean的一种简单方式。

现在我们已经声明了BraveKnight与Quest的关系,接下来我们只说明通过xml配置文件装配,并把应用启动起来。

观察它如何工作

Spring通过应用上下文(Application Context)装载bean的定义并把它们组装起来。Spring应用上下文全权负责对象的创建和组装。Spring自带了多种应用上下文的实现,它们之间主要的区别仅仅在于如何加载配置

因为Knights.xml中的bean是使用xml文件进行配置,所以选择ClassPathXMLApplicationContext作为应用上下文相对是比较合适的。该类加载位于应用程序类路径下的一个或多个XML配置文件。

1
2
3
4
5
6
7
8
9
public class KnightMain {
public static void main(String[] args) throws Exception {
//调用ClassPathXmlApplicationContext加载knights.xml,获得Knight对象的引用
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("/Springing/Simplifying/config/knights.xml");
Knight knight = context.getBean(Knight.class); //获取knight bean
knight.embarkOnQuest(); // 使用knight
context.close();
}
}

mian()方法基于knight.xml文件创建了Spring应用上下文。随后它调用该应用上下文获取一个ID为knight的bean。得到knight对象的引用后,只需简单调用embarkOnQuest()方法就可以获取运行结果。

应用切面

DI能够让相互协作的软件组件保持松散耦合,而面向切面编程(aspect-oriented programming,AOP)允许你把遍布应用各处的功能分离出来形成可重用的组件。

面向切面编程往往被定义为促使软件系统实现关注点分离的一项技术。系统由许多不同的组件组成,每一个组件各负责一块特定功能。除了实现自身核心的功能之外,这些组件还经常承担着额外的职责。诸如日志、事务管理和安全这样的系统服务经常融入到自身具有核心业务逻辑的组件中去,这些系统服务通常被称为横切关注点,因为它们会跨越系统的多个组件。

如果将这些关注点分散到多个组件中去,你的代码将会带来双重的复杂性。

实现系统关注点功能的代码将会重复出现在多个组件中。这意味着如果你要改变这些关注点的逻辑,必须修改各个模块中的相关实现。即使你把这些关注点抽象为一个独立的模块,其他模块只是调用它的方法,但方法的调用还是会重复出现在各个模块中。
组件会因为那些与自身核心业务无关的代码而变得混乱。一个向地址簿增加地址条目的方法应该只关注如何添加地址,而不应该关注它是不是安全的或者是否需要支持事务。

SpringInAction第4版源代码

Adhere to the original technology to share, your support will encourage me to continue to create!