第一部分 Spring核心
Spring提供了很多功能,但是所有这些功能的基础是是依赖注入(DI)和面向方面编程(AOP)。

第一章 Springing into action

本章包括:
Spring的bean容器
探索Spring的核心模块
强大的Spring生态系统
Spring的新特性

       现在是java程序员的好时代。在长达20年的发展过程中,java经历了一些好时光,也经历了一些坏时光。 尽管有一些粗糙的地方,例如applet,Enterprise javabean(EJB),Java数据对象(JDO),和无数的日志框架,Java已经成为很多企业软件的开发平台。 Spring是java成为开发平台这个故事的重要组成部分。
       早些时候,Spring只是重量级企业java框架的替代品,尤其是EJB。和EJB相比,Spring提供了一个更轻量级、更精简编程模型。它增强了POJO的能力,这种能力以前只在EJB或者其他java规范中才具备。
随着时间的推移,EJB和J2EE也不断发展。EJB开始提供一个简单的面向POJO的编程模型。现在EJB使用的思想如依赖注入(DI)和面向方面编程(AOP),可以说是来自Spring成功的灵感。
        尽管J2EE(现在被称为JEE)的发展可以赶上Spring,但是Spring从未停止前进。Spring持续进步的领域,即使是现在,JEE刚刚才开始探索,有的甚至是从未涉及。如移动开发,社交API的集成,NoSQL数据库,云计算和大数据。
正如我所说的,现在是java程序员的好时代!
        这本书是Spring的一个探索。在本章,我们从一个较高的高度看一下Spring,让你初步感受下Spring的味道。这一章将向你介绍Spring解决类型问题的好方法,本书其余部分并将围绕这个方法进行。

1.1 简化Java开发
       Spring是一个开源框架,最初由 Rod Johnson创建,并在其写的《 Expert One-on-One: J2EE Design and Development》一书中做了描述。Spring创建的目的是解决企业应用开发的复杂性,使以前只能使用EJB解决的问题,现在可以使用普通javabean实现。但是Spring的功能并不局限于服务器端的开发。任何Java应用程序都可以受益于Spring的简单、可测试性和松耦合等特性。虽然Spring经常使用bean和JavaBean来表示应用组件,但是这并不意味着一个Spring组件必须遵循JavaBean规范。一个Spring组件可以是任何类型的POJO。在本书中,我采用了一个松散的JavaBean的定义,是POJO的同义词。
       在本书中你会看到,Spring做了很多事情。但Spring提供的所有功能的根源是基于一些基本的想法,所有想法都关注于Spring的基本任务:Spring简化Java开发。
       这是一个大胆的声明!很多框架都声明简化某些东西。但是Spring旨在简化Java开发这个广泛的主题。 这就需要更多的解释。Spring 是如何简化Java开发的?

为了应对java的复杂性,Spring采用了四个关键策略:
  • 轻量级的、微侵入性的POJO开发
  • 使用DI实现松耦合、面向接口编程
  • 使用切面和约定实现声明式编程
  • 使用切面和模板减少样板代码
       Spring所做的几乎所有事情都可以追溯到上述四个策略上。在本章的其余部分,我会详细介绍每一个策略,通过具体的例子来展示Spring是如何实现其承诺的:简化java开发。首先来看下Spring是如何实现轻量级的、微侵入性的POJO开发的。

1.1.1 释放POJO的威力
       如果你有几年的java开发经验,你可能会遇到这样一些框架,他们要求你继承框架的某个类或者实现框架的某个接口。侵入性编程模型的典型例子是EJB2的无状态Bean。除此之外,在Struts、WebWork、Tapestry的早期版本中,和无数其他Java规范和框架中,都可以很容易看到侵入性编程的例子。
       Spring尽可能避免你的应用程序与其API耦合。Spring从来不要求你实现其一个特定的接口或者继承其一个特定的类。相反,在基于Spring的应用程序中的类通常没有迹象表明他们正在使用Spring。在最坏的情况下,一个类可能会使用Spring注解,但它仍然是一个POJO。

