第2章Spring IoC容器

本章开始讲解Spring IoC容器及Bean的装配,为了理解Spring的IoC容器,将通过具体的实例详细讲解IoC的概念。用户在使用Spring所提供的各种丰富而神奇的功能之前,必须在Spring IoC容器中装配好Bean,并建立Bean和Bean之间的关联关系。Spring的Bean配置文件虽然十分简单,但广大的开发者还希望它能够做得更好。Spring对这个呼声给予了高度的重视,并进行了很多重大的改进,使很多原来冗长的配置拥有了简洁优雅的版本。此外,Spring还提供了多种配置方式,用户既可以选择一种配置,也可以同时使用多种配置。

本章主要内容:

· IoC概念所包含的设计思想

· 如何使用基于Schema格式的配置

· 依赖注入的类型和配置方式

· 各种注入参数的详细讲解

· Bean的作用域

· 基于注解的配置

· 基于Java类的配置

本章亮点:

· 通过简单明了的实例逐步讲解IoC的概念

· 详细分析Bean的生命周期,并探讨生命周期接口的实际意义

· 从实际应用角度,对可达到相同目的的多种配置方式进行比较分析

2.1 IoC概述

控制反转(Inverse of Control,IoC)是Spring容器的内核,AOP、声明式事务等功能都是在此基础上开花结果的。所谓IoC,就是通过容器来控制业务对象之间的依赖关系,而非传统实现中,由代码直接操控。这也就是“控制反转”概念的所在:控制权由应用代码中转到了外部容器,控制权的转移,就是反转。控制权转移带来的好处就是降低了业务对象之间的依赖程度。

2.2 BeanFactory和ApplicationContext

Spring通过一个配置文件描述了Bean及Bean之间的依赖关系,利用Java语言的反射功能实例化Bean并建立Bean之间的依赖关系。Spring的IoC容器在完成这些底层工作的基础上,还提供了Bean实例缓存、生命周期管理、Bean实例代理、事件发布、资源装载等高级服务。

Bean工厂(com.springframework.beans.factory.BeanFactory)是Spring框架最核心的接口,它提供了高级IoC的配置机制。BeanFactory使管理不同类型的Java对象成为可能,应用上下文(com.springframework.context.ApplicationContext)建立在BeanFactory基础之上,提供了更多面向应用的功能,它还提供了国际化支持和框架事件体系,更易于创建实际应用。一般称BeanFactory为IoC容器,而称ApplicationContext为应用上下文。但有时为了行文方便,也将ApplicationContext称为Spring容器。

对于两者的用途,可以进行简单划分:BeanFactory是Spring框架的基础设施,面向Spring本身;ApplicationContext面向使用Spring框架的开发者,几乎所有的应用场合都直接使用ApplicationContext而非底层的BeanFactory。

2.2.1 BeanFactory介绍

诚如其名,BeanFactory是一个类工厂,但它和传统的类工厂不同,传统的类工厂仅负责构造一个或几个类的实例,而BeanFactory是类的通用工厂,它可以创建并管理各种类的对象。这些可被创建和管理的对象本身没有什么特别之处,仅是一个POJO,Spring称这些被创建和管理的Java对象为Bean。我们知道,JavaBean是要满足一定规范的,如必须提供一个默认不带参的构造函数、不依赖于某一特定的容器等,但Spring中所说的Bean比JavaBean更宽泛一些,所有可以被Spring容器实例化并管理的Java类都可以称为Bean。

初始化BeanFactory

下面使用Spring配置文件为Car提供配置信息,然后通过BeanFactory装载配置文件,启动Spring IoC容器。Spring配置文件如代码清单2-1所示。

代码清单2-1 beans.xml:Car的配置文件

        <?xml version="1.0" encoding="UTF-8" ?>
        <beans xmlns="http://www.springframework.org/schema/beans"
            xmlns:xsi="http://www.w2.org/2001/XMLSchema-instance"
            xmlns:p="http://www.springframework.org/schema/p"
            xsi:schemaLocation="http://www.springframework.org/schema/beans
            http://www.springframework.org/schema/beans/spring-beans-3.1.xsd">
            <bean id="car1" class="com.smart.Car"
                p:brand="红旗CA72"
                    p:color="黑色"
                    p:maxSpeed="200" />
        </beans>

下面通过XmlBeanFactory实现类启动Spring IoC容器,如代码清单2-2所示。

代码清单2-2 BeanFactoryTest

        package com.smart.beanfactory;
        import org.springframework.beans.factory.BeanFactory;
        import org.springframework.beans.factory.xml.XmlBeanFactory;
        import org.springframework.core.io.Resource;
        import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
        import org.springframework.core.io.support.ResourcePatternResolver;
        import com.smart.Car;
        public class BeanFactoryTest {
            public static void main(String[] args) throws Throwable{
                ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
                Resource res = resolver.getResource("classpath:com/smart/beanfactory/beans.xml");
                BeanFactory bf = new XmlBeanFactory(res);
                System.out.println("init BeanFactory.");
                Car car = bf.getBean("car",Car.class);
                System.out.println("car bean is ready for use!");
            }
        }

XmlBeanFactory通过Resource装载Spring配置信息并启动IoC容器,然后就可以通过BeanFactory#getBean(beanName)方法从IoC容器中获取Bean了。通过BeanFactory启动IoC容器时,并不会初始化配置文件中定义的Bean。初始化动作发生在第一个调用时,对于单实例(singleton)的Bean来说,BeanFactory会缓存Bean实例,所以第二次使用getBean()获取Bean时,将直接从IoC容器的缓存中获取Bean实例。

Spring在DefaultSingletonBeanRegistry类中提供了一个用于缓存单实例Bean的缓存器,它是一个用HashMap实现的缓存器,单实例的Bean以beanName为键保存在这个HashMap中。

值得一提的是,在初始化BeanFactory时,必须为其提供一种日志框架,这里使用Log4J,即在类路径下提供Log4J配置文件,这样启动Spring容器才不会报错。

2.2.2 ApplicationContext介绍

如果说BeanFactory是Spring的心脏,那么ApplicationContext就是完整的身躯了。ApplicationContext由BeanFactory派生而来,提供了更多面向实际应用的功能。在BeanFactory中,很多功能需要以编程的方式实现,而在ApplicationContext中则可以通过配置的方式实现。

与BeanFactory初始化相似,ApplicationContext的初始化也很简单,如果配置文件放置在类路径下,则可以优先使用ClassPathXmlApplicationContext实现类。

      ApplicationContext ctx =
                    new ClassPathXmlApplicationContext("com/smart/ context/beans.xml");

对于ClassPathXmlApplicationContext来说,“com/smart/context/beans.xml”等同于“classpath:com/smart/context/beans.xml”。

如果配置文件放置在文件系统的路径下,则可以优先考虑使用FilySystemXmlApplicationContext实现类。

      ApplicationContext ctx =
                new FileSystemXmlApplicationContext("com/smart/ context/beans.xml");

对于FileSystemXmlApplicationContext来说,“com/smart/context/beans.xml”等同于“file:com/smart/context/beans.xml”。

还可以指定一组配置文件,Spring会自动将多个配置文件在内存中“整合”成一个配置文件,如下所示。

      ApplicationContext ctx = new ClassPathXmlApplicationContext(
                                          new String[]{"conf/beans1.xml","conf/beans2.xml"});

当然FileSystemXmlApplicationContext和ClassPathXmlApplicationContext都可以显式使用带资源类型前缀的路径,它们的区别在于,如果不显式指定资源类型前缀,就分别将路径解析为文件系统路径和类路径。

在获取ApplicationContext实例后,就可以像BeanFactory一样调用getBean(beanName)返回Bean了。ApplicationContext的初始化和BeanFactory的初始化有一个重大的区别:BeanFactory在初始化容器时,并未实例化Bean,直到第一次访问某个Bean时才实例化目标Bean;而ApplicationContext在初始化应用上下文时就实例化所有单实例的Bean。因此ApplicationContext的初始化时间会比BeanFactory稍长一些,不过稍后的调用则没有“第一次惩罚”的问题。

