购买
下载掌阅APP,畅读海量书库
立即打开
畅读海量书库
扫码下载掌阅APP

3.2 IoC

IoC容器是Spring框架中非常重要的核心组件,它伴随了Spring的诞生和成长。Spring通过IoC容器来管理所有Java对象(bean)及其依赖关系。

本节将全面讲解IoC容器的概念及用法。

3.2.1 依赖注入与控制反转

在Java应用程序中,不管是受限的嵌入式应用程序,还是多层架构的服务端企业级应用程序,对象在应用程序中均通过彼此依赖来实现功能。那么“依赖注入”与“控制反转”又是什么关系呢?依赖注入(Dependency Injection)是Martin Fowler在2004年提出的关于控制反转的解释。 Martin Fowler认为控制反转一词让人产生疑惑,无法直白地理解“到底哪方面的控制被反转了”。所以Martin Fowler建议采用依赖注入一词来代替控制反转。

依赖注入和控制反转其实就是一个事物的两种不同说法而已,本质上是一回事。依赖注入是一种程序设计模式和架构模型,有些时候也称为控制反转。尽管从技术上来讲,依赖注入是控制反转的特殊实现,但依赖注入还指一个对象应用另一个对象来实现一种特殊的能力。例如,把一个数据库连接以参数形式传到一个对象的结构方法中,而不是在那个对象内部自行创建一个连接。依赖注入和控制反转的基本思想就是把类的依赖从类内部转到外部,以减少依赖。利用控制反转,对象在被创建时,会由一个调控系统统一进行对象实例的管理,将该对象所依赖对象的引用通过调控系统进行传递。也可以说,依赖被注入对象中。所以控制反转是关于一个对象如何获取其所依赖对象的引用的过程,而这个过程是“谁来传递依赖的引用”这个职责的反转。

控制反转一般分为依赖注入(Dependency Injection)和依赖查找(Dependency Lookup)两种实现类型。其中依赖注入应用比较广泛,Spring就是采用依赖注入这种方式来实现控制反转的。

3.2.2 IoC容器和bean

Spring通过IoC容器来管理所有Java对象及其依赖关系。在软件开发过程中,系统的各个对象之间、各个模块之间、软件系统与硬件系统之间,或多或少都会存在耦合关系,如果一个系统的耦合度过高,就会造成难以维护的问题。但是完全没有耦合的代码是不能工作的,代码之间需要相互协作、相互依赖来完成功能。IoC技术恰好解决了这类问题,各个对象之间不需要直接关联,而是在需要用到对方时,由IoC容器来管理对象之间的依赖关系。对于开发人员来说,只需维护相对独立的各个对象的代码即可。

IoC是一个过程,即对象定义其依赖关系,而其他与之配合的对象只能通过构造函数参数、工厂方法的参数,或者在从工厂方法构造后或返回后在对象实例上设置的属性来定义依赖关系,随后,IoC容器在创建bean时会注入这些依赖项。这个过程在职责上是反转的,就是把原先代码中需要实现的对象创建、依赖的代码反转给容器来帮忙实现和管理,所以称为控制反转。

IoC的应用有以下两种模式。

(1)反射模式:在运行状态中,根据提供的类的路径或类名,通过反射来动态地获取该类的所有属性和方法。

(2)工厂模式:把IoC容器当作一个工厂,在配置文件或注解中给出定义,然后利用反射技术,根据给出的类名生成相应的对象。对象生成的代码及对象之间的依赖关系在配置文件中定义,这样就实现了解耦。

org.springframework.beans和org.springframework.context包是Spring IoC容器的基础。BeanFactory接口提供了能够管理所有类型对象的高级配置机制。ApplicationContext是BeanFactory的子接口,它更容易与Spring的AOP功能集成,进行消息资源处理、事件发布,以及作为应用层特定的上下文(如用于Web应用程序的WebApplicationContext)。简言之,BeanFactory提供了基本的配置功能,而ApplicationContext在此基础上增加了更多的企业特定功能。

在Spring应用中,bean由Spring IoC容器进行实例化、组装,并受其管理。bean和Spring IoC容器之间的依赖关系反映在容器使用的配置元数据中。

3.2.3 配置元数据

配置元数据(Configuration Metadata)描述了Spring容器在应用程序中是如何实例化、配置和组装对象的。

最初,Spring用XML文件格式记录配置元数据,从而很好地实现了IoC容器本身与实际写入此配置元数据的格式完全分离。

当然,基于XML的元数据不是唯一允许的配置元数据形式。目前,比较流行的配置元数据的方式是采用注解或基于Java的配置。