举例说明,看下面代码清单中的HelloWorldBean类:

代码清单1.1 Spring对HelloWorldBean并不做任何不合理的要求。

package com.habuma.spring;
public class HelloWorldBean {
    public String sayHello() {
        return "Hello World";
    }
}
 
 正如您可以看到的,这是一个简单的,普通的Java类----一个POJO。没有什么特别的地方表明它是一个Spring组件。Spring的非入侵编程模型意味着一个类在Spring应用程序中具有的功能,在非Spring应用程序同样具备。POJO的形式非常简单,但是其功能可以非常强大。Spring增加POJO功能的一种方式是通过DI(译者注:依赖注入)将它们组装起来。让我们看一下DI是如何实现应用中对象之间的松耦合的。 
 
1.1.2 依赖注入
依赖注入( dependency injection)一词听起来可能有些吓人,好像是一种复杂的编程技术或者设计模式。但事实证明,DI并不像听起来的那样复杂。通过在你的项目中应用DI,你会发现你的代码将变得更简单,更容易理解,更容易测试。

DI是如何工作的
任何重要的应用程序(比Hello World示例更复杂的应用)都是由两个或两个以上的类相互协作,执行一些业务逻辑的。传统方式,每个对象负责获取自己合作对象的引用(这是依赖关系)。这可能会导致代码高度耦合并且难以测试。
例如,看下 Knight类,如下所示
代码清单1.2
package com.springinaction.knights;
public class DamselRescuingKnight implements Knight {
private RescueDamselQuest quest;
public DamselRescuingKnight() {
this.quest = new RescueDamselQuest();
}
public void embarkOnQuest() {
quest.embark();
}
}

正如你所看到的,DamselRescuingKnight(解救少女的骑士。译者注)类在构造函数中创建了自己的quest:RescueDamselQuest(解救少女任务。译者注)。这使得DamselRescuingKnight类与RescueDamselQuest类紧密耦合,严重限制了骑士所能执行的任务。如果一个少女需要解救,此骑士可以办到。但是如果此时需要杀死一条龙或者需要一个圆桌,那么这个骑士只能袖手旁观了。

更重要的是,为 DamselRescuingKnight编写一个单元测试将会非常难。而且在单元测试中,你要能判断 embarkOnQuest()调用了 embark()方法。
耦合是一把双刃剑。 一方面,紧密耦合的代码难以测试,很难重用,难以理解,修复一个bug可能导致出现多个新的bug。另一方面,一定的耦合又是必要的----完全非耦合的代码做不了任何事情。为了能做一些有用的事情,类之间需要了解彼此。耦合是必需的,但应该小心地管理。
使用DI,系统中对象间的依赖有第三方来管理。对象不需要创建或获取它们的依赖项。如图1.1所示,依赖在需要的时候被注入到对象中。

图1.1 依赖注入意思是:将依赖给一个对象,而不是一个对象自己获得这些依赖

为了说明这一点,让我们看一下下面代码清单中的BraveKnight类:骑士不仅勇敢,而且能够完成任何形式的任务。


如代码所示,与 DamselRescuingKnight类不同的是, BraveKnight类没有创建自己的quest。相反,在其构造函数中添加了一个quest参数。这种注入类型叫:构造函数注入。更重要的是,参数类型为Quest,这是一个"任务"接口,所有的任务都实现这个接口。所以 BraveKnight可以完成 RescueDamselQuest(解救少女任务) SlayDragonQuest( 杀死一条龙任务 ) , MakeRoundTableRounderQuest(制造圆桌任务)三个任务,或者其他实现了 Quest接口的任务。
关键点在于 BraveKnight没有与Quest接口的具体实现耦合。其被安排什么任务都没问题,只要任务实现了Quest接口即可。这是DI最主要的益处---解耦和。如果一个对象只知道他所依赖的接口(而不是接口的具体实现),那么 依赖就可以有不同的实现,依赖对象可以不知道各个实现间的区别。
依赖替换 最常见的用法之一是在测试过程中使用模拟实现来替换真实实现。 由于紧密耦合, 你无法充分测试DamselRescuingKnight类,但是您可以很容易地测试BraveKnight类,通过给它任务的模拟实现,如图所示。
在这里你使用模拟对象框架 Mockito创建了一个模拟实现的Quest。利用这个模拟对象,你创建了 BraveKnight对象,通过构造函数注入了模拟实现的Quest。调用过 embarkOnQuest()方法之后,使用 Mockito框架来验证模拟实现对象Quest的embark()方式是否只被调用了一次 。

