本节书摘来自异步社区《Spring实战(第4版)》一书中的第2章,第2.4节,作者: 【美】Craig Walls(沃尔斯)著,更多章节内容可以访问云栖社区“异步社区”公众号查看
2.4 通过XML装配bean
到此为止,我们已经看到了如何让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。
2.4.1 创建XML配置规范
在使用XML为Spring装配bean之前,你需要创建一个新的配置规范。在使用JavaConfig的时候,这意味着要创建一个带有@Configuration注解的类,而在XML配置中,这意味着要创建一个XML文件,并且要以元素为根。
最为简单的Spring XML配置如下所示:
很容易就能看出来,这个基本的XML配置已经比同等功能的JavaConfig类复杂得多了。作为起步,在JavaConfig中所需要的只是@Configuration,但在使用XML时,需要在配置文件的顶部声明多个XML模式(XSD)文件,这些文件定义了配置Spring的XML元素。
借助Spring Tool Suite创建XML配置文件创建和管理Spring XML配置文件的一种简便方式是使用Spring Tool Suite。在Spring Tool Suite的菜单中,选择File>New>Spring Bean Configuration File,能够创建Spring XML配置文件,并且可以选择可用的配置命名空间。
用来装配bean的最基本的XML元素包含在spring-beans模式之中,在上面这个XML文件中,它被定义为根命名空间。是该模式中的一个元素,它是所有Spring配置文件的根元素。
在XML中配置Spring时,还有一些其他的模式。尽管在本书中,我更加关注自动化以及基于Java的配置,但是在本书讲解的过程中,当出现其他模式的时候,我至少会提醒你。
就这样,我们已经有了一个合法的Spring XML配置。不过,它也是一个没有任何用处的配置,因为它(还)没有声明任何bean。为了给予它生命力,让我们重新创建一下CD样例,只不过我们这次使用XML配置,而不是使用JavaConfig和自动化装配。
2.4.2 声明一个简单的
要在基于XML的Spring配置中声明一个bean,我们要使用spring-beans模式中的另外一个元素:。元素类似于JavaConfig中的@Bean注解。我们可以按照如下的方式声明CompactDiscbean:
这里声明了一个很简单的bean,创建这个bean的类通过class属性来指定的,并且要使用全限定的类名。
因为没有明确给定ID,所以这个bean将会根据全限定类名来进行命名。在本例中,bean的ID将会是“soundsystem.SgtPeppers#0”。其中,“#0”是一个计数的形式,用来区分相同类型的其他bean。如果你声明了另外一个SgtPeppers,并且没有明确进行标识,那么它自动得到的ID将会是“soundsystem.SgtPeppers#1”。
尽管自动化的bean命名方式非常方便,但如果你要稍后引用它的话,那自动产生的名字就没有多大的用处了。因此,通常来讲更好的办法是借助id属性,为每个bean设置一个你自己选择的名字:
稍后将这个bean装配到CDPlayer bean之中的时候,你会用到这个具体的名字。
减少繁琐为了减少XML中繁琐的配置,只对那些需要按名字引用的bean(比如,你需要将对它的引用注入到另外一个bean中)进行明确地命名。
在进一步学习之前,让我们花点时间看一下这个简单bean声明的一些特征。
第一件需要注意的事情就是你不再需要直接负责创建SgtPeppers的实例,在基于JavaConfig的配置中,我们是需要这样做的。当Spring发现这个元素时,它将会调用SgtPeppers的默认构造器来创建bean。在XML配置中,bean的创建显得更加被动,不过,它并没有JavaConfig那样强大,在JavaConfig配置方式中,你可以通过任何可以想象到的方法来创建bean实例。
另外一个需要注意到的事情就是,在这个简单的声明中,我们将bean的类型以字符串的形式设置在了class属性中。谁能保证设置给class属性的值是真正的类呢?Spring的XML配置并不能从编译期的类型检查中受益。即便它所引用的是实际的类型,如果你重命名了类,会发生什么呢?
借助IDE检查XML的合法性使用能够感知Spring功能的IDE,如Spring Tool Suite,能够在很大程度上帮助你确保Spring XML配置的合法性。
以上介绍的只是JavaConfig要优于XML配置的部分原因。我建议在为你的应用选择配置风格时,要记住XML配置的这些缺点。接下来,我们继续Spring XML配置的学习进程,了解如何将SgtPeppersbean注入到CDPlayer之中。
2.4.3 借助构造器注入初始化bean
在Spring XML配置中,只有一种声明bean的方式:使用元素并指定class属性。Spring会从这里获取必要的信息来创建bean。
但是,在XML中声明DI时,会有多种可选的配置方案和风格。具体到构造器注入,有两种基本的配置方案可供选择:
元素
使用Spring 3.0所引入的c-命名空间
两者的区别在很大程度就是是否冗长烦琐。可以看到,元素比使用c-命名空间会更加冗长,从而导致XML更加难以读懂。另外,有些事情可以做到,但是使用c-命名空间却无法实现。
在介绍Spring XML的构造器注入时,我们将会分别介绍这两种可选方案。首先,看一下它们各自如何注入bean引用。
构造器注入bean引用
按照现在的定义,CDPlayerbean有一个接受CompactDisc类型的构造器。这样,我们就有了一个很好的场景来学习如何注入bean的引用。
现在已经声明了SgtPeppers bean,并且SgtPeppers类实现了CompactDisc接口,所以实际上我们已经有了一个可以注入到CDPlayerbean中的bean。我们所需要做的就是在XML中声明CDPlayer并通过ID引用SgtPeppers:
当Spring遇到这个元素时,它会创建一个CDPlayer实例。元素会告知Spring要将一个ID为compactDisc的bean引用传递到CDPlayer的构造器中。
作为替代的方案,你也可以使用Spring的c-命名空间。c-命名空间是在Spring 3.0中引入的,它是在XML中更为简洁地描述构造器参数的方式。要使用它的话,必须要在XML的顶部声明其模式,如下所示:
在c-命名空间和模式声明之后,我们就可以使用它来声明构造器参数了,如下所示:
在这里,我们使用了c-命名空间来声明构造器参数,它作为元素的一个属性,不过这个属性的名字有点诡异。图2.1描述了这个属性名是如何组合而成的。
图2.1 通过Spring的c-命名空间将bean引用注入到构造器参数中
属性名以“c:”开头,也就是命名空间的前缀。接下来就是要装配的构造器参数名,在此之后是“-ref”,这是一个命名的约定,它会告诉Spring,正在装配的是一个bean的引用,这个bean的名字是compactDisc,而不是字面量“compactDisc”。
很显然,使用c-命名空间属性要比使用元素简练得多。这是我很喜欢它的原因之一。除了更易读之外,当我在编写样例代码时,c-命名空间属性能够更加有助于使代码的长度保持在书的边框之内。
在编写前面的样例时,关于c-命名空间,有一件让我感到困扰的事情就是它直接引用了构造器参数的名称。引用参数的名称看起来有些怪异,因为这需要在编译代码的时候,将调试标志(debug symbol)保存在类代码中。如果你优化构建过程,将调试标志移除掉,那么这种方式可能就无法正常执行了。
替代的方案是我们使用参数在整个参数列表中的位置信息:
这个c-命名空间属性看起来似乎比上一种方法更加怪异。我将参数的名称替换成了“0”,也就是参数的索引。因为在XML中不允许数字作为属性的第一个字符,因此必须要添加一个下画线作为前缀。
使用索引来识别构造器参数感觉比使用名字更好一些。即便在构建的时候移除掉了调试标志,参数却会依然保持相同的顺序。如果有多个构造器参数的话,这当然是很有用处的。在这里因为只有一个构造器参数,所以我们还有另外一个方案——根本不用去标示参数:
到目前为止,这是最为奇特的一个c-命名空间属性,这里没有参数索引或参数名。只有一个下画线,然后就是用“-ref”来表明正在装配的是一个引用。
我们已经将引用装配到了其他的bean之中,接下来看一下如何将字面量值(literal value)装配到构造器之中。
将字面量注入到构造器中
迄今为止,我们所做的DI通常指的都是类型的装配——也就是将对象的引用装配到依赖于它们的其他对象之中——而有时候,我们需要做的只是用一个字面量值来配置对象。为了阐述这一点,假设你要创建CompactDisc的一个新实现,如下所示:
在SgtPeppers中,唱片名称和艺术家的名字都是硬编码的,但是这个CompactDisc实现与之不同,它更加灵活。像现实中的空磁盘一样,它可以设置成任意你想要的艺术家和唱片名。现在,我们可以将已有的SgtPeppers替换为这个类:
我们再次使用元素进行构造器参数的注入。但是这一次我们没有使用“ref”属性来引用其他的bean,而是使用了value属性,通过该属性表明给定的值要以字面量的形式注入到构造器之中。
如果要使用c-命名空间的话,这个例子又该是什么样子呢?第一种方案是引用构造器参数的名字:
可以看到,装配字面量与装配引用的区别在于属性名中去掉了“-ref”后缀。与之类似,我们也可以通过参数索引装配相同的字面量值,如下所示:
XML不允许某个元素的多个属性具有相同的名字。因此,如果有两个或更多的构造器参数的话,我们不能简单地使用下画线进行标示。但是如果只有一个构造器参数的话,我们就可以这样做了。为了完整地展现该功能,假设BlankDisc只有一个构造器参数,这个参数接受唱片的名称。在这种情况下,我们可以在Spring中这样声明它:
在装配bean引用和字面量值方面,和c-命名空间的功能是相同的。但是有一种情况是能够实现,c-命名空间却无法做到的。接下来,让我们看一下如何将集合装配到构造器参数中。
装配集合
到现在为止,我们假设CompactDisc在定义时只包含了唱片名称和艺术家的名字。如果现实世界中的CD也是这样的话,那么在技术上就不会任何的进展。CD之所以值得购买是因为它上面所承载的音乐。大多数的CD都会包含十多个磁道,每个磁道上包含一首歌。
如果使用CompactDisc为真正的CD建模,那么它也应该有磁道列表的概念。请考虑下面这个新的BlankDisc:
这个变更会对Spring如何配置bean产生影响,在声明bean的时候,我们必须要提供一个磁道列表。
最简单的办法是将列表设置为null。因为它是一个构造器参数,所以必须要声明它,不过你可以采用如下的方式传递null给它:
元素所做的事情与你的期望是一样的:将null传递给构造器。这并不是解决问题的好办法,但在注入期它能正常执行。当调用play()方法时,你会遇到NullPointerException异常,因此这并不是理想的方案。
更好的解决方法是提供一个磁道名称的列表。要达到这一点,我们可以有多个可选方案。首先,可以使用元素将其声明为一个列表:
其中,元素是的子元素,这表明一个包含值的列表将会传递到构造器中。其中,元素用来指定列表中的每个元素。
与之类似,我们也可以使用元素替代,实现bean引用列表的装配。例如,假设你有一个Discography类,它的构造器如下所示:
那么,你可以采取如下的方式配置Discography bean:
当构造器参数的类型是java.util.List时,使用元素是合情合理的。尽管如此,我们也可以按照同样的方式使用元素:
和元素的区别不大,其中最重要的不同在于当Spring创建要装配的集合时,所创建的是java.util.Set还是java.util.List。如果是Set的话,所有重复的值都会被忽略掉,存放顺序也不会得以保证。不过无论在哪种情况下,或都可以用来装配List、Set甚至数组。
在装配集合方面,比c-命名空间的属性更有优势。目前,使用c-命名空间的属性无法实现装配集合的功能。
使用和c-命名空间实现构造器注入时,它们之间还有一些细微的差别。但是到目前为止,我们所涵盖的内容已经足够了,尤其是像我之前所建议的那样,要首选基于Java的配置而不是XML。因此,与其不厌其烦地花费时间讲述如何使用XML进行构造器注入,还不如看一下如何使用XML来装配属性。
2.4.4 设置属性
到目前为止,CDPlayer和BlankDisc类完全是通过构造器注入的,没有使用属性的Setter方法。接下来,我们就看一下如何使用Spring XML实现属性注入。假设属性注入的CDPlayer如下所示:
该选择构造器注入还是属性注入呢?作为一个通用的规则,我倾向于对强依赖使用构造器注入,而对可选性的依赖使用属性注入。按照这个规则,我们可以说对于BlankDisc来讲,唱片名称、艺术家以及磁道列表是强依赖,因此构造器注入是正确的方案。不过,对于CDPlayer来讲,它对CompactDisc是强依赖还是可选性依赖可能会有些争议。虽然我不太认同,但你可能会觉得即便没有将CompactDisc装入进去,CDPlayer依然还能具备一些有限的功能。
现在,CDPlayer没有任何的构造器(除了隐含的默认构造器),它也没有任何的强依赖。因此,你可以采用如下的方式将其声明为Spring bean:
Spring在创建bean的时候不会有任何的问题,但是CDPlayerTest会因为出现NullPointerException而导致测试失败,因为我们并没有注入CDPlayer的compactDisc属性。不过,按照如下的方式修改XML,就能解决该问题:
元素为属性的Setter方法所提供的功能与元素为构造器所提供的功能是一样的。在本例中,它引用了ID为compactDisc的bean(通过ref属性),并将其注入到compactDisc属性中(通过setCompactDisc()方法)。如果你现在运行测试的话,它应该就能通过了。
我们已经知道,Spring为元素提供了c-命名空间作为替代方案,与之类似,Spring提供了更加简洁的p-命名空间,作为元素的替代方案。为了启用p-命名空间,必须要在XML文件中与其他的命名空间一起对其进行声明:
我们可以使用p-命名空间,按照以下的方式装配compactDisc属性:
p-命名空间中属性所遵循的命名约定与c-命名空间中的属性类似。图2.2阐述了p-命名空间属性是如何组成的。
图2.2 借助Spring的p-命名空间,将bean引用注入到属性中
首先,属性的名字使用了“p:”前缀,表明我们所设置的是一个属性。接下来就是要注入的属性名。最后,属性的名称以“-ref”结尾,这会提示Spring要进行装配的是引用,而不是字面量。
将字面量注入到属性中
属性也可以注入字面量,这与构造器参数非常类似。作为示例,我们重新看一下BlankDisc bean。不过,BlankDisc这次完全通过属性注入进行配置,而不是构造器注入。新的BlankDisc类如下所示:
现在,它不再强制要求我们装配任何的属性。你可以按照如下的方式创建一个BlankDiscbean,它的所有属性全都是空的:
当然,如果在装配bean的时候不设置这些属性,那么在运行期CD播放器将不能正常播放内容。play()方法可能会遇到的输出内容是“Playing null by null”,随之会抛出NullPointerException异常,这是因为我们没有指定任何的磁道。所以,我们需要装配这些属性,可以借助元素的value属性实现该功能:
在这里,除了使用元素的value属性来设置title和artist,我们还使用了内嵌的元素来设置tracks属性,这与之前通过装配tracks是完全一样的。
另外一种可选方案就是使用p-命名空间的属性来完成该功能:
与c-命名空间一样,装配bean引用与装配字面量的唯一区别在于是否带有“-ref”后缀。如果没有“-ref”后缀的话,所装配的就是字面量。
但需要注意的是,我们不能使用p-命名空间来装配集合,没有便利的方式使用p-命名空间来指定一个值(或bean引用)的列表。但是,我们可以使用Spring util-命名空间中的一些功能来简化BlankDiscbean。
首先,需要在XML中声明util-命名空间及其模式:
util-命名空间所提供的功能之一就是元素,它会创建一个列表的bean。借助,我们可以将磁道列表转移到BlankDisc bean之外,并将其声明到单独的bean之中,如下所示:
现在,我们能够像使用其他的bean那样,将磁道列表bean注入到BlankDisc bean的tracks属性中:
只是util-命名空间中的多个元素之一。表2.1列出了util-命名空间提供的所有元素。
在需要的时候,你可能会用到util-命名空间中的部分成员。但现在,在结束本章前,我们看一下如何将自动化配置、JavaConfig以及XML配置混合并匹配在一起。