Spring实战(第4版)

978-7-115-41730-5
作者: 【美】Craig Walls(沃尔斯)
译者: 张卫滨
编辑: 陈冀康
分类: Spring

图书目录:

详情

《Spring实战(第4版)从核心的Spring、Spring应用程序的核心组件、Spring集成3个方面,由浅入深、由易到难地对Spring展开了系统的讲解,包括Spring之旅、装配Bean、最小化Spring XML配置、面向切面的Spring、征服数据库、事务管理、使用Spring MVC构建Web应用程序等内容。

图书摘要

版权信息

书名:Spring实战(第4版)

ISBN:978-7-115-41730-5

本书由人民邮电出版社发行数字版。版权所有,侵权必究。

您购买的人民邮电出版社电子书仅供您个人使用,未经授权,不得以任何方式复制和传播本书内容。

我们愿意相信读者具有这样的良知和觉悟,与我们共同保护知识产权。

如果购买者有侵权行为,我们可能对该用户实施包括但不限于关闭该帐号等维权措施,并可能追究法律责任。

• 著     [美] Craig Walls

  译     张卫滨

  责任编辑 陈冀康

• 人民邮电出版社出版发行  北京市丰台区成寿寺路11号

  邮编 100164  电子邮件 315@ptpress.com.cn

  网址 http://www.ptpress.com.cn

• 读者服务热线:(010)81055410

 反盗版热线:(010)81055315



Craig Walls是Pivotal的高级工程师,是Spring Social和Spring Sync的项目领导者,同时也是Manning出版社《Spring In Action》的作者,目前这本书已经更新到了第四版。他非常热心于Spring框架的推广,经常在当地的用户组和会议上演讲并在博客上撰写Spring相关的内容。在不琢磨代码的时候,Craig Walls会尽可能多地陪伴他的妻子、两个女儿、两只小鸟以及两只小狗。

本书特色

全球有超过100 000的开发者使用本书来学习Spring

中文版累计销售超10万册,畅销经典Spring 技术图书,针对Spring 4 全新升级

作者Craig Walls,SpringSource的软件开发人员,也是一位畅销书作者。

第3版译者继续翻译新版,品质保障!


Original English language edition, entitled Spring in Action,4th Edition by Craig Walls Bibeault published by Manning Publications Co., 209 Bruce Park Avenue, Greenwich, CT 06830. Copyright ©2015 by Manning Publications Co.

Simplified Chinese-language edition copyright ©2016 by Posts & Telecom Press. All rights reserved.

本书中文简体字版由Manning Publications Co.授权人民邮电出版社独家出版。未经出版者书面许可,不得以任何方式复制本书内容。

版权所有,侵权必究。


本书是经典的、畅销的Spring学习和实践指南。

第4版针对Spring 4进行了全面更新。全书分为4部分。第1部分介绍Spring框架的核心知识。第2部分在此基础上介绍了如何使用Spring构建Web应用程序。第3部分告别前端,介绍了如何在应用程序的后端使用Spring。第4部分描述了如何使用Spring与其他的应用和服务进行集成。

本书适用于已具有一定Java 编程基础的读者,以及在Java 平台下进行各类软件开发的开发人员、测试人员,尤其适用于企业级Java 开发人员。本书既可以被刚开始学习Spring 的读者当作学习指南,也可以被那些想深入了解Spring 某方面功能的资深用户作为参考用书。


Spring框架是以简化Java EE应用程序的开发为目标而创建的。同样,本书是为了帮助读者更容易地使用Spring而编写的。我的目标不是为读者详细地列出Spring API,而是希望通过现实中的实际示例代码来为Java EE开发人员展现Spring框架。因为Spring是一个模块化的框架,所以这本书也是按照这种方式编写的。我们知道并不是所有的开发人员都有相同的需求,有些人想从头学习Spring,而有的可能只想排出几个主题,然后按照自己的节奏来学习。所以,本书既可以被刚开始学习Spring的读者当作学习指南,也可以被那些想深入了解某方面功能的读者作为参考。

本书适用于所有的Java开发人员,企业级Java开发人员将会发现更有帮助。我将会循序渐进地指导读者浏览本书中每章复杂的示例代码,但Spring的真正强大之处在于它能够使企业级应用程序的开发更简单。因此,企业级应用程序的开发人员会更加欣赏本书的示例代码。因为Spring的绝大部分内容都是提供企业级服务的,所以这里包含了许多Spring和EJB的比较。

本书分为4部分。第1部分介绍Spring框架的核心知识。第2部分在此基础上介绍如何使用Spring构建Web应用程序。第3部分告别前端,介绍如何在应用程序的后端使用Spring。第4部分描述如何使用Spring与其他的应用和服务进行集成。

在第1部分中,读者将会学习到Spring容器、依赖注入(dependency injection,DI)和面向切面编程(aspect-oriented programming,AOP),也就是Spring框架的核心。这能让读者很好地理解Spring的基础原理,而这些原理将会在本书各个章节都会用到。

在第2部分中,读者将会看到如何使用Spring来构建Web应用程序。

第3部分所关注的内容不再是应用程序的前端了,而是关注于如何处理和持久化数据。

本书的最后一部分会介绍如何将Spring应用程序与其他系统进行集成。

本书中有大量的示例代码。这些代码将会使用固定宽度的代码字体。本书正文中的类名、方法名或XML片段也都使用代码字体。

很多Spring类和包的名字很长(不过会有较强的表达性)。鉴于此,我们有时候会用到换行符(➥)。

本书中的示例代码并不都是完整的。为了关注某个主题,我有时候只会展示类的一个或两个方法。本书所构建的应用程序完整代码可以在出版社站点上下载,地址是www.manning.com/SpringinActionFourthEdition。

购买了本书,读者就可以免费访问Manning出版社提供的在线论坛,在这里读者可以给本书写评论,问一些技术问题并可以得到作者和其他用户的帮助。要进入这个论坛或订阅它,读者可以在浏览器中访问www.manning.com/SpringinActionFourthEdition。这个页面会告诉读者注册后怎样进入论坛,能够得到什么帮助以及论坛的规则。

Manning对读者的许诺是为读者提供一个交流平台,在这里读者之间以及读者和作者之间可以进行有意义的交流。对于作者来说,对论坛进行多少次的访问不是强制的,他们对本书论坛的贡献是自愿和免费的。我们建议读者尽量向作者问一些有挑战性的问题,以保持他们的兴趣!

只要本书还在发售,读者就可以访问作者在线论坛以及以前讨论的归档信息。

《Spring实战》第4版的封面人物是“Le Caraco”,也就是约旦西南部卡拉克(Karak)省的居民。该省的首府是Al-Karak,那里的山顶有座古城堡,对死海和周边的平原有着极佳的视野。这幅图出自1796年出版的法国旅游图书,Encyclopédiedes Voyages,该书由J.G.St.Sauveur编写。在那时,为了娱乐而去旅游还是相对新鲜的做法,而像这样的旅游指南是很流行的,它能够让旅行家和足不出户的人们了解法国其他地区和国外的居民。

Encyclopédiedes Voyages中多种多样的图画生动描绘了200年前世界上各个城镇和地区的独特魅力。在那时,相隔数十千米的两个地区着装就不相同,可以通过着装判断人们究竟属于哪个地区。这本旅行指南展现了那个时代和其他历史时代的隔离感和距离感,这与我们这个运动过度的时代是截然不同的。

从那以后,服装风格发生了改变,富有地方特色的多样性开始淡化。现在,有时很难说一个洲的居民和其他洲的居民有什么不同。从积极的方面来看,我们或许是用原来文化和视觉上的多样性换来了个人风格的多变性,或者可以说是更为多样化和有趣的知识科技生活。

这本旅行指南中的图片反映了两个世纪前各个地区生活的多样性,我们现在用图书封面的方式对其进行了再现。Manning出版社的员工都认为这是计算机行业中一个很有意思的创意。


百尺竿头更进一步。十几年前,Spring刚刚进入Java开发领域,其目标是简化企业级Java开发。它使用更为简单和轻量级的模型,该模型基于简单老式的Java对象,以此挑战了当时重量级的开发模型。

现在,已经过去了很多年,Spring也发布了众多的版本,我们可以看到Spring在企业级应用开发领域已经有了巨大的影响力。对于无数的Java项目来说,它就是事实上的标准,并且对于一些规范和它本来想取代的框架,Spring也对其演进产生了影响。毫无疑问,如果Spring不挑战之前版本的企业级JavaBean(EJB)规范的话,现在的EJB规范肯定是完全不同的一个样子。

但是,Spring本身也在持续地演化和提升,它一直致力于将困难的开发任务进行简化,不断地为Java开发人员带来创新性的特性。在Spring最初所挑战的领域,Spring已经突飞猛进,涉及的范围扩展到Java应用开发的各个方面。

因此,为了介绍Spring的现状,我们需要对这本书升级了。在本书上一版出版到现在的几年间,发生了太多的事情,想在这一版中将所有的变化都涵盖进来是不可能的。不过,在第4版的《Spring实战》中,我依然会使其包含尽可能多的内容。下面列出了在这一版中新增的一些令人兴奋的新内容:

如果在Spring方面读者已经有相当多经验的话,那么将会发现这些新元素对于自己的Spring工具箱来说是非常有价值的补充。如果读者是要学习Spring的新手,那么就赶上了学习Spring的一个好时代,这本书会帮助读者起步。

对于Spring的使用来说,这的确是一个令人兴奋的时代。在过去的12年里,在使用Spring进行开发以及编写与之相关的文章方面形成了一股浪潮。我迫不及待地想看到Spring接下来会做些什么!


3年前,有幸和耿渊同学合作翻译了《Spring实战(第3版)》。3年的时光过去了,技术在不断发展,这本书也推出了最新的第4版,顺利将这本书翻译完成后,顿时感觉轻松了许多。译书是一件比较辛苦的工作,但是在这3年的时间内,每当看到有朋友选择本书来学习Spring,自己觉得还是蛮有成就感的。所以,看到本书的第4版时,我迫不及待地联系编辑约定了本书的翻译事宜。

本书的作者Craig Walls先生,从10年前编写本书的第1版开始,持续把一件事情做好,紧跟技术的发展,不断地升级和更新这本书的内容,世界范围内无数的Java开发者通过这本书学习和掌握了Spring技术。

本书的主题是Spring框架,从十多年前问世以来,它一直致力于简化JEE应用的开发。从最初的挑战者,到现在诸多标准的制定者;从传统的JEE应用,到大数据、NoSQL、企业应用集成、批处理、移动开发等领域,Spring都在参与和发挥影响力。新版本的Spring提供了更加丰富的功能,但更重要的是Spring在想尽办法简化开发人员的使用,包括自动配置、基于Java的配置,还有现在越来越受到欢迎的Spring Boot。Spring Boot是对Spring本身的一种颠覆和革命,但是唯有这种颠覆,才会换来开发人员更多的喜爱和框架本身的发展。

这本书从第1版到第4版之所以长盛不衰,是因为它紧跟了技术的发展;Spring十多年来一直受到Java开发者的青睐,是因为它不断地进步和改善,并且坚持最初的目标:简化企业级Java的开发。处于一个不断革新的领域,我们技术人员何尝不需要如此呢,只有不断地汲取新的知识,学习新的技术,才能保证不被时代所淘汰。

本书涵盖了Spring框架的许多领域,既有核心框架,也有各种功能扩展,不少的同学曾经对我言及,感觉书中所讲述的内容深度不够,但是我个人认为,对于开源框架的学习,我们会有不同的掌握深度,从最初始的使用、配置,到设计原理,再到源码分析,一本书很难面面俱到深入介绍所有的内容,但是它却能够提供一个方向,让我们按图索骥深入学习更多的知识。

译书占用了大量的业余时间,因此感谢我的爱人,帮我承担了许多家务和带孩子的工作,还要感谢我的儿子,每天看到他的成长和进步,都让我感觉如果懈怠的话,该被小朋友嘲笑了。

尽管在翻译的过程中,我力争达到准确和通畅,并与作者进行了很多的沟通和交流,但限于水平和时间,肯定还有许多的不足或纰漏之处,热忱期待您提出意见,希望本书能够对您有用!您可以通过levinzhang1981@126.com联系到我。

张卫滨

2015年11月于大连


在本书付印之前,在本书捆扎之前,在本书装箱之前,在本书交付运输之前,在本书到达你手里之前,在整个过程中,有很多双手都曾经接触过它。即便你阅读电子版,省去了上面所述的流程,在你所下载的位和字节上依然凝结着很多双手的辛勤劳动——编辑、审阅、录入以及校对。如果没有这么多人的付出,这本书也就不会存在了。