以下示例展示了基于XML的配置元数据的基本结构。

在上面的XML文件中,id属性是用于标识单个bean定义的字符串,它的值是协作对象;class属性定义bean的类型,并使用完全限定的类名。

以下示例展示了基于注解的配置元数据的基本结构。

3.2.4 实例化容器

Spring IoC容器需要在应用启动时进行实例化。在实例化过程中,IoC容器会从各种外部资源(如本地文件系统、Java类路径等)加载配置元数据,提供给ApplicationContext构造函数。

下面是从类路径中加载基于XML的配置元数据的示例。

当系统规模比较大时,通常会将bean定义分到多个XML文件。这样,每个单独的XML配置文件通常能够表示系统结构中的逻辑层或模块,就像上面的示例所演示的那样。当某个构造函数需要多个资源位置时,可从外部文件加载bean的定义。例如:

3.2.5 使用容器

ApplicationContext是高级工厂的接口,能维护不同bean及其依赖项的注册表。其提供的方法T getBean(String name, Class<T>requiredType),可用于检索bean的实例。

ApplicationContext读取bean定义并按以下方式访问它们。

如果配置方式不是XML而是Groovy,则将ClassPathXmlApplicationContext更改为Generic-GroovyApplicationContext即可。GenericGroovyApplicationContext是另一个Spring框架上下文的实现。

以上是使用ApplicationContext的getBean来检索bean实例的方式。ApplicationContext接口还有其他检索bean的方法,但理想情况下,应用程序代码不应该使用它们。因为程序代码根本不需要调用getBean方法,就可以完全不依赖Spring API。例如,Spring与Web框架的集成为各种Web框架组件(如控制器和JSF托管的bean)提供了依赖注入,允许开发人员通过元数据(如自动装配注入)声明对特定bean的依赖关系。

3.2.6 实例化bean的方式

每个bean都有标识符,这些标识符在托管bean的容器中必须是唯一的。一个bean通常只有一个标识符,如果它需要多个标识符,额外的标识符会被认为是别名。

在基于XML的配置元数据中,使用id或name属性来指定bean标识符。id属性允许指定一个id。通常,这些标识符的名称是字母,如“myBean”“userService”等,但也可能包含特殊字符。如果想为bean引入别名,也可以在name属性中指定,用“,”“;”或空格分隔。由于历史原因,在Spring 3.1以前的版本中,id属性被定义为一个xsd:ID类型,因此限制了可能的字符。从Spring 3.1开始,它被定义为一个xsd:string类型。值得注意的是,虽然类型做了更改,但bean id的唯一性仍由容器强制执行。

用户可以不为bean提供名称或标识符。如果没有显式名称或标识符,则容器会为该bean自动生成唯一的名称。但是如果要通过名称引用该bean,则必须提供一个名称。

在命名bean时尽量使用标准Java约定。也就是说,bean的名称要遵循以一个小写字母开头的骆驼法命名规则,如“accountManager”“accountService”“userDao”“loginController”等,这样命名的bean会让应用程序的配置更易于阅读和理解。

Spring为未命名的组件生成bean名称,同样遵循以上规则。本质上,最简单的命名方式就是直接采用类名称并将其初始字符变为小写。但也有特例,当前两个字符或多个字符是大写时,可以不进行处理。例如,“URL”类bean的名词仍然是“URL”。这些命名规则定义在java.beans.Introspector.decapitalize方法中。

3.2.7 注入方式

所谓bean的实例化就是根据配置来创建对象的过程。如果是使用基于XML的配置方式,则在元素的class属性中指定需要实例化对象的类型(或类)。这个class属性在内部实现,通常是一个BeanDefinition实例的class属性。但也有例外情况,如使用工厂方法或bean定义继承进行实例化。使用class属性有以下两种方式。

(1)通常容器本身是通过反射机制来调用指定类的构造函数,从而创建bean。这与使用Java代码的new运算符相同。

(2)通过静态工厂方法创建的类中包含静态方法。通过调用静态方法返回对象的类型可能与class一样,也可能完全不一样。

如果想配置使用静态的内部类,必须用内部类的二进制名称。例如,在com.waylau包下有一个User类,而User类中有一个静态的内部类Account,这种情况下bean定义的class属性应该是“com.waylau.User”字符,用来分隔外部类和内部类的名称。

概括起来,bean的实例化有以下3种方式。

1.通过构造函数实例化