Spring 3.0支持基于类注解的配置方式,主要功能来自于Spring的一个名为JavaConfig的子项目,目前JavaConfig已经升级为Spring核心框架的一部分。一个标注@Configuration注解的POJO即可提供Spring所需的Bean配置信息,如代码清单2-3所示。

代码清单2-3以带注解的Java类提供的配置信息

和基于XML文件配置方式相比,基于类注解的配置方式可以很容易地让开发者控制Bean的初始化过程,比基于XML的配置更加灵活。

Spring为基于类注解的配置提供了专门的ApplicationContext实现类:AnnotationConfigApplicationContext。来看一个如何使用AnnotationConfigApplicationContext启动Spring容器的示例,如代码清单2-4所示。

代码清单2-4通过带@Configuration的配置类启动容器

        package com.smart.context;
        import org.springframework.context.ApplicationContext;
        import org.springframework.context.annotation.AnnotationConfigApplicationContext;
        import com.smart.Car;
        public class AnnotationApplicationContext {
                public static void main(String[] args) {
                    ApplicationContext ctx =
                                  new AnnotationConfigApplicationContext(Beans.class);
                    Car car =ctx.getBean("car",Car.class);
                }
        }

AnnotationConfigApplicationContext将加载Beans.class中的Bean定义并调用Beans.class中的方法实例化Bean,启动容器并装配Bean。关于使用JavaConfig配置方式的内容将在后面的章节中详细介绍。

WebApplicationContext类体系结构

WebApplicationContext是专门为Web应用准备的,它允许从相对于Web根目录的路径中装载配置文件,完成初始化工作。从WebApplicationContext中可以获得ServletContext的引用,整个Web应用上下文对象将作为属性放置到ServletContext中,以便Web应用环境可以访问Spring应用上下文。Spring专门为此提供一个工具类WebApplicationContextUtils,通过该类的getWebApplicationContext(ServletContext sc) 方 法,即 可 以 从ServletContext中 获 取WebApplicationContext实例。

Spring 2.0在WebApplicationContext中还为Bean添加了3个新的作用域:request作用域、session作用域和global session作用域。而在非Web应用的环境下,Bean只有singleton和prototype两个作用域。

WebApplicationContext初始化

WebApplicationContext的初始化方式和BeanFactory、ApplicationContext的初始化方式有所区别,因为WebApplicationContext需要ServletContext实例,也就是说它必须在拥有Web容器的前提下才能完成启动的工作。有过Web开发经验的读者都知道可以在web.xml中配置自启动的Servlet或定义Web容器监听器(ServletContextListener),借助这两者中的任何一个,就可以完成启动Spring Web应用上下文的工作。

提示 所有版本的Web容器都可以定义自启动的Servlet,但只有Servlet 2.3及以上版本的Web容器才支持Web容器监听器。有些即使支持Servlet 2.3的Web服务器,但也不能在Servlet初始化之前启动Web监听器,如Weblogic 8.1、WebSphere 5.x、Oracle OC4J 9.0。

Spring分别提供了用于启动WebApplicationContext的Servlet和Web容器监听器。

● org.springframework.web.context.ContextLoaderServlet。

● org.springframework.web.context.ContextLoaderListener。

两者的内部都实现了启动WebApplicationContext实例的逻辑,用户只要根据Web容器的具体情况选择两者之一,并在web.xml中完成配置就可以了。

代码清单2-5是使用ContextLoaderListener启动WebApplicationContext的具体配置。

代码清单2-5通过Web容器监听器引导

      …
      <!--❶指定配置文件-->
      <context-param>
          <param-name>contextConfigLocation</param-name>
          <param-value>
            /WEB-INF/viewspace-dao.xml, /WEB-INF/viewspace-service.xml
        </param-value>
        </context-param>
      <!--❷声明Web容器监听器-->
      <listener>
            <listener-class>org.springframework.web.context.ContextLoaderListener </listener-class>
      </listener>

ContextLoaderListener通过Web容器上下文参数contextConfigLocation获取Spring配置文件的位置。用户可以指定多个配置文件,用逗号、空格或分号分隔均可。对于未带资源类型前缀的配置文件路径,WebApplicationContext默认这些路径是相对于Web的部署根路径。当然,可以采用带资源类型前缀的路径配置,如“classpath*:/smart-*.xml”和上面的配置是等效的。

在不支持容器监听器的低版本Web容器中,可采用ContextLoaderServlet来完成相同的工作,如代码清单2-6所示。

代码清单2-6通过自启动的Servlet引导

      …
        <context-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>/WEB-INF/viewspace-dao.xml, /WEB-INF/viewspace-service.xml</param-value>
        </context-param>
        …
        <!--❶声明自动启动的Servlet -->
        <servlet>
            <servlet-name>springContextLoaderServlet</servlet-name>
            <servlet-class>org.springframework.web.context.ContextLoaderServlet</servlet-class>
            <!--❷启动顺序-->
            <load-on-startup>1</load-on-startup>
        </servlet>

由于WebApplicationContext需要使用日志功能,用户可以将Log4J的配置文件放置到类路径WEB-INF/classes下,这时Log4J引擎即可顺利启动。如果Log4J配置文件放置在其他位置,还必须在web.xml中指定Log4J的配置文件位置。Spring为启用Log4J引擎提供了两个类似于启动WebApplicationContext的实现类:Log4jConfigServlet和Log4jConfigListener,不管采用哪种方式都必须保证能够在装载Spring配置文件前先装载Log4J配置信息。

代码清单2-7指定Log4J配置文件时启动Spring Web应用上下文

        <context-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>
                /WEB-INF/viewspace-dao.xml,/WEB-INF/viewspace-service.xml
            </param-value>
        </context-param>
        <!--❶指定Log4J配置文件位置-->
        <context-param>
            <param-name>log4jConfigLocation</param-name>
            <param-value>/WEB-INF/log4j.properties</param-value>
        </context-param>
        <!--❷装载Log4J配置文件的自启动Servlet -->
        <servlet>
            <servlet-name>log4jConfigServlet</servlet-name>
            <servlet-class>org.springframework.web.util.Log4jConfigServlet</servlet-class>
            <load-on-startup>1</load-on-startup>
        </servlet>
        <servlet>
            <servlet-name> springContextLoaderServlet</servlet-name>
            <servlet-class>org.springframework.web.context.ContextLoaderServlet</servlet-class>
        <load-on-startup>2</load-on-startup>
        </servlet>

注意在代码清单2-7 中,将log4jConfigServlet的启动顺序号设置为1,而springContextLoaderServlet的顺序号设置为2。这样,前者将先启动,完成装载Log4J配置文件并初始化Log4J引擎的工作,紧接着后者再启动。如果使用Web监听器,则必须将Log4jConfigListener放置在ContextLoaderListener的前面。采用以上的配置方式,Spring将自动使用XmlWebApplicationContext启动Spring容器,即通过XML文件为Spring容器提供Bean的配置信息。

如果使用标注@Configuration的Java类提供配置信息,则web.xml的配置需要按如代码清单2-8所示的方式配置。

代码清单2-8使用@Configuration的Java类提供配置信息的配置

      <web-app>
        <!--通过指定context参数,让Spring使用AnnotationConfigWebApplicationContext而非
        XmlWebApplicationContext启动容器 -->
          <context-param>
                <param-name>contextClass</param-name>
                <param-value>
                org.springframework.web.context.support.AnnotationConfigWebApplicationContext
                </param-value>
          </context-param>
          <!-- 指定标注了@Configuration的配置类,多个可以使用逗号或空格分隔-->
          <context-param>
                <param-name>contextConfigLocation</param-name>
              <param-value>
                com.smart.AppConfig1,com.smart.AppConfig1
              </param-value>
          </context-param>
          <!-- ContextLoaderListener监听器将根据上面配置使用
                AnnotationConfigWebApplicationContext根据contextConfigLocation
              指定的配置类启动Spring容器-->
          <listener>
          <listener-class>
              org.springframework.web.context.ContextLoaderListener
          </listener-class>
          </listener>
      </web-app>

ContextLoaderListener如果发现配置了contextClass上下文参数,就会使用参数所指定的WebApplicationContext实现类(即AnnotationConfigWebApplicationContext)来初始化容器,该实现类会根据contextConfigLocation上下文参数指定的@Configuration的配置类所提供的Spring配置信息来初始化容器。

