第1章-Spring深入浅出IoC

文章目录

  • 一、模块
    • 1. 核心容器
    • 2. AOP和Instrumentation
    • 3. 消息
    • 4. 数据访问/集成
    • 5. Web
    • 6. 测试
    • 7. 使用场景
      • 7.1 典型的完整的Spring Web应用程序
      • 7.2 使用第三方Web框架的Spring中间层
      • 7.3 远程使用场景
      • 7.4 EJBs-包装现有的POJOs
  • 二、IoC容器
    • 1. BeanFactory
      • 1.1 使用容器
      • 1.2 依赖注入
        • 1.2.1 属性注入
        • 1.2.2 构造器注入
        • 1.2.3 静态工厂注入
      • 1.3 自动注入(autowire)
      • 1.4 指定依赖关系(depends-on)
      • 1.5 作用域(scope)
        • 1.5.1 单例作用域
        • 1.5.2 原型作用域
      • 1.6 懒加载(lazy-init)
      • 1.7 使用外部属性值(${ })
    • 2. ApplicationContext
      • 2.1 生命周期
      • 2.2 生命周期实例
    • 3. 基于注解的配置
      • 3.1 @Autowired
      • 3.2 @Qualifier
      • 3.3 @Resource
      • 3.4 @Value
      • 3.5 @PostConstruct和@PreDestroy
      • 3.6 @Component
      • 3.7 @Scope
    • 4. 基于Java的配置
      • 4.1 @Configuration
      • 4.2 @Bean
      • 4.3 @ComponentScan
      • 4.4 @Import
      • 4.5 @Conditional
      • 4.6 @ImportResource
        • 4.6.1 基于XML配置为主
        • 4.6.2 基于Java配置为主
    • 5. Environment
      • 5.1 @Profile
        • 5.1.1 基于Java定义Profile
        • 5.1.2 基于XML定义Profile
        • 5.1.3 激活Profile和默认Profile
      • 5.2 @PropertySource
    • 6. ApplicationContext扩展
      • 6.1 监听事件
        • 6.1.1 @EventListener
        • 6.1.2 自定义事件监听
        • 6.1.3 异步监听
        • 6.1.4 顺序监听
      • 6.2 Web应用实例化ApplicationContext

实验项目准备:新建一个Maven项目, pom.xml配置如下:


<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0modelVersion>

    <groupId>com.ajngroupId>
    <artifactId>springartifactId>
    <version>1.0.0-SNAPSHOTversion>

    <properties>
        <spring.version>5.1.3.RELEASEspring.version>
    properties>

    <dependencies>
        <dependency>
            <groupId>org.springframeworkgroupId>
            <artifactId>spring-coreartifactId>
            <version>${spring.version}version>
        dependency>
        <dependency>
            <groupId>org.springframeworkgroupId>
            <artifactId>spring-beansartifactId>
            <version>${spring.version}version>
        dependency>
        <dependency>
            <groupId>org.springframeworkgroupId>
            <artifactId>spring-contextartifactId>
            <version>${spring.version}version>
        dependency>
    dependencies>

    <build>
        <plugins>
            <plugin>
                <artifactId>maven-compiler-pluginartifactId>
                <version>3.7.0version>
                <configuration>
                    <source>1.8source>
                    <target>1.8target>
                configuration>
            plugin>
        plugins>
    build>
project>

项目的GAV(groupId, artifactId, version)配置不一定要和示例一样,可以自定义

Spring框架是一个Java平台,它为Java应用程序提供全面的基础框架支持。它也是现在非常流行的开源框架,只要是做Java开发,肯定接触过Spring的使用,不管我们对它的了解如何,多或者少,我们还是要尽力挖掘出Spring的功能价值。Spring框架的功能被分开为多个模块,如下图所示:
第1章-Spring深入浅出IoC_第1张图片

一、模块

1. 核心容器

  • spring-core:Spring核心模块
  • spring-beans:Bean容器支持
  • spring-context:建立在Bean模块基础上,扩展了功能
  • spring-context-support:支持整合第三方库到Spring上下文
  • spring-expression:提供表达式语言

2. AOP和Instrumentation

  • spring-aop:提供AOP(页面切面编程)支持
  • spring-aspects:提供AspectJ的集成
  • spring-instrumentation:提供类植入支持和类加载器的实现

3. 消息

  • spring-messaging:消息传递模块

4. 数据访问/集成

  • spring-jdbc:提供JDBC抽象层
  • spring-tx:支持编程和声明式事务管理
  • spring-orm:为流行的对象关系映射API提供集成层
  • spring-oxm:提供支持对象/XML映射实现的抽象层
  • spring-jms:用于生产和消费消息的功能

5. Web

  • spring-web:提供基本的面向Web的集成功能
  • spring-webmvc:用于Web应用程序的模型-视图-控制器(MVC)和REST Web Services实现
  • spring-websocket:提供Web Socket的支持

6. 测试

  • spring-test:支持使用JUnit或TestNG对Spring组件进行单元测试和集成测试。

在使用Maven或者其他构建工具管理项目时,可以选择性地添加Spring的依赖,使用什么或能就添加什么依赖。

7. 使用场景

7.1 典型的完整的Spring Web应用程序

第1章-Spring深入浅出IoC_第2张图片
这是Spring的完整使用案例,基于Tomcat Servlet容器,所有定制的业务逻辑都可以使用简单的POJO1实现,并由Spring的IoC容器进行管理,一般使用的结构是从上到下在项目中分层:

  • 顶层是MVC层,包括表单控制器,Multipart Resolver(文件上传解析器),动态绑定实体模型,集成JSP,Velocity等视图模板引擎。
  • 中间层是业务层,包括自定义业务,消息,以及远程访问
  • 底层是数据持久层,主要实现ORM映射

7.2 使用第三方Web框架的Spring中间层

第1章-Spring深入浅出IoC_第3张图片
假设我们要使用Spring框架来重构一个项目,情况不允许你完全切换到另一个不同的框架。Spring框架不强制你使用它所有的功能,这不是一个全有或全无的解决方案,我们从它由多个模块组件的构成就可以看出。我们完全可以使用第三方Web框架,比如Struts、Tapestry、JSF等UI框架,它们都可以与基于Spring的中间层集成,从而使用Spring的IoC和事务等功能。而你只需要做的就是使用ApplicationContext连接你的业务逻辑,并使用WebApplicationContext来集成你的Web层。此时分层结构是:

  • 顶层是Web层,使用第三方Web框架
  • 中间层是业务层
  • 底层是数据持久层

7.3 远程使用场景

第1章-Spring深入浅出IoC_第4张图片
你现在需要通过Web服务访问现有代码时,可以使用Spring的Hessian-,Rmi-或HttpInvokerProxyFactoryBean类,启用对现有应用程序的远程访问并不困难。

7.4 EJBs-包装现有的POJOs