KNIGHT注入一个QUEST

使用BraveKnight类,你可以给一个骑士安排任何任务,那么如何安排任务给骑士呢?例如,假设你想让 BraveKnight骑士执行任务:消灭一条龙。那么先要创建 SlayDragonQuest任务,如下代码清单:
如你所见,SlayDragonQuest类实现了Quest接口,使它适合BraveKnight。您可能还注意到,类中输出信息不是依靠system.out.println(),SlayDragonQuest采用了一种更通用的实现,通过其构造函数引用了PrintStream对象。这里的主要问题是,如何将SlayDragonQuest给BraveKnight?如何将 PrintStream给SlayDragonQuest?
应用程序组件之间创建关联的行为通常被称为装配。在Spring中,将组建装配到一起有很多中方式,但一种常用的方式是通过XML装配。下面的代码清单展示了一个简单的Spring配置文件:knights.xml,文件中将BraveKnight、SlayDragonQuest和PrintStream装配到了一起。
代码中,BraveKnight和 SlayDragonQuest都被声明为Spring的一个bean。在BraveKnight中,通过构造函数注入了SlayDragonQuest Bean。同时,SlayDragonQuest Bean使用Spring表达式语言将System.out(PrintStream类型)注入到了SlayDragonQuest的构造函数中。
如果XML配置文件不适合你的口味,Spring还允许你使用java快速配置。例如,下面是与XML配置相同的基于java的配置:
无论你是使用基于xml的配置还是使用基于java的配置,DI的好处是相同的。虽然BraveKnight依赖一个Quest,但他不知道注入的Quest类型,不知道Quest来自哪里。SlayDragonQuest同样。只有Spring,通过其配置,知道如何将所有的组件关联到一起。这可以实现在改变这些依赖项的时候,不需要改变依赖类。

这个例子展示了一个简单的方法来在Spring中装配bean。现在,不需要对更多的细节过多担心,第二章我们将学习更多Spring配置。我们也会看看其他Bean装配的方式,包括让Spring自动发现bean并创建它们之间的关系的方式。
现在您已经声明了BraveKnight和Quest之间的关系。你需要加载XML配置文件并启动应用程序。

看到它工作
在Spring应用程序中,通过应用程序上下文加载bean定义和将他们装配到一起。Spring应用程序上下文完全负责应用程序中对象的创建和装配。Spring有几个应用程序上下文的实现。各个实现之间的区别在于加载配置的方式不同。
knights.xml使用XML文件来声明Bean,选择比较合适的应用程序上下文是ClassPathXmlApplicationContext(对于基于java的配置,Spring所提供AnnotationConfigApplicationContext应用程序上下文.)。这个Spring上下文实现从应用程序的类路径中的一个或多个位于XML文件中加载Spring上下文。下面代码清单中的main( )方法使用了ClassPathXmlApplicationContext来加载knights.xml,并获取Knight对象的引用。
main()方法中使用knights.xml文件创建了Spring应用程序上下文。接着,使用这个应用程序上下文作为一个工厂来获取id为knight的Bean。利用Knight对象的引用调用其embarkOnQuest()方法。注意,这个Knight类并不知道其所执行的任务(Quest)类型是什么。只有 knights.xml文件知道其具体类型。

下面让我们看看Spring简化java开发的另一个策略:利用切面实现声明式编程。

1.1.3 使用切面