2.2.3 资源加载

为了访问不同类型的资源,必须使用相应的Resource实现类,这是比较麻烦的。是否可以在不显式使用Resource实现类的情况下,仅通过资源地址的特殊标识就可以加载相应的资源呢?Spring提供了一个强大的加载资源的机制,不但能够通过“classpath:”、“file:”等资源地址前缀识别不同的资源类型,还支持Ant风格带通配符的资源地址。

资源地址表达式

首先了解Spring支持哪些资源类型的地址前缀,如表2-1所示。

表2-1 资源类型的地址前缀

其中和“classpath:”对应的,还有一种比较难理解的“classpath*:”前缀。假设有多个JAR包或文件系统类路径都拥有一个相同的包名(如com.smart)。“classpath:”只会在第一个加载的com.smart包下查找,而“classpath*:”会到扫描所有这些JAR包及类路径下出现的com. smart类路径。

这对于分模块打包的应用非常有用,假设一个名为viewspace的应用共分成3个模块,一个模块都对应一个配置文件,分别是module1.xml、module2.xml及module3.xml,它们都放到com.smart目录下,每个模块单独打JAR包。使用“classpath*:com/smart/module*.xml”可以成功加载到这3个模块的配置文件,而使用“classpath:com/smart/module*.xml”时只会加载一个模块的配置文件。

Ant风格资源地址支持3种匹配符。

· ?:匹配文件名中的一个字符。

· *:匹配文件名中的任意个字符。

· **:匹配多层路径。

下面是几个Ant风格的资源路径的示例。

· classpath:com/t?st.xml:匹配com类路径下的com/test.xml、com/tast.xml或者com/txst.xml。

· file:D:/conf/*.xml:匹配文件系统D:/conf目录下所有以.xml为后缀的文件。

· classpath:com/**/test.xml:匹配com类路径下(当前目录及其子孙目录)的test.xml文件。

· classpath:org/springframework/**/*.xml:匹配类路径org/springframework下所有以.xml为后缀的文件。

· classpath:org/**/servlet/bla.xml:匹配类路径org/springframework/servlet/bla.xml、org/springframework/testing/servlet/bla.xml和org/servlet/bla.xml。

2.3 Bean装配

要使应用程序中的Spring容器成功启动,需要同时具备以下3方面的条件。

· Spring框架的类包都已经放到应用程序的类路径下。

· 应用程序为Spring提供完备的Bean配置信息。

· Bean的类都已经放到应用程序的类路径下。

Spring启动时读取应用程序提供的Bean配置信息,并在Spring容器中生成一份相应的Bean配置注册表,然后根据这张注册表实例化Bean,装配好Bean之间的依赖关系,为上层应用提供准备就绪的运行环境。

Bean配置信息是Bean的元数据信息,它由以下4个方面组成。

· Bean的实现类。

· Bean的属性信息,如数据源的连接数、用户名、密码等。

· Bean的依赖关系,Spring根据依赖关系配置完成Bean之间的装配。

· Bean的行为配置,如生命周期范围及生命周期各过程的回调函数等。

Bean元数据信息在Spring容器中的内部对应物是由一个个BeanDefinition形成的Bean注册表,Spring实现了Bean元数据信息内部表示和外部表示的解耦。Spring支持多种形式Bean配置方式。Spring 1.0仅支持基于XML的配置;Spring 2.0新增基于注解配置的支持,同时还对基于XML的配置进行了优化和扩展,提供了多个命名空间;Spring 2.0新增基于Java类配置的支持,同时对原来的配置方式也进行了部分优化。

图2-1描述了Spring容器、Bean配置信息、Bean实现类及应用程序这四者的相互关系。

图2-1 Spring容器高层视图

Bean配置信息定义了Bean的实现及依赖关系,Spring容器根据各种形式的Bean配置信息在容器内部建立Bean定义注册表,然后根据注册表加载、实例化Bean,并建立Bean和Bean的依赖关系,最后将这些准备就绪的Bean放到Bean缓存池中,以供外层的应用程序调用。

2.3.1 Bean基本配置

装配一个Bean

在Spring容器的配置文件中,定义一个简要Bean的配置片断,如图2-2所示。

一般情况下,Spring IoC容器中的一个Bean即对应配置文件中的一个<bean>,这种镜像对应关系应该容易理解。其中id为这个Bean的名称,通过容器的getBean("foo")即可获取对应的Bean,在容器中起到定位查找的作用,是外部程序和Spring IoC容器进行交互的桥梁。class属性指定了Bean对应的实现类。

图2-2 Bean的定义

下面基于XML的配置文件定义了两个简单的Bean。

        <?xml version="1.0" encoding="UTF-8" ?>
        <beans   xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w2.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
                http://www.springframework.org/schema/beans/spring-beans-3.1.xsd">
            <bean id="car" class="com.smart.simple.Car"/>
            <bean id="boss" class="com.smart.simple.Boss"/>
        </beans>

这段配置信息提供了实例化Car和Boss这两个Bean必需的信息,Spring IoC容器完全可以据此创建这两个Bean的实例。

Bean的命名

一般情况下,在配置一个Bean时,需要为其指定一个id属性作为Bean的名称。id在IoC容器中必须是唯一的,此外id的命名需要满足XML对id的命名规范(id是XML规定的特殊属性):必须以字母开始,后面可以是字母、数字、连字符、下画线、句号、冒号等完整结束(full stops)符,逗号和空格这些非完整结束符是非法的。在实际情况中,id命名约束并不会给用户带来影响,但是,如果用户确实希望用到一些特殊字符对进行Bean命名,可以使用<bean>的name属性进行命名,name属性没有字符上的限制,几乎可以使用任何字符,如?ab、123等,如下所示。

        <bean name="#car1" class="com.smart.simple.Car"/>

id和name都可以指定多个名字,名字之间可用逗号、分号或者空格进行分隔,例如:

        <bean name="#car1,123,$car" class="com.smart.simple.Car"/>

这里为Bean定义了3 个名称:#car1、123 和$car。用户可以使用getBean("#car1")、getBean("123")或getBean("$car")获取IoC容器中的Car Bean。

Spring配置文件不允许出现两个相同id的<bean>,但却可以出现两个相同name的<bean>,如果有多个name相同的<bean>,通过getBean(beanName)获取Bean时,将返回最后声明的那个Bean,原因是后面的Bean覆盖了前面同名的Bean。所以为了避免无意间Bean覆盖的隐患,应尽量使用id而非name命名Bean。

如果id和name两个属性都未指定,如<bean class="com.smart.simple.Car"/>,Spring自动将全限定类名作为Bean的名称,就可以通过getBean("com.smart.simple. Car")获取Car Bean。如果存在多个实现类相同的匿名<bean>,例如:

      <bean class="com.smart.simple.Car"/>
      <bean class="com.smart.simple.Car"/>
      <bean class="com.smart.simple.Car"/>

第一个Bean通过getBean("com.smart.simple.Car")获得;第二个Bean通过getBean ("com.smart.simple.Car#1")获得;第三个Bean通过getBean("com.smart. simple.Car#2")获得,依次类推。一般匿名<bean>出现在通过内部Bean为外层Bean提供注入值时使用,正如Java的匿名类一样。

2.3.2 依赖注入

属性注入

属性注入即通过setXxx()方法注入Bean的属性值或依赖对象,由于属性注入方式具有可选择性和灵活性高的优点,因此属性注入是实际应用中最常采用的注入方式。

属性注入要求Bean提供一个默认的构造函数,并为需要注入的属性提供对应的Setter方法。Spring先调用Bean的默认构造函数实例化Bean对象,然后通过反射的方式调用Setter方法注入属性值。来看一个简单的例子,如代码清单2-9所示。

代码清单2-9 Car:默认构造函数和Setter

      package com.smart.ditype;
      public class Car {
            private int maxSpeed;
            public String brand;
            private double price;
            public void setBrand(String brand) {
                this.brand = brand;
            }
            public void setMaxSpeed(int maxSpeed) {
                this.maxSpeed = maxSpeed;
            }
            public void setPrice(double price) {
                this.price = price;
            }
          …
      }

Car类中定义了3个属性,并分别提供了对应的Setter方法。