第1章-Spring深入浅出IoC_第5张图片
Spring还为EJBs2提供了一个访问和抽象层,使你能够重用现有的POJO,并将其包装在无状态会话bean中。

通过上面的使用场景了解到Spring在企业项目中的应用和定位,我们完全可以按照自己的意愿选择性地使用Spring,比如搭建一个基于SSM框架的项目:

  • 控制层使用SpringMVC模块
  • 业务层使用Spring框架管理并且可以整合其他组件
  • 持久层使用Spring整合MyBatis完成对数据库的访问

不管使用什么框架,Java类都可以交由Spring的IoC容器来管理。

二、IoC容器

IoC(Inversion of Control,控制反转)也被称为DI(Dependency Injection,依赖注入)。在实际操作中,假如A调用了B,我们可以称A依赖于B,此时A是调用者,B是被调用者,在传统的程序设计过程中,通常由调用者来创建被调用者的实例。在Spring依赖注入的模式下,创建被调用者的工作不再由调用者来完成,因此称为控制反转,创建被调用者实例的工作由Spring容器来完成,然后注入给调用者,因为也称为依赖注入3。在Spring中完成这个功能的两个重要的接口:

  • BeanFactory:提供一种高级的配置机制能够管理任何类型的对象。
  • ApplicationContext:是BeanFactory接口的子接口,它更容易集成Spring的AOP功能、消息资源处理、事件发布和特定的上下文应用层。比如:WebApplicationContext

在Spring中,由Spring的IoC容器管理的对象叫做beans,bean就是由Spring IoC容器实例化、组装和以其他方式管理的对象。此外bean只是你应用中许多对象中的一个,Beans以及他们之间的依赖关系是通过容器配置元数据反映出来的。下图是Spring如何工作的展示,你应用中所有的类都由元数据组装到一起,所以当ApplicationContext创建和实例化后,你就有一个完全可配置和中执行的系统或应用。
第1章-Spring深入浅出IoC_第6张图片
在使用Spring框架时,配置Bean的方式有三种:

  • 基于XML配置
  • 基于注解配置
  • 基于Java配置

在后面我们会讲到基于三种方式的Bean配置,为了由浅入深地讲解, 使用BeanFactory的实现类来写基于XML配置的Bean管理,使用ApplicationContext的实现类来实现基于注解和基于Java的配置。

1. BeanFactory

使用前面创建好的Maven项目开始实验,使用简单的XmlBeanFactory来实现Spring的IoC功能。

1.1 使用容器

  1. 创建两个实体类并且存在依赖关系

    // 这个类是被调用方
    public class BeanProvider {
           
        public void sayHello(String name, Integer age) {
           
            System.out.println("name = [" + name + "], age = [" + age + "]");
        }
    }
    
    // 这个类是调用方
    public class BeanExample {
           
        private String name;
        private Integer age;
        private BeanProvider beanProvider;
    
        // Getter and Setter
        
        public void run() {
           
            this.beanProvider.sayHello(this.name, this.age);
        }
    }
    

    Get和Set方法省略

  2. 配置元数据使用Spring来管理类

    在项目的src/main/resources目录下创建bean.xml文件内容如下:

    
    <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">
    
        <bean id="beanExample" class="com.ajn.spring.beanfactory.bean.BeanExample"/>
        <bean id="beanProvider" class="com.ajn.spring.beanfactory.bean.BeanProvider"/>
    
    beans>
    
  3. 编写主方法类开始实验

    public class Main {
           
    
        public static void main(String[] args) {
           
            BeanFactory beanFactory = new XmlBeanFactory(new ClassPathResource("bean.xml"));
            BeanExample bean = (BeanExample) beanFactory.getBean("beanExample");
            bean.run();
        }
    
    }
    

    此时运行代码是会报空指针的,因为BeanExample类中依赖的beanProvider还未注入

1.2 依赖注入

1.2.1 属性注入

<bean id="beanExample" class="com.ajn.spring.beanfactory.bean.BeanExample">
    <property name="name" value="ajn"/>
    <property name="age" value="24"/>
    <property name="beanProvider" ref="beanProvider"/>
bean>

将前面配置好的bean添加property子标签,这些属性会通过set方法注入到创建的实例中。此时运行主方法成功输出:

name = [ajn], age = [24]

我们发现BeanExample类不仅注入了基本数据类型,还有引用的beanProvider。配置实例属性的值有两种方式:

  • value:等值注入,将具体的值注入到实例属性中
  • ref:引用注入,将被依赖类的实例引用注入到属性中

当有依赖注入的情况时,所有参与的类都必须交由Spring容器管理。

1.2.2 构造器注入

BeanExample类,添加一个全参构造方法:

public BeanExample() {
     
}

public BeanExample(String name, Integer age, BeanProvider beanProvider) {
     
    this.name = name;
    this.age = age;
    this.beanProvider = beanProvider;
}

注上面配置的bean子标签替换成constructor-arg配置,使用index指定参数位置:

<bean id="beanExample" class="com.ajn.spring.beanfactory.bean.BeanExample">
    <constructor-arg index="0" value="ajn"/>
    <constructor-arg index="1" value="24"/>
    <constructor-arg index="2" ref="beanProvider"/>
bean>

此时运行主方法输出结果是一样的,不过在这里我们要注意,Spring创建实例也是通过类的构造器来创建的,如果我们配置了一个全参的构造器,那么就无法使用无参构造器,这种情况我们在编码过程中要避免,养成一个好的习惯,创建有参构造器时,也要创建一个无参构造器。

1.2.3 静态工厂注入

创建一个静态工厂类如下:

public class BeanProivderFactory {
     

    public static BeanProvider getBeanProvider() {
     
        return new BeanProvider();
    }

}

配置通过静态工厂方式注入:

<bean id="beanExample" class="com.ajn.spring.beanfactory.bean.BeanExample">
    <constructor-arg index="0" value="ajn"/>
    <constructor-arg index="1" value="24"/>
    <constructor-arg index="2" ref="beanProviderFactory"/>
bean>

<bean id="beanProviderFactory" class="com.ajn.spring.beanfactory.bean.BeanProivderFactory" factory-method="getBeanProvider"/>

首先把工厂类的bean,然后配置factory-method属性指定工厂方法,再把beanProvider的引用换成工厂类的引用。运行主方法会输出同样的内容。

1.3 自动注入(autowire)

<bean id="beanExample" class="com.ajn.spring.beanfactory.bean.BeanExample" autowire="byName">
    <property name="name" value="ajn"/>
    <property name="age" value="24"/>
bean>

将Bean添加属性autowire值为byName配置自动注入,几种常用的自动注入方式:

  • no:默认值,不使用自动装配,使用注入
  • byName:通过属性名称和bean的名称自动注入
  • byType:通过属性类型和bean的类型自动注入
  • constructor:通过构造器参数以byType的方式注入

