本节书摘来自异步社区《Spring实战(第4版)》一书中的第1章,第1.1节,作者: 【美】Craig Walls(沃尔斯)著,更多章节内容可以访问云栖社区“异步社区”公众号查看
第1部分 Spring的核心
Spring可以做很多事情,它为企业级开发提供给了丰富的功能,但是这些功能的底层都依赖于它的两个核心特性,也就是依赖注入(dependency injection,DI)和面向切面编程(aspect-oriented programming,AOP)。
作为本书的开始,在第1章“Spring之旅”中,我将快速介绍一下Spring框架,包括Spring DI和AOP的概况,以及它们是如何帮助读者解耦应用组件的。
在第2章“装配Bean”中,我们将深入探讨如何将应用中的各个组件拼装在一起,读者将会看到Spring所提供的自动配置、基于Java的配置以及XML配置。
在第3章“高级装配”中,将会告别基础的内容,为读者展现一些最大化Spring威力的技巧和技术,包括条件化装配、处理自动装配时的歧义性、作用域以及Spring表达式语言。
在第4章“面向切面的Spring”中,展示如何使用Spring的AOP特性把系统级的服务(例如安全和审计)从它们所服务的对象中解耦出来。本章也为后面的第9章、第13章和第14章做了铺垫,这几章将会分别介绍如何将Spring AOP用于声明式安全以及缓存。
第1章 Spring之旅
本章内容:
Spring的bean容器
介绍Spring的核心模块
更为强大的Spring生态系统
Spring的新功能
对于Java程序员来说,这是一个很好的时代。
在Java近20年的历史中,它经历过很好的时代,也经历过饱受诟病的时代。尽管有很多粗糙的地方,如applet、企业级JavaBean(Enterprise JavaBean,EJB)、Java数据对象(Java Data Object,JDO)以及无数的日志框架,但是作为一个平台,Java的历史是丰富多彩的,有很多的企业级软件都是基于这个平台构建的。Spring是Java历史中很重要的组成部分。
在诞生之初,创建Spring的主要目的是用来替代更加重量级的企业级Java技术,尤其是EJB。相对于EJB来说,Spring提供了更加轻量级和简单的编程模型。它增强了简单老式Java对象(Plain Old Java object,POJO)的功能,使其具备了之前只有EJB和其他企业级Java规范才具有的功能。
随着时间的推移,EJB以及Java 2企业版(Java 2 Enterprise Edition,J2EE)在不断演化。EJB自身也提供了面向简单POJO的编程模型。现在,EJB也采用了依赖注入(Dependency Injection,DI)和面向切面编程(Aspect-Oriented Programming,AOP)的理念,这毫无疑问是受到Spring成功的启发。
尽管J2EE(现在称之为JEE)能够赶上Spring的步伐,但Spring也没有停止前进。Spring继续在其他领域发展,而JEE则刚刚开始涉及这些领域,或者还完全没有开始在这些领域的创新。移动开发、社交API集成、NoSQL数据库、云计算以及大数据都是Spring正在涉足和创新的领域。Spring的前景依然会很美好。
正如我之前所言,对于Java开发者来说,这是一个很好的时代。
本书会对Spring进行研究,在这一章中,我们将会在较为宏观的层面上介绍Spring,让你对Spring是什么有直观的体验。本章将让读者对Spring所解决的各类问题有一个清晰的认识,同时为其他章奠定基础。
1.1 简化Java开发
Spring是一个开源框架,最早由Rod Johnson创建,并在《Expert One-on-One:J2EE Design and Development》这本著作中进行了介绍。Spring是为了解决企业级应用开发的复杂性而创建的,使用Spring可以让简单的JavaBean实现之前只有EJB才能完成的事情。但Spring不仅仅局限于服务器端开发,任何Java应用都能在简单性、可测试性和松耦合等方面从Spring中获益。
bean的各种名称……虽然Spring用bean或者JavaBean来表示应用组件,但并不意味着Spring组件必须要遵循JavaBean规范。一个Spring组件可以是任何形式的POJO。在本书中,我采用JavaBean的广泛定义,即POJO的同义词。
纵览全书,读者会发现Spring 可以做非常多的事情。但归根结底,支撑Spring的仅仅是少许的基本理念,所有的理念都可以追溯到Spring最根本的使命上:简化Java开发。
这是一个郑重的承诺。许多框架都声称在某些方面做了简化,但Spring的目标是致力于全方位的简化Java开发。这势必引出更多的解释,Spring是如何简化Java开发的?
为了降低Java开发的复杂性,Spring采取了以下4种关键策略:
基于POJO的轻量级和最小侵入性编程;
通过依赖注入和面向接口实现松耦合;
基于切面和惯例进行声明式编程;
通过切面和模板减少样板式代码。
几乎Spring所做的任何事情都可以追溯到上述的一条或多条策略。在本章的其他部分,我将通过具体的案例进一步阐述这些理念,以此来证明Spring是如何完美兑现它的承诺的,也就是简化Java开发。让我们先从基于POJO的最小侵入性编程开始。
1.1.1 激发POJO的潜能
如果你从事Java编程有一段时间了,那么你或许会发现(可能你也实际使用过)很多框架通过强迫应用继承它们的类或实现它们的接口从而导致应用与框架绑死。一个典型的例子是EJB 2时代的无状态会话bean。早期的EJB是一个很容易想到的例子,不过这种侵入式的编程方式在早期版本的Struts、WebWork、Tapestry以及无数其他的Java规范和框架中都能看到。
Spring竭力避免因自身的API而弄乱你的应用代码。Spring不会强迫你实现Spring规范的接口或继承Spring规范的类,相反,在基于Spring构建的应用中,它的类通常没有任何痕迹表明你使用了Spring。最坏的场景是,一个类或许会使用Spring注解,但它依旧是POJO。
不妨举个例子,请参考下面的HelloWorldBean类:
程序清单1.1 Spring不会在HelloWorldBean上有任何不合理的要求
可以看到,这是一个简单普通的Java类——POJO。没有任何地方表明它是一个Spring组件。Spring的非侵入编程模型意味着这个类在Spring应用和非Spring应用中都可以发挥同样的作用。
尽管形式看起来很简单,但POJO一样可以具有魔力。Spring赋予POJO魔力的方式之一就是通过DI来装配它们。让我们看看DI是如何帮助应用对象彼此之间保持松散耦合的。
1.1.2 依赖注入
依赖注入这个词让人望而生畏,现在已经演变成一项复杂的编程技巧或设计模式理念。但事实证明,依赖注入并不像它听上去那么复杂。在项目中应用DI,你会发现你的代码会变得异常简单并且更容易理解和测试。
DI功能是如何实现的
任何一个有实际意义的应用(肯定比Hello World示例更复杂)都会由两个或者更多的类组成,这些类相互之间进行协作来完成特定的业务逻辑。按照传统的做法,每个对象负责管理与自己相互协作的对象(即它所依赖的对象)的引用,这将会导致高度耦合和难以测试的代码。
举个例子,考虑下程序清单1.2所展现的Knight类。
程序清单1.2 DamselRescuingKnight只能执行RescueDamselQuest探险任务
可以看到,DamselRescuingKnight
在它的构造函数中自行创建了Rescue DamselQuest
。这使得DamselRescuingKnight
紧密地和RescueDamselQuest
耦合到了一起,因此极大地限制了这个骑士执行探险的能力。如果一个少女需要救援,这个骑士能够召之即来。但是如果一条恶龙需要杀掉,或者一个圆桌……额……需要滚起来,那么这个骑士就爱莫能助了。
更糟糕的是,为这个DamselRescuingKnight
编写单元测试将出奇地困难。在这样的一个测试中,你必须保证当骑士的embarkOnQuest()
方法被调用的时候,探险的embark()方法也要被调用。但是没有一个简单明了的方式能够实现这一点。很遗憾,DamselRescuingKnight
将无法进行测试。
耦合具有两面性(two-headed beast)。一方面,紧密耦合的代码难以测试、难以复用、难以理解,并且典型地表现出“打地鼠”式的bug特性(修复一个bug,将会出现一个或者更多新的bug)。另一方面,一定程度的耦合又是必须的——完全没有耦合的代码什么也做不了。为了完成有实际意义的功能,不同的类必须以适当的方式进行交互。总而言之,耦合是必须的,但应当被小心谨慎地管理。
通过DI,对象的依赖关系将由系统中负责协调各对象的第三方组件在创建对象的时候进行设定。对象无需自行创建或管理它们的依赖关系,如图1.1所示,依赖关系将被自动注入到需要它们的对象当中去。
图1.1 依赖注入会将所依赖的关系自动交给目标对象,而不是让对象自己去获取依赖
为了展示这一点,让我们看一看程序清单1.3中的BraveKnight
,这个骑士不仅勇敢,而且能挑战任何形式的探险。
程序清单1.3 BraveKnight足够灵活可以接受任何赋予他的探险任务
我们可以看到,不同于之前的DamselRescuingKnight
,BraveKnight
没有自行创建探险任务,而是在构造的时候把探险任务作为构造器参数传入。这是依赖注入的方式之一,即构造器注入(constructor injection)。
更重要的是,传入的探险类型是Quest,也就是所有探险任务都必须实现的一个接口。所以,BraveKnight
能够响应RescueDamselQuest
、 SlayDragonQuest
、 MakeRoundTableRounderQuest
等任意的Quest实现。
这里的要点是BraveKnight
没有与任何特定的Quest实现发生耦合。对它来说,被要求挑战的探险任务只要实现了Quest接口,那么具体是哪种类型的探险就无关紧要了。这就是DI所带来的最大收益——松耦合。如果一个对象只通过接口(而不是具体实现或初始化过程)来表明依赖关系,那么这种依赖就能够在对象本身毫不知情的情况下,用不同的具体实现进行替换。
对依赖进行替换的一个最常用方法就是在测试的时候使用mock
实现。我们无法充分地测试DamselRescuingKnight
,因为它是紧耦合的;但是可以轻松地测试BraveKnigh
t,只需给它一个Quest
的mock
实现即可,如程序清单1.4所示。
程序清单1.4 为了测试BraveKnight,需要注入一个mock Quest
你可以使用mock框架Mockito去创建一个Quest
接口的mock实现。通过这个mock对象,就可以创建一个新的BraveKnight
实例,并通过构造器注入这个mock Quest
。当调用embarkOnQuest()
方法时,你可以要求Mockito
框架验证Quest
的mock实现的embark()
方法仅仅被调用了一次。
将Quest注入到Knight中
现在BraveKnight
类可以接受你传递给它的任意一种Quest
的实现,但该怎样把特定的Quest
实现传给它呢?假设,希望BraveKnight
所要进行探险任务是杀死一只怪龙,那么程序清单1.5中的SlayDragonQuest
也许是挺合适的。
程序清单1.5 SlayDragonQuest是要注入到BraveKnight中的Quest实现
我们可以看到,SlayDragonQuest
实现了Quest
接口,这样它就适合注入到BraveKnight
中去了。与其他的Java入门样例有所不同,SlayDragonQuest
没有使用System.out.println()
,而是在构造方法中请求一个更为通用的PrintStream
。这里最大的问题在于,我们该如何将SlayDragonQuest
交给BraveKnight
呢?又如何将PrintStream
交给SlayDragonQuest
呢?
创建应用组件之间协作的行为通常称为装配(wiring)。Spring有多种装配bean的方式,采用XML是很常见的一种装配方式。程序清单1.6展现了一个简单的Spring配置文件:knights.xml,该配置文件将BraveKnight
、SlayDragonQuest
和PrintStream
装配到了一起。
程序清单1.6 使用Spring将SlayDragonQuest注入到BraveKnight中
在这里,BraveKnight
和SlayDragonQuest
被声明为Spring中的bean。就BraveKnight bean
来讲,它在构造时传入了对SlayDragonQuest bean
的引用,将其作为构造器参数。同时,SlayDragonQuest bean
的声明使用了Spring表达式语言(Spring Expression Language),将System.out
(这是一个PrintStream
)传入到了SlayDragonQuest
的构造器中。
如果XML配置不符合你的喜好的话,Spring还支持使用Java来描述配置。比如,程序清单1.7展现了基于Java的配置,它的功能与程序清单1.6相同。
程序清单1.7 Spring提供了基于Java的配置,可作为XML的替代方案
不管你使用的是基于XML的配置还是基于Java的配置,DI所带来的收益都是相同的。尽管BraveKnight
依赖于Quest
,但是它并不知道传递给它的是什么类型的Quest
,也不知道这个Quest
来自哪里。与之类似,SlayDragonQuest
依赖于PrintStream
,但是在编码时它并不需要知道这个`PrintStream
是什么样子的。只有Spring通过它的配置,能够了解这些组成部分是如何装配起来的。这样的话,就可以在不改变所依赖的类的情况下,修改依赖关系。
这个样例展现了在Spring中装配bean的一种简单方法。谨记现在不要过多关注细节。第2章我们会深入讲解Spring的配置文件,同时还会了解Spring装配bean的其他方式,甚至包括一种让Spring自动发现bean并在这些bean之间建立关联关系的方式。
现在已经声明了BraveKnight
和Quest
的关系,接下来我们只需要装载XML配置文件,并把应用启动起来。
观察它如何工作
Spring通过应用上下文(Application Context)装载bean的定义并把它们组装起来。Spring应用上下文全权负责对象的创建和组装。Spring自带了多种应用上下文的实现,它们之间主要的区别仅仅在于如何加载配置。
因为knights.xml中的bean是使用XML文件进行配置的,所以选择ClassPathXmlApplicationContext
[1]作为应用上下文相对是比较合适的。该类加载位于应用程序类路径下的一个或多个XML配置文件。程序清单1.8中的main()
方法调用ClassPathXmlApplicationContext
加载knights.xml,并获得Knight
对象的引用。
程序清单1.8 KnightMain.java加载包含Knight的Spring上下文
这里的main()
方法基于knights.xml文件创建了Spring应用上下文。随后它调用该应用上下文获取一个ID为knight的bean。得到Knight
对象的引用后,只需简单调用embarkOnQuest()
方法就可以执行所赋予的探险任务了。注意这个类完全不知道我们的英雄骑士接受哪种探险任务,而且完全没有意识到这是由BraveKnight
来执行的。只有knights.xml文件知道哪个骑士执行哪种探险任务。
通过示例我们对依赖注入进行了一个快速介绍。纵览全书,你将对依赖注入有更多的认识。如果你想了解更多关于依赖注入的信息,我推荐阅读Dhanji R. Prasanna的《Dependency Injection》,该著作覆盖了依赖注入的所有内容。
现在让我们再关注Spring简化Java开发的下一个理念:基于切面进行声明式编程。
1.1.3 应用切面
DI能够让相互协作的软件组件保持松散耦合,而面向切面编程(aspect-oriented programming,AOP)允许你把遍布应用各处的功能分离出来形成可重用的组件。
面向切面编程往往被定义为促使软件系统实现关注点的分离一项技术。系统由许多不同的组件组成,每一个组件各负责一块特定功能。除了实现自身核心的功能之外,这些组件还经常承担着额外的职责。诸如日志、事务管理和安全这样的系统服务经常融入到自身具有核心业务逻辑的组件中去,这些系统服务通常被称为横切关注点,因为它们会跨越系统的多个组件。
如果将这些关注点分散到多个组件中去,你的代码将会带来双重的复杂性。
实现系统关注点功能的代码将会重复出现在多个组件中。这意味着如果你要改变这些关注点的逻辑,必须修改各个模块中的相关实现。即使你把这些关注点抽象为一个独立的模块,其他模块只是调用它的方法,但方法的调用还是会重复出现在各个模块中。
组件会因为那些与自身核心业务无关的代码而变得混乱。一个向地址簿增加地址条目的方法应该只关注如何添加地址,而不应该关注它是不是安全的或者是否需要支持事务。
图1.2展示了这种复杂性。左边的业务对象与系统级服务结合得过于紧密。每个对象不但要知道它需要记日志、进行安全控制和参与事务,还要亲自执行这些服务。
图1.2 在整个系统内,关注点(例如日志和安全)
的调用经常散布到各个模块中,而这些关注点并不是模块的核心业务
AOP能够使这些服务模块化,并以声明的方式将它们应用到它们需要影响的组件中去。所造成的结果就是这些组件会具有更高的内聚性并且会更加关注自身的业务,完全不需要了解涉及系统服务所带来复杂性。总之,AOP能够确保POJO的简单性。
如图1.3所示,我们可以把切面想象为覆盖在很多组件之上的一个外壳。应用是由那些实现各自业务功能的模块组成的。借助AOP,可以使用各种功能层去包裹核心业务层。这些层以声明的方式灵活地应用到系统中,你的核心应用甚至根本不知道它们的存在。这是一个非常强大的理念,可以将安全、事务和日志关注点与核心业务逻辑相分离。
图1.3 利用AOP,系统范围内的关注点覆盖在它们所影响组件之上
为了示范在Spring中如何应用切面,让我们重新回到骑士的例子,并为它添加一个切面。
AOP应用
每一个人都熟知骑士所做的任何事情,这是因为吟游诗人用诗歌记载了骑士的事迹并将其进行传唱。假设我们需要使用吟游诗人这个服务类来记载骑士的所有事迹。程序清单1.9展示了我们会使用的Minstrel
类。
程序清单1.9 吟游诗人是中世纪的音乐记录器
正如你所看到的那样,Minstrel
是只有两个方法的简单类。在骑士执行每一个探险任务之前,singBeforeQuest()
方法会被调用;在骑士完成探险任务之后,singAfterQuest()
方法会被调用。在这两种情况下,Minstrel
都会通过一个PrintStream
类来歌颂骑士的事迹,这个类是通过构造器注入进来的。
把Minstrel
加入你的代码中并使其运行起来,这对你来说是小事一桩。我们适当做一下调整从而让BraveKnight
可以使用Minstrel
。程序清单1.10展示了将BraveKnight
和Minstrel
组合起来的第一次尝试。
程序清单1.10 BraveKnight必须要调用Minstrel的方法
这应该可以达到预期效果。现在,你所需要做的就是回到Spring
配置中,声明Minstrel bean
并将其注入到BraveKnight
的构造器之中。但是,请稍等……
我们似乎感觉有些东西不太对。管理他的吟游诗人真的是骑士职责范围内的工作吗?在我看来,吟游诗人应该做他份内的事,根本不需要骑士命令他这么做。毕竟,用诗歌记载骑士的探险事迹,这是吟游诗人的职责。为什么骑士还需要提醒吟游诗人去做他份内的事情呢?
此外,因为骑士需要知道吟游诗人,所以就必须把吟游诗人注入到BarveKnight
类中。这不仅使BraveKnight
的代码复杂化了,而且还让我疑惑是否还需要一个不需要吟游诗人的骑士呢?如果Minstrel
为null
会发生什么呢?我是否应该引入一个空值校验逻辑来覆盖该场景?
简单的BraveKnight
类开始变得复杂,如果你还需要应对没有吟游诗人时的场景,那代码会变得更复杂。但利用AOP
,你可以声明吟游诗人必须歌颂骑士的探险事迹,而骑士本身并不用直接访问Minstrel
的方法。
要将Minstrel
抽象为一个切面,你所需要做的事情就是在一个Spring
配置文件中声明它。程序清单1.11是更新后的knights.xml
文件,Minstrel被声明为一个切面。
程序清单1.11 将Minstrel声明为一个切面
这里使用了Spring的aop配置命名空间把Minstrel bean声明为一个切面。首先,需要把Minstrel声明为一个bean,然后在元素中引用该bean。为了进一步定义切面,声明(使用)在embarkOnQuest()方法执行前调用Minstrel的singBeforeQuest()方法。这种方式被称为前置通知(before advice)。同时声明(使用)在embarkOnQuest()方法执行后调用singAfter Quest()方法。这种方式被称为后置通知(after advice)。
在这两种方式中,pointcut-ref属性都引用了名字为embark的切入点。该切入点是在前边的元素中定义的,并配置expression属性来选择所应用的通知。表达式的语法采用的是AspectJ的切点表达式语言。
现在,你无需担心不了解AspectJ或编写AspectJ切点表达式的细节,我们稍后会在第4章详细地探讨Spring AOP的内容。现在你已经知道,Spring在骑士执行探险任务前后会调用Minstrel的singBeforeQuest()和singAfterQuest()方法,这就足够了。
这就是我们需要做的所有的事情!通过少量的XML配置,就可以把Minstrel声明为一个Spring切面。如果你现在还没有完全理解,不必担心,在第4章你会看到更多的Spring AOP示例,那将会帮助你彻底弄清楚。现在我们可以从这个示例中获得两个重要的观点。
首先,Minstrel仍然是一个POJO,没有任何代码表明它要被作为一个切面使用。当我们按照上面那样进行配置后,在Spring的上下文中,Minstrel实际上已经变成一个切面了。
其次,也是最重要的,Minstrel可以被应用到BraveKnight中,而BraveKnight不需要显式地调用它。实际上,BraveKnight完全不知道Minstrel的存在。
必须还要指出的是,尽管我们使用Spring魔法把Minstrel转变为一个切面,但首先要把它声明为一个Spring bean。能够为其他Spring bean做到的事情都可以同样应用到Spring切面中,例如为它们注入依赖。
应用切面来歌颂骑士可能只是有点好玩而已,但是Spring AOP可以做很多有实际意义的事情。在后续的各章中,你还会了解基于Spring AOP实现声明式事务和安全(第9章和第14章)。
但现在,让我们再看看 Spring简化Java开发的其他方式。
1.1.4 使用模板消除样板式代码
你是否写过这样的代码,当编写的时候总会感觉以前曾经这么写过?我的朋友,这不是似曾相识。这是样板式的代码(boilerplate code)。通常为了实现通用的和简单的任务,你不得不一遍遍地重复编写这样的代码。
遗憾的是,它们中的很多是因为使用Java API而导致的样板式代码。样板式代码的一个常见范例是使用JDBC访问数据库查询数据。举个例子,如果你曾经用过JDBC,那么你或许会写出类似下面的代码。
程序清单1.12 许多Java API,例如JDBC,会涉及编写大量的样板式代码
正如你所看到的,这段JDBC代码查询数据库获得员工姓名和薪水。我打赌你很难把上面的代码逐行看完,这是因为少量查询员工的代码淹没在一堆JDBC的样板式代码中。首先你需要创建一个数据库连接,然后再创建一个语句对象,最后你才能进行查询。为了平息JDBC可能会出现的怒火,你必须捕捉SQLException,这是一个检查型异常,即使它抛出后你也做不了太多事情。
最后,毕竟该说的也说了,该做的也做了,你不得不清理战场,关闭数据库连接、语句和结果集。同样为了平息JDBC可能会出现的怒火,你依然要捕捉SQLException。
程序清单1.12中的代码和你实现其他JDBC操作时所写的代码几乎是相同的。只有少量的代码与查询员工逻辑有关系,其他的代码都是JDBC的样板代码。
JDBC不是产生样板式代码的唯一场景。在许多编程场景中往往都会导致类似的样板式代码,JMS、JNDI和使用REST服务通常也涉及大量的重复代码。
Spring旨在通过模板封装来消除样板式代码。Spring的JdbcTemplate使得执行数据库操作时,避免传统的JDBC样板代码成为了可能。
举个例子,使用Spring的JdbcTemplate(利用了 Java 5特性的JdbcTemplate实现)重写的getEmployeeById()方法仅仅关注于获取员工数据的核心逻辑,而不需要迎合JDBC API的需求。程序清单1.13展示了修订后的getEmployeeById()方法。
程序清单1.13 模板能够让你的代码关注于自身的职责
正如你所看到的,新版本的getEmployeeById()简单多了,而且仅仅关注于从数据库中查询员工。模板的queryForObject()方法需要一个SQL查询语句,一个RowMapper对象(把数据映射为一个域对象),零个或多个查询参数。GetEmp loyeeById()方法再也看不到以前的JDBC样板式代码了,它们全部被封装到了模板中。
我已经向你展示了Spring通过面向POJO编程、DI、切面和模板技术来简化Java开发中的复杂性。在这个过程中,我展示了在基于XML的配置文件中如何配置bean和切面,但这些文件是如何加载的呢?它们被加载到哪里去了?让我们再了解下Spring容器,这是应用中的所有bean所驻留的地方。