提示 默认构造函数是不带参的构造函数。Java语言规定如果类中没有定义任何构造函数,则JVM自动为其生成一个默认的构造函数。反之,如果类中显式定义了构造函数,则JVM不会为其生成默认的构造函数。所以假设Car类中显式定义了一个带参的构造函数,如public Car(String brand),则需要同时提供一个默认构造函数public Car(),否则使用属性注入时将抛出异常。

代码清单2-10是在Spring配置文件中对Car进行属性注入的配置片断。

代码清单2-10 Car:属性注入配置

        <bean id="car" class="com.smart.ditype.Car">
          <property name="maxSpeed"><value>200</value></property>
          <property name="brand"><value>红旗CA72</value></property>
          <property name="price"><value>20000.00</value></property>
        </bean>

在上述代码中配置了一个Bean,并为该Bean的3个属性提供了属性值。具体来说,Bean的每一个属性对应一个<property>标签,name为属性的名称,在Bean实现类中拥有与其对应的Setter方法:maxSpeed对应setMaxSpeed(),brand对应setBrand()。

需要指出的是:Spring只会检查Bean中是否有对应的Setter方法,至于Bean中是否有对应的属性变量则不做要求。例如,配置文件中<property name="brand"/>的属性配置项仅要求Car类中拥有setBrand()方法,但Car类不一定要拥有brand成员变量。

虽然如此,但是在一般情况下,还是按照约定俗成的方式在Bean中提供同名的属性变量。

提示 一般情况下,Java的属性变量名都以小写字母起头,如maxSpeed、brand等。但也存在特殊的情况,考虑到一些特定意义的大写英文缩略词(如USA、XML等),JavaBean也允许大写字母起头的属性变量名,不过必须满足“变量的前两个字母要么全部大写,要么全部小写”的要求,如brand、IDCode、IC、ICCard等属性变量名是合法的,而iC、iCcard、iDCode等属性变量名则是非法的。这个并不广为人知的JavaBean规范条款引发众多让人困惑的配置问题。

构造函数注入

构造函数注入是除属性注入之外的另一种常用的注入方式,它保证一些必要的属性在Bean实例化时就得到设置,并且确保了Bean实例在实例化后就可以使用。

(1)按类型匹配入参。

如果任何可用的Car对象都必须提供brand和price的值,使用属性注入方式只能人为在配置时提供保证,而无法在语法级提供保证,这时通过构造函数注入就可以很好地满足这一要求。使用构造函数注入的前提是Bean必须提供带参的构造函数,下面为Car提供一个可设置brand和price属性的构造函数。

        package com.smart.ditype;
        public class Car {
            …
          public Car(String brand, double price) {
                this.brand = brand;
                this.price = price;
          }
      }

构造函数注入的配置方式和属性注入方式的配置有所不同,在Spring配置文件中使用构造函数注入装配这个Car Bean,如代码清单2-11所示。

代码清单2-11通过构造函数注入Car

      <bean id="car1" class="com.smart.ditype.Car">
        <constructor-arg type="java.lang.String"> ❶
          <value>红旗CA72</value>
        </constructor-arg>
        <constructor-arg type="double"> ❷
          <value>20000</value>
        </constructor-arg>
      </bean>

在<constructor-arg>的元素中有一个type属性,它为Spring提供了判断配置项和构造函数入参对应关系的“信息”。细心的读者可能会提出以下疑问:配置文件中<bean>元素的<constructor-arg>声明顺序难道不可以用于确定构造函数入参的顺序吗?在只有一个构造函数的情况下当然是可以的,如果在Car中定义了多个具有相同数量入参的构造函数,那么这种顺序标识方法就失效了。此外,Spring的配置文件采用和元素标签顺序无关的策略,这种策略可以在一定程度上保证配置信息的确定性,避免一些似是而非的问题,这和web.xml配置文件顺序相关的策略是不同的。因此,❶和❷处的<constructor-arg>位置并不会对最终的配置效果产生影响。

(2)按类型匹配入参。

我们知道,Java语言通过入参的类型及顺序区分不同的重载方法。对于代码清单2-19 中的Car类,Spring仅通过type属性指定的参数类型就可以知道“红旗CA72”对应String类型的brand入参,而“20000”对应double类型的price入参。但是,如果Car构造函数3个入参的类型相同,仅通过type就无法确定对应关系了,这时需要通过入参索引的方式进行确定。

为了更好地演示按索引匹配入参的配置方式,特意对Car构造函数进行以下调整。

brand和corp的入参类型都是String,所以Spring将无法确定type为String的<constructor-arg>到底对应的是brand还是corp。但是,通过显式指定参数的索引能够消除这种不确定性,如代码清单2-12所示。

代码清单2-12通过入参位置索引确定对应关系

      <bean id="car2" class="com.smart.ditype.Car">
          <!--❶注意索引从0开始-->
            <constructor-arg index="0" value="红旗CA72"/>
            <constructor-arg index="1" value="中国一汽"/>
            <constructor-arg index="2" value="20000"/>
        </bean>

构造函数第一个参数索引为0,第二个为1,依次类推,因此很容易知道“红旗CA72”对应brand入参,而“中国一汽”对应corp入参。

(3)联合使用类型和索引匹配入参。

有时需要联合使用type和index才能确定配置项和构造函数入参的对应关系,来看如代码清单2-13所示的例子。

代码清单2-13 Car:入参数目相同的构造函数

        …
        public Car(String brand, String corp, double price) {
            this.brand = brand;
            this.corp = corp;
            this.price = price;
        }
        public Car(String brand, String corp, int maxSpeed) {
            this.brand = brand;
            this.corp = corp;
            this.maxSpeed = maxSpeed;
        }
        …

这里,Car拥有两个重载的构造函数,它们都有两个入参。针对这种情况,代码清单2-12按照入参位置索引的配置方式又难以满足要求了,这时需要联合使用<constructor-arg>的type和index才能解决问题,如代码清单2-14所示。

代码清单2-14 Car:通过入参类型和位置索引确定对应关系

        <!--对应Car (String brand,String corp, int maxSpeed)构造函数-->
        <bean id="car3" class="com.smart.ditype.Car">
                  <constructor-arg index="0" type="java.lang.String">
                    <value>红旗CA72</value>
                  </constructor-arg>
                  <constructor-arg index="1" type="java.lang.String">
                    <value>中国一汽</value>
                  </constructor-arg>
                  <constructor-arg index="2" type="int">
                    <value>200</value>
                  </constructor-arg>
        </bean>

对于代码清单2-13中的两个构造函数,如果仅通过index进行配置,Spring将无法确定第3个入参配置项究竟是对应int的maxSpeed还是double的price,采用索引匹配配置时,真正引起歧义的地方在于第3个入参,因此仅需要明确指定第3个入参的类型就可以取消歧义了。所以在代码清单2-14中,第1个和第2个<constructor-arg>元素的type属性可以去除。

对于由于参数数目相同而类型不同所引起的潜在配置歧义问题,Spring容器可以正确启动且不会给出报错信息,它将随机采用一个匹配的构造函数实例化Bean,而被选择的构造函数可能并不是用户所希望的。因此,必须特别谨慎,以避免潜在的错误。

(4)通过自身类型反射匹配入参。

当然如果Bean构造函数入参的类型是可辨别的(非基础数据类型且入参类型各异),由于Java反射机制可以获取构造函数入参的类型,即使构造函数注入的配置不提供类型和索引的信息,Spring依旧可以正确地完成构造函数的注入工作。下面Boss类构造函数的入参就是可辨别的。

      public Boss(String name, Car car, Office office) {
          this.name = name;
          this.car = car;
          this.office = office;
      }

由于car、office和name入参的类型都是可辨别的,所以无须在配置构造函数注入时指定<constructor-arg>的类型和索引,因此可以采用如下简易的配置方式。

      <bean id="boss" class="com.smart.ditype.Boss">
          <!--❶没有设置type和index属性,通过入参值的类型完成匹配映射-->
          <constructor-arg>
              <value>John</value>
          </constructor-arg>
          <constructor-arg>
            <ref bean="car"/>
          </constructor-arg>
          <constructor-arg>
            <ref bean="office"/>
          </constructor-arg>
      </bean>
      <bean id="car" class="com.smart.ditype.Car"/>
      <bean id="office" class="com.smart.ditype.Office"/>