首先,我要感谢Manning辛苦工作的每个人,当这本书的进展速度没有达到预期时,他们给予了足够的耐心,并促使我完成这本书:Marjan Bace、Michael Stephens、Cynthia Kane、Andy Carroll、Benjamin Berg、Alyson Brener、Dottie Marisco、Mary Piergies、Janet Vail以及幕后的其他很多人。

写书的时候,尽早和频繁的反馈是相当重要的,这一点与开发软件是一样的。当这本书还非常粗糙的时候,有些人审阅了初稿并提供反馈,帮助本书最终成型。要感谢下面的人:Bob Casazza、Chaoho Hsieh、Christophe Martini、Gregor Zurowski、James Wright、Jeelani Basha、Jens Richter、Jonathan Thoms、Josh Hart、Karen Christenson、Mario Arias、Michael Roberts、Paul Balogh、Ricardo da Silva Lima。尤其要感谢John Ryan,在本书交付前,他对书稿进行了全面的技术审校。

当然,我要感谢美丽的妻子,感谢她容忍我开始了这个新的写作工程,感谢她整个过程中所给予我的鼓励。我深深地爱着你。

Maisy和Madi,世界上最可爱的小姑娘,感谢你们的拥抱、欢笑以及对本书内容别出心裁的见解。

对于Spring团队的同事,怎么说呢?你们太酷了!能够作为推动Spring前进的团队中的一员,我感到非常荣幸和感激。你们层出不穷的新创意总是让我感到惊叹。

感谢我在用户组和No Fluff/Just Stuff会议上演讲时所遇到的每个人。

最后,感谢Phoenicians,你们(以及Epcot)太棒了![1]

[1]  Phoenicians指的是远古时代的腓尼基人,他们被认为是字母系统的创建者,基于字母的所有现代语言都由此衍生而来。在迪斯尼世界的Epcot,有名为Spaceship Earth的时光穿梭体验,我们可以了解到人类交流的历史,甚至能够回到腓尼基人的时代,在这段旅程的旁白中这样说道:如果你觉得学习字母语言很容易的话,那感谢腓尼基人吧,是他们发明了它。这是作者的一种幽默说法。——译者注


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用于声明式安全以及缓存。

本章内容:

对于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所解决的各类问题有一个清晰的认识,同时为其他章奠定基础。