Spring IoC容器可以管理几乎所有用户想让它管理的类,而不仅仅是管理POJO。大多数Spring用户更喜欢使用POJO,但在容器中使用非bean形式的类也是可以的。例如,遗留系统中的连接池很显然与JavaBean规范不符,但Spring也能管理它。

当开发人员使用构造方法来创建bean时,Spring对类来说并没有什么特殊性。也就是说,正在开发的类不需要实现任何特定的接口或以特定的方式进行编码。但是,根据所使用的IoC类型,很可能需要一个默认(无参)的构造方法。当使用基于XML的元数据配置文件时,可以按如下所示来指定bean类。

当采用静态工厂方法创建bean时,除了需要指定class属性,还需要通过factory-method属性来指定创建bean实例的工厂方法,Spring将调用此方法返回实例对象。就此而言,它与通过普通构造器创建类实例没什么区别。下面的bean定义展示了如何通过工厂方法来创建bean实例。

以下是基于XML的元数据配置文件。

以下是需要创建实例的类的定义。

注意,在此例中,createInstance()必须是一个static方法。

3.使用工厂实例方法实例化

通过调用工厂实例的非静态方法进行实例化,与通过静态工厂方法实例化类似。使用这种方式时,class属性设置为空,而factory-bean属性必须指定为当前(或其祖先)容器中包含工厂方法的bean的名称,而该工厂bean的工厂方法本身必须通过factory-method属性来设定。

以下是基于XML的元数据配置文件。

以下是需要创建实例的类的定义。

当然,一个工厂类也可以有多个工厂方法。以下是基于XML的元数据配置文件。

以下是需要创建实例的类的定义。

3.2.8 实战:依赖注入

本节将创建一个名为dependency-injection的应用,用于Spring框架的依赖注入。

该示例中采用了基于XML的配置方式,将会演示基于构造函数的依赖注入,同时也会演示如何解析构造函数的参数。

1.添加依赖

要使用依赖注入功能,只需要添加spring-context模块即可。pom.xml文件配置如下。

2.创建服务类

定义了消息服务接口MessageService,该接口描述如下。

MessageService的主要职责是获取消息。

创建该消息服务类接口的实现MessageServiceImpl,用来返回真实的、想要的业务消息。MessageServiceImpl代码如下。

其中,MessageServiceImpl是带参构造函数,username、age这两个参数的值将在getMessage方法中返回。

3.创建打印器

创建打印器MessagePrinter,用于打印消息。MessagePrinter代码如下。

执行printMessage方法后就能将消息内容打印出来,消息内容是依赖MessageService提供的。稍后会通过XML配置,将MessageService的实现注入到MessagePrinter中。

4.应用主类

Application是应用的入口类,参考如下代码对其进行修改。

由于应用是基于XML的配置,因此这里需要ClassPathXmlApplicationContext类,该类是Spring上下文的一种实现,可以实现基于XML的配置加载。按照约定,Spring应用的配置文件spring.xml放置在应用的resources目录下。

5.创建配置文件

在应用的resources目录下创建一个Spring应用的配置文件spring.xml,内容如下。

在该spring.xml文件中,可以清楚地看到bean之间的依赖关系。messageServiceImpl有两个构造函数的参数username和age,其参数值在实例化时就解析了。messagePrinter引用了messageServiceImpl作为其构造函数的参数。

6.运行

运行Application类,就能在控制台中看到“Hello World!Way Lau, age is 30”字样的信息了。打印效果如图3-1所示。

图3-1 运行结果

3.2.9 bean scope

默认情况下,所有Spring bean都是单例的,也就是在整个Spring应用中,bean的实例只有一个。

开发人员可以在bean中添加scope属性来修改这个默认值。scope属性可用值及描述如表3-1所示。

表3-1 scope属性可用值及描述

3.2.10 自定义scope

如果用户觉得Spring内置的几种scope不能满足需求,则可以定制自己的scope,即实现org.springframework.beans.factory.config.Scope接口即可。Scope接口定义了以下方法。

3.2.11 基于注解的配置

Spring应用支持多种配置方式,除XML配置外,开发人员更加青睐基于注解的配置。

基于注解的配置方式允许开发人员将配置信息移入组件类本身,在相关的类、方法或字段上声明使用注解。

Spring提供了非常多的注解。例如,Spring 2.0引入的用@Required注解来强制所需属性不能为空,在Spring 2.5中可以使用相同的处理方法来驱动Spring的依赖注入。从本质上来说,@Autowired注解提供了更细粒度的控制和更广泛的适用性。Spring 2.5也添加了对JSR-250注解的支持,如@Resource、@PostConstruct和@PreDestroy等。Spring 3.0添加了对JSR-330注解的支持,包含在javax.inject包下,如@Inject、@Qualifier、@Named和@Provider等。使用这些注解也需要在Spring容器中注册特定的BeanPostProcessor。