但是为了避免潜在配置歧义引起的张冠李戴问题,在Bean存在多个构造函数时,使用显式指定index和type属性依然不失为一种良好的配置习惯。

提示 实构造函数注入和属性注入都各有优缺点,Spring并没有强制用户使用哪一种方式,用户完全可以根据个人偏好做出选择,在某些情况下使用构造函数注入,而在另一些情况下使用属性注入。对于一个全新开发的应用来说,不推荐使用工厂方法的注入方式,因为工厂方法需要额外的类和代码,这些功能和业务是没有关系的,既然Spring容器已经以一种更优雅的方式实现了传统工厂模式的所有功能,就大可不必再去做这项重复性的工作。

循环依赖问题

Spring容器能顺利实例化以构造函数注入方式配置的Bean有一个前提:Bean构造函数入参引用的对象必须已经准备就绪。由于这个机制的限制,如果两个Bean都采用构造函数注入,而且都通过构造函数入参引用对方,就会发生类似于线程死锁的循环依赖问题。来看一个发生循环依赖问题的例子。

假设在Spring配置文件中按照以下构造函数注入方式进行配置。

        <bean id="car" class="com.smart.cons.Car">
            <constructor-arg index="0"  value="红旗CA72"/>
              <!--❶引用❷处的boss -->
            <constructor-arg index="1" ref="boss"/>
        </bean>
        <bean id="boss" class="com.smart.cons.Boss">
            <constructor-arg index="0"  value="John"/>
            <!-- ❷引用❶处的car -->
            <constructor-arg index="1" ref="car"/>
        </bean>

当启动Spring IoC容器时,因为存在循环依赖问题,Spring容器将无法成功启动。解决这个问题呢只要修改Bean的代码,将构造函数注入方式调整为属性注入方式就可以了。

2.3.3 注入参数详解

在Spring配置文件中,用户不但可以将String、int等字面值注入Bean中,还可以将集合、Map等类型的数据注入Bean中,此外还可以注入配置文件中其他定义的Bean。

字面值

“字面值”一般是指可用字符串表示的值,这些值可以通过<value>元素标签进行注入。在默认情况下,基本数据类型及其封装类、String等类型都可以采取字面值注入的方式。Spring容器在内部为字面值提供了编辑器,它可以将以字符串表示的字面值转换为内部变量的相应类型。Spring允许用户注册自定义的编辑器,以处理其他类型属性注入时的转换工作。

在如代码清单2-15所示的示例中,为Car注入了两个属性值并在Spring配置文件中使用字面值提供配置值。

代码清单2-15字面值注入字面值

      <bean id="car" class="com.smart.attr.Car">
          <property name="maxSpeed">
                <value>200</value>
          </property>
          <property name="brand">❶
                <value><![CDATA[红旗&CA72]]></value>
          </property>
      </bean>

由于❶处的brand属性值包含了一个XML的特殊符号,因此特意在属性值外添加了一个XML特殊处理标签<![CDATA[ ]]>,<![CDATA[ ]]>的作用是让XML解析器将标签中的字符串当作普通的文本对待,以防止某些字符串对XML格式造成破坏。

引用其他Bean

Spring IoC容器中定义的Bean可以相互引用,IoC容器则充当“红娘”的角色。下面创建一个新的Boss类,Boss类中拥有一个Car类型的属性。

      package com.smart.attr;
      public class Boss {
          private Car car;
          //设置car属性
          public void setCar(Car car) {
                this.car = car;
          }
          …
      }

boss的Bean通过<ref>元素引用car Bean,建立起boss对car的依赖。

        <!--❶car Bean -->
      <bean id="car" class="com.smart.attr.Car"/>
      <bean id="boss" class="com.smart.attr.Boss">
          <property name="car">
    <!--❷引用❶处定义的car Bean -->
    <ref bean="car"></ref>
          </property>
      </bean>

<ref>元素可以通过以下3个属性引用容器中的其他Bean。

· bean:通过该属性可以引用同一容器或父容器的Bean,这是最常见的形式。

· local:通过该属性只能引用同一配置文件中定义的Bean,它可以利用XML解析器自动检验引用的合法性,以便在开发编写配置时能够及时发现并纠正配置的错误。

· parent:引用父容器中的Bean,如<ref parnet="car">的配置说明car的Bean是父容器中的Bean。

为了说明子容器对父容器中Bean的引用,来看一个具体的例子。假设有两个配置文件:beans1.xml和beans2.xml,其中beans1.xml被父容器加载,其配置内容如下。

      <!--❶在父容器中定义的car -->
      <bean id="car" class="com.smart.attr.Car">
            <property name="brand" value="红旗CA72" />
                <property name="maxSpeed" value="200" />
                <property name="price" value="2000.00" />
        </bean>

而beans2.xml被子容器加载,其配置内容如下。

        <!--❶该Bean和父容器的car Bean具有相同的id -->
        <bean id="car" class="com.smart.attr.Car">
                  <property name="brand" value="吉利CT5" />
                  <property name="maxSpeed" value="100" />
                  <property name="price" value=”1000.00” />
        </bean>
        <bean id="boss" class="com.smart.attr.Boss">
                    <property name="car">
                        <!--❷引用父容器中的car,而非❷处定义的Bean,如
                        果采用<ref bean="car"/>将引用本容器❶处的car -->
                    <ref parent="car"/>
                    </property>
        </bean>

在beans1.xml中配置了一个car Bean,在bean2.xml中也配置了一个car Bean。分别通过父子容器加载beans1.xml和beans2.xml,beans2.xml中的boss通过<ref parent=“car”>将引用到父容器中的car。

下面是分别和父子容器加载beans1.xml和beans2.xml配置文件的代码。

        //❶父容器
        ClassPathXmlApplicationContext pFactory = new ClassPathXmlApplicationContext(
                              new String[]{"com/smart/attr/beans1.xml"});
        //❷指定pFactory为该容器的父容器
        ApplicationContext factory = new ClassPathXmlApplicationContext(
        new String[]{"com/smart/attr/beans2.xml"},pFactory);
        Boss boss = (Boss)factory.getBean("boss");
        System.out.println(boss.getCar().toString());

运行这段代码,控制台中打印出以下的信息。

        brand:红旗CA72/maxSpeed:200/price:2000.0

集合类型属性

java.util包中的集合类是最常用的数据结构类型,主要包括List、Set、Map、Properties,Spring为这些集合类型属性提供了专门的配置元素标签。

(1)List。

为Boss添加一个List类型的favorites属性。

        package com.smart.attr;
        … public class Boss {
            private List favorites = new ArrayList();
            public List getFavorites() {
                  return favorites;
            } public void setFavorites(List favorites) {
                  this.favorites = favorites;
            }
            …
        }

对应Spring中的配置片断如下所示。

      <bean id="boss1" class="com.smart.attr.Boss">
          <property name="favorites">
            <list>
                <value>看报</value>
                <value>赛车</value>
                <value>高尔夫</value>
            </list>
          </property>
      </bean>

List属性既可以通过<value>注入字符串,也可以通过<ref>注入容器中的其他Bean。

提示 假设一个属性类型可以通过字符串字面值进行配置,那么该类型对应的数组类型的属性(如String[]、int[]等)也可以采用<list>的方式进行配置。

(2)Map。

为Boss添加一个Map类型的jobs属性。

      public class Boss {
          …
          private Map jobs = new HashMap();
          public Map getJobs() {
                return jobs;
          }
          public void setJobs(Map jobs) {
                this.jobs = jobs;
          }
          …
      }

在配置文件中可以通过以下方式为jobs属性提供配置值。

假如某一Map元素的键和值都是对象,则可以采取以下的配置方式。

      <entry>
          <key><ref bean="keyBean"/></key>
          <ref bean="valueBean"/>
      </entry>

(3)Properties。

Properties类型其实可以看成是Map类型的特例。Map元素的键和值可以是任何类型的对象,而Properties属性的键和值都只能是字符串。为Boss添加一个Properties类型的mails属性。

        public class Boss {
            …
            private Properties mails = new Properties();
            public Properties getMails() {
                  return mails;
            }
            public void setMails(Properties mails) {
                  this.mails = mails;
            }
            …
        }