Spring是一个开源框架,最早由Rod Johnson创建,并在《Expert One-on-One:J2EE Design and Development》(http://amzn.com/076454385)这本著作中进行了介绍。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种关键策略:

几乎Spring所做的任何事情都可以追溯到上述的一条或多条策略。在本章的其他部分,我将通过具体的案例进一步阐述这些理念,以此来证明Spring是如何完美兑现它的承诺的,也就是简化Java开发。让我们先从基于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是如何帮助应用对象彼此之间保持松散耦合的。

依赖注入这个词让人望而生畏,现在已经演变成一项复杂的编程技巧或设计模式理念。但事实证明,依赖注入并不像它听上去那么复杂。在项目中应用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足够灵活可以接受任何赋予他的探险任务

我们可以看到,不同于之前的DamselRescuingKnightBraveKnight没有自行创建探险任务,而是在构造的时候把探险任务作为构造器参数传入。这是依赖注入的方式之一,即构造器注入(constructor injection)。

更重要的是,传入的探险类型是Quest,也就是所有探险任务都必须实现的一个接口。所以,BraveKnight能够响应RescueDamselQuestSlayDragonQuestMakeRoundTableRounderQuest等任意的Quest实现。

这里的要点是BraveKnight没有与任何特定的Quest实现发生耦合。对它来说,被要求挑战的探险任务只要实现了Quest接口,那么具体是哪种类型的探险就无关紧要了。这就是DI所带来的最大收益——松耦合。如果一个对象只通过接口(而不是具体实现或初始化过程)来表明依赖关系,那么这种依赖就能够在对象本身毫不知情的情况下,用不同的具体实现进行替换。

对依赖进行替换的一个最常用方法就是在测试的时候使用mock实现。我们无法充分地测试DamselRescuingKnight,因为它是紧耦合的;但是可以轻松地测试BraveKnight,只需给它一个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实现

package com.springinaction.knights;

import java.io.PrintStream;

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是很常见的一种装配方式。程序清单1.6展现了一个简单的Spring配置文件:knights.xml,该配置文件将BraveKnightSlayDragonQuestPrintStream装配到了一起。

程序清单1.6 使用Spring将SlayDragonQuest注入到BraveKnight中

在这里,BraveKnightSlayDragonQuest被声明为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的替代方案

package com.springinaction.knights.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import com.springinaction.knights.BraveKnight;
import com.springinaction.knights.Knight;
import com.springinaction.knights.Quest;
import com.springinaction.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,但是它并不知道传递给它的是什么类型的Quest,也不知道这个Quest来自哪里。与之类似,SlayDragonQuest依赖于PrintStream,但是在编码时它并不需要知道这个PrintStream是什么样子的。只有Spring通过它的配置,能够了解这些组成部分是如何装配起来的。这样的话,就可以在不改变所依赖的类的情况下,修改依赖关系。

这个样例展现了在Spring中装配bean的一种简单方法。谨记现在不要过多关注细节。第2章我们会深入讲解Spring的配置文件,同时还会了解Spring装配bean的其他方式,甚至包括一种让Spring自动发现bean并在这些bean之间建立关联关系的方式。

现在已经声明了BraveKnightQuest的关系,接下来我们只需要装载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开发的下一个理念:基于切面进行声明式编程。

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展示了将BraveKnightMinstrel组合起来的第一次尝试。

程序清单1.10 BraveKnight必须要调用Minstrel的方法

这应该可以达到预期效果。现在,你所需要做的就是回到Spring配置中,声明Minstrel bean并将其注入到BraveKnight的构造器之中。但是,请稍等……

我们似乎感觉有些东西不太对。管理他的吟游诗人真的是骑士职责范围内的工作吗?在我看来,吟游诗人应该做他份内的事,根本不需要骑士命令他这么做。毕竟,用诗歌记载骑士的探险事迹,这是吟游诗人的职责。为什么骑士还需要提醒吟游诗人去做他份内的事情呢?

此外,因为骑士需要知道吟游诗人,所以就必须把吟游诗人注入到BarveKnight类中。这不仅使BraveKnight的代码复杂化了,而且还让我疑惑是否还需要一个不需要吟游诗人的骑士呢?如果Minstrelnull会发生什么呢?我是否应该引入一个空值校验逻辑来覆盖该场景?

简单的BraveKnight类开始变得复杂,如果你还需要应对没有吟游诗人时的场景,那代码会变得更复杂。但利用AOP,你可以声明吟游诗人必须歌颂骑士的探险事迹,而骑士本身并不用直接访问Minstrel的方法。

要将Minstrel抽象为一个切面,你所需要做的事情就是在一个Spring配置文件中声明它。程序清单1.11是更新后的knights.xml文件,Minstrel被声明为一个切面。

程序清单1.11 将Minstrel声明为一个切面

这里使用了Spring的aop配置命名空间把Minstrel bean声明为一个切面。首先,需要把Minstrel声明为一个bean,然后在<aop:aspect>元素中引用该bean。为了进一步定义切面,声明(使用<aop:before>)在embarkOnQuest()方法执行前调用MinstrelsingBeforeQuest()方法。这种方式被称为前置通知(before advice)。同时声明(使用<aop:after>)在embarkOnQuest()方法执行后调用singAfter Quest()方法。这种方式被称为后置通知(after advice)。

在这两种方式中,pointcut-ref属性都引用了名字为embark的切入点。该切入点是在前边的<pointcut>元素中定义的,并配置expression属性来选择所应用的通知。表达式的语法采用的是AspectJ的切点表达式语言。

现在,你无需担心不了解AspectJ或编写AspectJ切点表达式的细节,我们稍后会在第4章详细地探讨Spring AOP的内容。现在你已经知道,Spring在骑士执行探险任务前后会调用MinstrelsingBeforeQuest()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开发的其他方式。

你是否写过这样的代码,当编写的时候总会感觉以前曾经这么写过?我的朋友,这不是似曾相识。这是样板式的代码(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所驻留的地方。

在基于Spring的应用中,你的应用对象生存于Spring容器(container)中。如图1.4所示,Spring容器负责创建对象,装配它们,配置它们并管理它们的整个生命周期,从生存到死亡(在这里,可能就是newfinalize())

图1.4 在Spring应用中,对象由Spring容器创建和装配,并存在容器之中

在下一章,你将了解如何配置Spring,从而让它知道该创建、配置和组装哪些对象。但首先,最重要的是了解容纳对象的容器。理解容器将有助于理解对象是如何被管理的。

容器是Spring框架的核心。Spring容器使用DI管理构成应用的组件,它会创建相互协作的组件之间的关联。毫无疑问,这些对象更简单干净,更易于理解,更易于重用并且更易于进行单元测试。

Spring容器并不是只有一个。Spring自带了多个容器实现,可以归为两种不同的类型。bean工厂(由org.springframework.beans.factory.BeanFactory接口定义)是最简单的容器,提供基本的DI支持。应用上下文(由org.springframework.context.ApplicationContext接口定义)基于BeanFactory构建,并提供应用框架级别的服务,例如从属性文件解析文本信息以及发布应用事件给感兴趣的事件监听者。

虽然我们可以在bean工厂和应用上下文之间任选一种,但bean工厂对大多数应用来说往往太低级了,因此,应用上下文要比bean工厂更受欢迎。我们会把精力集中在应用上下文的使用上,不再浪费时间讨论bean工厂。

Spring自带了多种类型的应用上下文。下面罗列的几个是你最有可能遇到的。

当在第8章讨论基于Web的Spring应用时,我们会对AnnotationConfigWeb-ApplicationContextXmlWebApplicationContext进行更详细的讨论。现在我们先简单地使用FileSystemXmlApplicationContext从文件系统中加载应用上下文或者使用ClassPathXmlApplicationContext从类路径中加载应用上下文。

无论是从文件系统中装载应用上下文还是从类路径下装载应用上下文,将bean加载到bean工厂的过程都是相似的。例如,如下代码展示了如何加载一个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文件。

如果你想从Java配置中加载应用上下文,那么可以使用AnnotationConfig-ApplicationContext

ApplicationContext context = new AnnotationConfigApplicationContext(
    com.springinaction.knights.config.KnightConfig.class);

在这里没有指定加载Spring应用上下文所需的XML文件,AnnotationConfig-ApplicationContext通过一个配置类加载bean。

应用上下文准备就绪之后,我们就可以调用上下文的getBean()方法从Spring容器中获取bean。

现在你应该基本了解了如何创建Spring容器,让我们对容器中bean的生命周期做更进一步的探究。

在传统的Java应用中,bean的生命周期很简单。使用Java关键字new进行bean实例化,然后该bean就可以使用了。一旦该bean不再被使用,则由Java自动进行垃圾回收。

相比之下,Spring容器中的bean的生命周期就显得相对复杂多了。正确理解Spring bean的生命周期非常重要,因为你或许要利用Spring提供的扩展点来自定义bean的创建过程。图1.5展示了bean装载到Spring应用上下文中的一个典型的生命周期过程。

图1.5 bean在Spring容器中从创建到销毁经历了
若干阶段,每一阶段都可以针对Spring如何管理bean进行个性化定制

正如你所见,在bean准备就绪之前,bean工厂执行了若干启动步骤。我们对图1.5进行详细描述:

1.Spring对bean进行实例化;

2.Spring将值和bean的引用注入到bean对应的属性中;

3.如果bean实现了BeanNameAware接口,Spring将bean的ID传递给setBean-Name()方法;

4.如果bean实现了BeanFactoryAware接口,Spring将调用setBeanFactory()方法,将BeanFactory容器实例传入;

5.如果bean实现了ApplicationContextAware接口,Spring将调用setApplicationContext()方法,将bean所在的应用上下文的引用传入进来;

6.如果bean实现了BeanPostProcessor接口,Spring将调用它们的post-ProcessBeforeInitialization()方法;

7.如果bean实现了InitializingBean接口,Spring将调用它们的after-PropertiesSet()方法。类似地,如果bean使用init-method声明了初始化方法,该方法也会被调用;

8.如果bean实现了BeanPostProcessor接口,Spring将调用它们的post-ProcessAfterInitialization()方法;

9.此时,bean已经准备就绪,可以被应用程序使用了,它们将一直驻留在应用上下文中,直到该应用上下文被销毁;

10.如果bean实现了DisposableBean接口,Spring将调用它的destroy()接口方法。同样,如果bean使用destroy-method声明了销毁方法,该方法也会被调用。

现在你已经了解了如何创建和加载一个Spring容器。但是一个空的容器并没有太大的价值,在你把东西放进去之前,它里面什么都没有。为了从Spring的DI中受益,我们必须将应用对象装配进Spring容器中。我们将在第2章对bean装配进行更详细的探讨。

我们现在首先浏览一下Spring的体系结构,了解一下Spring框架的基本组成部分和最新版本的Spring所发布的新特性。

正如你所看到的,Spring框架关注于通过DI、AOP和消除样板式代码来简化企业级Java开发。即使这是Spring所能做的全部事情,那Spring也值得一用。但是,Spring实际上的功能超乎你的想象。

在Spring框架的范畴内,你会发现Spring简化Java开发的多种方式。但在Spring框架之外还存在一个构建在核心框架之上的庞大生态圈,它将Spring扩展到不同的领域,例如Web服务、REST、移动开发以及NoSQL。

首先让我们拆开Spring框架的核心来看看它究竟为我们带来了什么,然后我们再浏览下Spring Portfolio中的其他成员。

当我们下载Spring发布版本并查看其lib目录时,会发现里面有多个JAR文件。在Spring 4.0中,Spring框架的发布版本包括了20个不同的模块,每个模块会有3个JAR文件(二进制类库、源码的JAR文件以及JavaDoc的JAR文件)。完整的库JAR文件如图1.6所示。

图1.6 Spring框架由20个不同的模块组成

这些模块依据其所属的功能可以划分为6类不同的功能,如图1.7所示。

总体而言,这些模块为开发企业级应用提供了所需的一切。但是你也不必将应用建立在整个Spring框架之上,你可以自由地选择适合自身应用需求的Spring模块;当Spring不能满足需求时,完全可以考虑其他选择。事实上,Spring甚至提供了与其他第三方框架和类库的集成点,这样你就不需要自己编写这样的代码了。

图1.7 Spring框架由6个定义良好的模块分类组成

让我们逐一浏览Spring的模块,看看它们是如何构建起Spring整体蓝图的。

Spring核心容器

容器是Spring框架最核心的部分,它管理着Spring应用中bean的创建、配置和管理。在该模块中,包括了Spring bean工厂,它为Spring提供了DI的功能。基于bean工厂,我们还会发现有多种Spring应用上下文的实现,每一种都提供了配置Spring的不同方式。

除了bean工厂和应用上下文,该模块也提供了许多企业服务,例如E-mail、JNDI访问、EJB集成和调度。

所有的Spring模块都构建于核心容器之上。当你配置应用时,其实你隐式地使用了这些类。贯穿本书,我们都会涉及到核心模块,在第2章中我们将会深入探讨Spring的DI。

Spring的AOP模块

在AOP模块中,Spring对面向切面编程提供了丰富的支持。这个模块是Spring应用系统中开发切面的基础。与DI一样,AOP可以帮助应用对象解耦。借助于AOP,可以将遍布系统的关注点(例如事务和安全)从它们所应用的对象中解耦出来。

我们将在第4章深入探讨Spring对AOP支持。

数据访问与集成

使用JDBC编写代码通常会导致大量的样板式代码,例如获得数据库连接、创建语句、处理结果集到最后关闭数据库连接。Spring的JDBC和DAO(Data Access Object)模块抽象了这些样板式代码,使我们的数据库代码变得简单明了,还可以避免因为关闭数据库资源失败而引发的问题。该模块在多种数据库服务的错误信息之上构建了一个语义丰富的异常层,以后我们再也不需要解释那些隐晦专有的SQL错误信息了!

对于那些更喜欢ORM(Object-Relational Mapping)工具而不愿意直接使用JDBC的开发者,Spring提供了ORM模块。Spring的ORM模块建立在对DAO的支持之上,并为多个ORM框架提供了一种构建DAO的简便方式。Spring没有尝试去创建自己的ORM解决方案,而是对许多流行的ORM框架进行了集成,包括Hibernate、Java Persisternce API、Java Data Object和iBATIS SQL Maps。Spring的事务管理支持所有的ORM框架以及JDBC。

在第10章讨论Spring数据访问时,你会看到Spring基于模板的JDBC抽象层能够极大地简化JDBC代码。

本模块同样包含了在JMS(Java Message Service)之上构建的Spring抽象层,它会使用消息以异步的方式与其他应用集成。从Spring 3.0开始,本模块还包含对象到XML映射的特性,它最初是Spring Web Service项目的一部分。

除此之外,本模块会使用Spring AOP模块为Spring应用中的对象提供事务管理服务。

Web与远程调用

MVC(Model-View-Controller)模式是一种普遍被接受的构建Web应用的方法,它可以帮助用户将界面逻辑与应用逻辑分离。Java从来不缺少MVC框架,Apache的Struts、JSF、WebWork和Tapestry都是可选的最流行的MVC框架。

虽然Spring能够与多种流行的MVC框架进行集成,但它的Web和远程调用模块自带了一个强大的MVC框架,有助于在Web层提升应用的松耦合水平。在第5章到第7章中,我们将会学习Spring的MVC框架。

除了面向用户的Web应用,该模块还提供了多种构建与其他应用交互的远程调用方案。Spring远程调用功能集成了RMI(Remote Method Invocation)、Hessian、Burlap、JAX-WS,同时Spring还自带了一个远程调用框架:HTTP invoker。Spring还提供了暴露和使用REST API的良好支持。

我们将会在第15章讨论Spring的远程调用功能。在第16章学习如何创建和使用REST API。

Instrumentation

Spring的Instrumentation模块提供了为JVM添加代理(agent)的功能。具体来讲,它为Tomcat提供了一个织入代理,能够为Tomcat传递类文件,就像这些文件是被类加载器加载的一样。

如果这听起来有点难以理解,不必对此过于担心。这个模块所提供的Instrumentation使用场景非常有限,在本书中,我们不会介绍该模块。

测试

鉴于开发者自测的重要性,Spring提供了测试模块以致力于Spring应用的测试。

通过该模块,你会发现Spring为使用JNDI、Servlet和Portlet编写单元测试提供了一系列的mock对象实现。对于集成测试,该模块为加载Spring应用上下文中的bean集合以及与Spring上下文中的bean进行交互提供了支持。

在本书中,有很多的样例都是测试驱动的,将会使用到Spring所提供的测试功能。

当谈论Spring时,其实它远远超出我们的想象。事实上,Spring远不是Spring框架所下载的那些。如果仅仅停留在核心的Spring框架层面,我们将错过Spring Portfolio所提供的巨额财富。整个Spring Portfolio包括多个构建于核心Spring框架之上的框架和类库。概括地讲,整个Spring Portfolio几乎为每一个领域的Java开发都提供了Spring编程模型。

或许需要几卷书才能覆盖Spring Portfolio所提供的所有内容,这也远远超出了本书的范围。不过,我们会介绍Spring Portfolio中的一些项目,同样,我们将体验一下核心框架之外的另一番风景。

Spring Web Flow

Spring Web Flow建立于Spring MVC框架之上,它为基于流程的会话式Web应用(可以想一下购物车或者向导功能)提供了支持。我们将在第8章讨论更多关于Spring Web Flow的内容,你还可以访问Spring Web Flow的主页(http://projects.spring.io/spring-webflow/)。

Spring Web Service

虽然核心的Spring框架提供了将Spring bean以声明的方式发布为Web Service的功能,但是这些服务是基于一个具有争议性的架构(拙劣的契约后置模型)之上而构建的。这些服务的契约由bean的接口来决定。 Spring Web Service提供了契约优先的Web Service模型,服务的实现都是为了满足服务的契约而编写的。

本书不会再探讨Spring Web Service,但是你可以浏览站点http://docs.spring.io/spring- ws/site/来了解更多关于Spring Web Service的信息。

Spring Security

安全对于许多应用都是一个非常关键的切面。利用Spring AOP,Spring Security为Spring应用提供了声明式的安全机制。你将会在第9章看到如何为应用的Web层添加Spring Security功能。同时,我们还会在第14章重新回到Spring Security的话题,学习如何保护方法调用。你可以在主页http://projects.spring.io/spring-security/上获得关于Spring Security的更多信息。

Spring Integration

许多企业级应用都需要与其他应用进行交互。Spring Integration提供了多种通用应用集成模式的Spring声明式风格实现。

我们不会在本书覆盖Spring Integration的内容,但是如果你想了解更多关于Spring Integration的信息,我推荐Mark Fisher、Jonas Partner、Marius Bogoevici和Iwein Fuld编写的《Spring Integration in Action》(Manning,2012,www.manning.com/fisher/);或者你还可以访问Spring Integration的主页http://projects.spring.io/spring-integration/

Spring Batch

当我们需要对数据进行大量操作时,没有任何技术可以比批处理更胜任这种场景。如果需要开发一个批处理应用,你可以通过Spring Batch,使用Spring强大的面向POJO的编程模型。

Spring Batch超出了本书的范畴,但是你可以阅读Arnaud Cogoluegnes、Thierry Templier、Gary Gregory和Olivier Bazoud编写的《Spring Batch in Action》(Manning,2012,www.manning.com/templier/),或者访问Spring Batch的主页http://projects.spring.io/ spring-batch/

Spring Data

Spring Data使得在Spring中使用任何数据库都变得非常容易。尽管关系型数据库统治企业级应用多年,但是现代化的应用正在认识到并不是所有的数据都适合放在一张表中的行和列中。一种新的数据库种类,通常被称之为NoSQL数据库[2],提供了使用数据的新方法,这些方法会比传统的关系型数据库更为合适。

不管你使用文档数据库,如MongoDB,图数据库,如Neo4j,还是传统的关系型数据库,Spring Data都为持久化提供了一种简单的编程模型。这包括为多种数据库类型提供了一种自动化的Repository机制,它负责为你创建Repository的实现。

我们将会在第11章看到如何使用Spring Data简化Java Persistence API(JPA)开发,然后在第12章,将相关的讨论拓展至几种NoSQL数据库。

Spring Social

社交网络是互联网领域中新兴的一种潮流,越来越多的应用正在融入社交网络网站,例如Facebook或者Twitter。如果对此感兴趣,你可以了解一下Spring Social,这是Spring的一个社交网络扩展模块。

不过,Spring Social并不仅仅是tweet和好友。尽管名字是这样,但Spring Social更多的是关注连接(connect),而不是社交(social)。它能够帮助你通过REST API连接Spring应用,其中有些Spring应用可能原本并没有任何社交方面的功能目标。

限于篇幅,我们在本书中不会涉及Spring Social。但是,如果你对Spring如何帮助你连接Facebook或Twitter感兴趣的话,可以查看网址https://spring.io/guides/gs/accessing- facebook/https://spring.io/guides/gs/accessing-twitter/中的入门指南。

Spring Mobile

移动应用是另一个引人瞩目的软件开发领域。智能手机和平板设备已成为许多用户首选的客户端。Spring Mobile是Spring MVC新的扩展模块,用于支持移动Web应用开发。

Spring for Android

与Spring Mobile相关的是Spring Android项目。这个新项目,旨在通过Spring框架为开发基于Android设备的本地应用提供某些简单的支持。最初,这个项目提供了Spring RestTemplate的一个可以用于Android应用之中的版本。它还能与Spring Social协作,使得原生应用可以通过REST API进行社交网络的连接。

本书中,我不会讨论Spring for Android,不过你可以通过http://projects.spring.io /spring-android/了解更多内容。

Spring Boot

Spring极大地简化了众多的编程任务,减少甚至消除了很多样板式代码,如果没有Spring的话,在日常工作中你不得不编写这样的样板代码。Spring Boot是一个崭新的令人兴奋的项目,它以Spring的视角,致力于简化Spring本身。

Spring Boot大量依赖于自动配置技术,它能够消除大部分(在很多场景中,甚至是全部)Spring配置。它还提供了多个Starter项目,不管你使用Maven还是Gradle,这都能减少Spring工程构建文件的大小。

在本书即将结束的第21章,我们将会学习Spring Boot。

当本书的第3版交付印刷的时候,当时Spring的最新版本是3.0.5。那大约是在3年前,从那时到现在发生了很多的变化。Spring框架经历了3个重要的发布版本——3.1、3.2以及现在的4.0——每个版本都带来了新的特性和增强,以简化应用程序的研发。Spring Portfolio中的一些成员项目也经历了重要的变更。

本书也进行了更新,试图涵盖这些发布版本中众多最令人兴奋和有用的特性。但现在,我们先简要地了解一下Spring带来了哪些新功能。

Spring 3.1带来了多项有用的新特性和增强,其中有很多都是关于如何简化和改善配置的。除此之外,Spring 3.1还提供了声明式缓存的支持以及众多针对Spring MVC的功能增强。下面的列表展现了Spring 3.1重要的功能升级:

Spring 3.1还包含了多项针对Spring MVC的功能增强:

除了Spring 3.1所提供的新功能以外,同等重要的是要注意Spring 3.1不再支持的功能。具体来讲,为了支持原生的EntityManager,Spring的JpaTemplateJpaDaoSupport类被废弃掉了。尽管它们已经被废弃了,但直到Spring 3.2版本,它依然是可以使用的。但最好不要再使用它们了,因为它们不会进行更新以支持JPA 2.0,并且已经在Spring 4中移除掉了。

现在,让我们看一下Spring 3.2提供了什么新功能。

Spring 3.1在很大程度上聚焦于配置改善以及其他的一些增强,包括Spring MVC的增强,而Spring 3.2是主要关注Spring MVC的一个发布版本。Spring MVC 3.2带来了如下的功能提升:

虽然Spring MVC是Spring 3.2改善的核心内容,但是它依然还增加了多项非MVC的功能改善。下面列出了Spring 3.2中几项最为有意思的新特性:

在本书的多个章节中,都能看到Spring 3.2的特性,尤其是在Web和REST相关的章节中。

当编写本书时,Spring 4.0是最新的发布版本。在Spring 4.0中包含了很多令人兴奋的新特性,包括:

可以看到,在Spring框架的最新发布版本中,包含了很多令人兴奋的新特性。在本书中,我们将会看到很多这样的新特性,同时也会学习Spring中长期以来一直存在的特性。

现在,你应该对Spring的功能特性有了一个清晰的认识。Spring致力于简化企业级Java开发,促进代码的松散耦合。成功的关键在于依赖注入和AOP。

在本章,我们先体验了Spring的DI。DI是组装应用对象的一种方式,借助这种方式对象无需知道依赖来自何处或者依赖的实现方式。不同于自己获取依赖对象,对象会在运行期赋予它们所依赖的对象。依赖对象通常会通过接口了解所注入的对象,这样的话就能确保低耦合。

除了DI,我们还简单介绍了Spring对AOP的支持。AOP可以帮助应用将散落在各处的逻辑汇集于一处——切面。当Spring装配bean的时候,这些切面能够在运行期编织起来,这样就能非常有效地赋予bean新的行为。

依赖注入和AOP是Spring框架最核心的部分,因此只有理解了如何应用Spring最关键的功能,你才有能力使用Spring框架的其他功能。在本章,我们只是触及了Spring DI和AOP特性的皮毛。在以后的几章,我们将深入探讨DI和AOP。

闲言少叙,我们立即转到第2章学习如何在Spring中使用DI装配对象。

[1]对于基于Java的配置,Spring提供了AnnotationConfigApplicationContext。

[2]相对于NoSQL,我更喜欢非关系型(non-relational)或无模式(schema-less)这样的术语。将这些数据库称之为NoSQL,实际上将问题归因于查询语言,而不是数据模型。


本章内容:

在看电影的时候,你曾经在电影结束后留在位置上继续观看片尾字幕吗?一部电影需要由这么多人齐心协力才能制作出来,这真是有点令人难以置信!除了主要的参与人员——演员、编剧、导演和制片人,还有那些幕后人员——音乐师、特效制作人员和艺术指导,更不用说道具师、录音师、服装师、化妆师、特技演员、广告师、第一助理摄影师、第二助理摄影师、布景师、灯光师和伙食管理员(或许是最重要的人员)了。

现在想象一下,如果这些人彼此之间没有任何交流,你最喜爱的电影会变成什么样子?让我这么说吧,他们都出现在摄影棚中,开始各做各的事情,彼此之间互不合作。如果导演保持沉默不喊“开机”,摄影师就不会开始拍摄。或许这并没什么大不了的,因为女主角还呆在她的保姆车里,而且因为没有雇佣灯光师,一切处于黑暗之中。或许你曾经看过类似这样的电影。但是大多数电影(总之,都还是很优秀的)都是由成千上万的人一起协作来完成的,他们有着共同的目标:制作一部广受欢迎的佳作。

在这方面,一个优秀的软件与之相比并没有太大区别。任何一个成功的应用都是由多个为了实现某一个业务目标而相互协作的组件构成的。这些组件必须彼此了解,并且相互协作来完成工作。例如,在一个在线购物系统中,订单管理组件需要和产品管理组件以及信用卡认证组件协作。这些组件或许还需要与数据访问组件协作,从数据库读取数据以及把数据写入数据库。

但是,正如我们在第1章中所看到的,创建应用对象之间关联关系的传统方法(通过构造器或者查找)通常会导致结构复杂的代码,这些代码很难被复用也很难进行单元测试。如果情况不严重的话,这些对象所做的事情只是超出了它应该做的范围;而最坏的情况则是,这些对象彼此之间高度耦合,难以复用和测试。

在Spring中,对象无需自己查找或创建与其所关联的其他对象。相反,容器负责把需要相互协作的对象引用赋予各个对象。例如,一个订单管理组件需要信用卡认证组件,但它不需要自己创建信用卡认证组件。订单管理组件只需要表明自己两手空空,容器就会主动赋予它一个信用卡认证组件。

创建应用对象之间协作关系的行为通常称为装配(wiring),这也是依赖注入(DI)的本质。在本章我们将介绍使用Spring装配 bean的基础知识。因为DI是Spring的最基本要素,所以在开发基于Spring的应用时,你随时都在使用这些技术。

在Spring中装配bean有多种方式。作为本章的开始,我们先花一点时间来介绍一下配置Spring容器最常见的三种方法。

如第1章中所述,Spring容器负责创建应用程序中的bean并通过DI来协调这些对象之间的关系。但是,作为开发人员,你需要告诉Spring要创建哪些bean并且如何将其装配在一起。当描述bean如何进行装配时,Spring具有非常大的灵活性,它提供了三种主要的装配机制:

乍看上去,提供三种可选的配置方案会使Spring变得复杂。每种配置技术所提供的功能会有一些重叠,所以在特定的场景中,确定哪种技术最为合适就会变得有些困难。但是,不必紧张——在很多场景下,选择哪种方案很大程度上就是个人喜好的问题,你尽可以选择自己最喜欢的方式。

Spring有多种可选方案来配置bean,这是非常棒的,但有时候你必须要在其中做出选择。

这方面,并没有唯一的正确答案。你所做出的选择必须要适合你和你的项目。而且,谁说我们只能选择其中的一种方案呢?Spring的配置风格是可以互相搭配的,所以你可以选择使用XML装配一些bean,使用Spring基于Java的配置(JavaConfig)来装配另一些bean,而将剩余的bean让Spring去自动发现。

即便如此,我的建议是尽可能地使用自动配置的机制。显式配置越少越好。当你必须要显式配置bean的时候(比如,有些源码不是由你来维护的,而当你需要为这些代码配置bean的时候),我推荐使用类型安全并且比XML更加强大的JavaConfig。最后,只有当你想要使用便利的XML命名空间,并且在JavaConfig中没有同样的实现时,才应该使用XML。

在本章中,我们会详细介绍这三种技术并且在整本书中都会用到它们。现在,我们会尝试一下每种方法,对它们是什么样子的有一个直观的印象。作为Spring配置的开始,我们先看一下Spring的自动化配置。

在本章稍后的内容中,你会看到如何借助Java和XML来进行Spring装配。尽管你会发现这些显式装配技术非常有用,但是在便利性方面,最强大的还是Spring的自动化配置。如果Spring能够进行自动化装配的话,那何苦还要显式地将这些bean装配在一起呢?

Spring从两个角度来实现自动化装配:

组件扫描和自动装配组合在一起就能发挥出强大的威力,它们能够将你的显式配置降低到最少。

为了阐述组件扫描和装配,我们需要创建几个bean,它们代表了一个音响系统中的组件。首先,要创建CompactDisc类,Spring会发现它并将其创建为一个bean。然后,会创建一个CDPlayer类,让Spring发现它,并将CompactDiscbean注入进来。

在这个MP3和流式媒体音乐的时代,CD(compact disc)显得有点典雅甚至陈旧。它不像卡带机、八轨磁带、塑胶唱片那么普遍,随着以物理载体进行音乐交付的方式越来越少,CD也变得越来越稀少了。

尽管如此,CD为我们阐述DI如何运行提供了一个很好的样例。如果你不将CD插入(注入)到CD播放器中,那么CD播放器其实是没有太大用处的。所以,可以这样说,CD播放器依赖于CD才能完成它的使命。

为了在Spring中阐述这个例子,让我们首先在Java中建立CD的概念。程序清单2.1展现了CompactDisc,它是定义CD的一个接口:

程序清单2.1 CompactDisc接口在Java中定义了CD的概念

package soundsystem;

public interface CompactDisc {
  void play();
}

CompactDisc的具体内容并不重要,重要的是你将其定义为一个接口。作为接口,它定义了CD播放器对一盘CD所能进行的操作。它将CD播放器的任意实现与CD本身的耦合降低到了最小的程度。

我们还需要一个CompactDisc的实现,实际上,我们可以有CompactDisc接口的多个实现。在本例中,我们首先会创建其中的一个实现,也就是程序清单2.2所示的SgtPeppers类。

程序清单2.2 带有@Component注解的CompactDisc实现类SgtPeppers

package soundsystem;
import org.springframework.stereotype.Component;

@Component
public class SgtPeppers implements CompactDisc {

  private String title = "Sgt. Pepper's Lonely Hearts Club Band";
  private String artist = "The Beatles";

  public void play() {
    System.out.println("Playing " + title + " by " + artist);
  }

}

CompactDisc接口一样,SgtPeppers的具体内容并不重要。你需要注意的就是SgtPeppers类上使用了@Component注解。这个简单的注解表明该类会作为组件类,并告知Spring要为这个类创建bean。没有必要显式配置SgtPeppersbean,因为这个类使用了@Component注解,所以Spring会为你把事情处理妥当。

不过,组件扫描默认是不启用的。我们还需要显式配置一下Spring,从而命令它去寻找带有@Component注解的类,并为其创建bean。程序清单2.3的配置类展现了完成这项任务的最简洁配置。

程序清单2.3 @ComponentScan注解启用了组件扫描

package soundsystem;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

@Configuration
@ComponentScan
public class CDPlayerConfig {
}

CDPlayerConfig通过Java代码定义了Spring的装配规则。在2.3节中,我们还会更为详细地介绍基于Java的Spring配置。不过,现在我们只需观察一下CDPlayerConfig类并没有显式地声明任何bean,只不过它使用了@ComponentScan注解,这个注解能够在Spring中启用组件扫描。

如果没有其他配置的话,@ComponentScan默认会扫描与配置类相同的包。因为CDPlayerConfig类位于soundsystem包中,因此Spring将会扫描这个包以及这个包下的所有子包,查找带有@Component注解的类。这样的话,就能发现CompactDisc,并且会在Spring中自动为其创建一个bean。

如果你更倾向于使用XML来启用组件扫描的话,那么可以使用Spring context命名空间的<context:component-scan>元素。程序清单2.4展示了启用组件扫描的最简洁XML配置。

程序清单2.4 通过XML启用组件扫描

<?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:context="http://www.springframework.org/schema/context"
  xsi:schemaLocation="http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans.xsd
    http://www.springframework.org/schema/context
    http://www.springframework.org/schema/context/spring-context.xsd">

  <context:component-scan base-package="soundsystem" />

</beans>

尽管我们可以通过XML的方案来启用组件扫描,但是在后面的讨论中,我更多的还是会使用基于Java的配置。如果你更喜欢XML的话,<context:component-scan>元素会有与@ComponentScan注解相对应的属性和子元素。

可能有点让人难以置信,我们只创建了两个类,就能对功能进行一番尝试了。为了测试组件扫描的功能,我们创建一个简单的JUnit测试,它会创建Spring上下文,并判断CompactDisc是不是真的创建出来了。程序清单2.5中的CDPlayerTest就是用来完成这项任务的。

程序清单2.5 测试组件扫描能够发现CompactDisc

package soundsystem;

import static org.junit.Assert.*;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes=CDPlayerConfig.class)
public class CDPlayerTest {

  @Autowired
  private CompactDisc cd;

  @Test
  public void cdShouldNotBeNull() {
    assertNotNull(cd);
  }

}

CDPlayerTest使用了Spring的SpringJUnit4ClassRunner,以便在测试开始的时候自动创建Spring的应用上下文。注解@ContextConfiguration会告诉它需要在CDPlayerConfig中加载配置。因为CDPlayerConfig类中包含了@ComponentScan,因此最终的应用上下文中应该包含CompactDiscbean。

为了证明这一点,在测试代码中有一个CompactDisc类型的属性,并且这个属性带有@Autowired注解,以便于将CompactDiscbean注入到测试代码之中(稍后,我会讨论@Autowired)。最后,会有一个简单的测试方法断言cd属性不为null。如果它不为null的话,就意味着Spring能够发现CompactDisc类,自动在Spring上下文中将其创建为bean并将其注入到测试代码之中。

这个代码应该能够通过测试,并以测试成功的颜色显示(在你的测试运行器中,或许会希望出现绿色)。你第一个简单的组件扫描练习就成功了!尽管我们只用它创建了一个bean,但同样是这么少的配置能够用来发现和创建任意数量的bean。在soundsystem包及其子包中,所有带有@Component注解的类都会创建为bean。只添加一行@ComponentScan注解就能自动创建无数个bean,这种权衡还是很划算的。

现在,我们会更加深入地探讨@ComponentScan@Component,看一下使用组件扫描还能做些什么。

Spring应用上下文中所有的bean都会给定一个ID。在前面的例子中,尽管我们没有明确地为SgtPeppersbean设置ID,但Spring会根据类名为其指定一个ID。具体来讲,这个bean所给定的ID为sgtPeppers,也就是将类名的第一个字母变为小写。

如果想为这个bean设置不同的ID,你所要做的就是将期望的ID作为值传递给@Component注解。比如说,如果想将这个bean标识为lonelyHeartsClub,那么你需要将SgtPeppers类的@Component注解配置为如下所示:

@Component("lonelyHeartsClub")
public class SgtPeppers implements CompactDisc {
  ...
}

还有另外一种为bean命名的方式,这种方式不使用@Component注解,而是使用Java依赖注入规范(Java Dependency Injection)中所提供的@Named注解来为bean设置ID:

package soundsystem;
import javax.inject.Named;

@Named("lonelyHeartsClub")
public class SgtPeppers implements CompactDisc {
  ...
}

Spring支持将@Named作为@Component注解的替代方案。两者之间有一些细微的差异,但是在大多数场景中,它们是可以互相替换的。

话虽如此,我更加强烈地喜欢@Component注解,而对于@Named……怎么说呢,我感觉它的名字起得很不好。它并没有像@Component那样清楚地表明它是做什么的。因此在本书及其示例代码中,我不会再使用@Named。

到现在为止,我们没有为@ComponentScan设置任何属性。这意味着,按照默认规则,它会以配置类所在的包作为基础包(base package)来扫描组件。但是,如果你想扫描不同的包,那该怎么办呢?或者,如果你想扫描多个基础包,那又该怎么办呢?

有一个原因会促使我们明确地设置基础包,那就是我们想要将配置类放在单独的包中,使其与其他的应用代码区分开来。如果是这样的话,那默认的基础包就不能满足要求了。

要满足这样的需求其实也完全没有问题!为了指定不同的基础包,你所需要做的就是在@ComponentScan的value属性中指明包的名称:

@Configuration
@ComponentScan("soundsystem")
public class CDPlayerConfig {}

如果你想更加清晰地表明你所设置的是基础包,那么你可以通过basePackages属性进行配置:

@Configuration
@ComponentScan(basePackages="soundsystem")
public class CDPlayerConfig {}

可能你已经注意到了basePackages属性使用的是复数形式。如果你揣测这是不是意味着可以设置多个基础包,那么恭喜你猜对了。如果想要这么做的话,只需要将basePackages属性设置为要扫描包的一个数组即可:

@Configuration
@ComponentScan(basePackages={"soundsystem", "video"})
public class CDPlayerConfig {}

在上面的例子中,所设置的基础包是以String类型表示的。我认为这是可以的,但这种方法是类型不安全(not type-safe)的。如果你重构代码的话,那么所指定的基础包可能就会出现错误了。

除了将包设置为简单的String类型之外,@ComponentScan还提供了另外一种方法,那就是将其指定为包中所包含的类或接口:

@Configuration
@ComponentScan(basePackageClasses={CDPlayer.class, DVDPlayer.class})
public class CDPlayerConfig {}

可以看到,basePackages属性被替换成了basePackageClasses。同时,我们不是再使用String类型的名称来指定包,为basePackageClasses属性所设置的数组中包含了类。这些类所在的包将会作为组件扫描的基础包。

尽管在样例中,我为basePackageClasses设置的是组件类,但是你可以考虑在包中创建一个用来进行扫描的空标记接口(marker interface)。通过标记接口的方式,你依然能够保持对重构友好的接口引用,但是可以避免引用任何实际的应用程序代码(在稍后重构中,这些应用代码有可能会从想要扫描的包中移除掉)。

在你的应用程序中,如果所有的对象都是独立的,彼此之间没有任何依赖,就像SgtPeppersbean这样,那么你所需要的可能就是组件扫描而已。但是,很多对象会依赖其他的对象才能完成任务。这样的话,我们就需要有一种方法能够将组件扫描得到的bean和它们的依赖装配在一起。要完成这项任务,我们需要了解一下Spring自动化配置的另外一方面内容,那就是自动装配。

简单来说,自动装配就是让Spring自动满足bean依赖的一种方法,在满足依赖的过程中,会在Spring应用上下文中寻找匹配某个bean需求的其他bean。为了声明要进行自动装配,我们可以借助Spring的@Autowired注解。

比方说,考虑程序清单2.6中的CDPlayer类。它的构造器上添加了@Autowired注解,这表明当Spring创建CDPlayerbean的时候,会通过这个构造器来进行实例化并且会传入一个可设置给CompactDisc类型的bean。

程序清单2.6 通过自动装配,将一个CompactDisc注入到CDPlayer之中

package soundsystem;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class CDPlayer implements MediaPlayer {
  private CompactDisc cd;

  @Autowired
  public CDPlayer(CompactDisc cd) {
    this.cd = cd;
  }

  public void play() {
    cd.play();
  }

}

@Autowired注解不仅能够用在构造器上,还能用在属性的Setter方法上。比如说,如果CDPlayer有一个setCompactDisc()方法,那么可以采用如下的注解形式进行自动装配:

@Autowired
public void setCompactDisc(CompactDisc cd) {
  this.cd = cd;
}

在Spring初始化bean之后,它会尽可能得去满足bean的依赖,在本例中,依赖是通过带有@Autowired注解的方法进行声明的,也就是setCompactDisc()

实际上,Setter方法并没有什么特殊之处。@Autowired注解可以用在类的任何方法上。假设CDPlayer类有一个insertDisc()方法,那么@Autowired能够像在setCompactDisc()上那样,发挥完全相同的作用:

@Autowired
public void insertDisc(CompactDisc cd) {
  this.cd = cd;
}

不管是构造器、Setter方法还是其他的方法,Spring都会尝试满足方法参数上所声明的依赖。假如有且只有一个bean匹配依赖需求的话,那么这个bean将会被装配进来。

如果没有匹配的bean,那么在应用上下文创建的时候,Spring会抛出一个异常。为了避免异常的出现,你可以将@Autowiredrequired属性设置为false

@Autowired(required=false)
public CDPlayer(CompactDisc cd) {
  this.cd = cd;
}

required属性设置为false时,Spring会尝试执行自动装配,但是如果没有匹配的bean的话,Spring将会让这个bean处于未装配的状态。但是,把required属性设置为false时,你需要谨慎对待。如果在你的代码中没有进行null检查的话,这个处于未装配状态的属性有可能会出现NullPointerException

如果有多个bean都能满足依赖关系的话,Spring将会抛出一个异常,表明没有明确指定要选择哪个bean进行自动装配。在第3章中,我们会进一步讨论自动装配中的歧义性。

@Autowired是Spring特有的注解。如果你不愿意在代码中到处使用Spring的特定注解来完成自动装配任务的话,那么你可以考虑将其替换为@Inject

package soundsystem;
import javax.inject.Inject;
import javax.inject.Named;

@Named
public class CDPlayer {
  ...

  @Inject
  public CDPlayer(CompactDisc cd) {
    this.cd = cd;
  }

  ...
}

@Inject注解来源于Java依赖注入规范,该规范同时还为我们定义了@Named注解。在自动装配中,Spring同时支持@Inject@Autowired。尽管@Inject@Autowired之间有着一些细微的差别,但是在大多数场景下,它们都是可以互相替换的。

@Inject@Autowired中,我没有特别强烈的偏向性。实际上,在有的项目中,我会发现我同时使用了这两个注解。不过在本书的样例中,我会一直使用@Autowired,而你可以根据自己的情况,选择其中的任意一个。

现在,我们已经在CDPlayer的构造器中添加了@Autowired注解,Spring将把一个可分配给CompactDisc类型的bean自动注入进来。为了验证这一点,让我们修改一下CDPlayerTest,使其能够借助CDPlayer bean播放CD:

package soundsystem;
import static org.junit.Assert.*;
import org.junit.Rule;
import org.junit.Test;
import org.junit.contrib.java.lang.system.StandardOutputStreamLog;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes=CDPlayerConfig.class)
public class CDPlayerTest {

  @Rule
  public final StandardOutputStreamLog log =
                          new StandardOutputStreamLog();

  @Autowired
  private MediaPlayer player;

  @Autowired
  private CompactDisc cd;

  @Test
  public void cdShouldNotBeNull() {
    assertNotNull(cd);
  }

  @Test
  public void play() {
    player.play();
    assertEquals(
        "Playing Sgt. Pepper's Lonely Hearts Club Band" +
        " by The Beatles\n",
        log.getLog());
  }

}

现在,除了注入CompactDisc,我们还将CDPlayerbean注入到测试代码的player成员变量之中(它是更为通用的MediaPlayer类型)。在play()测试方法中,我们可以调用CDPlayer的play()方法,并断言它的行为与你的预期一致。

在测试代码中使用System.out.println()是稍微有点棘手的事情。因此,该样例中使用了StandardOutputStreamLog,这是来源于System Rules库(http://stefanbirkner.github.io/system-rules/index.html)的一个JUnit规则,该规则能够基于控制台的输出编写断言。在这里,我们断言SgtPeppers.play()方法的输出被发送到了控制台上。

现在,你已经了解了组件扫描和自动装配的基础知识,在第3章中,当我们介绍如何处理自动装配的歧义性时,还会继续研究组件扫描。

但是现在,我们先将组件扫描和自动装配放在一边,看一下在Spring中如何显式地装配bean,首先从通过Java代码编写配置开始。

尽管在很多场景下通过组件扫描和自动装配实现Spring的自动化配置是更为推荐的方式,但有时候自动化配置的方案行不通,因此需要明确配置Spring。比如说,你想要将第三方库中的组件装配到你的应用中,在这种情况下,是没有办法在它的类上添加@Component@Autowired注解的,因此就不能使用自动化装配的方案了。

在这种情况下,你必须要采用显式装配的方式。在进行显式配置的时候,有两种可选方案:Java和XML。在这节中,我们将会学习如何使用Java配置,接下来的一节中将会继续学习Spring的XML配置。

就像我之前所说的,在进行显式配置时,JavaConfig是更好的方案,因为它更为强大、类型安全并且对重构友好。因为它就是Java代码,就像应用程序中的其他Java代码一样。

同时,JavaConfig与其他的Java代码又有所区别,在概念上,它与应用程序中的业务逻辑和领域代码是不同的。尽管它与其他的组件一样都使用相同的语言进行表述,但JavaConfig是配置代码。这意味着它不应该包含任何业务逻辑,JavaConfig也不应该侵入到业务逻辑代码之中。尽管不是必须的,但通常会将JavaConfig放到单独的包中,使它与其他的应用程序逻辑分离开来,这样对于它的意图就不会产生困惑了。

接下来,让我们看一下如何通过JavaConfig显式配置Spring。

在本章前面的程序清单2.3中,我们第一次见识到JavaConfig。让我们重温一下那个样例中的CDPlayerConfig

package soundsystem;
import org.springframework.context.annotation.Configuration;

@Configuration
public class CDPlayerConfig {
}

创建JavaConfig类的关键在于为其添加@Configuration注解,@Configuration注解表明这个类是一个配置类,该类应该包含在Spring应用上下文中如何创建bean的细节。

到此为止,我们都是依赖组件扫描来发现Spring应该创建的bean。尽管我们可以同时使用组件扫描和显式配置,但是在本节中,我们更加关注于显式配置,因此我将CDPlayerConfig@ComponentScan注解移除掉了。

移除了@ComponentScan注解,此时的CDPlayerConfig类就没有任何作用了。如果你现在运行CDPlayerTest的话,测试会失败,并且会出现BeanCreation- Exception异常。测试期望被注入CDPlayerCompactDisc,但是这些bean根本就没有创建,因为组件扫描不会发现它们。

为了再次让测试通过,你可以将@ComponentScan注解添加回去,但是我们这一节关注显式配置,因此让我们看一下如何使用JavaConfig装配CDPlayerCompactDisc

要在JavaConfig中声明bean,我们需要编写一个方法,这个方法会创建所需类型的实例,然后给这个方法添加@Bean注解。比方说,下面的代码声明了CompactDisc bean

@Bean
public CompactDisc sgtPeppers() {
  return new SgtPeppers();
}

@Bean注解会告诉Spring这个方法将会返回一个对象,该对象要注册为Spring应用上下文中的bean。方法体中包含了最终产生bean实例的逻辑。

默认情况下,bean的ID与带有@Bean注解的方法名是一样的。在本例中,bean的名字将会是sgtPeppers。如果你想为其设置成一个不同的名字的话,那么可以重命名该方法,也可以通过name属性指定一个不同的名字:

@Bean(name="lonelyHeartsClubBand")
public CompactDisc sgtPeppers() {
  return new SgtPeppers();
}

不管你采用什么方法来为bean命名,bean声明都是非常简单的。方法体返回了一个新的SgtPeppers实例。这里是使用Java来进行描述的,因此我们可以发挥Java提供的所有功能,只要最终生成一个CompactDisc实例即可。

请稍微发挥一下你的想象力,我们可能希望做一点稍微疯狂的事情,比如说,在一组CD中随机选择一个CompactDisc来播放:

@Bean
public CompactDisc randomBeatlesCD() {
  int choice = (int) Math.floor(Math.random() * 4);
  if (choice == 0) {
    return new SgtPeppers();
  } else if (choice == 1) {
    return new WhiteAlbum();
  } else if (choice == 2) {
    return new HardDaysNight();
  } else {
    return new Revolver();
  }
}

现在,你可以自己想象一下,借助@Bean注解方法的形式,我们该如何发挥出Java的全部威力来产生bean。当你想完之后,我们要回过头来看一下在JavaConfig中,如何将CompactDisc注入到CDPlayer之中。

我们前面所声明的CompactDisc bean是非常简单的,它自身没有其他的依赖。但现在,我们需要声明CDPlayerbean,它依赖于CompactDisc。在JavaConfig中,要如何将它们装配在一起呢?

在JavaConfig中装配bean的最简单方式就是引用创建bean的方法。例如,下面就是一种声明CDPlayer的可行方案:

@Bean
public CDPlayer cdPlayer() {
  return new CDPlayer(sgtPeppers());
}

cdPlayer()方法像sgtPeppers()方法一样,同样使用了@Bean注解,这表明这个方法会创建一个bean实例并将其注册到Spring应用上下文中。所创建的bean ID为cdPlayer,与方法的名字相同。

cdPlayer()的方法体与sgtPeppers()稍微有些区别。在这里并没有使用默认的构造器构建实例,而是调用了需要传入CompactDisc对象的构造器来创建CDPlayer实例。

看起来,CompactDisc是通过调用sgtPeppers()得到的,但情况并非完全如此。因为sgtPeppers()方法上添加了@Bean注解,Spring将会拦截所有对它的调用,并确保直接返回该方法所创建的bean,而不是每次都对其进行实际的调用。

比如说,假设你引入了一个其他的CDPlayerbean,它和之前的那个bean完全一样:

@Bean
public CDPlayer cdPlayer() {
  return new CDPlayer(sgtPeppers());
}

@Bean
public CDPlayer anotherCDPlayer() {
  return new CDPlayer(sgtPeppers());
}

假如对sgtPeppers()的调用就像其他的Java方法调用一样的话,那么每个CDPlayer实例都会有一个自己特有的SgtPeppers实例。如果我们讨论的是实际的CD播放器和CD光盘的话,这么做是有意义的。如果你有两台CD播放器,在物理上并没有办法将同一张CD光盘放到两个CD播放器中。

但是,在软件领域中,我们完全可以将同一个SgtPeppers实例注入到任意数量的其他bean之中。默认情况下,Spring中的bean都是单例的,我们并没有必要为第二个CDPlayer bean创建完全相同的SgtPeppers实例。所以,Spring会拦截对sgtPeppers()的调用并确保返回的是Spring所创建的bean,也就是Spring本身在调用sgtPeppers()时所创建的CompactDiscbean。因此,两个CDPlayer bean会得到相同的SgtPeppers实例。

可以看到,通过调用方法来引用bean的方式有点令人困惑。其实还有一种理解起来更为简单的方式:

@Bean
public CDPlayer cdPlayer(CompactDisc compactDisc) {
  return new CDPlayer(compactDisc);
}

在这里,cdPlayer()方法请求一个CompactDisc作为参数。当Spring调用cdPlayer()创建CDPlayerbean的时候,它会自动装配一个CompactDisc到配置方法之中。然后,方法体就可以按照合适的方式来使用它。借助这种技术,cdPlayer()方法也能够将CompactDisc注入到CDPlayer的构造器中,而且不用明确引用CompactDisc@Bean方法。

通过这种方式引用其他的bean通常是最佳的选择,因为它不会要求将CompactDisc声明到同一个配置类之中。在这里甚至没有要求CompactDisc必须要在JavaConfig中声明,实际上它可以通过组件扫描功能自动发现或者通过XML来进行配置。你可以将配置分散到多个配置类、XML文件以及自动扫描和装配bean之中,只要功能完整健全即可。不管CompactDisc是采用什么方式创建出来的,Spring都会将其传入到配置方法中,并用来创建CDPlayer bean。

另外,需要提醒的是,我们在这里使用CDPlayer的构造器实现了DI功能,但是我们完全可以采用其他风格的DI配置。比如说,如果你想通过Setter方法注入CompactDisc的话,那么代码看起来应该是这样的:

@Bean
public CDPlayer cdPlayer(CompactDisc compactDisc) {
  CDPlayer cdPlayer = new CDPlayer(compactDisc);
  cdPlayer.setCompactDisc(compactDisc);
  return cdPlayer;
}

再次强调一遍,带有@Bean注解的方法可以采用任何必要的Java功能来产生bean实例。构造器和Setter方法只是@Bean方法的两个简单样例。这里所存在的可能性仅仅受到Java语言的限制。

到此为止,我们已经看到了如何让Spring自动发现和装配bean,还看到了如何进行手动干预,即通过JavaConfig显式地装配bean。但是,在装配bean的时候,还有一种可选方案,尽管这种方案可能不太合乎大家的心意,但是它在Spring中已经有很长的历史了。

在Spring刚刚出现的时候,XML是描述配置的主要方式。在Spring的名义下,我们创建了无数行XML代码。在一定程度上,Spring成为了XML配置的同义词。

尽管Spring长期以来确实与XML有着关联,但现在需要明确的是,XML不再是配置Spring的唯一可选方案。Spring现在有了强大的自动化配置和基于Java的配置,XML不应该再是你的第一选择了。

不过,鉴于已经存在那么多基于XML的Spring配置,所以理解如何在Spring中使用XML还是很重要的。但是,我希望本节的内容只是用来帮助你维护已有的XML配置,在完成新的Spring工作时,希望你会使用自动化配置和JavaConfig。

在使用XML为Spring装配bean之前,你需要创建一个新的配置规范。在使用JavaConfig的时候,这意味着要创建一个带有@Configuration注解的类,而在XML配置中,这意味着要创建一个XML文件,并且要以<beans>元素为根。

最为简单的Spring XML配置如下所示:

<?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
    http://www.springframework.org/schema/context">

  <!-- configuration details go here -->

</beans>

很容易就能看出来,这个基本的XML配置已经比同等功能的JavaConfig类复杂得多了。作为起步,在JavaConfig中所需要的只是@Configuration,但在使用XML时,需要在配置文件的顶部声明多个XML模式(XSD)文件,这些文件定义了配置Spring的XML元素。

借助Spring Tool Suite创建XML配置文件创建和管理Spring XML配置文件的一种简便方式是使用Spring Tool Suite(https://spring.io/tools/sts)。在Spring Tool Suite的菜单中,选择File>New>Spring Bean Configuration File,能够创建Spring XML配置文件,并且可以选择可用的配置命名空间。

用来装配bean的最基本的XML元素包含在spring-beans模式之中,在上面这个XML文件中,它被定义为根命名空间。<beans>是该模式中的一个元素,它是所有Spring配置文件的根元素。

在XML中配置Spring时,还有一些其他的模式。尽管在本书中,我更加关注自动化以及基于Java的配置,但是在本书讲解的过程中,当出现其他模式的时候,我至少会提醒你。

就这样,我们已经有了一个合法的Spring XML配置。不过,它也是一个没有任何用处的配置,因为它(还)没有声明任何bean。为了给予它生命力,让我们重新创建一下CD样例,只不过我们这次使用XML配置,而不是使用JavaConfig和自动化装配。

要在基于XML的Spring配置中声明一个bean,我们要使用spring-beans模式中的另外一个元素:<bean><bean>元素类似于JavaConfig中的@Bean注解。我们可以按照如下的方式声明CompactDiscbean:

<bean class="soundsystem.SgtPeppers" />

这里声明了一个很简单的bean,创建这个bean的类通过class属性来指定的,并且要使用全限定的类名。

因为没有明确给定ID,所以这个bean将会根据全限定类名来进行命名。在本例中,bean的ID将会是“soundsystem.SgtPeppers#0”。其中,“#0”是一个计数的形式,用来区分相同类型的其他bean。如果你声明了另外一个SgtPeppers,并且没有明确进行标识,那么它自动得到的ID将会是“soundsystem.SgtPeppers#1”。

尽管自动化的bean命名方式非常方便,但如果你要稍后引用它的话,那自动产生的名字就没有多大的用处了。因此,通常来讲更好的办法是借助id属性,为每个bean设置一个你自己选择的名字:

<bean id="compactDisc" class="soundsystem.SgtPeppers" />

稍后将这个bean装配到CDPlayer bean之中的时候,你会用到这个具体的名字。

减少繁琐为了减少XML中繁琐的配置,只对那些需要按名字引用的bean(比如,你需要将对它的引用注入到另外一个bean中)进行明确地命名。

在进一步学习之前,让我们花点时间看一下这个简单bean声明的一些特征。

第一件需要注意的事情就是你不再需要直接负责创建SgtPeppers的实例,在基于JavaConfig的配置中,我们是需要这样做的。当Spring发现这个<bean>元素时,它将会调用SgtPeppers的默认构造器来创建bean。在XML配置中,bean的创建显得更加被动,不过,它并没有JavaConfig那样强大,在JavaConfig配置方式中,你可以通过任何可以想象到的方法来创建bean实例。

另外一个需要注意到的事情就是,在这个简单的<bean>声明中,我们将bean的类型以字符串的形式设置在了class属性中。谁能保证设置给class属性的值是真正的类呢?Spring的XML配置并不能从编译期的类型检查中受益。即便它所引用的是实际的类型,如果你重命名了类,会发生什么呢?

借助IDE检查XML的合法性使用能够感知Spring功能的IDE,如Spring Tool Suite,能够在很大程度上帮助你确保Spring XML配置的合法性。

以上介绍的只是JavaConfig要优于XML配置的部分原因。我建议在为你的应用选择配置风格时,要记住XML配置的这些缺点。接下来,我们继续Spring XML配置的学习进程,了解如何将SgtPeppersbean注入到CDPlayer之中。

在Spring XML配置中,只有一种声明bean的方式:使用<bean>元素并指定class属性。Spring会从这里获取必要的信息来创建bean。

但是,在XML中声明DI时,会有多种可选的配置方案和风格。具体到构造器注入,有两种基本的配置方案可供选择:

两者的区别在很大程度就是是否冗长烦琐。可以看到,<constructor-arg>元素比使用c-命名空间会更加冗长,从而导致XML更加难以读懂。另外,有些事情<constructor-arg>可以做到,但是使用c-命名空间却无法实现。

在介绍Spring XML的构造器注入时,我们将会分别介绍这两种可选方案。首先,看一下它们各自如何注入bean引用。

构造器注入bean引用

按照现在的定义,CDPlayerbean有一个接受CompactDisc类型的构造器。这样,我们就有了一个很好的场景来学习如何注入bean的引用。

现在已经声明了SgtPeppers bean,并且SgtPeppers类实现了CompactDisc接口,所以实际上我们已经有了一个可以注入到CDPlayerbean中的bean。我们所需要做的就是在XML中声明CDPlayer并通过ID引用SgtPeppers

<bean id="cdPlayer" class="soundsystem.CDPlayer">
  <constructor-arg ref="compactDisc" />
</bean>

当Spring遇到这个<bean>元素时,它会创建一个CDPlayer实例。<constructor-arg>元素会告知Spring要将一个ID为compactDisc的bean引用传递到CDPlayer的构造器中。

作为替代的方案,你也可以使用Spring的c-命名空间。c-命名空间是在Spring 3.0中引入的,它是在XML中更为简洁地描述构造器参数的方式。要使用它的话,必须要在XML的顶部声明其模式,如下所示:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
  xmlns:c="http://www.springframework.org/schema/c"
  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">

 ...

</beans>

在c-命名空间和模式声明之后,我们就可以使用它来声明构造器参数了,如下所示:

<bean id="cdPlayer" class="soundsystem.CDPlayer"
      c:cd-ref="compactDisc" />

在这里,我们使用了c-命名空间来声明构造器参数,它作为<bean>元素的一个属性,不过这个属性的名字有点诡异。图2.1描述了这个属性名是如何组合而成的。

图2.1 通过Spring的c-命名空间将bean引用注入到构造器参数中

属性名以“c:”开头,也就是命名空间的前缀。接下来就是要装配的构造器参数名,在此之后是“-ref”,这是一个命名的约定,它会告诉Spring,正在装配的是一个bean的引用,这个bean的名字是compactDisc,而不是字面量“compactDisc”。

很显然,使用c-命名空间属性要比使用<constructor-arg>元素简练得多。这是我很喜欢它的原因之一。除了更易读之外,当我在编写样例代码时,c-命名空间属性能够更加有助于使代码的长度保持在书的边框之内。

在编写前面的样例时,关于c-命名空间,有一件让我感到困扰的事情就是它直接引用了构造器参数的名称。引用参数的名称看起来有些怪异,因为这需要在编译代码的时候,将调试标志(debug symbol)保存在类代码中。如果你优化构建过程,将调试标志移除掉,那么这种方式可能就无法正常执行了。

替代的方案是我们使用参数在整个参数列表中的位置信息:

<bean id="cdPlayer" class="soundsystem.CDPlayer"
      c:_0-ref="compactDisc" />

这个c-命名空间属性看起来似乎比上一种方法更加怪异。我将参数的名称替换成了“0”,也就是参数的索引。因为在XML中不允许数字作为属性的第一个字符,因此必须要添加一个下画线作为前缀。

使用索引来识别构造器参数感觉比使用名字更好一些。即便在构建的时候移除掉了调试标志,参数却会依然保持相同的顺序。如果有多个构造器参数的话,这当然是很有用处的。在这里因为只有一个构造器参数,所以我们还有另外一个方案——根本不用去标示参数:

<bean id="cdPlayer" class="soundsystem.CDPlayer"
      c:_-ref="compactDisc" />

到目前为止,这是最为奇特的一个c-命名空间属性,这里没有参数索引或参数名。只有一个下画线,然后就是用“-ref”来表明正在装配的是一个引用。

我们已经将引用装配到了其他的bean之中,接下来看一下如何将字面量值(literal value)装配到构造器之中。

将字面量注入到构造器中

迄今为止,我们所做的DI通常指的都是类型的装配——也就是将对象的引用装配到依赖于它们的其他对象之中——而有时候,我们需要做的只是用一个字面量值来配置对象。为了阐述这一点,假设你要创建CompactDisc的一个新实现,如下所示:

package soundsystem;

public class BlankDisc implements CompactDisc {

  private String title;
  private String artist;

  public BlankDisc(String title, String artist) {
    this.title = title;
    this.artist = artist;
  }

  public void play() {
    System.out.println("Playing " + title + " by " + artist);
  }
}

SgtPeppers中,唱片名称和艺术家的名字都是硬编码的,但是这个CompactDisc实现与之不同,它更加灵活。像现实中的空磁盘一样,它可以设置成任意你想要的艺术家和唱片名。现在,我们可以将已有的SgtPeppers替换为这个类:

<bean id="compactDisc"
      class="soundsystem.BlankDisc">
  <constructor-arg value="Sgt. Pepper's Lonely Hearts Club Band" />
  <constructor-arg value="The Beatles" />
</bean>

我们再次使用<constructor-arg>元素进行构造器参数的注入。但是这一次我们没有使用“ref”属性来引用其他的bean,而是使用了value属性,通过该属性表明给定的值要以字面量的形式注入到构造器之中。

如果要使用c-命名空间的话,这个例子又该是什么样子呢?第一种方案是引用构造器参数的名字:

<bean id="compactDisc"
      class="soundsystem.BlankDisc"
      c:_title="Sgt. Pepper's Lonely Hearts Club Band"
      c:_artist="The Beatles" />

可以看到,装配字面量与装配引用的区别在于属性名中去掉了“-ref”后缀。与之类似,我们也可以通过参数索引装配相同的字面量值,如下所示:

<bean id="compactDisc"
      class="soundsystem.BlankDisc"
      c:_0="Sgt. Pepper's Lonely Hearts Club Band"
      c:_1="The Beatles" />

XML不允许某个元素的多个属性具有相同的名字。因此,如果有两个或更多的构造器参数的话,我们不能简单地使用下画线进行标示。但是如果只有一个构造器参数的话,我们就可以这样做了。为了完整地展现该功能,假设BlankDisc只有一个构造器参数,这个参数接受唱片的名称。在这种情况下,我们可以在Spring中这样声明它:

<bean id="compactDisc" class="soundsystem.BlankDisc"
      c:_="Sgt. Pepper's Lonely Hearts Club Band" />

在装配bean引用和字面量值方面,<constructor-arg>和c-命名空间的功能是相同的。但是有一种情况是<constructor-arg>能够实现,c-命名空间却无法做到的。接下来,让我们看一下如何将集合装配到构造器参数中。

装配集合

到现在为止,我们假设CompactDisc在定义时只包含了唱片名称和艺术家的名字。如果现实世界中的CD也是这样的话,那么在技术上就不会任何的进展。CD之所以值得购买是因为它上面所承载的音乐。大多数的CD都会包含十多个磁道,每个磁道上包含一首歌。

如果使用CompactDisc为真正的CD建模,那么它也应该有磁道列表的概念。请考虑下面这个新的BlankDisc

package soundsystem.collections;
import java.util.List;
import soundsystem.CompactDisc;

public class BlankDisc implements CompactDisc {

  private String title;
  private String artist;
  private List<String> tracks;

  public BlankDisc(String title, String artist, List<String> tracks) {
    this.title = title;
    this.artist = artist;
    this.tracks = tracks;
  }
  public void play() {
    System.out.println("Playing " + title + " by " + artist);
    for (String track : tracks) {
      System.out.println("-Track: " + track);
    }
  }

}

这个变更会对Spring如何配置bean产生影响,在声明bean的时候,我们必须要提供一个磁道列表。

最简单的办法是将列表设置为null。因为它是一个构造器参数,所以必须要声明它,不过你可以采用如下的方式传递null给它:

<bean id="compactDisc" class="soundsystem.BlankDisc">
  <constructor-arg value="Sgt. Pepper's Lonely Hearts Club Band" />
  <constructor-arg value="The Beatles" />
  <constructor-arg><null/></constructor-arg>
</bean>

<null/>元素所做的事情与你的期望是一样的:将null传递给构造器。这并不是解决问题的好办法,但在注入期它能正常执行。当调用play()方法时,你会遇到NullPointerException异常,因此这并不是理想的方案。

更好的解决方法是提供一个磁道名称的列表。要达到这一点,我们可以有多个可选方案。首先,可以使用<list>元素将其声明为一个列表:

<bean id="compactDisc" class="soundsystem.BlankDisc">
  <constructor-arg value="Sgt. Pepper's Lonely Hearts Club Band" />
  <constructor-arg value="The Beatles" />
  <constructor-arg>
    <list>
      <value>Sgt. Pepper's Lonely Hearts Club Band</value>
      <value>With a Little Help from My Friends</value>
      <value>Lucy in the Sky with Diamonds</value>
      <value>Getting Better</value>
      <value>Fixing a Hole</value>
      <!-- ...other tracks omitted for brevity... -->
    </list>
  </constructor-arg>
</bean>

其中,<list>元素是<constructor-arg>的子元素,这表明一个包含值的列表将会传递到构造器中。其中,<value>元素用来指定列表中的每个元素。

与之类似,我们也可以使用<ref>元素替代<value>,实现bean引用列表的装配。例如,假设你有一个Discography类,它的构造器如下所示:

public Discography(String artist, List<CompactDisc> cds) { ... }

那么,你可以采取如下的方式配置Discography bean:

<bean id="beatlesDiscography"
      class="soundsystem.Discography">
  <constructor-arg value="The Beatles" />
  <constructor-arg>
    <list>
      <ref bean="sgtPeppers" />
      <ref bean="whiteAlbum" />
      <ref bean="hardDaysNight" />
      <ref bean="revolver" />
      ...
    </list>
  </constructor-arg>
</bean>

当构造器参数的类型是java.util.List时,使用<list>元素是合情合理的。尽管如此,我们也可以按照同样的方式使用<set>元素:

<bean id="compactDisc" class="soundsystem.BlankDisc">
  <constructor-arg value="Sgt. Pepper's Lonely Hearts Club Band" />
  <constructor-arg value="The Beatles" />
  <constructor-arg>
    <set>
      <value>Sgt. Pepper's Lonely Hearts Club Band</value>
      <value>With a Little Help from My Friends</value>
      <value>Lucy in the Sky with Diamonds</value>
      <value>Getting Better</value>
      <value>Fixing a Hole</value>
      <!-- ...other tracks omitted for brevity... -->
    </set>
  </constructor-arg>
</bean>

<set><list>元素的区别不大,其中最重要的不同在于当Spring创建要装配的集合时,所创建的是java.util.Set还是java.util.List。如果是Set的话,所有重复的值都会被忽略掉,存放顺序也不会得以保证。不过无论在哪种情况下,<set><list>都可以用来装配List、Set甚至数组。

在装配集合方面,<constructor-arg>比c-命名空间的属性更有优势。目前,使用c-命名空间的属性无法实现装配集合的功能。

使用<constructor-arg>和c-命名空间实现构造器注入时,它们之间还有一些细微的差别。但是到目前为止,我们所涵盖的内容已经足够了,尤其是像我之前所建议的那样,要首选基于Java的配置而不是XML。因此,与其不厌其烦地花费时间讲述如何使用XML进行构造器注入,还不如看一下如何使用XML来装配属性。

到目前为止,CDPlayerBlankDisc类完全是通过构造器注入的,没有使用属性的Setter方法。接下来,我们就看一下如何使用Spring XML实现属性注入。假设属性注入的CDPlayer如下所示:

package soundsystem;
import org.springframework.beans.factory.annotation.Autowired;
import soundsystem.CompactDisc;
import soundsystem.MediaPlayer;

public class CDPlayer implements MediaPlayer {
  private CompactDisc compactDisc;

  @Autowired
  public void setCompactDisc(CompactDisc compactDisc) {
    this.compactDisc = compactDisc;
  }
  public void play() {
    compactDisc.play();
  }
}

该选择构造器注入还是属性注入呢?作为一个通用的规则,我倾向于对强依赖使用构造器注入,而对可选性的依赖使用属性注入。按照这个规则,我们可以说对于BlankDisc来讲,唱片名称、艺术家以及磁道列表是强依赖,因此构造器注入是正确的方案。不过,对于CDPlayer来讲,它对CompactDisc是强依赖还是可选性依赖可能会有些争议。虽然我不太认同,但你可能会觉得即便没有将CompactDisc装入进去,CDPlayer依然还能具备一些有限的功能。

现在,CDPlayer没有任何的构造器(除了隐含的默认构造器),它也没有任何的强依赖。因此,你可以采用如下的方式将其声明为Spring bean:

<bean id="cdPlayer"
      class="soundsystem.CDPlayer" />

Spring在创建bean的时候不会有任何的问题,但是CDPlayerTest会因为出现NullPointerException而导致测试失败,因为我们并没有注入CDPlayercompactDisc属性。不过,按照如下的方式修改XML,就能解决该问题:

<bean id="cdPlayer"
      class="soundsystem.CDPlayer">
  <property name="compactDisc" ref="compactDisc" />
</bean>

<property>元素为属性的Setter方法所提供的功能与<constructor-arg>元素为构造器所提供的功能是一样的。在本例中,它引用了ID为compactDisc的bean(通过ref属性),并将其注入到compactDisc属性中(通过setCompactDisc()方法)。如果你现在运行测试的话,它应该就能通过了。

我们已经知道,Spring为<constructor-arg>元素提供了c-命名空间作为替代方案,与之类似,Spring提供了更加简洁的p-命名空间,作为<property>元素的替代方案。为了启用p-命名空间,必须要在XML文件中与其他的命名空间一起对其进行声明:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
  xmlns:p="http://www.springframework.org/schema/p"
  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>

我们可以使用p-命名空间,按照以下的方式装配compactDisc属性:

<bean id="cdPlayer"
      class="soundsystem.CDPlayer"
      p:compactDisc-ref="compactDisc" />

p-命名空间中属性所遵循的命名约定与c-命名空间中的属性类似。图2.2阐述了p-命名空间属性是如何组成的。

图2.2 借助Spring的p-命名空间,将bean引用注入到属性中

首先,属性的名字使用了“p:”前缀,表明我们所设置的是一个属性。接下来就是要注入的属性名。最后,属性的名称以“-ref”结尾,这会提示Spring要进行装配的是引用,而不是字面量。

将字面量注入到属性中

属性也可以注入字面量,这与构造器参数非常类似。作为示例,我们重新看一下BlankDisc bean。不过,BlankDisc这次完全通过属性注入进行配置,而不是构造器注入。新的BlankDisc类如下所示:

package soundsystem;
import java.util.List;
import soundsystem.CompactDisc;

public class BlankDisc implements CompactDisc {

  private String title;
  private String artist;
  private List<String> tracks;

  public void setTitle(String title) {
    this.title = title;
  }

  public void setArtist(String artist) {
    this.artist = artist;
  }


  public void setTracks(List<String> tracks) {
    this.tracks = tracks;
  }

  public void play() {
    System.out.println("Playing " + title + " by " + artist);
    for (String track : tracks) {
      System.out.println("-Track: " + track);
    }
  }

}

现在,它不再强制要求我们装配任何的属性。你可以按照如下的方式创建一个BlankDiscbean,它的所有属性全都是空的:

<bean id="reallyBlankDisc"
      class="soundsystem.BlankDisc" />

当然,如果在装配bean的时候不设置这些属性,那么在运行期CD播放器将不能正常播放内容。play()方法可能会遇到的输出内容是“Playing null by null”,随之会抛出NullPointerException异常,这是因为我们没有指定任何的磁道。所以,我们需要装配这些属性,可以借助<property>元素的value属性实现该功能:

<bean id="compactDisc"
      class="soundsystem.BlankDisc">
  <property name="title"
               value="Sgt. Pepper's Lonely Hearts Club Band" />
  <property name="artist" value="The Beatles" />
  <property name="tracks">
    <list>
      <value>Sgt. Pepper's Lonely Hearts Club Band</value>
      <value>With a Little Help from My Friends</value>
      <value>Lucy in the Sky with Diamonds</value>
      <value>Getting Better</value>
      <value>Fixing a Hole</value>
      <!-- ...other tracks omitted for brevity... -->
    </list>
  </property>
</bean>

在这里,除了使用<property>元素的value属性来设置titleartist,我们还使用了内嵌的<list>元素来设置tracks属性,这与之前通过<constructor-arg>装配tracks是完全一样的。

另外一种可选方案就是使用p-命名空间的属性来完成该功能:

<bean id="compactDisc"
      class="soundsystem.BlankDisc"
      p:title="Sgt. Pepper's Lonely Hearts Club Band"
      p:artist="The Beatles">
  <property name="tracks">
    <list>
      <value>Sgt. Pepper's Lonely Hearts Club Band</value>
      <value>With a Little Help from My Friends</value>
      <value>Lucy in the Sky with Diamonds</value>
      <value>Getting Better</value>
      <value>Fixing a Hole</value>
      <!-- ...other tracks omitted for brevity... -->
    </list>
  </property>
</bean>

与c-命名空间一样,装配bean引用与装配字面量的唯一区别在于是否带有“-ref”后缀。如果没有“-ref”后缀的话,所装配的就是字面量。

但需要注意的是,我们不能使用p-命名空间来装配集合,没有便利的方式使用p-命名空间来指定一个值(或bean引用)的列表。但是,我们可以使用Spring util-命名空间中的一些功能来简化BlankDiscbean。

首先,需要在XML中声明util-命名空间及其模式:

<?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:p="http://www.springframework.org/schema/p"
  xmlns:util="http://www.springframework.org/schema/util"
  xsi:schemaLocation="http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans.xsd
    http://www.springframework.org/schema/util
    http://www.springframework.org/schema/util/spring-util.xsd">
    ...
</beans>

util-命名空间所提供的功能之一就是<util:list>元素,它会创建一个列表的bean。借助<util:list>,我们可以将磁道列表转移到BlankDisc bean之外,并将其声明到单独的bean之中,如下所示:

<util:list id="trackList">
  <value>Sgt. Pepper's Lonely Hearts Club Band</value>
  <value>With a Little Help from My Friends</value>
  <value>Lucy in the Sky with Diamonds</value>
  <value>Getting Better</value>
  <value>Fixing a Hole</value>
  <!-- ...other tracks omitted for brevity... -->
</util:list>

现在,我们能够像使用其他的bean那样,将磁道列表bean注入到BlankDisc bean的tracks属性中:

<bean id="compactDisc"
      class="soundsystem.BlankDisc"
      p:title="Sgt. Pepper's Lonely Hearts Club Band"
      p:artist="The Beatles"
      p:tracks-ref="trackList" />

<util:list>只是util-命名空间中的多个元素之一。表2.1列出了util-命名空间提供的所有元素。

在需要的时候,你可能会用到util-命名空间中的部分成员。但现在,在结束本章前,我们看一下如何将自动化配置、JavaConfig以及XML配置混合并匹配在一起。

表2.1 Spring util-命名空间中的元素

元 素

描 述

<util:constant>

引用某个类型的public static域,并将其暴露为bean

util:list

创建一个java.util.List类型的bean,其中包含值或引用

util:map

创建一个java.util.Map类型的bean,其中包含值或引用

util:properties

创建一个java.util.Properties类型的bean

util:property-path

引用一个bean的属性(或内嵌属性),并将其暴露为bean

util:set

创建一个java.util.Set类型的bean,其中包含值或引用

在典型的Spring应用中,我们可能会同时使用自动化和显式配置。即便你更喜欢通过JavaConfig实现显式配置,但有的时候XML却是最佳的方案。

幸好在Spring中,这些配置方案都不是互斥的。你尽可以将JavaConfig的组件扫描和自动装配和/或XML配置混合在一起。实际上,就像在2.2.1小节中所看到的,我们至少需要有一点显式配置来启用组件扫描和自动装配。

关于混合配置,第一件需要了解的事情就是在自动装配时,它并不在意要装配的bean来自哪里。自动装配的时候会考虑到Spring容器中所有的bean,不管它是在JavaConfig或XML中声明的还是通过组件扫描获取到的。

你可能会想在显式配置时,比如在XML配置和Java配置中该如何引用bean呢。让我们先看一下如何在JavaConfig中引用XML配置的bean。

现在,我们临时假设CDPlayerConfig已经变得有些笨重,我们想要将其进行拆分。当然,它目前只定义了两个bean,远远称不上复杂的Spring配置。不过,我们假设两个bean就已经太多了。

我们所能实现的一种方案就是将BlankDiscCDPlayerConfig拆分出来,定义到它自己的CDConfig类中,如下所示:

package soundsystem;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class CDConfig {
  @Bean
  public CompactDisc compactDisc() {
    return new SgtPeppers();
  }
}

compactDisc()方法已经从CDPlayerConfig中移除掉了,我们需要有一种方式将这两个类组合在一起。一种方法就是在CDPlayerConfig中使用@Import注解导入CDConfig

package soundsystem;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;

@Configuration
@Import(CDConfig.class)
public class CDPlayerConfig {

  @Bean
  public CDPlayer cdPlayer(CompactDisc compactDisc) {
    return new CDPlayer(compactDisc);
  }

}

或者采用一个更好的办法,也就是不在CDPlayerConfig中使用@Import,而是创建一个更高级别的SoundSystemConfig,在这个类中使用@Import将两个配置类组合在一起:

package soundsystem;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;

@Configuration
@Import({CDPlayerConfig.class, CDConfig.class})
public class SoundSystemConfig {
}

不管采用哪种方式,我们都将CDPlayer的配置与BlankDisc的配置分开了。现在,我们假设(基于某些原因)希望通过XML来配置BlankDisc,如下所示:

<bean id="compactDisc"
      class="soundsystem.BlankDisc"
      c:_0="Sgt. Pepper's Lonely Hearts Club Band"
      c:_1="The Beatles">
  <constructor-arg>
    <list>
      <value>Sgt. Pepper's Lonely Hearts Club Band</value>
      <value>With a Little Help from My Friends</value>
      <value>Lucy in the Sky with Diamonds</value>
      <value>Getting Better</value>
      <value>Fixing a Hole</value>
      <!-- ...other tracks omitted for brevity... -->
    </list>
  </constructor-arg>
</bean>

现在BlankDisc配置在了XML之中,我们该如何让Spring同时加载它和其他基于Java的配置呢?

答案是@ImportResource注解,假设BlankDisc定义在名为cd-config.xml的文件中,该文件位于根类路径下,那么可以修改SoundSystemConfig,让它使用@ImportResource注解,如下所示:

package soundsystem;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.ImportResource;

@Configuration
@Import(CDPlayerConfig.class)
@ImportResource("classpath:cd-config.xml")
public class SoundSystemConfig {
}

两个bean——配置在JavaConfig中的CDPlayer以及配置在XML中BlankDisc——都会被加载到Spring容器之中。因为CDPlayer中带有@Bean注解的方法接受一个CompactDisc作为参数,因此BlankDisc将会装配进来,此时与它是通过XML配置的没有任何关系。

让我们继续这个练习,但是这一次,我们需要在XML中引用JavaConfig声明的bean。

假设你正在使用Spring基于XML的配置并且你已经意识到XML逐渐变得无法控制。像前面一样,我们正在处理的是两个bean,但事情实际上会变得更加糟糕。在被无数的尖括号淹没之前,我们决定将XML配置文件进行拆分。

在JavaConfig配置中,我们已经展现了如何使用@Import@ImportResource来拆分JavaConfig类。在XML中,我们可以使用import元素来拆分XML配置。

比如,假设希望将BlankDisc bean拆分到自己的配置文件中,该文件名为cd-config.xml,这与我们之前使用@ImportResource是一样的。我们可以在XML配置文件中使用<import>元素来引用该文件:

<?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:c="http://www.springframework.org/schema/c"
  xsi:schemaLocation="http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans.xsd">
  <import resource="cd-config.xml" />

  <bean id="cdPlayer"
        class="soundsystem.CDPlayer"
        c:cd-ref="compactDisc" />
</beans>

现在,我们假设不再将BlankDisc配置在XML之中,而是将其配置在JavaConfig中,CDPlayer则继续配置在XML中。基于XML的配置该如何引用一个JavaConfig类呢?

事实上,答案并不那么直观。<import>元素只能导入其他的XML配置文件,并没有XML元素能够导入JavaConfig类。

但是,有一个你已经熟知的元素能够用来将Java配置导入到XML配置中:<bean>元素。为了将JavaConfig类导入到XML配置中,我们可以这样声明bean:

<?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:c="http://www.springframework.org/schema/c"
  xsi:schemaLocation="http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans.xsd">

  <bean class="soundsystem.CDConfig" />

  <bean id="cdPlayer"
        class="soundsystem.CDPlayer"
        c:cd-ref="compactDisc" />

</beans>

采用这样的方式,两种配置——其中一个使用XML描述,另一个使用Java描述——被组合在了一起。类似地,你可能还希望创建一个更高层次的配置文件,这个文件不声明任何的bean,只是负责将两个或更多的配置组合起来。例如,你可以将CDConfig bean从之前的XML文件中移除掉,而是使用第三个配置文件将这两个组合在一起:

<?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:c="http://www.springframework.org/schema/c"
  xsi:schemaLocation="http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans.xsd">

  <bean class="soundsystem.CDConfig" />

  <import resource="cdplayer-config.xml" />

</beans>

不管使用JavaConfig还是使用XML进行装配,我通常都会创建一个根配置(root configuration),也就是这里展现的这样,这个配置会将两个或更多的装配类和/或XML文件组合起来。我也会在根配置中启用组件扫描(通过<context:component-scan>@ComponentScan)。你会在本书的很多例子中看到这种技术。

Spring框架的核心是Spring容器。容器负责管理应用中组件的生命周期,它会创建这些组件并保证它们的依赖能够得到满足,这样的话,组件才能完成预定的任务。

在本章中,我们看到了在Spring中装配bean的三种主要方式:自动化配置、基于Java的显式配置以及基于XML的显式配置。不管你采用什么方式,这些技术都描述了Spring应用中的组件以及这些组件之间的关系。

我同时建议尽可能使用自动化配置,以避免显式配置所带来的维护成本。但是,如果你确实需要显式配置Spring的话,应该优先选择基于Java的配置,它比基于XML的配置更加强大、类型安全并且易于重构。在本书中的例子中,当决定如何装配组件时,我都会遵循这样的指导意见。

因为依赖注入是Spring中非常重要的组成部分,所以本章中介绍的技术在本书中所有的地方都会用到。基于这些基础知识,下一章将会介绍一些更为高级的bean装配技术,这些技术能够让你更加充分地发挥Spring容器的威力。


相关图书

深入浅出Spring Boot 3.x
深入浅出Spring Boot 3.x
云原生Spring实战Spring Boot与?Kubernetes实践
云原生Spring实战Spring Boot与?Kubernetes实践
Spring实战(第6版)
Spring实战(第6版)
Java研发自测入门与进阶
Java研发自测入门与进阶
Spring核心技术和案例实战
Spring核心技术和案例实战
Java EE企业级应用开发实战(Spring Boot+Vue+Element)
Java EE企业级应用开发实战(Spring Boot+Vue+Element)

相关文章

相关课程