可以在 标签添加 default-autowire 属性来指定默认自动注入方式。此时Spring中所有的bean都是自动注入的候选者,有时候我们不希望其中某一个bean通过自动注入机制注入到其他bean,可以在这个bean上添加 autowire-candidate="false" 属性,这样就从自动注入中排除这个bean。还有一种情况,当有多个候选者bean都符合注入的要求时,比如一个接口有多个实现类,这时Spring不知道会注入哪一个,这时可以在一个bean上的 标签上添加 primary="true" 属性来确定主要候选者。

根节点上可以配置很多默认属性,自己慢慢去挖掘。

这里我们最简单地展示注入,还有一些特殊情况配置,比如:注入内部类、注入集合、注入空值等。

1.4 指定依赖关系(depends-on)

有时候Bean之间的依赖关系并不是特别明确,比如初始化一个静态类,可以使用depends-on属性来明确指定在初始化该bean之前,强制初始化一个或多个bean。例如:

<bean id="beanOne" class="ExampleBean" depends-on="manager,accountDao">
    <property name="manager" ref="manager" />
bean>

<bean id="manager" class="ManagerBean" />
<bean id="accountDao" class="x.y.jdbc.JdbcAccountDao" />

depends-on配置多个bean分隔符可以用逗号、空格或分号

除了指定依赖关系之外,标签还可以配置抽象abstract="true"属性和配置继承parent="accountDao"指定继承关系。

1.5 作用域(scope)

1.5.1 单例作用域

默认Spring容器管理的bean都会以单例模式创建一个实例,当这个bean被别的bean引用时,都只会引用这一个实例,Spring的单例模式不同于我们平时使用的单例模式,四人帮定义的单例是指在JVM进程中仅有一个实例,而Spring的单例是指在一个Spring容器中仅有一个实例。
第1章-Spring深入浅出IoC_第7张图片
XML模式配置单例:


<bean id="accountService" class="com.something.DefaultAccountService"/>

<bean id="accountService" class="com.something.DefaultAccountService" scope="singleton"/>

1.5.2 原型作用域

每次请求以原型作用域模式创建的bean时都会创建一个该bean新的实例,也就是在该bean被注入其他bean或者使用getBean()方法来请求它的时候。一般情况下,我们会对有状态bean使用原型作用域,对无状态bean使用单例作用域。比如:DAO4层不会配置成原型作用域,因为典型的DAO不会保持任何会话状态。与其他作用域不同的是,Spring不管理一个原型作用域bean的完整的生命周期。
第1章-Spring深入浅出IoC_第8张图片
XML模式配置原型:

<bean id="accountService" class="com.something.DefaultAccountService" scope="prototype"/>

除了单例模式和原型模式的作用域之外,还有Request, Session, Application, 和WebSocket作用域,我们还可以自定义作用域,具体怎么操作自己拓展。

1.6 懒加载(lazy-init)

默认情况下,IoC容器会以单例模式初始化创建好所有的bean实例,通常这种预先实例化的方案是可取的,因为配置或环境的错误在容器启动的时候就会被立即发现。如果这种方案不能满足你的需求,可以配置在第一次请求bean的时候创建实例,在XML配置中,在 标签使用 lazy-init 属性:

<bean id="lazy" class="com.something.ExpensiveToCreateBean" lazy-init="true"/>

但是,如果一个懒加载的bean被别的非懒加载单例bean依赖时,它也会在容器启动的时候创建,因为必须要保持一个安全的依赖关系。

1.7 使用外部属性值(${ })

我们可以将一些配置属性,比如:数据库连接地址、用户名和密码等相关配置,这些配置通常是与环境相关并且修改的几率很大,所以为了降低修改主XML文件或容器文件的复杂性和风险,我们会把它们抽象到一个.properties文件中,并通过点位符来注入到bean属性中。

修改bean.xml配置,使用来导入资源文件:


<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:property-placeholder location="classpath:spring.properties"/>

    <bean id="beanExample" class="com.ajn.spring.beanfactory.bean.BeanExample">
        <property name="name" value="${name}"/>
        <property name="age" value="${age}"/>
        <property name="beanProvider" ref="beanProvider"/>
    bean>

    <bean id="beanProvider" class="com.ajn.spring.beanfactory.bean.BeanProvider"/>

beans>

src/main/resources目录下新建资源文件spring.properties如下:

name=ajn
age=24

classpath表示类所在的路径,${key}为占位符,key对应.properties文件中的key。

2. ApplicationContext

前面我们使用简单的BeanFactory接口完成了IoC容器管理Bean的实验,后面将使用功能更多的ApplicationContext来实现其他功能。该接口下有很多开箱即用的实现类,比如ClassPathXmlApplicationContextFileSystemXmlApplicationContext,因为我们在实际应用中,用的最多的也是该接口,它是BeanFactory的子接口,所有功能更强大。修改主方法类:

public class Main {
     

    public static void main(String[] args) {
     
        ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("bean.xml");
        // applicationContext.registerShutdownHook();
        BeanExample bean = (BeanExample) applicationContext.getBean("beanExample");
        bean.run();
        applicationContext.close();
    }

}

applicationContext.registerShutdownHook();applicationContext.close();这两个方法都是为了演示销毁的生命周期而用,至于它们有什么区别自己拓展。

2.1 生命周期

第1章-Spring深入浅出IoC_第9张图片
生命周期

  • InitializingBean接口只含有一个方法afterPropertiesSet(),在容器初始化bean之前执行
  • DisposableBean接口也只有一个方法destroy(),在容器销毁bean之后执行
  • BeanPostProcessor接口提供两个接口postProcessBeforeInitialization()postProcessAfterInitialization分别在初始化前后执行,如果你需要自定义一些Spring默认不提供的特性或生命周期行为,你可以使用它
  • 一般不推荐使用InitializingBean接口和DisposableBean接口,因为它们会加大代码与Spring的耦合,推荐使用bean的自定义初始化方法init-method和销毁方法destroy-method,配置方法如下:

添加初始化和销毁方法:

public class BeanProvider {
     

    // ...

    public void init() {
     
        System.out.println("BeanProvider.init");
    }

    public void destroy() {
     
        System.out.println("BeanProvider.destroy");
    }

}

然后在对应标签添加两个属性:

<bean id="beanProvider" class="com.ajn.spring.beanfactory.bean.BeanProvider" init-method="init" destroy-method="destroy"/>

在基于注解配置情况时,可以使用 @PostConstruct@PreDestroy 来代替init-methoddestroy-method

Aware接口

容器管理的bean一般不需要了解容器的状态和直接使用容器,但是在某些情况下,是需要在bean中直接对IoC容器进行操作的,这时候,就需要在Bean中设定对容器的感知。该类接口就会提供对应操作的感知,例如:ApplicationContextAwareBeanNameAware,可以获取相应的ApplicationContext和BeanName的信息。需要注意的是,这些接口会将代码绑定到Spring API而且不遵循IoC风格,所以我们只建议将他们使用于需要以编程方式访问容器的基础框架bean。