下面的配置片断为mails提供了配置。

        <bean id="boss1" class="com.smart.attr.Boss">
            <property name="mails">
                  <props>
                  <prop key="jobMail">john-office@smart.com</prop>
                  <prop key="lifeMail">john-life@smart.com</prop>
                  </props>
            </property>
        </bean>

因为Properties键值对只能是字符串,因此其配置比Map的配置要简单一些,注意值的配置没有<value>子元素标签。

(4)通过util命名空间配置集合类型的Bean。

如果希望配置一个集合类型的Bean,而非一个集合类型的属性,则可以通过util命名空间进行配置。首先需要在Spring配置文件头中引入util命名空间的声明。

下面配置一个List类型的Bean,使其可以通过list-class显式指定List的实现类。

        <util:list id="favoriteList1" list-class="java.util.LinkedList">
            <value>看报</value>
            <value>赛车</value>
            <value>高尔夫</value>
        </util:list>

下面配置一个Set类型的Bean,使其可以通过set-class指定Set实现类。

        <util:set id="favoriteSet1">
            <value>看报</value>
            <value>赛车</value>
            <value>高尔夫</value>
        </util:set>

最后配置Map类型Bean,使其可以通过map-class指定Set实现类。

        <util:map id="emails1">
          <entry key="AM" value="会见客户" />
          <entry key="PM" value="公司内部会议" />
      </util:map>

此外,<util:list>和<util:set>还支持value-type属性,指定集合中的值类型,而<util:map>支持key-type和value-type属性,指定Map的键和值和类型。

简化配置方式

为了简化XML文件的配置,越来越多的XML文件采用属性而非子元素配置信息。Spring从2.5版本开始引入了一个新的p命名空间,可以通过<bean>元素属性的方式配置Bean的属性。使用p命名空间后,基于XML的配置方式将进一步简化。

使用p命名空间前,配置如代码清单2-16所示。

代码清单2-16未采用p命名空间的配置

      <?xml version="1.0" encoding="UTF-8" ?>
      <beans xmlns="http://www.springframework.org/schema/beans"
          xmlns:xsi="http://www.w2.org/2001/XMLSchema-instance"
          xsi:schemaLocation="http://www.springframework.org/schema/beans
                http://www.springframework.org/schema/beans/spring-beans-3.1.xsd">
          <bean id="car" class="com.smart.ditype.Car">
                <property name="brand" value="红旗&amp;CA72"/>
                <property name="maxSpeed" value="200"/>
                <property name="price" value="20000.00"/>
          </bean>
          <bean id="boss" class="com.smart.ditype.Boss">
                <property name="car" ref="car"/>
          </bean>
      </beans>

使用p命名空间后,配置如代码清单2-17所示。

代码清单2-17采用p命名空间的配置

未采用p命名空间前,<bean>使用<property>子元素配置Bean的属性,采用p命名空间后,采用<bean>的元素属性配置Bean的属性。对于字面值属性,其格式为:

      p:<属性名>="xxx"

对于引用对象的属性,其格式为:

        p:<属性名>_ref="xxx"

正是由于p命名空间中的属性名是可变的,所以p命名空间没有对应的Schema定义文件,也就无须在xsi:schemaLocation中为p命名空间指定Schema定义文件。

自动装配

Spring IoC容器了解容器中所有Bean的配置信息,此外通过Java反射机制还可以获知实现类的结构信息(如构造函数方法的结构、属性等信息)。掌握容器中所有Bean的这些信息后,Spring IoC容器就可以按照某种规则对容器中的Bean进行自动装配,而无须通过显式方式进行配置。Spring为厌恶配置的开发人员提供了一条偷懒的方法,可以按照某些规则进行Bean的自动装配。

<bean>元素提供了一个指定自动装配类型的属性:autowire="<自动装配类型>"。Spring提供了4种自动装配类型,用户可以根据具体情况进行选择,如表2-2所示。

表2-2 自动装配类型

<beans>元素标签中的default-autowire属性可以配置全局自动匹配,default-autowire属性的默认值为no,表示不启用自动装配,其他几个可选配置值分别为byName、byType、constructor和autodetect,这几个配置值的意义是不言自明的。不过,在<beans>中定义的自动装配策略可以被<bean>的自动装配策略覆盖。

自动装配以一两拨千斤的方式完成容器中Bean之间的装配工作,这种省心省力的自动化装配机制确实省去了大量的配置工作。但像很多事物一样,这个功能也是一把“双刃剑”,在减轻配置工作量的同时也造成了配置文件中Bean之间关系不清晰的问题,此外还容易引发一些潜在的错误。

一般情况下,在实际的项目中很少使用自动装配功能,因为和自动装配功能所带来的好处比起来,明确清晰的配置文档以及不存在潜在错误的需求显得更有说服力一些。

2.3.4 Bean作用域

在配置文件中定义Bean时,用户不但可以配置Bean的属性值以及相互之间的依赖关系,还可以定义Bean的作用域。作用域将对Bean的生命周期和创建方式产生影响。在低版本的Spring中,仅有两个作用域:singleton和prototype,在Spring 2.0中,针对WebApplicationContext新添了3个新的作用域。表2-3列出了Spring 2.0支持的所有作用域类型。

表2-3 Bean作用域类型

在低版本的Spring中,由于只有两个Bean作用域,所以采用singleton="true|false"的配置方式,Spring为了向后兼容,依旧支持这种配置方式。不过,Spring推荐采用新的配置方式:scope="<作用域类型>"。

除了以上5种预定义的Bean作用域外,Spring还允许用户自定义Bean的作用域,可以通过org.springframework.beans.factory.config.Scope接口定义新的作用域,再通过org.springframework.beans.factory.config.CustomScopeConfigurer这个BeanFactoryPostProcessor注册自定义的Bean作用域。但是,在一般的应用中,Spring所提供的作用域已经能够满足应用的要求,用户很少需要自定义新的Bean作用域。所以本书不对此进行深入讲解,感兴趣的读者可以自行阅读Scope接口的Javadoc文档。

2.3.5 基于注解的配置

使用注解定义Bean

前面说过,不管是XML还是注解,它们都是表达Bean定义的载体,其实质都是为Spring容器提供Bean定义的信息,表现形式上是将XML定义的东西通过类注解进行描述。Spring从2.0开始引入基于注解的配置方式,在3.1时得到了进一步的完善。

我们知道,Spring容器成功启动的三大要件分别是:Bean定义信息、Bean实现类以及Spring本身。如果采用基于XML的配置,Bean定义信息和Bean实现类本身是分离的,而采用基于注解的配置方式时,Bean定义信息通过在Bean实现类上标注注解实现。

下面是使用注解定义一个DAO的Bean。

在❶处使用@Component注解在UserDao类声明处对类进行标注,它可以被Spring容器识别,Spring容器自动将POJO转换为容器管理的Bean。

它和以下的XML配置是等效的。

        <bean id="userDao" class="com.smart.anno.UserDao"/>

除了@Component以外,Spring提供了如下3个功能基本和@Component等效的注解,它们分别用于对DAO、Service及Web层的Controller进行注解,所以也称这些注解为Bean的衍型注解。

· @Repository:用于对DAO实现类进行标注。

· @Service:用于对Service实现类进行标注。

· @Controller:用于对Controller实现类进行标注。

在@Component之外提供这3个特殊的注解是为了让注解类本身的用途清晰化,此外Spring将赋予它们一些特殊的功能。因此,推荐使用特定的注解标注特定的Bean。

使用注解配置信息启动Spring容器

Spring在2.5后提供了一个context命名空间,它提供了通过扫描类包以应用注解定义Bean的方式。

        <?xml version="1.0" encoding="UTF-8" ?>
            <!--❶声明context命名空间-->
        <beans xmlns="http://www.springframework.org/schema/beans"
            xmlns:xsi="http://www.w2.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-3.1.xsd
                    http://www.springframework.org/schema/context
                    http://www.springframework.org/schema/context/spring-context-3.1.xsd">
            <!--❷扫描类包以应用注解定义的Bean-->
            <context:component-scan base-package="com.smart.anno"/>
        </beans>