下面将详细介绍这些注解的用法。

1.@Required

@Required注解应用于bean属性的setter方法,如下面的示例。

这个注解只是表明受影响的bean的属性必须在bean的定义中或自动装配中通过明确的属性值在配置时来填充。如果受影响的bean的属性没有被填充,那么容器就会抛出异常。这就是通过快速失败的机制来避免NullPointerException。

2.@Autowired

如下面的示例,可以将@Autowired注解应用于传统的setter方法。

JSR-330的@Inject注解可以代替以上示例中Spring的@Autowired注解。

下面是可以将注解应用于任意名称和(或)多个参数的方法的示例。

也可以将它用于构造方法和字段:

还可以通过添加注解到期望类型的数组的字段或方法上,从而提供ApplicationContext中特定类型的所有bean。

同样,也可以用于特定类型的集合:

默认情况下,当出现0个候选bean时,自动装配就会失败。默认的行为是将被注解的方法、构造方法和字段作为需要的依赖关系。这种行为也可以通过以下做法来改变。

推荐使用@Autowired的required属性(不是@Required)注解。一方面,required属性表示了属性对于自动装配目的不是必需的,如果它不能被自动装配,那么属性就会被忽略。另一方面,@Required更健壮一些,它强制了由容器支持的各种方式的属性设置,如果没有注入任何值,就会抛出对应的异常。

3.@Primary

因为通过类型的自动装配可能有多个候选者,所以在选择过程中通常需要更多控制。达成这个目的的一种做法是使用Spring的@Primary注解。当一个依赖有多个候选者bean时,@Primary会指定一个优先提供的特殊bean。当多个候选者bean中存在一个明确指定的@Primary的bean时,就会自动装载这个bean。例如:

对于上面的配置,下面的MovieRecommender将会使用firstMovieCatalog自动注解。

4.@Qualifier

达成更多控制目的的另一种做法是使用Spring的@Qualifier注解。开发者可以用特定的参数来关联限定符的值,缩小类型的匹配范围,这样就能选择指定的bean了。其用法如下。

@Qualifier注解也可以在独立的构造方法的参数中指定。

5.@Resource

Spring也支持使用JSR-250的@Resource注解在字段或bean属性的setter方法上注入。这在Java EE 5和Java EE 6中是一个通用的模式,如在JSF 1.2中管理的bean或JAX-WS 2.0端点。Spring也为其所管理的对象支持这种模式。

@Resource使用name属性,默认情况下Spring解析这个值作为要注入的bean的名称。也就是说,如果遵循by-name语义,就如同以下示例。

如果没有明确地指定name值,那么默认的名称就从字段名称或setter方法中派生出来。如果是字段,就会选用字段名称;如果是setter方法,就会选用bean的属性名称。所以在下面的示例中,名称为“movieFinder”的bean通过setter方法来注入。

6.@PostConstruct和@PreDestroy

CommonAnnotationBeanPostProcessor不但能识别@Resource注解,还能识别JSR-250生命周期注解。在以下示例中,初始化后缓存会预先填充,并在销毁后清理。

3.2.12 类路径扫描及组件管理

本小节将介绍另一种通过扫描类路径来隐式检测候选组件的方式。候选组件是匹配过滤条件的类库,并在容器中注册对应bean的定义。这就可以不用XML来执行bean的注册,那么开发人员可以使用注解(如@Component)、AspectJ风格的表达式,或者自定义过滤条件来选择哪些类在容器中注册bean。

自Spring 3.0开始,很多由Spring JavaConfig项目提供的特性成为Spring核心框架的一部分。这就允许开发人员使用Java(而不是传统的XML文件)来定义bean。可以参考@Configuration、@Bean、@Import和@DependsOn注解的例子来了解如何使用它们的新特性。

1.@Component及其同义的注解

在Spring 2.0版本之后,@Repository注解是用于数据仓库(如数据访问对象DAO)的类标记。这个标记有多种用途,其中之一就是异常自动转换。

Spring 2.5引入了更多的典型注解,如@Repository、@Component、@Service和@Controller。@Component注解是对受Spring管理的组件的通用注解。@Repository、@Service和@Controller注解相较于@Component注解另有特殊用途,分别对应了持久层、服务层和表现层。因此,开发人员可以使用@Component注解自己的组件类,但是如果使用@Repository、@Service或@Controller注解来替代,那么这些类更适合由工具来处理或与切面进行关联。而且@Repository、@Service和@Controller注解也可以在将来Spring框架的发布中携带更多的语义。因此,对于服务层,如果在@Component和@Service注解之间进行选择,那么@Service注解无疑是更好的选择。同样,在持久层中,@Repository注解已经支持作为自动异常转换的标记。