BeanPostProcessor接口

BeanPostProcessor接口可以用来自定义扩展bean,实现该接口来提供你自己的实例逻辑、依赖解析逻辑等等。如果要在Spring容器完成实例化、配置和初始化bean之后实现一些自定义逻辑,你也可以定义一个或多个BeanValidationPostProcessor接口的实现类来完成。

2.2 生命周期实例

新建类BeanLifeCycle.java并实现相关接口如下:

public class BeanLifeCycle implements BeanNameAware, BeanFactoryAware, ApplicationContextAware, BeanPostProcessor, InitializingBean, DisposableBean {
     

    @Override
    public void setBeanName(String name) {
     
        System.out.println("BeanLifeCycle.setBeanName");
    }

    @Override
    public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
     
        System.out.println("BeanLifeCycle.setBeanFactory");
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
     
        System.out.println("BeanLifeCycle.setApplicationContext");
    }

    @Override
    public void afterPropertiesSet() throws Exception {
     
        System.out.println("BeanLifeCycle.afterPropertiesSet");
    }

    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
     
        System.out.println("BeanLifeCycle.postProcessBeforeInitialization beanName: " + beanName);
        return bean;
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
     
        System.out.println("BeanLifeCycle.postProcessAfterInitialization beanName: " + beanName);
        return bean;
    }

    @Override
    public void destroy() throws Exception {
     
        System.out.println("BeanLifeCycle.destroy");
    }

    private void init() {
     
        System.out.println("BeanLifeCycle.init");
    }

    private void destroyByClass() {
     
        System.out.println("BeanLifeCycle.destroyByClass");
    }
}

添加XML配置如下:

<bean id="beanLifeCycle" class="com.ajn.spring.applicationcontext.BeanLifeCycle" init-method="init" destroy-method="destroyByClass"/>

运行输出结果:

BeanLifeCycle.setBeanName
BeanLifeCycle.setBeanFactory
BeanLifeCycle.setApplicationContext
BeanLifeCycle.afterPropertiesSet
BeanLifeCycle.init
BeanLifeCycle.postProcessBeforeInitialization beanName: beanProvider
BeanProvider.init
BeanLifeCycle.postProcessAfterInitialization beanName: beanProvider
BeanLifeCycle.postProcessBeforeInitialization beanName: beanExample
BeanExample.init
BeanLifeCycle.postProcessAfterInitialization beanName: beanExample
name = [ajn], age = [24]
BeanExample.destroy
BeanProvider.destroy
BeanLifeCycle.destroy
BeanLifeCycle.destroyByClass

3. 基于注解的配置

bean.xml中配置:


<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:annotation-config/>

    <bean id="beanExample" class="com.ajn.spring.beanfactory.bean.BeanExample"/>

    <bean id="beanProvider" class="com.ajn.spring.beanfactory.bean.BeanProvider"/>

beans>

配置只能在该配置所在容器的bean中寻找注解,如果你在DispatcherServlet中一个WebApplicationContext中配置,它只能寻找到Controller中的注解,不会找到Service中的注解。

3.1 @Autowired

@Autowired注解提供的功能和前面讲的自动注入(autowire)功能一样。重新修改两个类:

// 被注入的bean
public class BeanProvider {
     

    public void sayHello(String name, Integer age) {
     
        System.out.println("name = [" + name + "], age = [" + age + "]");
    }

}

// 通过字段注入
public class BeanExample {
     

    @Autowired
    private BeanProvider beanProvider;

    public void run() {
     
        String name = "ajn";
        Integer age = 24;
        this.beanProvider.sayHello(name, age);
    }

}

// 通过构造器方法注入
public class BeanExample {
     

    private BeanProvider beanProvider;

    @Autowired
    public BeanExample(BeanProvider beanProvider) {
     
        this.beanProvider = beanProvider;
    }

    public void run() {
     
        String name = "ajn";
        Integer age = 24;
        this.beanProvider.sayHello(name, age);
    }

}

运行主方法类输出结果可知,beanProvider已经被注入了。@Autowired注解不仅能在字段上注入bean,还可以用其他方式:

  • 在构造器方法上使用
  • 在任何方法上使用,只要该方法参数中有被注入的bean
  • 在任何方法的参数前使用,该参数为被注入的bean

所以,总的来讲就是使用该注解可以把bean注入到另一个的bean的字段中,或方法的参数中,这些方法不局限于构造器方法或setter方法。

默认情况下自动注入的bean没有候选者,程序就会因为自动注入失败而报异常。当一个bean是否成功注入对程序没有什么影响,你可以在该注解上添加属性:@Autowired(required = false) 来避免报异常。

3.2 @Qualifier

前面说过通过类型自动注入时,当有多个候选者bean都符合注入的要求时,比如一个接口有多个实现类,这时Spring不知道会注入哪一个,这时可以在一个bean上的 标签上添加 primary="true" 属性来确定主要候选者(参考:2.1.3)。这是一种解决方案,还有另一种方案就是在@Autowired注解后使用@Qualifier注解来指定候选者,也就是指定某个需要注入的bean。

可以指定bean的名称:

public class BeanExample {
     

    @Autowired
    @Qualifier("beanProvider")
    private BeanProvider beanProvider;

    // ...
}

可以自定义Qualifier名称:

<bean class="example.SimpleMovieCatalog">
    <qualifier value="main"/> 
bean>
public class MovieRecommender {
     

    @Autowired
    @Qualifier("main")
    private MovieCatalog movieCatalog;

    // ...
}

使用@Qualifier可以更细粒度地选择bean的注入候选者,上面演示的是在XML中标签下配置子标签来配置名称,当基于注解配置bean时,可以在类上面使用该注解来配置qualifier名称,如下:

@Component
@Qualifier("Action")
public class ActionMovieCatalog implements MovieCatalog {
     
    // ...
}

3.3 @Resource

@Resource注解作用和@Autowired作用类似,它是JDK自带的注解,只不过它默认是通过bean名称注入,也就是我们前面讲过的byName自动注入方式。

public class SimpleMovieLister {
     

    private MovieFinder movieFinder;

    @Resource(name="myMovieFinder") 
    public void setMovieFinder(MovieFinder movieFinder) {
     
        this.movieFinder = movieFinder;
    }
}

如果不指定name属性,注解在字段上则会取字段名,注解在setter方法上则会取bean的属性名称:

public class SimpleMovieLister {
     

    private MovieFinder movieFinder;

    @Resource
    public void setMovieFinder(MovieFinder movieFinder) {
     
        this.movieFinder = movieFinder;
    }
}