在❶处声明context命名空间,在❷处即可通过context命名空间的component-scan的base-package属性指定一个需要扫描的基类包,Spring容器将会扫描这个基类包里的所有类,并从类的注解信息中获取Bean的定义信息。

如果仅希望扫描特定的类而非基类包下的所有类,那么可以使用resource-pattern属性过滤出特定的类,如下所示。

        <context:component-scan base-package="com.smart" resource-pattern="anno/*.class"/>

这里将基类包设置为com.smart,默认情况下resource-pattern属性的值为“**/*.class”,即基类包里的所有类。这里设置为“anno/*.class”,则Spring仅会扫描基类包里anno子包中的类。

通过resource-pattern属性仅可按资源名称对基类包中的类进行过滤,如果仅使用resource-pattern,就会发现很多时候它并不能满足用户的要求,如仅过滤基类包中实现了XxxService接口的类或标注了某个特定注解的类等。

不过,这些需求可以很容易地通过<context:component-scan>的过滤子元素实现,例如:

      <context:component-scan base-package="com.smart">
        <context:include-filter type="regex" expression="com\.smart\.anno.*"/>
        <context:exclude-filter type="aspectj" expression="com.smart..*Controller+"/>
      </context:component-scan>

<context:include-filter>表示要包含的目标类,<context:exclude-filter>表示要排除在外的目标类。一个<context:component-scan>下可以拥有若干<context:exclude-filter>和<context:include-filter>元素。这两个过滤元素均支持多种类型的过滤表达式,如表2-4所示。

表2-4 过滤表达式

在所有这些过滤类型中,除custom类型外,aspectj的过滤表达能力是最强的,它可以轻易实现其他类型所能表达的过滤规则。

自动装配Bean

Spring通过@Autowired注解实现Bean的依赖注入,来看一个LogonService的例子。

在❶处使用@Service将LogonService标注为一个Bean,在❷处,通过@Autowired注入LogDao及UserDao的Bean。@Autowired默认按类型匹配的方式,在容器查找匹配的Bean,当有且仅有一个匹配的Bean时,Spring将其注入@Autowired标注的变量中。

(1)使用@Autowired的required属性。

如果容器中没有一个和标注变量类型匹配的Bean,Spring容器启动时将抛出NoSuchBean DefinitionException异常。如果希望Spring即使找不到匹配的Bean完成注入也不要抛出异常,那么就可以使用@Autowired(required=false)进行标注。

        @Service
        public class LogonService {
            @Autowired(required=false)
            private LogDao logDao;
            …
        }

默认情况下,@Autowired的required属性值为ture,即要求一定要找到匹配的Bean,否则将报异常。

(2)使用@Qualifier指定注入Bean的名称。

如果容器中有一个以上匹配的Bean,则可以通过@Qualifier注解限定Bean的名称,如代码清单2-18所示。

代码清单2-18 LogonService.java

        package com.smart.anno;
        import org.springframework.beans.factory.annotation.Autowired;
        import org.springframework.beans.factory.annotation.Qualifier;
        import org.springframework.stereotype.Service;
        @Service
        public class LogonService {
            @Autowired
            private LogDao logDao;
            @Autowired
            @Qualifier("userDao") ❶
            private UserDao userDao;
          }

这时,假设容器有两个类型为UserDao的Bean,一个名为userDao,另一个名为otherUserDao,则❶处会注入名为userDao的Bean。

(3)对类方法进行标注。

@Autowired可以对类成员变量及方法的入参进行标注,如代码清单2-19所示,在类的方法上使用@Autowired注解。

代码清单2-19 LogonService.java

如果一个方法拥有多个入参,在默认情况下,Spring自动选择匹配入参类型的Bean进行注入。Spring允许对方法入参标注@Qualifier以指定注入Bean的名称,如下所示。

      @Autowired
      public void init(@Qualifier("userDao")UserDao userDao,LogDao logDao){
          System.out.println("multi param inject");
          this.userDao = userDao;
          this.logDao =logDao;
      }

在以上的例子中,UserDao的入参注入名为userDao的Bean,而LogDao的入参注入LogDao类型的Bean。

一般情况下,Spring容器中的大部分Bean都是单实例的,所以一般无须通过@Repository、@Service等注解的value属性为Bean指定名称,也无须使用@Qualifier按名称进行注入。

(4)对集合类进行标注。

如果对类中集合类的变量或方法入参进行@Autowired标注,那么Spring就会将容器中类型匹配的所有Bean都自动注入进来。下面来看一个具体的例子,如代码清单2-20所示。

代码清单2-20 MyComponent.java

Spring如果发现变量是一个集合类,则它会将容器中匹配集合元素类型的所有Bean都注入进来。这里,Plugin为一个接口,它拥有两个实现类,分别是OnePlugin和TwoPlugin,这两个实现类都通过@Component标注为Bean,而Spring会将这两个Bean都注入plugins中。

2.3.6 基于Java类的配置

使用Java类提供Bean定义信息

JavaConfig是Spring的一个子项目,它旨在通过Java类的方式提供Bean的定义信息,该项目早在Spring 2.0 时就已经发布了1.0 版本。Spring 2.0 基于Java类配置的核心即取材于JavaConfig,JavaConfig经过若干年的努力终于成为了Spring 2.0的核心功能。

普通的POJO只要标注@Configuration注解,就可以为Spring容器提供Bean定义的信息了,每个标注了@Bean的类方法都相当于提供一个Bean的定义信息。

代码清单2-21 AppConf.java

❶处在AppConf类的定义处标注了@Configuration注解,说明这个类可用于为Spring提供Bean的定义信息。类的方法处可以标注@Bean注解,Bean的类型由方法返回值类型决定,名称默认和方法名相同,也可通过入参显式指定Bean名称,如@Bean(name="userDao")。直接在@Bean所标注的方法中提供Bean的实例化逻辑。

在❷处,userDao()和logDao()方法定义了一个UserDao和一个LogDao的Bean,它们的Bean名称分别为userDao和logDao。在❸处又定义了一个logonService Bean,并且在❹处注入❷处所定义的两个Bean。

因此以上的配置和以下XML配置是等效的。

          <bean id="userDao" class="com.smart.anno.UserDao"/>
          <bean id="logDao" class="com.smart.anno.LogDao"/>
        <bean id="logonService" class="com.smart.conf.LogonService"
          p:logDao-ref="userDao" p:userDao-ref="logDao"/>

基于Java类的配置方式和基于XML或基于注解的配置方式相比,前者通过代码的方式更加灵活地实现Bean的实例化及Bean之间的装配,而后两者都是通过配置声明的方式,在灵活性上要稍逊一些,但是配置上要更简单一些。

如果Bean在多个@Configuration配置类中定义,如何引用不同配置类中定义的Bean呢?例如,UserDao和LogDao这两个Bean在DaoConfig中定义,而LogonService Bean在ServiceConfig中定义,LogonService Bean需要引用DaoConfig中定义的两个Bean。下面通过如代码清单2-22所示的例子说明。

代码清单2-22 DaoConfig.java

      package com.smart.conf;
      import org.springframework.context.annotation.Bean;
      import org.springframework.context.annotation.Configuration;
      @Configuration
      public class DaoConfig {
          @Bean
          public UserDao userDao(){
                return new UserDao();
          }
          @Bean
          public LogDao logDao(){
                return new LogDao();
          }
      }

由于@Configuration注解类本身已经标注了@Component注解,所以任何标注了@Configuration的类,本身也相当于标注了@Component,即它们可以像普通的Bean一样被注入其他Bean中。DaoConfig标注了@Configuration注解后就成为一个Bean,它可以被自动注入ServiceConfig中,如代码清单2-23所示。

代码清单2-23 ServiceConfig.java

调用daoConfig的logDao()和userDao()方法,就相当于将DaoConfig配置类中定义的Bean注入进来。Spring会对配置类所有标注@Bean的方法进行“改造”(AOP增强),将对Bean生命周期管理的逻辑植入进来。所以,在❷处调用daoConfig.logDao()及daoConfig.userDao()方法时,不是简单地执行DaoConfig类中定义的方法逻辑,而是从Spring容器中返回相应Bean的单例。换句话说,多次调用daoConfig.logDao()返回的都是Spring容器中相同的Bean。在@Bean处,还可以标注@Scope注解以控制Bean的作用范围。如果在@Bean处标注了@Scope("prototype"),则每次调用daoConfig.logDao()都会返回一个新的LogDao Bean,如代码清单2-24所示。

代码清单2-24 DaoConfig.java

        package com.smart.conf;
        import org.springframework.context.annotation.Bean;
        import org.springframework.context.annotation.Configuration;
        import org.springframework.context.annotation.Scope;
        @Configuration
        public class DaoConfig {
            @Scope("prototype")
            @Bean
            public LogDao logDao(){
                  return new LogDao();
            }
          …
        }

由于Spring容器会自动对@Configuration的类进行“改造”,以植入Spring容器对Bean的管理逻辑,所以使用基于Java类的配置必须保证将Spring aop类包和CGLIB类包加载到类路径下。

直接通过@Configuration类启动Spring容器

Spring提供了一个AnnotationConfigApplicationContext类,它能够直接通过标注@Configuration的Java类启动Spring容器,如代码清单2-25所示。

代码清单2-25 JavaConfigTest.java

在❶处,通过AnnotationConfigApplicationContext类的构造函数直接传入标注@Configuration的Java类,直接用该类中提供的Bean定义信息启动Spring容器。

此外,AnnotationConfigApplicationContext还支持通过编码的方式加载多个@Configuration配置类,然后通过刷新容器应用这些配置类,如代码清单2-26所示。

代码清单2-26 JavaConfigTest.java

      package com.smart.conf;
      import org.springframework.context.ApplicationContext;
      import org.springframework.context.annotation.AnnotationConfigApplicationContext;
      public class JavaConfigTest {
          public static void main(String[] args) {
                AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
                //❶注册多个@Configuration配置类
                ctx.register(DaoConfig.class);
                ctx.register(ServiceConfig.class);
                //❷刷新容器以应用这些注册的配置类
                ctx.refresh();
                LogonService logonService = ctx.getBean(LogonService.class);
                logonService.printHelllo();
          }
      }

用户可以通过代码逐个注册配置类,也可以通过@Import将多个配置类组装到一个配置类中,这样仅需要注册这个组装好的配置类就可以启动容器了,如代码清单2-27所示。

代码清单2-27 ServiceConfig.java

      package com.smart.conf;
      import org.springframework.context.annotation.Bean;
      import org.springframework.context.annotation.Configuration;
      import org.springframework.context.annotation.Import;
      @Configuration
      @Import(DaoConfig.class)
      public class ServiceConfig {
          @Bean
          public LogonService logonService(){
                LogonService logonService = new LogonService();
                return logonService;
          }
      }

通过XML配置文件引用@Configuration的配置

标注了@Configuration的配置类本身相当于一个标注了@Component的类,也是一个Bean,它可以被Spring的<context:component-scan>扫描到。因此,如果希望将配置类组装到XML配置文件中,通过XML配置文件启动Spring容器,仅需要在XML中通过<context:component-scan>扫描到相应的配置类就可以了,如代码清单2-28所示。

代码清单2-28 beans2.xml:装配了配置类的XML配置文件

      <?xml version="1.0" encoding="UTF-8" ?>
      <beans xmlns="http://www.springframework.org/schema/beans"
          xmlns:xsi="http://www.w2.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-3.1.xsd
                  http://www.springframework.org/schema/context
                  http://www.springframework.org/schema/context/spring-context-3.1.xsd">
            <!--❶通过上下文扫描加载到AppConf的配置类-->
            <context:component-scan base-package="com.smart.conf"
                          resource-pattern="AppConf.class" />
        </beans>

通过Configuration配置类引用XML配置信息

假设在beans2.xml中定义了两个Bean,如代码清单2-29所示。

代码清单2-29 beans2.xml:定义了两个Bean

        <?xml version="1.0" encoding="UTF-8" ?>
        <beans xmlns="http://www.springframework.org/schema/beans"
            xmlns:xsi="http://www.w2.org/2001/XMLSchema-instance"
            xsi:schemaLocation="http://www.springframework.org/schema/beans
                  http://www.springframework.org/schema/beans/spring-beans-3.1.xsd">
            <bean id="userDao" class="com.smart.conf.UserDao"/>
            <bean id="logDao" class="com.smart.conf.LogDao"/>
        </beans>

在@Configuration配置类中可通过@ImportResource引入XML配置文件,在配置类中即可直接通过@Autowired引用XML配置文件中定义的Bean,如代码清单2-30所示。

代码清单2-30 LogonAppConfig.java:引入并组装XML配置信息

在❷处完成了两项功能:其一,定义了一个LogonService的Bean;其二,通过方法入参自动注入UserDao和LogDao的Bean,这两个Bean是在XML定义的。

需要说明的是:在❶处引入定义UserDao和LogDao Bean的XML配置文件不是❷处可成功自动注入UserDao和LogDao Bean的前提条件。只要不同形式的Bean定义信息能够加载到Spring容器中,Spring就能足够“智能”地完成Bean之间的装配。

2.3.7 不同配置方式比较

同一功用商品或服务的品牌多样性是市场健康的基本要素。对于Spring来说,同样是实现Bean信息定义的目标,它提供了基于XML、基于注解及基于Java类这三种选项,而且还允许多项组合的自由而非取此舍彼。Spring张开虚怀若谷的胸怀包容气象万千的世界,同时让百态生象可以互通有无,取长补短,最终达到本质纯一,世界的大同。Spring优雅地实现了这个目标,我们把赞誉毫无保留地敬献给Spring的大师们。

下面通过表2-5总结这3种配置方式的差异。

表2-5 Bean不同配置方式比较

这3种配置文件很难说孰优孰劣,只能说它们都有自己的舞台和使用场景,表2-6给出了一些参考意见。

表2-6 Bean不同配置方式的适用场合

笔者一般采用XML配置DataSource、SessionFactory等资源Bean,在XML中利用aop、context命名空间进行相关主题的配置。其他所有项目中开发的Bean,都通过基于注解配置的方式进行配置,即整个项目采用“基于XML+基于注解”的配置方式,很少采用基于Java类的配置方式。

2.4 小结

本章分析了IoC的概念,控制反转其实包含两个层面的意思,“控制”是接口实现类的选择控制权;而“反转”是指这种选择控制权从调用类转移到外部第三方类或容器的中。

BeanFactory、ApplicationContext和WebApplicationContext是Spring框架3个最核心的接口,框架中其他大部分的类都围绕它们展开、为它们提供支持和服务。Spring提供了一个强大加载资源的机制,不但能够通过“classpath:”、“file:”等资源地址前缀识别不同的资源类型,还支持Ant风格带通配符的资源地址。

讲解如何在Spring配置文件中配置Bean的各种知识,用户不但可以通过属性注入的方式建立Bean和Bean的依赖,也可以通过构造函数的方式完成相同的任务,但前者不管是对于代码的编写还是Bean的配置都具有更大的灵活性,成为大多数开发者选择的方式。不但容器中的Bean可以通过配置建立起关联关系,配置文档中的<bean>标签也可以建立继承、依赖、引用的关系,合理地使用这些关系可以简化配置、提高配置质量。

Spring提供了5个Bean作用范围,在Web应用环境下,可以使用request、session和globalSession的Bean作用域,此外还允许通过编程的方式定义新的Bean作用域。

通过@Component及另外3 个衍型注解(@Repository、@Service及@Controller)配合@Autowired就可以很好地使用基于注解的配置进行Bean的定义和注入,这种方式比在XML文件中通过<bean>提供的配置更加简单。

任务POJO标注了@Configuration注解后就可以为Spring容器提供Bean的定义信息,在类方法中标注@Bean相当于定义了一个Bean,同时还提供了Bean的实例化逻辑。由于Bean的实例化逻辑是在方法中定义的,因此它可以应对一些复杂Bean实例化的场景。

不管使用何种配置方式,Spring都可以很好地将它们整合起来,在Spring容器内部,这些不同方式的Bean定义信息大体相同,三者之间并不存在谁替换谁的问题,它们都有自己最适合的应用场合。