2.元注解

Spring提供了很多元注解。元注解就是能被应用到另一个注解上的注解。

元注解也可以被用于创建组合注解。例如,Spring Web MVC的@RestController注解就是@Controller和@ResponseBody的组合。

另外,组合注解可能从元注解中重新声明任意属性来允许用户自定义。这在开发者只想暴露一个元注解的子集时会特别有用。例如,下面是一个自定义的@Scope注解,将作用域名称指定到@Session注解上,但依然允许自定义proxyMode。

@SessionScope可以不声明proxyMode就使用,例如:

或者为proxyMode重载一个值,例如:

3.自动检测类并注册bean定义

Spring可以自动检测固有的类并在ApplicationContext中注册对应的BeanDefinition。下面的两个类就是自动检测的例子。

要自动检测这些类并注册对应的bean,开发人员需要添加@ComponentScan到自己的@Configuration类上,其中的base-package元素是这两个类的公共父类包。开发人员可以任意选择使用逗号、分号、空格分隔的列表将每个类引入父包。

为了更简洁,上面的示例可以使用注解的value属性,也就是ComponentScan("com.waylau")。下面的示例使用XML配置。

4.使用过滤器来自定义扫描

默认情况下,使用@Component、@Repository、@Service、@Controller注解,或者使用自定义的@Component注解的类本身仅仅用于检测候选组件。开发人员可以修改并扩展这种行为,只需应用自定义的过滤器,即在@ComponentScan注解中添加include-filter或exclude-filter参数即可(或者作为component-scan元素的include-filter或exclude-filter子元素)。每个过滤器元素需要type和expression属性。过滤器选项的描述如表3-2所示。

表3-2 过滤器选项及描述

以下示例展示了忽略所有@Repository注解并使用“stub”库来替代。

同样地,可以使用XML配置,代码如下。

3.2.13 基于Java的容器配置

Spring中新的Java配置支持的核心就是@Bean注解的方法和@Configuration注解的类。

(1)@Bean注解用来指定一个方法实例,配置和初始化一个新对象交给Spring IoC容器管理。对于那些熟悉Spring配置的人来说,@Bean注解和<bean>元素扮演着相同的角色。@Bean注解的方法可以在@Component类中使用,但一般常用在@Configuration类中。

(2)@Configuration注解的类表示其主要目的是作为bean定义的来源。另外,@Configuration类允许内部bean依赖通过简单地调用同一类中的其他@Bean方法进行定义。最简单的@Configuration类的示例如下。

上面基于AppConfig类的配置方式的效果,等同于下面基于Spring XML的配置方式的效果。

3.2.14 环境抽象

Environment是一个集成到容器中的特殊抽象,它针对应用的环境建立了profile和properties两个关键的概念。

profile是包含了多个bean定义的一个逻辑集合,只有当指定的profile被激活时,其中的bean才会被激活。无论是通过XML定义还是通过注解,bean都可以配置到profile中。而Environment对象的角色就是与profile相关联,然后决定激活哪一个profile,以及哪一个profile为默认的profile。

properties在几乎所有的应用当中都有着重要的作用,当然也可能存在多个数据源,如property文件、JVM系统property、系统环境变量、JNDI、Servlet上下文参数、ad-hoc属性对象、Map等。

Environment对象与property相关联,然后给开发者一个方便的服务接口来配置这些数据源,并正确解析。

以下是配置profile的示例。

从上述例子中可以看到,@Profile注解也可以在方法级别使用,还可以声明在包含@Bean注解的方法中。

定义完环境抽象之后,还需要通知Spring要激活哪一个profile。有多种方法来激活一个profile,最直接的方式就是通过编程来直接调用Environment API。ApplicationContext中包含以下接口。

此外,profile还可以通过spring.profiles.active中的属性来指定,可以通过系统环境变量、JVM系统变量、Servlet上下文中的参数,甚至是JNDI的一个参数来写入。在集成测试中,激活profile可以通过spring-test中的@ActiveProfiles来实现。以下是一个示例。 e4UrMdTDRkyWTS7xR5bcfBADRibuOfg4I0iM4UA7vy4k/pi9X2LgET/tlVDV72F6

点击中间区域
呼出菜单
上一章
目录
下一章
×