@Resource默认会通过名称注入bean,如果找不到对应名称的bean,才会通过类型注入,但一但指定name属性,则只会通过名称注入。@Autowired注解只能通过类型注入,要想实现通过名称注入,则要结合@Qualifier注解使用。

3.4 @Value

在1.7节我们讲了使用外部属性值,使用@Value在基于注解的情况下注入属性值:

@Component
public class BeanExample {
     

    @Value("${name}")
    private String name;
    @Value("${age}")
    private Integer age;
    @Resource
    private BeanProvider beanProvider;

    public void run() {
     
        this.beanProvider.sayHello(this.name, this.age);
    }

}

3.5 @PostConstruct和@PreDestroy

在前面讲生命周期的时候说到了配置自定义初始化方法init-method和销毁方法destroy-method,在基于注解配置的情况下,我们使用@PostConstruct@PreDestroy来指定自定义初始化方法和销毁方法:

public class BeanExample {
     

    // ...

    @PostConstruct
    public void init() {
     
        System.out.println("BeanExample.init");
    }

    @PreDestroy
    public void destroy() {
     
        System.out.println("BeanExample.destroy");
    }

}

3.6 @Component

前面我们都是使用XML方式来配置元数据,定义一个bean的,即便前面讲了好多使用注解方式配置,但bean的定义依然是在XML文件中配置的,本节介绍通过注解指定并通过扫描类路径来检测候选组件。只需将bean.xml配置改写成:


<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="com.ajn.spring"/>

beans>

包含了 的功能,所以我们只需配置一个。

修改类为:

@Component
public class BeanProvider {
     

    public void sayHello(String name, Integer age) {
     
        System.out.println("name = [" + name + "], age = [" + age + "]");
    }

}

@Component
public class BeanExample {
     

    @Resource
    private BeanProvider beanProvider;

    public void run() {
     
        String name = "ajn";
        Integer age = 24;
        this.beanProvider.sayHello(name, age);
    }
}

Spring还提供了同样功能的注解@Repository@Service@Controller,它们用于区分分层开发模型中bean的类型,比如:@Repository用于数据持久层,它可以自动转换异常,@Service用于业务层,@Controller用于控制层。

使用这些注解创建的bean的名称默认是类名转化成小驼峰的形式,例如:BeanProvider的bean的名称是beanProvider,也可以自己指定bean的名称:

@Component("beanProvider")
public class BeanProvider {
     

    public void sayHello(String name, Integer age) {
     
        System.out.println("name = [" + name + "], age = [" + age + "]");
    }

}

3.7 @Scope

基于注解配置bean时也可以使用注解@Scope来指定作用域,例如使用原型模式:

@Component
@Scope(BeanDefinition.SCOPE_PROTOTYPE)
public class BeanProvider {
     

    public void sayHello(String name, Integer age) {
     
        System.out.println("name = [" + name + "], age = [" + age + "]");
    }

}

4. 基于Java的配置

4.1 @Configuration

这两个注解用于基于Java方式注册bean,下面是基于XML配置的bean:

<beans>
    <bean id="beanExample" class="com.ajn.spring.beanfactory.bean.BeanExample"/>
    <bean id="beanProvider" class="com.ajn.spring.beanfactory.bean.BeanProvider"/>
beans>

替换成基于Java配置的形式如下:

@Configuration
public class AppConfig {
     

    @Bean
    public BeanExample beanExample() {
     
        return new BeanExample();
    }

    @Bean
    public BeanProvider beanProvider() {
     
        return new BeanProvider();
    }

}

修改主方法类:

  1. 通过AnnotationConfigApplicationContext简单启动

    public class Main {
           
    
        public static void main(String[] args) {
           
            AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
            BeanExample beanExample = applicationContext.getBean(BeanExample.class);
            beanExample.run();
            applicationContext.close();
        }
    
    }
    
  2. 通过register(Class…)方法构建容器

    public class Main {
           
    
        public static void main(String[] args) {
           
            AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext();
            applicationContext.register(AppConfig.class);
            applicationContext.refresh();
            BeanExample beanExample = applicationContext.getBean(BeanExample.class);
            beanExample.run();
            applicationContext.close();
        }
    
    }
    

4.2 @Bean

前面说过@Bean注解可以用于基于Java定义bean,下面是该注解的详细用法:

  1. 参数自动注入,使用注解的方法可以有任意个参数,依赖的bean都会通过参数注入,比如:BeanExample依赖BeanProvider,可以通过如下方式使用参数自动依赖

    @Configuration
    public class AppConfig {
           
    
        @Bean
        public BeanExample beanExample(BeanProvider beanProvider) {
           
            BeanExample beanExample = new BeanExample();
            beanExample.setBeanProvider(beanProvider);
            return beanExample;
        }
    
        @Bean
        public BeanProvider beanProvider() {
           
            return new BeanProvider();
        }
    
    }
    
  2. 添加属性initMethoddestroyMethod使用生命周期方法

    @Configuration
    public class AppConfig {
           
    
        @Bean(initMethod = "init",  = "destroy")
        public BeanExample beanExample() {
           
            return new BeanExample();
        }
    }
    
    // 在类中添加方法
    public class BeanExample {
           
    
        // ...
    
        private void init() {
           
            System.out.println("BeanExample.init");
        }
    
        private void destroy() {
           
            System.out.println("BeanExample.destroy");
        }
    }
    
  3. 默认情况下使用该注解所在的方法名作为bean的名称,添加属性name可以另行配置

    @Configuration
    public class AppConfig {
           
    
        @Bean(name = "myThing")
        public Thing thing() {
           
            return new Thing();
        }
    }
    
  4. 可以结合@Profile@Scope@Lazy@DependsOn@Primary等注解使用。

    • @Profile用来区分环境,我们后面会讲到,表示只有在某个环境下才实例化当前bean
    • @Scope可以用来配置单例模式还是原型模式(见1.5)
    • @Lazy表示懒加载(见1.6)
    • @DependsOn指定依赖关系(见1.4)
    • @Primary定义该bean为依赖的主要候选者(见1.3)
    @Configuration
    public class AppConfig {
           
    
        @Bean
        @DependsOn("beanProvider")
        @Scope(BeanDefinition.SCOPE_SINGLETON)
        @Primary
        @Profile("prod")
        @Lazy
        public BeanExample beanExample() {
           
            return new BeanExample();
        }
    
        @Bean
        public BeanProvider beanProvider() {
           
            return new BeanProvider();
        }
    
    }
    

4.3 @ComponentScan

基于XML方式配置使用注解@Component@Repository@Service@Controller创建bean的配置如下:

<beans>
    <context:component-scan base-package="com.ajn.spring"/>
beans>

修改为使用@ComponentScan注解配置:

@Configuration
@ComponentScan("com.ajn.spring")
public class AppConfig {
     

}

也可以在主方法中使用scan(String…)方法来实现这个功能:

public class Main {
     

    public static void main(String[] args) {
     
        AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext();
        applicationContext.scan("com.ajn.spring");
        applicationContext.refresh();
        BeanExample beanExample = applicationContext.getBean(BeanExample.class);
        beanExample.run();
        applicationContext.close();
    }

}

4.4 @Import

在基于XML配置时,我们使用标签来导入另一个bean.xml文件,在程序设计的时候,可以根据模块来定义不同的bean.xml文件。在使用基于Java配置时,可以使用@Import注解来导入另一个bean配置类:

@Configuration
public class ConfigA {
     

    @Bean
    public A a() {
     
        return new A();
    }
}

@Configuration
@Import(ConfigA.class)
public class ConfigB {
     

    @Bean
    public B b() {
     
        return new B();
    }
}

这样处理的话,就不需要在主方法里面实例化容器的时候配置两个类了,上面的例子只需要配置ConfigB就可以。

4.5 @Conditional

有时我们需要会有条件的启用或禁用@Configuration注解的类和@Bean注解的方法,基于一些系统状态而对bean是否注册的控制。此时可以使用@Conditional来完成这个功能,新建一个类实现Spring提供的Condition接口:

public class TestCondition implements Condition {
     

    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
     
        return false;
    }

}

@Configuration注解下或@Bean注解下使用@Conditional注解并使用前面的类:

@Configuration
@Conditional(TestCondition.class)
public class AppConfig {
     

    @Bean
    public BeanExample beanExample() {
     
        return new BeanExample();
    }

    @Bean
    public BeanProvider beanProvider() {
     
        return new BeanProvider();
    }

}

此时,matches()方法返回falseAppConfig类就不会生效,两个bean也不会被注册进容器,返回true时才能正常注册。

4.6 @ImportResource

在Spring项目中可以组合使用基于Java和基于XML的配置。

4.6.1 基于XML配置为主

基于XML配置的Spring容器要使用@Configuration注解,只需把声明该注解的类当作普通的bean即可,例如存在如下类:

@Configuration
public class AppConfig {
     

    @Autowired
    private DataSource dataSource;

    @Bean
    public AccountRepository accountRepository() {
     
        return new JdbcAccountRepository(dataSource);
    }

    @Bean
    public TransferService transferService() {
     
        return new TransferService(accountRepository());
    }
}

要想使上面的配置类生效,只要system-test-config.xml在配置文件中声明bean:

<beans>
    
    <context:annotation-config/>
    <context:property-placeholder location="classpath:/com/acme/jdbc.properties"/>

    <bean class="com.acme.AppConfig"/>

    <bean class="org.springframework.jdbc.datasource.DriverManagerDataSource">
        <property name="url" value="${jdbc.url}"/>
        <property name="username" value="${jdbc.username}"/>
        <property name="password" value="${jdbc.password}"/>
    bean>
beans>

此时主方法加载XML配置:

public static void main(String[] args) {
     
    ApplicationContext ctx = new ClassPathXmlApplicationContext("classpath:/com/acme/system-test-config.xml");
    TransferService transferService = ctx.getBean(TransferService.class);
    // ...
}

查看@Configuration源码,可以看出它使用了元注解@Component,所以我们可以使用,通过扫描配置类所在包来创建bean。将XML文件修改如下:

<beans>
    
    <context:component-scan base-package="com.acme"/>
    <context:property-placeholder location="classpath:/com/acme/jdbc.properties"/>

    <bean class="org.springframework.jdbc.datasource.DriverManagerDataSource">
        <property name="url" value="${jdbc.url}"/>
        <property name="username" value="${jdbc.username}"/>
        <property name="password" value="${jdbc.password}"/>
    bean>
beans>

4.6.2 基于Java配置为主

基于Java配置的Spring容器要使用XML文件配置,很简单,只需使用@ImportResource注解来导入XML文件,例如:

@Configuration
@ImportResource("classpath:/com/acme/properties-config.xml")
public class AppConfig {
     

    @Value("${jdbc.url}")
    private String url;

    @Value("${jdbc.username}")
    private String username;

    @Value("${jdbc.password}")
    private String password;

    @Bean
    public DataSource dataSource() {
     
        return new DriverManagerDataSource(url, username, password);
    }
}
properties-config.xml
<beans>
    <context:property-placeholder location="classpath:/com/acme/jdbc.properties"/>
beans>

此时主方法加载注解启动:

public static void main(String[] args) {
     
    ApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class);
    TransferService transferService = ctx.getBean(TransferService.class);
    // ...
}

5. Environment

5.1 @Profile

Environment接口是集成在容器中抽象,它包含了应用程序环境两个关键的点:profiles和properties。

profiles是在bean定义时的命名、逻辑组,只有在profile激活状态下才能将bean注册到容器中。beans被分配到profile中,不管是基于XML定义还是基于注解定义的。Environment与profiles有关的作用是决定哪些profile(如果有)被激活,或者哪些profile(如果有)应该要默认激活。

properties几乎在所有应用程序中扮演着重要的角色,它有多个来源:properties文件、JVM系统属性、系统环境变量、JNDI、Servlet上下文参数、特殊设置的Properties对象和Map对象等等。Environment与properties有关的作用是给用户提供一个方便的服务接口来配置属性和解析属性。

@Profile注解是基于@Conditional注解实现的,源码如下:

// 注解源码
@Target({
     ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(ProfileCondition.class)
public @interface Profile {
     

	/**
	 * The set of profiles for which the annotated component should be registered.
	 */
	String[] value();

}

// 接口实现源码
class ProfileCondition implements Condition {
     

	@Override
	public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
     
		MultiValueMap<String, Object> attrs = metadata.getAllAnnotationAttributes(Profile.class.getName());
		if (attrs != null) {
     
			for (Object value : attrs.get("value")) {
     
				if (context.getEnvironment().acceptsProfiles(Profiles.of((String[]) value))) {
     
					return true;
				}
			}
			return false;
		}
		return true;
	}

}

Bean定义profile在核心容器中提供了一种在不同环境中注册不同bean的机制。environment这个词对于不同的用户意味着不同的东西,这个特性有很多使用场景,包括:

  • 在QA或生产环境中注册不同的数据源
  • 应用程序部署到性能环境时注册监控基础框架
  • 为消费者A与消费者B部署注册不同的实现

拿第一个场景举例,在实际应用中需要配置一个数据源DataSource,在开发环境配置如下:

@Bean
public DataSource dataSource() {
     
    return new EmbeddedDatabaseBuilder()
        .setType(EmbeddedDatabaseType.HSQL)
        .addScript("my-schema.sql")
        .addScript("my-test-data.sql")
        .build();
}

假设在QA或者生产环境,数据源配置如下:

@Bean
public DataSource dataSource() throws Exception {
     
    Context ctx = new InitialContext();
    return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource");
}

问题是怎样基于当前环境来选择两个配置。现在,Spring用户有很多种解决方案,比如使用一组环境变量和XML配置${placeholder}占位符来实现,这样当前配置就可以根据环境变量来区分。bean定义profile这个核心容器的特性也提供了一种解决方案。使用这个特性,我们可以定义一个环境专有的bean。

5.1.1 基于Java定义Profile

使用@Profile注解表示当一个或多个profile激活的时候注册某个组件。使用前面的例子:

类级配置

// 开发环境
@Configuration
@Profile("development")
public class StandaloneDataConfig {
     

    @Bean
    public DataSource dataSource() {
     
        return new EmbeddedDatabaseBuilder()
            .setType(EmbeddedDatabaseType.HSQL)
            .addScript("classpath:com/bank/config/sql/schema.sql")
            .addScript("classpath:com/bank/config/sql/test-data.sql")
            .build();
    }
}

// 生产环境
@Configuration
@Profile("production")
public class JndiDataConfig {
     

    @Bean(destroyMethod="")
    public DataSource dataSource() throws Exception {
     
        Context ctx = new InitialContext();
        return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource");
    }
}

指定profile时可以使用一些简单的逻辑运算符:

  • !:非,例如:!prod表示在prod没激活的时候注册
  • &:与,例如:local & dev表示在local和dev同时激活的时候注册
  • |:或,例如:local | dev表示在local或dev激活的时候注册

建议将环境的profile定义使用自定义注解封装起来:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Profile("production")
public @interface Production {
     
}

方法级配置

@Configuration
public class AppConfig {
     

    @Bean("dataSource")
    @Profile("development") 
    public DataSource standaloneDataSource() {
     
        return new EmbeddedDatabaseBuilder()
            .setType(EmbeddedDatabaseType.HSQL)
            .addScript("classpath:com/bank/config/sql/schema.sql")
            .addScript("classpath:com/bank/config/sql/test-data.sql")
            .build();
    }

    @Bean("dataSource")
    @Profile("production") 
    public DataSource jndiDataSource() throws Exception {
     
        Context ctx = new InitialContext();
        return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource");
    }
}

为什么这里@Bean注解要指定bean的名称?基于前面讲解的这个注解,自己想想为什么。

5.1.2 基于XML定义Profile

配置文件级别配置


<beans profile="development"
    xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:jdbc="http://www.springframework.org/schema/jdbc"
    xsi:schemaLocation="...">

    <jdbc:embedded-database id="dataSource">
        <jdbc:script location="classpath:com/bank/config/sql/schema.sql"/>
        <jdbc:script location="classpath:com/bank/config/sql/test-data.sql"/>
    jdbc:embedded-database>
beans>


<beans profile="production"
    xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:jee="http://www.springframework.org/schema/jee"
    xsi:schemaLocation="...">

    <jee:jndi-lookup id="dataSource" jndi-name="java:comp/env/jdbc/datasource"/>
beans>

标签级别配置

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:jdbc="http://www.springframework.org/schema/jdbc"
    xmlns:jee="http://www.springframework.org/schema/jee"
    xsi:schemaLocation="...">

    

    
    <beans profile="development">
        <jdbc:embedded-database id="dataSource">
            <jdbc:script location="classpath:com/bank/config/sql/schema.sql"/>
            <jdbc:script location="classpath:com/bank/config/sql/test-data.sql"/>
        jdbc:embedded-database>
    beans>

    
    <beans profile="production">
        <jee:jndi-lookup id="dataSource" jndi-name="java:comp/env/jdbc/datasource"/>
    beans>
beans>

5.1.3 激活Profile和默认Profile

如果我们配置好上面的示例后启动项目,肯定会报NoSuchBeanDefinitionException异常,因为容器找不到命名为dataSource的bean。需要激活profile才能注册相应环境配置的bean,在主方法类里面激活:

AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.getEnvironment().setActiveProfiles("development");
ctx.register(SomeConfig.class, StandaloneDataConfig.class, JndiDataConfig.class);
ctx.refresh();

另外也可以通过声明spring.profiles.active属性来激活,可以在系统环境变量、JVM系统属性、在web.xml中的Servlet上下文参数等等(见4.8),在测试模块中也可以使用@ActiveProfiles注解来配置激活。也可以同时激活多个profile:

ctx.getEnvironment().setActiveProfiles("profile1", "profile2");

也可以配置默认就激活的Profile:

@Configuration
@Profile("default")
public class DefaultDataConfig {
     

    @Bean
    public DataSource dataSource() {
     
        return new EmbeddedDatabaseBuilder()
            .setType(EmbeddedDatabaseType.HSQL)
            .addScript("classpath:com/bank/config/sql/schema.sql")
            .build();
    }
}

如果没有profile被激活,则会使用默认值,只要有一个profile被激活,默认值就会失效。你也可以用EnvironmentsetDefaultProfiles()方法来配置,或者声明spring.profiles.default属性。

5.2 @PropertySource

在了解这个注解之前讲讲Spring的环境属性机制,前面说到Environment包含profile和properties,现在就讲讲properties。它提供了层级属性源的搜索功能,如下:

ApplicationContext ctx = new GenericApplicationContext();
Environment env = ctx.getEnvironment();
boolean containsMyProperty = env.containsProperty("my-property");
System.out.println("Does my environment contain the 'my-property' property? " + containsMyProperty);

在上面的例子中,我们使用一种高级的方式来判断当前环境是否定义了my-property属性。Environment对象会从一组PropertySource对象中搜索,一个PropertySource是一些简单的键值对属性,Spring中StandardEnvironment包含两个PropertySource对象,JVM系统属性(System.getProperties())和系统环境变量(System.getenv())。上面例子中,当Java系统属性或环境变量中配置了my-property属性,env.containsProperty("my-property")就会返回true

属性的搜索存在优先级,比如在多个地方存在同样的属性时,Spring会优先选择某个属性。在一个普通的StandardServletEnvironment中,优先级从高到低如下:

  • ServletConfig参数(如果有,例如DispatcherServlet上下文)
  • ServletContext参数(web.xml上下文入口)
  • JNDI环境变量(java:comp/env/入口)
  • JVM系统属性(-D指定的命令行参数)
  • JVM系统环境(包括操作系统环境变量)

最重要的是,这个机制是完全可配置的。你可能需要集成自己的属性源,为此,只要实现PropertySource并实例化,然后添加到Environment中的PropertySources中,例如:

ConfigurableApplicationContext ctx = new GenericApplicationContext();
MutablePropertySources sources = ctx.getEnvironment().getPropertySources();
sources.addFirst(new MyPropertySource());

在上面的示例中,MyPropertySource拥有最高的优先级,MutablePropertySources提供了一些操作数据源有用的方法。

@PropertySource注解给Spring的Environment添加PropertySource提供了一个简单的的声明机制。创建一个文件app.properties,添加键值对`testbean.name=myTestBean,然后创建下面示例:

@Configuration
@PropertySource("classpath:/com/myco/app.properties")
public class AppConfig {
     

    @Autowired
    Environment env;

    @Bean
    public TestBean testBean() {
     
        TestBean testBean = new TestBean();
        testBean.setName(env.getProperty("testbean.name"));
        return testBean;
    }
}

除了使用Environment来获取属性,也可以使用@Value${}占位符来获取。

6. ApplicationContext扩展

在本节前面讲了org.springframework.beans.factory包的BeanFactory接口,后来又说了org.springframework.context包的ApplicationContext接口,它继承了BeanFactory接口,提供了更强大的功能,为了使BeanFactory功能更具面向框架风格,context包还提供了下面的功能:

  • 通过MessageSource接口以国际化风格访问消息
  • 通过ResourceLoader接口访问像URL和文件资源
  • 通过ApplicationListenerApplicationEventPublisher接口来监听和发布事件
  • 通过HierarchicalBeanFactory接口来加载多个分级context,让每个容器都专注特定的层,例如:应用程序的Web层

这些功能就不全讲,需要使用的时候完全可以去找资料文档来使用这些功能。

6.1 监听事件

ApplicationContext通过ApplicationEvent类和ApplicationListener接口提供了事件处理机制,每次ApplicationEvent事件发布到ApplicationContext时,实现了ApplicationListener接口的bean就会被通知,这运用了标准的观察者模式。

6.1.1 @EventListener

Spring提供了一些标准事件如下:

  • ContextRefreshedEvent
  • ContextStartedEvent
  • ContextStoppedEvent
  • ContextClosedEvent
  • RequestHandledEvent

它们的功能就不介绍了,自己去查询资料,我们拿ContextClosedEvent举例,新建监听类:

@Service
public class ApplicationNotifier implements ApplicationListener<ContextClosedEvent> {
     

    @Override
    public void onApplicationEvent(ContextClosedEvent event) {
     
        System.out.println("ApplicationNotifier.onApplicationEvent");
    }

}

运行程序,当ApplicationContext调用close()方法时就会通知该类。在Spring4.2以后,还提供了基于注解创建监听器,把上面代码修改如下:

@Service
public class ApplicationNotifier {
     

    @EventListener
    public void onApplicationEvent(ContextClosedEvent event) {
     
        System.out.println("ApplicationNotifier.onApplicationEvent");
    }

}

也能达到同样的效果,使用注解配置不需要实现ApplicationListener接口,默认@EventListener注解是根据方法参数指定监听的事件,也可以使用该注解的属性来添加被监听的事件,可以配置多个,例如:

@EventListener({
     ContextStartedEvent.class, ContextRefreshedEvent.class})
public void handleContextStart() {
     
    // ...
}

6.1.2 自定义事件监听

我们除了使用Spring提供的事件外,还可以自己创建和发布自定义事件,创建事件类如下:

public class BlackListEvent extends ApplicationEvent {
     

    private String address;
    private String content;

    public BlackListEvent(Object source, String address, String content) {
     
        super(source);
        this.address = address;
        this.content = content;
    }

    // Getter, Setter and toString
}

通过调用ApplicationEventPublisherpublishEvent()方法来发布一个自定义ApplicationEvent。通常会创建一个bean类来实现ApplicationEventPublisherAware接口并注册,例如:

@Service
public class EmailService implements ApplicationEventPublisherAware {
     

    private List<String> blackList;
    private ApplicationEventPublisher publisher;

    public void setBlackList(List<String> blackList) {
     
        this.blackList = blackList;
    }

    @Override
    public void setApplicationEventPublisher(ApplicationEventPublisher publisher) {
     
        this.publisher = publisher;
    }

    public void sendEmail(String address, String content) {
     
        if (blackList.contains(address)) {
     
            publisher.publishEvent(new BlackListEvent(this, address, content));
            return;
        }
        System.out.println("EmailService.sendEmail");
    }

}

创建监听类:

@Service
public class BlackListNotifier {
     

    @EventListener
    public void onApplicationEvent(BlackListEvent event) {
     
        System.out.println("BlackListNotifier.onApplicationEvent:" + event.toString());
    }
}

6.1.3 异步监听

使用@Async配置监听器异常执行

@EventListener
@Async
public void processBlackListEvent(BlackListEvent event) {
     
    // BlackListEvent is processed in a separate thread
}

使用异步监听要注意两点:

  • 如果监听器抛出了异常,不会传播给调用者,详情看AsyncUncaughtExceptionHandler
  • 这类事件监听不能发送回复。如果需要将处理结果发送给另一个事件,注入ApplicationEventPublisher手动发送

6.1.4 顺序监听

如果一个事件被多个监听器监听,我们可以使用@Order注解来指定监听器执行的顺序

@EventListener
@Order(42)
public void processBlackListEvent(BlackListEvent event) {
     
    // notify appropriate parties via notificationAddress...
}

6.2 Web应用实例化ApplicationContext

你可以使用ContextLoader以声明方式创建ApplicationContext实例。当然,你也可以使用ApplicationContext的任何一个实现来以编程方式创建ApplicationContext实例。

你可以使用ContextLoaderListener来注册一个ApplicationContext,如下:

<context-param>
    <param-name>contextConfigLocationparam-name>
    <param-value>/WEB-INF/daoContext.xml /WEB-INF/applicationContext.xmlparam-value>
context-param>

<listener>
    <listener-class>org.springframework.web.context.ContextLoaderListenerlistener-class>
listener>

监听器会检查contextConfigLocation参数,如果该参数不存在,则会默认使用/WEB-INF/applicationContext.xml。当存在参数并且有多个参数时,可能使用逗号、分号或空格来隔开。也可以使用模糊路径,比如/WEB-INF/*Context.xml(在WEB-INF目录下的所有以Context.xml结尾的文件)和/WEB-INF/**/*Context.xml(在WEB-INF目录下和所有子目录下以Context.xml结尾的文件)。

敬请期待下一章【第4章-架构篇之Spring-卷2】

参考文献

Spring Framework 5.0.0.RC1官方文档:https://docs.spring.io/spring/docs/5.0.0.RC1/spring-framework-reference/overview.html

Spring Framework 5.1.3 官方文档:https://docs.spring.io/spring/docs/5.1.3.RELEASE/spring-framework-reference/


  1. POJO: (Plain Ordinary Java Object),普通的Java类 ↩︎

  2. EJB: (Enterprise Java Bean),企业级JavaBean ↩︎

  3. 参考文献:Spring的核心机制:依赖注入(控制反转)https://blog.csdn.net/a906998248/article/details/7514085 ↩︎

  4. DAO: (Data Access Object),数据访问对象 ↩︎

你可能感兴趣的:(Spring,java,spring,框架)