尽管DI可以以松耦合的方式将软件组件组合在一起,但是面向切面编程(AOP)使你能够捕获应用中使用的可重用组件的功能。
AOP是通常被看做一种技术,一种促进软件系统的关注点分离的技术。软件系统是由多个组件组成的,每个组件负责一个特定的功能。但往往这些组件还承担了其核心功能之外的责任,如日志、事务管理和安全性等。这些系统服务通常称为横切关注点,因为他们往往跨越一个系统中的多个组件。
在多个组件中传播这些关注点,导致代码引入了两个级别的复杂性:
1、系统级别的关注点的代码实现在多个组件中重复出现。这意味着,如果你要修改这些关注点的工作方式,您将需要修改多个组件。即使你将关注点抽取出一个单独的模块,在每个组件中调用这个组件的一个方法,那么这个方法在多个组件中都会出现。
2、组件中到处充斥着与核心功能无关的代码。一个方法用来将一个条目添加到地址簿,它应用只关注如何添加这个功能,不应该关注这个功能是否安全或者事务等方面。

图1.2说明了这种复杂性。左边的业务对象都密切参与了右边的系统服务。不仅每个对象知道它在做日志、安全和参与事务上下文等工作,而且他们也是自己负责执行这些服务。
AOP可以将这些服务模块化,然后以声明的方式将这些服务应用到相应的组件上面。这使组件,更有凝聚力,更专注于自己的具体问题,完全不用关注可能涉及的系统服务。简而言之,切面确保了pojo是简单的。可以将切面看做毯子,覆盖应用程序的许多组件。如图1.3所示。在其核心,应用程序包含了实现业务功能的模块。利用AOP,您可以使用功能层覆盖你的核心应用程序。 这些层可以灵活声明的方式应用 在您的应用程序,你的核心应用程序甚至不需要知道它们的存在。这是一个强大的概念,因为其组织了安全、事务、日志等关注点污染应用程序的核心业务逻辑。
为了演示Spring中的切面是如何工作的,让我们重温前面的骑士例子,给其添加一个基本的Spring切面。

正如您可以看到的, Minstrel是一个简单的类,有两个方法。 singBeforeQuest()方法将在骑士执行任务之前被调用, singAfterQuest()方法将在骑士完成任务之后被调用。在两个方法中, Minstrel都使用了注入的PrintStream来记录骑士的事迹。

Minstrel类应用到 BraveKnight 代码中是很容易的----可以注入到 BraveKnight中,对吗?那就让我们使用这种方式。下面代码清单显示了把BraveKnight和Minstrel组织在一起的第一次尝试。

人们大多是通过说书人描述的骑士的事迹来了解骑士的。假设你想使用说书人这个服务来记录BraveKnight的一些事迹。下面的代码清单列出了你可能会用到的说书人( Minstrel)类。
这应该足够了。现在你需要做的就是回到你的Spring配置文件,在其中声明一个Minstrel Bean并将其注入到BraveKnight Bean的构造方法中。但是请等一下......
事情似乎并不正确。骑士的职责范围中是否应该关注他的说书人呢?在我看来,说书人应该自己做自己的工作,而不应该被要求来做工作。总之,这是一个说书人的工作----用来记录传播骑士的事迹。为什么骑士总是要提醒说书人呢?
此外,由于骑士需要知道说书人,你被迫将Minstrel注入到BraveKnight中。这不仅使BraveKnight代码变得复杂,而且让我产生了疑问:你是否会想要一个没有说书人的骑士?如果Minstrel为null,你是否应该为这个例子引入检查null的业务逻辑呢?
简单的BraveKnight类开始变得复杂,如果你处理 nullMinstrel的情况,将会变得更加复杂。但是使用AOP,你可以通过声明的方式实现说书人记录骑士的事迹,使骑士不再直接调用Minstrel方法。
Minstrel变成一个切面,你所需要做的就是在Spring配置文件中将其声明为一个切面。下面是更新后的knight.xml文件:












Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