第一部分 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的功能并不局限于server端的开发。不论什么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规范和框架中,都能够非常easy看到侵入性编程的样例。
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的形式很easy。可是其功能能够很强大。Spring添加POJO功能的一种方式是通过DI(译者注:依赖注入)将它们组装起来。让我们看一下DI是怎样实现应用中对象之间的松耦合的。
1.1.2 依赖注入
依赖注入(
dependency injection)一词听起来可能有些吓人,好像是一种复杂的编程技术或者设计模式。但事实证明,DI并不像听起来的那样复杂。通过在你的项目中应用DI,你会发现你的代码将变得更简单,更easy理解,更easy測试。
不论什么重要的应用程序(比Hello World演示样例更复杂的应用)都是由两个或两个以上的类相互协作,运行一些业务逻辑的。传统方式,每一个对象负责获取自己合作对象的引用(这是依赖关系)。这可能会导致代码高度耦合而且难以測试。
代码清单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类,可是您能够非常easy地測试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
代码中是非常easy的----能够注入到
BraveKnight中,对吗?那就让我们使用这样的方式。
以下代码清单显示了把BraveKnight和Minstrel组织在一起的第一次尝试。
人们大多是通过说书人描写叙述的骑士的事迹来了解骑士的。
如果你想使用说书人这个服务来记录BraveKnight的一些事迹。
以下的代码清单列出了你可能会用到的说书人(Minstrel)类。
这应该足够了。
如今你须要做的就是回到你的Spring配置文件,在当中声明一个Minstrel Bean并将其注入到BraveKnight Bean的构造方法中。可是请等一下......
事情似乎并不对。骑士的职责范围中是否应该关注他的说书人呢?在我看来。说书人应该自己做自己的工作,而不应该被要求来做工作。总之。这是一个说书人的工作----用来记录传播骑士的事迹。
为什么骑士总是要提醒说书人呢?
此外,因为骑士须要知道说书人,你被迫将Minstrel注入到BraveKnight中。这不仅使BraveKnight代码变得复杂。并且让我产生了疑问:你是否会想要一个没有说书人的骑士?假设Minstrel为null。你是否应该为这个样例引入检查null的业务逻辑呢?
简单的BraveKnight类開始变得复杂。假设你处理
nullMinstrel的情况。将会变得更加复杂。可是使用AOP,你能够通过声明的方式实现说书人记录骑士的事迹。使骑士不再直接调用Minstrel方法。
将
Minstrel变成一个切面,你所须要做的就是在Spring配置文件里将其声明为一个切面。以下是更新后的knight.xml文件: