Spring学习笔记(基础使用篇)

前言

Spring是Java EE编程领域的一个轻量级开源框架,最早由Rod Johnson在 2002 年提出并随后创建,是为了解决企业级编程开发中的复杂性,实现敏捷开发的应用型框架 。框架的构成如下图所示,核心的模块有控制反转容器、面向切面编程、Web开发支持等。

Spring与传统的EJB相比,具有下面一些优势。

  • IOC支持:提供IOC容器,可以将对象间的依赖关系交由Spring进行控制,避免硬编码所造成的过度程序耦合。
  • AOP支持:使用面向切面编程可以十分方便的解决一些传统OOP非常棘手的问题。
  • 事务支持:使用声明式事务方式灵活的进行事务的管理, 提高开发效率和质量。
  • 拥抱开源:提供了各种优秀框架和第三方库的集成和封装,降低其使用难度。
  • 轻量且高效:Spring的 jar 包非常小,且运行时占用资源少,运行效率高。

第一章 Spring IOC

第一节 程序中的耦合

  1. 程序中的耦合

程序中的耦合指的是代码之间的相互依赖关系。如下面一段代码,业务层需要调用持久层进行保存操作,此时业务层就对持久层产生了直接依赖,如果需要对持久层实现类进行替换,需要修改业务层代码,不符合开闭原则。

public class AccountServiceImpl implements AccountService {
    @Override
    public void saveAccount() {
        // 业务层在此处直接依赖了持久层的实现类
        AccountDao accountDao = new AccountDaoImpl(); 
        accountDao.saveAccount();
    }
}
  1. 使用工厂模式来解决耦合

首先将持久层对象的创建交给一个工厂类来实现,那么依赖关系将由服务层->持久层转变为服务层->工厂类。然后在工厂类通过反射来创建我们所需的对象,并且可以将全类名放置在外部配置文件中,以此来解决代码中存在的直接依赖关系。

// 1. 定义一个通用工厂,从配置文件读取全类名,并通过反射创建对象
public class BeanFactory{
    private static Properties env = new Properties();
    
    // 加载配置文件中的类名信息:key = accountDao value = org.example.dao.impl.AccountDaoImpl
    static{
        try {
            InputStream inputStream = BeanFactory.class.getResourceAsStream("/beans.properties");
            env.load(inputStream);
            inputStream.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    
    // 从通用工厂构造对象
    public static Object getBean(String key){
         Object ret = null;
         try {
             Class clazz = Class.forName(env.getProperty(key));
             ret = clazz.newInstance();
         } catch (Exception e) {
            e.printStackTrace();
         }
         return ret;
     }
}

// 2. 服务层通过 key 从工厂获取所需依赖
public class AccountServiceImpl implements AccountService {
    @Override
    public void saveAccount() {
        AccountDao accountDao = BeanFactory.getBean(“accountDao”);
        accountDao.saveAccount();
    }
}

现在对持久层实现类进行动态替换时,只需要修改外部配置文件就可以了,上述这种将业务层对持久层的依赖转移给工厂的解决思路,被称为控制反转(Inversion of Control)。

控制反转是一个概念,是一种思想,指将传统上由程序代码直接操控的对象调用权交给容器,通过容器来实现对象的装配和管理。控制反转就是对象控制权的转移,从程序代码本身反转到了外部容器,通过容器实现对象的创建,属性赋值,依赖的管理。

  1. 使用Spring解决程序耦合

程序中的耦合是一个普遍存在问题,我们没必要为每一个应用都重复的编写上述类似代码,业界已经提供了一个通用的,被大众所认可的优秀解决方案Spring,让我们来看看如何使用Spring来解决程序中的耦合问题。

  • 导入Spring依赖

    <?xml version="1.0" encoding="UTF-8"?>


    4.0.0

      <groupId>org.example</groupId>
      <artifactId>Spring-demo01</artifactId>
      <version>1.0-SNAPSHOT</version>
    
      <dependencies>
          
          <!-- 引入spring-context依赖,同时会引入spring-core、spring-beans、spring-aop、spring-expression-->
          <dependency>
              <groupId>org.springframework</groupId>
              <artifactId>spring-context</artifactId>
              <version>5.2.9.RELEASE</version>
          </dependency>
         
      </dependencies>
    

注意:一般我们会选择较新的Spring 5 版本,这需要你的JDK版本在1.8及以上,Tomcat版本在8.0及以上。

  • 配置IOC容器

需要一个配置文件用来配置 key 和全类名的映射关系,这个文件名一般为application.xml,放在任意类路径下即可。

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">

    <!-- 配置accountDao -->
    <bean id="accountDao" class="org.example.dao.impl.AccountDaoImpl"/>
    
</beans>
  • 使用IOC容器

在业务代码中通过容器来获取所需的依赖,而不是直接创建依赖对象。

public class AccountServiceImpl implements AccountService {
    @Override
    public void saveAccount() {
        // 通过Spring的IOC容器来获取accountDao实现类
        ApplicationContext ctx = new ClassPathXmlApplicationContext("application.xml");
        AccountDao accountDao = (AccountDao)ctx.getBean("accountDao");
        
        accountDao.saveAccount();
    }
}

第二节 IOC容器

  1. ApplicationContext接口

ApplicationContext接口继承自 BeanFactory,代表 Spring 的 IOC 容器,容器读取配置元数据中的指令,进行对象的实例化、配置和组装。Spring为适应不同的场景,提供了多个实现类,常见的几种如下:

实现类 说明
ClassPathXmlApplicationContext 从类路径下加载配置文件,独立应用程序一般使用此种方式
FileSystemXmlApplicationContext 从磁盘位置加载配置文件,受限于操作系统,一般不常用
AnnotationConfigApplicationContext 从Java代码加载配置文件,一般适用于基于Java配置的注解开发模式
XmlWebApplicationContext 适用于Web应用的容器实现类,只有引入了web相关的依赖才能使用

ApplicationContext和BeanFactory的区别

BeanFactory 提供了最基本的容器功能,而 ApplicationContext 是 BeanFactory 的完整超集,添加了更多的企业特定的功能。

  • 面向切面编程(AOP)
  • Web应用(WebApplicationContext)
  • 国际化(MessageSource)
  • 事件传递(Event publication)

特别的,BeanFactory 总是会懒初始化 Bean,而 ApplicationContext 默认情况下在容器启动时初始化所有的Singleton Bean。

  1. 容器的创建

容器的创建需要提供配置元数据,这些元数据可以是XML、注解或Java代码的形式。

  • XML配置方式

    // 加载services.xml和daos.xml中配置的元数据,创建一个Spring的IOC容器
    ApplicationContext context = new ClassPathXmlApplicationContext(“services.xml”, “daos.xml”);

  • 注解配置方式

    // 加载 SpringConfiguration 配置的注解创建容器
    ApplicationContext ac = new AnnotationConfigApplicationContext(SpringConfiguration.class);

SpringConfiguration 的代码如下,一般为一个配置类(@Configuration注解的类)。

// Spring 配置类
//    1. @ComponentScan:扫描 com.itheima.Spring 包中的 spring 组件
//    2. @Import:导入其它类中的配置信息
@Configuration
@ComponentScan(basePackages = "com.itheima.Spring")
@Import({ JdbcConfig.class })
public class SpringConfiguration {}

当然,也可以使用普通的组件类(@Component注解的类)或带有 JSR-330 元数据注解的类。

  • 编程配置方式

    public static void main(String[] args) {
    // 定义一个容器(无参方式)
    AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();

      // 注册配置类
      ctx.register(AppConfig.class, OtherConfig.class);
      ctx.register(AdditionalConfig.class);
      
      // 扫描配置类并刷新容器
      ctx.scan("com.acme");
      ctx.refresh();
      
      // 使用容器
      MyService myService = ctx.getBean(MyService.class);
      myService.show();
    

    }

  1. 容器的关闭

在Web环境中,Spring 的基于 Web 的容器实现已经具有适当的代码,可以在相关 Web 应用程序关闭时正常关闭 Spring IoC 容器。在非 Web 应用程序环境中,请向 JVM 注册一个关闭钩子,这样Spring可以调用你单例 bean 上设置的 destroy 方法,以便释放所有资源。

import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public final class Boot {

    public static void main(final String[] args) throws Exception {
        // 创建容器
        ConfigurableApplicationContext ctx = new ClassPathXmlApplicationContext("beans.xml");

        // 注册关闭钩子
        ctx.registerShutdownHook();	
    }
}

JVM的关闭钩子在什么场景下有效?

答:正常退出、System.exit()、Ctrl+C中断、OutofMemory宕机、Kill pid杀死进程(kill -9除外)、系统关闭等。

  1. 容器的使用

容器创建完成后,我们就可以通过使用方法T getBean(String name, Class requiredType)检索 bean 的实例。

// 通过key和类型检索Spring实例
PetStoreService service = context.getBean("petStore", PetStoreService.class);

除此之外,ApplicationContext接口还有其他几种检索 bean 的方法,但是理想情况下,您的应用程序代码永远不要使用它们。

// 通过key获取对象后,进行强制类型转换
Person person = (Person)ctx.getBean("person");

// 通过类型匹配获取对象(注意:此时只能有一个Bean是Person类型)
Person person = ctx.getBean(Person.class);

容器除了获取对象的方法外,还提供了一些辅助方法,用于获取容器的相关信息。

// 获取Bean总数
int count = ctx.getBeanDefinitionCount();

// 获取所有Bean的id
String[] beanDefinitionNames = ctx.getBeanDefinitionNames();

// 获取所有某类型Bean的id
String[] beanNamesForType = ctx.getBeanNamesForType(UserDao.class);

// 判断是否存在指定id的Bean(不能判断name指定的别名)
boolean isExist = ctx.containsBeanDefinition("userDao")
 
// 判断是否存在指定id的Bean(可以判断name指定的别名)
boolean isExist = ctx.containsBean("userDao")
  1. 容器的扩展
  • BeanPostProcessor

BeanPostProcessor 接口可以在 Bean 的初始化生命周期前后植入一些自定义逻辑,对创建的 Bean 进行二次加工。

// 1. 创建一个 BeanPostProcessor
public class MyBeanPostProcessor implements BeanPostProcessor {
    
	/**
    * 在Bean完成实例化和注入之后、初始化之前执行
    *
    * @bean 		已完成注入后的Bean对象
    * @beanName 	bean的id属性
    * @return 		修改后的Bean对象
    */
    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        return bean;
    }

    /**
    * 在Bean完成初始化之后执行
    *
    * @bean 		已完成初始化后的Bean对象
    * @beanName 	bean的id属性
    * @return 		修改后的Bean对象
    */
    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        System.out.println("Bean '" + beanName + "' created : " + bean.toString());
        return categroy;
    }
}

<!-- 2. 将自定义BeanPostProcessor注册到Spring容器-->
<bean id="myBeanPostProcessor" class="xxx.MyBeanPostProcessor"/>
  • BeanFactoryPostProcessor

BeanFactoryPostProcessor 接口用于读取配置元数据,并在Bean的实例化之前对其进行修改。Spring定义了一些内置的Bean工厂后处理器,如PropertyPlaceholderConfigurer用于解析配置文件中的占位符,并进行字符串替换操作。

<!-- 注册一个内置的BeanFactoryPostProcessor:PropertyPlaceholderConfigurer 
	1. locations:指定外部属性配置文件,一般为properties格式
	2. properties:指定一些键值对形式的属性值
	3. systemPropertiesMode:是否检查System属性
		* never					从不查找System属性
		* fallback				如果从properties-ref和location中未找到需要的属性值,则去System属性查找
		* override/evironment	始终查找System属性,并覆盖其他方式配置的值
-->
<bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
	<property name="locations" value="classpath:com/something/strategy.properties"></property>
	<property name="properties" value="custom.strategy.class=com.something.DefaultStrategy"></property>
</bean>

<!-- 使用占位符来获取配置的属性值,这将会在Bean实例化之前进行替换-->
<bean id="serviceStrategy" class="${custom.strategy.class}"/>

除此之外,与属性相关的Bean工厂后处理器还有 PropertyOverrideConfigurer,用于对配置的属性进行覆盖操作。

<!-- 注册一个内置的BeanFactoryPostProcessor:PropertyOverrideConfigurer 
	location:指定一个外部配置文件,存放需要覆盖的属性和值,以 beanName.property=value 的格式,并支持复合属性
--> 
<context:property-override location="classpath:override.properties"/>

一个 override.properties 配置文件的示例如下。

# 覆盖 dataSource 的 driverClassName 属性值为 com.mysql.jdbc.Driver
dataSource.driverClassName=com.mysql.jdbc.Driver
# 覆盖 tom 的 fred.bob.sammy 属性值为 123
tom.fred.bob.sammy=123
  • Ban(Factory)PostProcessor 的一些处理细节
    1. Bean(Factory)PostProcessor 是容器范围的,对该容器内的所有 Bean 都生效。
    2. Bean(Factory)PostProcessor 会尽早被实例化,即使你为它们配置了懒加载属性,容器也会忽略。
    3. 你定义了多个 Bean(Factory)PostProcessor,可以通过实现Ordered接口并使用order属性来控制它们的执行顺序。
    4. 如果你使用@Bean来创建Bean(Factory)PostProcessor,则必须保证返回类型是该接口的实现类,否则容器无法正确检测。
    5. AOP 中一些基础结构类是基于BeanPostProcessor来实现的,因此该接口的实现类和依赖其的Bean都不适合自动代理。
    6. BeanFactoryPostProcessor 不能对实现了BeanFactoryPostProcessor接口的Bean定义进行修改。
    7. 虽然可以通过BeanFactory.getBean()来提早实例化Bean,但这是不被推荐的,因为可以会绕过 bean 的后处理。
  1. 多文件配置
  • 引入外部配置文件

你也可以将所有元数据写在同一个配置文件中,或者通过标签引入其它文件中的元数据配置。

<beans>
    <!-- 引入根标签为beans的外部配置文件-->
    <import resource="services.xml"/>
    <import resource="resources/messageSource.xml"/>
    <import resource="/resources/themeSource.xml"/>
    
    <!-- 使用 * 作为通配符 (注意:必须保证父配置文件名不能满足 * 所能匹配的格式,否则将出现循环递归包含)-->
    <import resource="dao/spring-*-dao.xml"/>


    <bean id="bean1" class="..."/>
    <bean id="bean2" class="..."/>
</beans>
  • 引入外部属性文件

对于一些需要经常修改的内容,如Jdbc连接信息等,可以单独放在一个小配置文件中,方便维护。

<!--引入外部属性文件方式一:使用PropertyPlaceholderConfigurer -->
<bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
    <property name="db-connection" value="db.properties"></property>
</bean>

<!-- 引入外部属性文件方法二: 要引入context命名空间-->
<!-- <context:property-placeholder location="classpath:db.properties"/>-->

<!--配置连接池-->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
    <property name="driverClassName" value="${db.driverclass}"></property>
    <property name="url" value="${db.url}"></property>
    <property name="username" value="${db.username}"></property>
    <property name="password" value="${db.password}"></property>
</bean>

db.properties文件示例如下:

db.driverclass=com.mysql.jdbc.Driver
db.url=jdbc:mysql://localhost:3306/userDb?characterEncoding=utf-8
db.username=root
db.password=root

第三节 Bean实例化

  1. 通过构造方法创建

Spring配置文件的顶层标签为beans,可以配置多个标签,一个简单的bean配置示例如下:

<bean id="accountService" class="org.example.service.impl.AccountServiceImpl"/>

其中id属性是全局的唯一标识,用于从容器中获取对象。class属性则是对应的全限定类名,默认情况下,使用反射调用类中的无参构造函数来创建对象,如果缺少无参构造则无法创建。

  1. 通过FactoryBean创建

随着业务的复杂性提升,某些复杂对象的创建不能直接通过new关键字来实现,如Connect、SqlSessionFactory等,为此Spring提供了实现FactoryBean接口的方式来完成复杂对象的配置,首先实现FactoryBean接口,示例如下。

然后在配置文件中引用FactoryBean实现类来创建所需要的对象,配置如下:

<bean id="conn" class="com.baizhiedu.factorybean.ConnectionFactoryBean"/>

值得注意的是,这和简单对象的配置很相似,但实际上,Spring会检测class属性配置的类,如果是FactoryBean接口的实现类,那么通过id值获得的是这个类所创建的复杂对象(如:Connection)。

提示:就想获得FactoryBean类型的对象,可以通过ctx.getBean("&conn")的方式获取,得到的就是ConnectionFactoryBean对象。

  1. 通过普通工厂类来创建

虽然上述方式解决了复杂对象创建的问题,但使用的工厂类必须实现FacotryBean接口,为了整合一些遗留系统,Spring还可以使用普通的工厂类来创建所需的对象。

  • 工厂方法为静态方法

这种方式也是在class属性指定使用的工厂类,但需要通过factory-method属性额外指定创建对象的静态方法,示例如下:

public class StaticFactory {
  	// 创建对象的静态方法
  	public static IAccountService createAccountService() {
    	return new AccountServiceImpl();
  	}
}

<bean id="accountService" class="com.itheima.factory.StaticFactory"   
      factory-method="createAccountService"></bean>  
  • 工厂方法为非静态方法

如果factory-method是非静态方法,那么将class属性留空,使用factory-bean属性引用工厂类对象,示例如下:

public class InstanceFactory {
    // 创建对象的非静态方法
    public IAccountService createAccountService(){  
        return new AccountServiceImpl();  
    }  
}  

<!-- 创建工厂类对象 -->
<bean id="instancFactory" class="com.itheima.factory.InstanceFactory"></bean>  

<!-- 创建业务对象
        * factory-bean 属性:用于指定工厂类bean的id
        * factory-method 属性:用于指定实例工厂中创建对象的方法
-->
 <bean id="accountService"   
       factory-bean="instancFactory"   
       factory-method="createAccountService"></bean> 

第四节 Bean的属性

  1. Bean的生命周期

生命周期指Bean从创建到销毁的整个过程,Spring提供了一些机制允许我们对bean生命周期的管理进行干预。

  • @PostConstruct /@PreDestroy注解

@PostConstruct 和 @PreDestroy 是 JSR-250 的生命周期注解,Spring 在 CommonAnnotationBeanPostProcessor 中对其做了实现。在 Bean 的初始化期间会调用被@PostConstruct注解的方法,在销毁期间会调用被@PreDestroy注解的方法。

public class CachingMovieLister {

    @PostConstruct
    public void populateMovieCache() {
        // populates the movie cache upon initialization...
    }

    @PreDestroy
    public void clearMovieCache() {
        // clears the movie cache upon destruction...
    }
}
  • InitializingBean/DisposableBean接口

如果bean实现了InitializingBean接口,容器会在创建bean的时候调用其afterPropertiesSet()方法,如果实现了DisposableBean接口,容器在销毁bean的时候调用其destroy()方法。

<bean id="exampleInitBean" class="examples.AnotherExampleBean"/>

public class AnotherExampleBean implements InitializingBean, DisposableBean {

    public void afterPropertiesSet() {
        // do some initialization work
    }

    public void destroy() {
        // do some destruction work (like releasing pooled connections)
    }
}
  • init-method/destroy-method属性

对于基于 XML 的配置元数据,可以使用init-method/destroy-method属性指定无参无返回值方法的名称作为初始化方法和销毁方法。

<bean id="exampleInitBean" class="examples.ExampleBean" init-method="init" destroy-method="cleanup"/>


public class ExampleBean {

    public void init() {
        // do some initialization work
    }

    public void cleanup() {
        // do some destruction work (like releasing pooled connections)
    }
}

为了简化配置,你可以使用标签的default-init-method/default-destroy-method属性来为所有子标签中的bean指定默认初始化和销毁方法。但必须注意,Spring 容器保证在为 bean 提供所有依赖项后立即调用配置的初始化回调,因此,在原始 bean 引用上调用了初始化回调,这意味着 AOP 拦截器等尚未应用于 bean。

提示:使用Java代码配置元数据时,使用@Bean的initMethod和destroyMethod属性来指定初始化和销毁方法。

  • 不同生命周期机制的执行顺序

如果为一个 bean 配置了多个生命周期机制,当方法名称相同时,该方法将只执行一次。如果方法名称不一致,那么将按照注解配置优先,接口配置次之,XML配置最后执行的原则调用生命周期方法。

1)用 @PostConstruct 注解的方法

2)InitializingBean 回调接口定义的 afterPropertiesSet()方法

3)自定义配置的 init() 方法

4)用 @PreDestroy 注解的方法

5)DisposableBean 回调接口定义的 destroy()方法

6)自定义配置的 destroy() 方法
  • 启动和停止事件回调

可以通过实现Lifecycle接口来定义 Bean 的启动和停止事件回调,当容器本身接收到启动和停止 signal 时(例如,对于运行时的停止/重新启动场景),它将把这些调用级联到在该上下文中定义的所有Lifecycle实现。

// 实现了Lifecycle接口的Bean
@Component
public class MyLifeCycleBean1 implements Lifecycle {
	private boolean status = true;

    // 容器start的时候,如果 isRunning==false,则执行该方法
	@Override
	public void start() {
		System.out.println("start");
	}
    
    // 判断Bean是否正在运行
	@Override
	public boolean isRunning() {
		System.out.println("isRunning");
		status = !status;
		return status;
	}
    
    // 容器stop的时候,如果 isRunning==true,则执行该方法
	@Override
	public void stop() {
		System.out.println("stop");
	}
}

public class SpringTest {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(MyLifeCycleBean1.class);
        ctx.registerShutdownHook();
        ctx.start();
        //ctx.stop(); // 注册关闭钩子后会在容器关闭时调用该方法
    }
}
/*
isRunning
start
isRunning
stop
 */

如果想对 start 和 stop 生命周期进行更精细的控制,可以实现SmartLifecycle接口。如通过isAutoStartup方法返回 true 可以实现容器创建后自动启动;通过getPhase()方法返回合适的相位值来控制启动和停止的顺序,该值越大,则越晚执行,越早销毁,默认值为Integer.MAX_VALUE(如果实现的是LifeCycle接口,则默认为0),详细用法请参考Spring官方文档。

  1. Bean的作用范围

Bean的作用范围决定了Bean的作用域以及在获取对象时是否创建新的实例等,通过scope属性进行配置。

  • singleton

默认配置的作用范围为singleton,表示单例范围,只创建一次,一般在容器启动时创建,容器销毁时随之销毁。

<bean id="accountDao" class="org.example.dao.impl.AccountDaoImpl" scope="singleton"/>
  • prototype

prototype表示原型范围(多例),在每次从容器获取对象时,始终会创建新的实例返回。

<bean id="accountDao" class="org.example.dao.impl.AccountDaoImpl" scope="prototype"/>

与其它作用范围不同的是,Spring不管理原型Bean的完整生命周期,不会调用已配置的生命周期销毁回调。

注意:关于单例bean中注入原型bean的一些细节

  • 在单例Bean中依赖原型Bean时,由于单例Bean只会被IoC容器初始化一次,其依赖也只会被处理一次,因此其依赖的原型Bean也将“隐式”的成为单例。

  • 如何解决这个问题,有两种办法,一种是在使用原型Bean时每次都依赖查找,这样IoC容器会每次都重新创建原型Bean;

  • 另一种办法就是使用@Lookup注解来解决,这种是官方给出解决方案,需注意的是使用@Lookup注解的方法必须声明为抽象方法。

  • 适用于web的作用范围

在Web应用中,提供了一些特殊的bean作用范围,如request、session、application、websocket等,分别与HTTP中的Request、Session、ServletContext、WebSocket作用域一致,这里不做详细讲解,具体可参考Spring的官方文档。

  • 自定义作用范围

Bean 作用域机制是可扩展的,您可以定义自己的范围,甚至重新定义一些内置范围(singleton和prototype除外)。要将自定义范围集成到 Spring 容器中,您需要实现Scope接口。Scope接口有四种方法可以从范围中获取对象,从范围中删除对象,然后销毁它们。

// 从基础范围返回对象,如果找不到该对象,则创建新实例返回
Object get(String name, ObjectFactory objectFactory)

// 从基础范围中删除该对象并返回删除的值,如果找不到该对象则返回null
Object remove(String name)
    
// 注册在销毁作用域或销毁作用域中的指定对象时作用域应执行的回调
void registerDestructionCallback(String name, Runnable destructionCallback)
    
// 获取基础范围的会话标识符,每个范围的标识符都不相同。对于会话范围的实现,此标识符可以是会话标识符。
String getConversationId()

在编写自定义Scope实现之后,您需要使 Spring 容器意识到您的新作用域。通过以下方法在 Spring 容器中注册新的Scope。

Scope threadScope = new SimpleThreadScope();
beanFactory.registerScope("thread", threadScope);

除了使用程序进行Scope的注册外,您还可以通过使用CustomScopeConfigurer类以声明方式进行Scope注册,如以下示例所示:

......

    <bean class="org.springframework.beans.factory.config.CustomScopeConfigurer">
        <property name="scopes">
            <map>
                <entry key="thread">
                    <bean class="org.springframework.context.support.SimpleThreadScope"/>
                </entry>
            </map>
        </property>
    </bean>

    <bean id="thing2" class="x.y.Thing2" scope="thread"/>

......

从 Spring 3.0 开始,线程作用域可用,但默认情况下未注册。有关更多信息,请参见SimpleThreadScope的文档。

  1. Bean的感知接口

感知接口(Aware)表示 Bean 向容器声明它们需要某种基础结构依赖性。这些接口回调在填充常规 bean 属性之后,但在初始化回调(例如InitializingBean,afterPropertiesSet或自定义 init-method)之前调用。

  • ApplicationContextAware

当 ApplicationContext 在创建实现ApplicationContextAware接口的对象实例时,该实例将获得对该ApplicationContext的引用。

// ApplicationContextAware接口定义
public interface ApplicationContextAware {
    void setApplicationContext(ApplicationContext applicationContext) throws BeansException;
}
  • BeanNameAware

当 ApplicationContext 在创建实现BeanNameAware接口的类时,该类将获得对在其关联的对象定义中定义的名称的引用。

// BeanNameAware接口定义
public interface BeanNameAware {
    void setBeanName(String name) throws BeansException;
}
  • 其它感知接口

    Name Injected Dependency
    ApplicationEventPublisherAware ApplicationContext的事件发布者
    BeanFactoryAware BeanFactory
    BeanClassLoaderAware 类加载器(用于加载 Bean 类)
    LoadTimeWeaverAware 编织器(用于在加载时处理类定义)
    MessageSourceAware 解析消息的配置策略(支持参数化和国际化)
    NotificationPublisherAware Spring JMX 通知发布者
    ResourceLoaderAware 资源加载器(用于低级资源访问)
    ServletConfigAware 当前容器运行的ServletConfig(仅在Web环境有效)
    ServletContextAware 当前容器运行的ServletContext(仅在Web环境有效)

注意:使用这些接口会将您的代码与 Spring API 绑定在一起,并且不遵循“控制反转”样式。因此,我们建议将它们用于需要以编程方式访问容器的基础结构 Bean。

  1. Bean的自动装配

Spring 容器可以自动装配协作 Bean 之间的关系,即在创建对象后,自动从容器中查找所需的依赖并进行注入。这可以大大减少指定属性或构造函数参数的需要,并且当类中新增依赖项时,无需修改配置即可自动满足该依赖项。

自动装配的模式共有四种,通过bean标签的autowire属性来指定。

Mode Explanation
no(默认) 不进行自动装配, 仅由 ref 属性来定义 Bean 之间的依赖关系,可以提供更好的控制和清晰度。
byName 按属性名称自动装配,Spring通过属性名在容器中查找匹配的依赖 Bean 进行注入。
byType 按属性类型自动装配,如果查找到唯一匹配的依赖Bean,则进行注入。但如果查找到多个,则引发致命异常。
constructor 类似于byType,适用于构造函数参数的自动装配。不同的是,如果未查找到匹配的依赖Bean也会引发致命异常。

<!-- byName 自动注入 -->
<bean id="myStudent" class="com.bjpowernode.ba04.Student" autowire="byName">
    <property name="name" value="李四"/>
    <property name="age" value="22" />
    <!--引用类型的赋值-->
    <!--<property name="school" ref="mySchool" />-->
</bean>

<!-- byType 自动注入 -->
<bean id="myStudent" class="com.bjpowernode.ba05.Student" autowire="byType">
    <property name="name" value="张三"/>
    <property name="age" value="26" />
    <!--引用类型的赋值-->
    <!--<property name="school" ref="mySchool" />-->
</bean>

如果使用ByType自动装配匹配到了多个Bean,则可以设置某些Bean的 autowire-candidate属性为false,从候选列表去除。或者也可以通过设置某个Bean的primary属性为true,将其作为主要候选对象。上面两种配置仅对按类型自动装配(byType/Constructor)有效,按名称自动装配(byName)不受其影响。

  1. Bean定义继承

在Bean的配置中,可以通过parent属性来指定另一个Bean作为该Bean的 ParentBean, 从而继承一些可复用的配置数据,并可以覆盖某些值或根据需要添加其他值,这是一种模板方法模式的一种体现。

<!-- ParentBean的定义。一般将 ParentBean 标记为 abstract,用作纯模板 Bean 使用,同时可以省略 class 属性 -->
<bean id="inheritedTestBean" abstract="true" >
    <property name="name" value="parent"/>
    <property name="age" value="1"/>
</bean>

<!-- ChildBean的定义-->
<bean id="inheritsWithDifferentClass" class="org.springframework.beans.DerivedTestBean" init-method="initialize"
      parent="inheritedTestBean"> 
    <property name="name" value="override"/>
    <!-- 将从 ParentBean 继承 age 属性的注入 -->
</bean>

ChildBean 可以从 ParentBean 继承Bean的作用范围、生命周期、属性注入等信息,但依赖项、自动装配模式等一些信息始终从子类获取,这需要我们在开发过程中多加关注。

注意:

  1. Spring的继承是对象层面的继承,子类继承父类对象的属性值。因此,Spring 中,不同类之间可以互相继承。

  2. Java是类层面的继承,继承的是父类的类结构信息。如果用 Java 代码的方式来配置元数据,那么可以直接使用 java 的继承机制来复用元数据的配置信息。

  3. Bean其它属性

  • 别名(name/alias)

Bean除了具有唯一性的 id 属性外,还可以定义若干个别名,以兼容不同的业务系统,通常使用name属性或标签来实现。

<!-- 配置service,并指定别名,别名以逗号、分号或空格间隔-->
<bean id="accountService" name="accountService2,accountService3" class="org.example.service.impl.AccountServiceImpl"/>

<!--使用alias标签定义别名-->
<alias name="accountService2" alias="accountServiceA"/>
  • 初始化顺序(depends-on)

depends-on属性可以指定初始化时间依赖性,也可以在Bean是单例的的情况下指定相应的销毁时间依赖性。

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

<!--
	depends-on:在初始化beanOne之前强制先初始化manager和accountDao,在销毁时先销毁manager和accountDao,再销毁beanOne。
-->
<bean id="beanOne" class="ExampleBean" depends-on="manager,accountDao">
    <property name="manager" ref="manager" />
</bean>
  • 懒加载(lazy-init)

单例Bean默认在容器创建时进行实例化,如果想让Bean在使用时再创建,则可指定lazy-init属性为true。

<!-- 配置 ExpensiveToCreateBean 延迟加载 -->
<bean id="lazy" class="com.something.ExpensiveToCreateBean" lazy-init="true"/>

如果需要对多个Bean进行配置,也可以选择beans标签的default-lazy-init属性,为其内部的所有Bean设置可覆盖的默认值。

<beans default-lazy-init="true">
    <!-- no beans will be pre-instantiated... -->
</beans>

提示:如果你使用注解配置,可以使用 @Lazy 注解来实现懒加载,并且该注解可以放置在标有@Autowired或@Inject的注入点上。在这种情况下,它导致注入了惰性解析代理。

第五节 依赖注入配置

依赖注入 (Dependency Inject)是控制反转思想的一种实现方式,指的是程序运行过程中,若需要调用另一个对象协助时,无须在代码中创建被调用者,而是依赖于外部容器,由外部容器创建后传递给程序。

  1. 构造函数注入

基于构造函数的注入是通过容器调用有参构造函数来完成的,每个参数代表了一个依赖项。

<!--构造注入,使用name属性
	1. name:指定构造函数参数名称。(在进行set注入时可以使用复合属性名,如student.birthday.year)
	2. value:用于注入基本属性值和String
	3. ref:用于注入引用类型
	4. 集合、空值、内联Bean的注入应使用子标签来完成
-->
<bean id="myStudent" class="com.bjpowernode.ba03.Student">
    <constructor-arg name="myage" value="22" />
    <constructor-arg name="myname" value="李四"/>
    <constructor-arg name="mySchool" ref="mySchool"/>
</bean>

对于极少数情况下无法使用构造函数自变量名称的情况(通常,如果字节码是在没有调试信息的情况下编译的),可以通过参数索引进行构造函数的匹配,并且允许在能够推断的情况下省略索引属性。

<!--构造注入,使用index,参数的位置,构造方法参数从左往右位置是0,1,2-->
<bean id="myStudent2" class="com.bjpowernode.ba03.Student">
    <constructor-arg index="1" value="28"/>
    <constructor-arg index="0" value="张三"/>
    <constructor-arg index="2" ref="mySchool" />
</bean>

<!--构造注入,省略index属性-->
<bean id="myStudent3" class="com.bjpowernode.ba03.Student">
    <constructor-arg  value="张峰"/>
    <constructor-arg  value="28"/>
    <constructor-arg  ref="mySchool" />
</bean>

还可以使用更为简洁的语法来进行构造函数的注入配置,但这最好是在创建 bean 定义时使用支持自动属性完成的 IDE,否则错字是在运行时而不是设计时发现的。

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

    <bean id="thingOne" class="x.y.ThingTwo"/>
    <bean id="thingTwo" class="x.y.ThingThree"/>

    <!-- 使用c名称空间简化构造注入的书写方式 
  			1. 基本类型使用 c:属性名="属性值"
			2. 引用类型使用 c:属性名_ref="属性值"
	-->
    <bean id="thingOne" class="x.y.ThingOne" 
          c:thingTwo-ref="thingTwo" 
          c:thingThree-ref="thingThree" 
          c:email="[emailprotected]"/>
</beans>

提示:使用工厂创建Bean对象时,可以通过constructor-arg标签进行工厂方法的参数注入,使用方式同构造函数注入一致。

  1. Set方法注入

基于Set方法的注入是在对象创建完成后,调用对象的set方法来进行属性注入。

<bean id="now" class="java.util.Date"></bean> 

<bean id="accountService" class="com.itheima.service.impl.AccountServiceImpl">
    <property name="name" value="test"></property>
    <property name="age" value="21"></property>
    <property name="birthday" ref="now"></property>
</bean>

类似的,可以使用p名称空间来进行简化书写。

<beans xmlns="http://www.Springframework.org/schema/beans"
    xmlns:p="http://www.Springframework.org/schema/p"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=" http://www.Springframework.org/schema/beans        
                    http://www.Springframework.org/schema/beans/Spring-beans.xsd">

    <bean id="now" class="java.util.Date"></bean> 
    
    <bean id="accountService" class="com.itheima.service.impl.AccountServiceImpl4" 
          p:name="test" 
          p:age="21" 
          p:birthday-ref="now"/>
</beans> 
  1. 注入集合属性

顾名思义,就是给类中的集合成员进行属性注入,用的也是set方法注入的方式,只不过变量的数据类型都是集合。

<bean id="accountService" class="com.itheima.service.impl.AccountServiceImpl">
    <!-- 给数组注入数据 -->
    <property name="myStrs">
        <set>
            <value>AAA</value>
            <value>BBB</value>
            <value>CCC</value>
        </set>
    </property>

    <!-- 注入list 集合数据 -->
    <property name="myList">
        <array>
            <value>AAA</value>
            <value>BBB</value>
            <value>CCC</value>
        </array>
    </property>

    <!-- 注入set 集合数据 -->
    <property name="mySet">
        <list>
            <value>AAA</value>
            <value>BBB</value>
            <value>CCC</value>
        </list>
    </property>

    <!-- 注入Map 数据 -->
    <property name="myMap">
        <props>
            <prop key="testA">aaa</prop>
            <prop key="testB">bbb</prop>
        </props>
    </property>

    <!-- 注入properties 数据 -->
    <property name="myProps">
        <map>
            <entry key="testA" value="aaa"></entry>
            <entry key="testB">
                <value>bbb</value>
            </entry>
        </map>
    </property>
</bean> 

提示:集合数据分两种,单列集合(array,list,set)和双列集合(map,entry,props,prop),同类型集合注入方式可以兼容。

如果Bean存在继承关系,则可以通过merge属性来合并父类的集合。

<!-- 定义一个ParentBean,并给adminEmails集合进行注入 -->
<beans>
    <bean id="parent" abstract="true" class="example.ComplexObject">
        <property name="adminEmails">
            <props>
                <prop key="administrator">[emailprotected]</prop>
                <prop key="support">[emailprotected]</prop>
            </props>
        </property>
    </bean>

<!-- 定义一个ChildBean,同样给adminEmails集合进行注入 
	1. merge:继承父类集合中的属性,并进行选择性覆盖
-->
    <bean id="child" parent="parent">
        <property name="adminEmails">
            <props merge="true">
                <prop key="sales">[emailprotected]</prop>
                <prop key="support">[emailprotected]</prop>
            </props>
        </property>
    </bean>
<beans>
  1. 注入原型Bean

如果某个Bean是单例的,并且依赖了原型Bean,则该原型Bean不能使用上述方式直接注入,必须在每次使用时都创建新的实例。

  • 实现感知接口

通过实现感知接口ApplicationContextAware或BeanFactoryAware获取容器的引用,进而调用getBean方法创建原型Bean。

public class SingleBeanDemo implements BeanFactoryAware {
    private BeanFactory beanFactory;

    // 实现感知接口的抽象方法,在创建Bean的时候回调,注入容器的引用
    @Override
	public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
        this.beanFactory = beanFactory;
    }
    
    // 从容器获取原型Bean实例
	protected PrototypeBeanDemo getPrototypeBeanDemo(){
        this.beanFactory.getBean("prototypeBeanDemo", PrototypeBeanDemo.class)();
    }
    
    // 在业务方法中使用原型Bean
    public void bizMethod() {
        PrototypeBeanDemo prototypeBeanDemo = getPrototypeBeanDemo();
    }
}
  • 使用查找方法

上述方式依赖了Spring框架,并且由程序代码主动获取对象,不符合控制反转的原则,下面将使用XML配置方式由Spring来完成上述代码。

// SingleBeanDemo 创建为抽象类,并设置 getPrototypeBeanDemo() 方法为抽象方法,待Spring为我们实现
public abstract class SingleBeanDemo {
    protected abstract PrototypeBeanDemo getPrototypeBeanDemo();
    
    public void bizMethod() {
        PrototypeBeanDemo prototypeBeanDemo = getPrototypeBeanDemo();
    }
}

<!-- PrototypeBeanDemo 定义-->
<bean id="prototypeBeanDemo" class="fiona.apple.PrototypeBeanDemo" scope="prototype">
</bean>

<!-- SingleBeanDemo 定义
	lookup-method:指定查找方法和查找的原型Bean
-->
<bean id="singleBeanDemo" class="fiona.apple.SingleBeanDemo">
    <lookup-method name="getPrototypeBeanDemo" bean="prototypeBeanDemo"/>
</bean>

也可以使用注解方式来配置,并且@Lookup的 value 属性可以根据查找方法声明的返回类型来解析。

@Component
public abstract class SingleBeanDemo {
    @Lookup /*("prototypeBeanDemo")*/
    protected abstract PrototypeBeanDemo getPrototypeBeanDemo();

    public void bizMethod() {
        PrototypeBeanDemo prototypeBeanDemo = getPrototypeBeanDemo();
    }
}

注意:

  1. 查找方法的签名必须是<public|protected> [abstract] theMethodName(no-arguments)形式。
  2. 查找方法允许是非 abstract 的,Spring会将原来的方法覆盖。
  3. 由于Spring是通过CGLib来实现该方式的,因此该类和查找方法都不能被final修饰。
  4. 查找方法不适用于工厂方法和配置类中的@Bean方法,因此在这种情况下,实例并不是由Spring创建的,无法进行动态代理。
  • 注入ObjectFactory

    // 先注入单例的ObjectFactory,再通过getObject方法获取原型Bean的实例
    @Component
    public class SingleBeanDemo {
    @Autowired
    ObjectFactory prototypeBeanDemoFactory;

      public void bizMethod() {
          PrototypeBeanDemo prototypeBeanDemo = (PrototypeBeanDemo)factory.getObject();
      }
    

    }

  1. 循环依赖问题

在进行依赖注入时,如果A依赖B,B依赖C,而C又依赖A,则有可能出现循环依赖问题,抛出BeanCurrentlyInCreationException异常,下面将使用案例对三种不同的注入场景进行分析。

// 有三个需要创建的对象,它们之间相互依赖(A依赖B,B依赖C,而C又依赖A)。
public class StudentA { private StudentB studentB; public void setStudentB(StudentB studentB) { this.studentB = studentB; } public StudentA() { } public StudentA(StudentB studentB) { this.studentB = studentB; } } 
public class StudentB { private StudentC studentC ; public void setStudentC(StudentC studentC) { this.studentC = studentC; } public StudentB() { } public StudentB(StudentC studentC) { this.studentC = studentC; } } 
public class StudentC { private StudentA studentA; public void setStudentA(StudentA studentA) { this.studentA = studentA; } public StudentC() { } public StudentC(StudentA studentA) { this.studentA = studentA; } } 
  • 通过构造函数注入

首先通过构造函数注入,来描述上面所述的依赖关系,在创建容器时抛出了预期的BeanCurrentlyInCreationException异常。

<bean id="a" class="cn.mayday.springrecycledp.demo1.StudentA"> 
    <constructor-arg index="0" ref="b"></constructor-arg> 
</bean> 

<bean id="b" class="cn.mayday.springrecycledp.demo1.StudentB"> 
    <constructor-arg index="0" ref="c"></constructor-arg> 
</bean> 

<bean id="c" class="cn.mayday.springrecycledp.demo1.StudentC"> 
    <constructor-arg index="0" ref="a"></constructor-arg> 
</bean> 

Spring容器会将每一个正在创建的Bean标识符放在一个“当前创建Bean池”中,Bean标识符在创建过程中将一直保持在这个池中。因此如果在创建Bean过程中发现自己已经在“当前创建Bean池”里时将抛出BeanCurrentlyInCreationException异常表示循环依赖;而对于创建完毕的Bean将从“当前创建Bean池”中清除掉。

根据上述源码实现分析:Spring容器先创建单例StudentA,StudentA依赖StudentB,然后将A放在“当前创建Bean池”中,此时创建StudentB, StudentB依赖StudentC ,然后将B放在“当前创建Bean池”中,此时创建StudentC,StudentC又依赖StudentA, 但是,此时StudentA已经在池中,所以会报错,因为在池中的Bean都是未初始化完的,所以会依赖错误 ,(初始化完的Bean会从池中移除)。

  • 通过Set方法注入(单例范围)

再来看下使用Set方法注入,并设置Bean范围为单例范围,发现在创建容器时并未抛出异常。

<bean id="a" class="cn.mayday.springrecycledp.demo1.StudentA" scope="singleton"> 
    <property name="studentB" ref="b"></property> 
</bean> 

<bean id="b" class="cn.mayday.springrecycledp.demo1.StudentB" scope="singleton"> 
    <property name="studentC" ref="c"></property> 
</bean> 

<bean id="c" class="cn.mayday.springrecycledp.demo1.StudentC" scope="singleton"> 
    <property name="studentA" ref="a"></property> 
</bean> 

为什么使用Set方法不抛异常呢?关键在于Spring先将Bean对象实例化之后再设置对象属性的。Spring先用构造函数实例化Bean对象 ,然后存入到一个Map中,当StudentA、StudentB、StudentC都实例化完成后,然后才去设置对象的属性,此时StudentA依赖StudentB,就会去Map中取出存在里面的单例StudentB对象,以此类推,不会出来循环的问题喽。

  • 通过Set方法注入(原型范围)

如果Bean的范围为原型,即使使用Set注入,也会抛出BeanCurrentlyInCreationException异常。

<bean id="a" class="cn.mayday.springrecycledp.demo1.StudentA" scope="prototype"> 
    <property name="studentB" ref="b"></property> 
</bean> 

<bean id="b" class="cn.mayday.springrecycledp.demo1.StudentB" scope="prototype"> 
    <property name="studentC" ref="c"></property> 
</bean> 

<bean id="c" class="cn.mayday.springrecycledp.demo1.StudentC" scope="prototype"> 
    <property name="studentA" ref="a"></property> 
</bean> 

因为“prototype”作用域的Bean,Spring容器不进行三级缓存,因此无法提前暴露一个创建中的Bean。

  1. 静态变量注入

我们封装工具类的时候,大多数提供的是静态方法,而静态方法只能访问静态变量,此时,则需要将我们所需的依赖注入到静态变量中。

  • 通过Set方法注入

    @Component
    public class RedisLockUtil {
    private static RedisTemplate<Object, Object> redisTemplate;

      @Autowired
      public void setRedisTemplate(RedisTemplate<Object, Object> redisTemplate) {
          RedisLockUtil.redisTemplate = redisTemplate;
      }
    

    }

  • 使用@PostConstruct注解

    @Component
    public class RedisLockUtil {
    private static RedisTemplate<Object, Object> redisTemplate;

      @Autowired
      private RedisTemplate<Object,Object> redisTemplate_copy;
    
      @PostConstruct
      public void init(){
      	RedisLockUtil.redisTemplate=redisTemplate_copy;
      }
    

    }

  1. 普通类使用Bean

如果某个类是一个普通的Java,并且并没有被Spring容器所管理,那么如何使用Spring容器创建的Bean实例呢?

  • SpringUtils工具类

定义一个工具类,实现容器的感知接口,通过静态方法向外部提供获取Bean的功能。

@Component
public class SpringUtils implements ApplicationContextAware {
    private static ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        SpringUtils.applicationContext = applicationContext;
    }

    public static <T> T getBean(Class<T> requiredType) {
        return applicationContext.getBean(requiredType);
    }

    public static <T> T getBean(String beanName) {
        return (T) applicationContext.getBean(beanName);
    }

    public static <T> T getBean(Class<T> requiredType, Object... args) {
        return applicationContext.getBean(requiredType, args);
    }

    public static <T> T getBean(String beanName, Object... args) {
        return (T) applicationContext.getBean(beanName, args);
    }

    public static <T> T getBean(String beanName, Class<T> requiredType) {
        return (T) applicationContext.getBean(beanName, requiredType);
    }

    public int getBeanDefinitionCount(){
        return applicationContext.getBeanDefinitionCount();
    }

    public String[] getBeanDefinitionNames(){
        return applicationContext.getBeanDefinitionNames();
    }
}
  • 静态化Bean

修改 Bean 的代码,在初始化回调时将 Bean 的 this 指针设置到一个静态变量中。

@Component
public class FundDispatchLogUtils {
    // 定义一个静态变量,用于保存this指针
    private static FundDispatchLogUtils instance;

    // 把this保存在静态变量中
    @PostConstruct
    public void init() {
        instance = this;
    }

    // 提供获取this的静态方法
    public static FundDispatchLogUtils getInstance() {
        return instance;
    }
}
  1. 自定义类型处理器

Spring通过类型转换器把配置文件中字符串类型的数据,转换成了对象中成员变量对应类型的数据,进而完成了注入,当Spring内部没有提供特定类型的转换器时,那么就需要程序员自己定义类型转换器。

  • 实现 Converter 接口

    /**

    • 自定义类型转换器,完成String->Date的转换。
      */
      public class MyDateConverter implements Converter<String, Date> {
      private String pattern;

      public void setPattern(String pattern) {
      this.pattern = pattern;
      }

      @Override
      public Date convert(String source) {
      Date date = null;
      try {
      SimpleDateFormat sdf = new SimpleDateFormat(pattern);
      date = sdf.parse(source);
      } catch (ParseException e) {
      e.printStackTrace();
      }
      return date;
      }
      }

  • 注册自定义类型转换器

第六节 基于注解的IOC配置

Spring从2.x版本开始,提供了注解方式来配置IOC容器和注册Bean, 充分利用程序中提供的上下文,使得配置更加简短和整洁。

在使用基于XML配置的ClassPathXmlApplicationContext来构建IOC容器时,需要在XML中开启注解配置开关。

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context.xsd">

    <!-- 开启注解配置开关 -->
    <context:annotation-config/>

</beans>

开启注解配置开关会自动注册下面一些后处理器,并且在定义它的应用程序上下文中扫描Bean上的注解。

  1. AutowiredAnnotationBeanPostProcessor
  2. CommonAnnotationBeanPostProcessor
  3. PersistenceAnnotationBeanPostProcessor
  4. RequiredAnnotationBeanPostProcessor

也可以使用AnnotationConfigApplicationContext直接基于注解配置来构建容器,然后使用@ImportResuorce注解导入XML配置。

@Configuration
@ImportResource("classpath:beans.xml")
public class AppConfig {

}

注意

  1. 注解配置对源代码存在侵入,并且配置分散不利于维护,在使用时需结合实际情况选用。

  2. Spring优先对注解配置的属性进行注入,如果在XML中配置了相同的属性,那么将会对之前的配置进行覆盖。

  3. 使用@Component标记组件

@Component 注解作用于类、接口、枚举或其它注解之上,标记该元素为Spring的一个组件,唯一的 value 属性用于指定组件的名称。

// 定义一个Bean,id为类名的首字母小写(userServiceImpl)
@Component("userServiceImpl")
public class UserServiceImpl implements UserService {
}

为了区分该组件属于持久层、服务层或是控制层,分别为此定义了三个语义性注解:@Repository、@Service、@Controller。这在将来可能还会有一些特殊的语义,如@Repository被支持作为持久层中自动异常转换的标记等。

  1. 使用@Componnet进行组件扫描

@ComponentScan 一般作用于 @Configuration 类上,用于自动检测标记的组件,并将构造型类向ApplicationContext注册相应的BeanDefinition实例。

@Configuration
@ComponentScan(basePackages = "org.example") // basePackages:扫描的包名,以分号、逗号或空格分隔。
// @ComponentScan("org.example")
public class AppConfig  {
    ...
}

等效的XML配置如下。

<!-- 配置组件扫描,并隐式开启注解配置功能(<context:annotation-config>) -->
<context:component-scan base-package="org.example"/>
  • 配置扫描过滤器

默认情况下,会扫描所有被 @Component 及其衍生注解标记的组件,你可以通过includeFilters和excludeFilters属性设置过滤器来修改和扩展此行为,过滤器的类型和描述如下列表所示。

Filter Type Example Expression Description
annotation (default) org.example.SomeAnnotation 在目标组件的类型级别上存在的注解。
assignable org.example.SomeClass 目标组件可分配给(扩展或实现)的类(或接口)。
aspectj org.example…Service+ 目标组件要匹配的 AspectJ 类型表达式。
regex org.example.Default.
要与目标组件类名称匹配的正则表达式。
custom org.example.MyTypeFilter org.springframework.core.type .TypeFilter 接口的自定义实现。

/**
	@Configuration:声明该类为一个配置类
	@ComponentScan:组件扫描
		basePackages:扫描的包名
		includeFilters:包含过滤器,仅扫描满足条件的组件
		excludeFilters:排除过滤器,不扫描满足条件的组件
*/
@Configuration
@ComponentScan(basePackages = "org.example",
        includeFilters = @Filter(type = FilterType.REGEX, pattern = ".*Stub.*Repository"),
        excludeFilters = @Filter(Repository.class))
public class AppConfig {
    ...
}

等效的XML配置如下。

<beans>
    <context:component-scan base-package="org.example">
        <context:include-filter type="regex" expression=".*Stub.*Repository"/>
        <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Repository"/>
    </context:component-scan>
</beans>

也可以参考如下,下面是一个线上项目中使用的案例。

@SpringBootApplication(exclude = { DataSourceAutoConfiguration.class })
@RestController
@EnableTransactionManagement
@ComponentScan(basePackages = { "com.szkingdom.kfms.**", "com.szkingdom.fs.**", "com.szkingdom.koca.**" })
@MapperScan(basePackages = { "com.szkingdom.**.dao.**" })
public class KfmsBootApplication {
    public static void main(String[] args) {
        SpringApplication springApplication = new SpringApplication(KfmsBootApplication.class);
        springApplication.run(args);
    }
}

提示:
你可以将 useDefaultFilters (或 use-default-filters)属性设置为 false 来禁止移除默认过滤器,这将禁用对带有@Component,@Repository,@Service,@Controller或@Configuration注解的类的自动检测。

  • 自定义名称生成策略

在扫描过程中自动检测到某个组件时,其 bean 名称由该扫描器已知的BeanNameGenerator策略生成。默认使用配置的 value 属性值,如果未配置则使用类名的首字母小写作为 bean 名称。

// 使用 value 属性值 myMovieLister 作为 bean 名称
@Service("myMovieLister")
public class SimpleMovieLister {
    // ...
}

// 使用 movieFinderImpl 作为 bean 名称
@Repository
public class MovieFinderImpl implements MovieFinder {
    // ...
}

如果你想对名称生成策略进行修改,可以实现 BeanNameGenerator 接口,提供无参构造函数,然后在 @ComponentScan 注解的 nameGenerator 属性进行配置。

// nameGenerator:指定自定义名称生成策略
@Configuration
@ComponentScan(basePackages = "org.example", nameGenerator = MyNameGenerator.class)
public class AppConfig {
    ...
}

等效的XML配置如下:

<beans>
    <context:component-scan base-package="org.example"
        name-generator="org.example.MyNameGenerator" />
</beans>
  • 生成组件索引

在大型应用程序中启动时,组件扫描可能会花费不短的时间,可以通过在编译时创建候选静态列表(索引)来提高大型应用程序的启动性能。

<!-- 在每个包含组件的模块中加入该依赖,则在编译时会自动生成索引到 META-INF/spring.components 文件中 -->
<dependencies>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context-indexer</artifactId>
        <version>5.1.3.RELEASE</version>
        <optional>true</optional>
    </dependency>
</dependencies>

注意

  1. 如果ApplicationContext在启动时检测到组件索引,则会直接使用索引,而不会再进行组件扫描。这意味着你必须给所有存在组件的模块都加上依赖。

  2. 可以使用全局属性spring.index.ignore=true来忽略组件索引,回退为组件扫描的方式。

  3. 配置Bean的属性

  • 使用@Scope配置组件的作用范围

组件默认注册为 singleton 范围,如果相对其进行修改,可以使用 @Scope 注解。

@Scope("prototype")
@Repository
public class MovieFinderImpl implements MovieFinder {
    // ...
}

注意:

与XML中的scope属性不同,@Scope属性仅在具体的 bean 类(对于带注解的组件)或工厂方法(对于@Bean方法)上有效,不能被继承。

可以实现 ScopeMetadataResolver 接口提供用于范围解析的自定义策略,而不是依赖于基于注解的方法。

@Configuration
@ComponentScan(basePackages = "org.example", scopeResolver = MyScopeResolver.class)
public class AppConfig {
    ...
}

等效XML配置

<beans>
    <context:component-scan base-package="org.example" scope-resolver="org.example.MyScopeResolver"/>
</beans>

使用某些非单作用域时,可能有必要为作用域对象生成代理,可以使用 scoped-proxy 属性配置,下面配置产生标准的JDK动态代理。

// scopedProxy:选择动态代理,可选 no、interfaces 和 targetClass
@Configuration
@ComponentScan(basePackages = "org.example", scopedProxy = ScopedProxyMode.INTERFACES)
public class AppConfig {
    ...
}

等效XML配置如下:

<beans>
    <context:component-scan base-package="org.example" scoped-proxy="interfaces"/>
</beans>
  1. 使用@Autowired进行自动注入

@Autowired 注解一般用于引用类型属性的自动注入,可作用于构造器、方法、参数、成员变量和注解上。

  • 应用于单个变量

如果被注解的是单个变量,则进行唯一性匹配,即优先按类型进行匹配,如果存在多个匹配的类型,再使用变量名称进行匹配。

public class MovieRecommender {
    @Autowired
    private CustomerPreferenceDao customerPreferenceDao;
}
  • 应用于数组或单列集合

如果被注解的是数组或单列集合,则将该类型的所有 Bean 按照定义顺序依次注入到其中。

public class MovieRecommender {
    @Autowired
    private MovieCatalog[] movieCatalogs;
    
    @Autowired
    private Set<MovieCatalog> movieCatalogs;
}

如果你想精确控制它们的注入顺序,可以实现 Ordered接口或使用@Order/@Priority注解。

  • 应用于双列集合

如果被注解的是一个双列集合(Key为String类型),则同样可以注入所有该类型的 Bean,并且使用 Bean 的id属性作为键值对的key值。

public class MovieRecommender {
    @Autowired
    private Map<String, MovieCatalog> movieCatalogs;
}
  • 应用于构造函数

如果被注解的是构造函数,则表示使用该函数来实例化 Bean 对象,通过构造参数来进行属性注入。

public class MovieRecommender {
    private final CustomerPreferenceDao customerPreferenceDao;

    @Autowired
    public MovieRecommender(CustomerPreferenceDao customerPreferenceDao) {
        this.customerPreferenceDao = customerPreferenceDao;
    }
}

你可以注解多个构造函数作为“候选者”,Spring会选择参数最多的那个进行实例化,但必须保证它们的 required 属性都为false。

public class MovieRecommender {
    private final CustomerPreferenceDao customerPreferenceDao;
    private final MovieFinder movieFinder;

    // required 属性必须修改为false
    @Autowired(required = false)
    public MovieRecommender(CustomerPreferenceDao customerPreferenceDao) {
        this.customerPreferenceDao = customerPreferenceDao;
    }
    
    // required 属性必须修改为false
    @Autowired(required = false)
    public MovieRecommender(CustomerPreferenceDao customerPreferenceDao, MovieFinder movieFinder) {
        this.customerPreferenceDao = customerPreferenceDao;
        this.movieFinder = movieFinder;
    }
}

如果未对构造函数使用 @Autowired 注解,则默认使用无参构造进行实例化。如果也没有无参构造,那么必须保证存在唯一的有参构造。

  • 应用于其它方法

@Autowired 也可以支持注解 Bean 中的其它类型方法,但一般来说,传统 setter 方法使用的更多。

public class SimpleMovieLister {
    private MovieFinder movieFinder;

    // 注解 setter 方法 :先对 movieFinder 参数进行注入,然后调用 setMovieFinder 方法
    @Autowired
    public void setMovieFinder(MovieFinder movieFinder) {
        this.movieFinder = movieFinder;
    }
}

public class MovieRecommender {
    private MovieCatalog movieCatalog;
    private CustomerPreferenceDao customerPreferenceDao;

    // 注解任意方法
    @Autowired
    public void prepare(MovieCatalog movieCatalog, CustomerPreferenceDao customerPreferenceDao) {
        this.movieCatalog = movieCatalog;
        this.customerPreferenceDao = customerPreferenceDao;
    }
}
  • 依赖必须性

在自动注入时,如果无法找到合适的 Bean,则抛出 NoSuchBeanDefinitionException 异常,可以通过 required 属性对此进行修改。

public class SimpleMovieLister {
    private MovieFinder movieFinder;

    @Autowired(required = false)
    public void setMovieFinder(MovieFinder movieFinder) {
        this.movieFinder = movieFinder;
    }
}
  • 注入内置 Bean

使用 @Autowired 注解可以直接注入常用的可解决依赖项,而无需进行特殊设置,如BeanFactory、ApplicationContext、Environment、ResourceLoader、ApplicationEventPublisher以及MessageSource等。

public class MovieRecommender {
    @Autowired
    private ApplicationContext context;
}
  • 使用泛型作为自动装配限定符

除了@Qualifier注解外,您还可以将 Java 泛型类型用作自动装配的匹配条件。

@Configuration
public class MyConfiguration {
	// 实例化一个 StringStore 对象,StringStore 实现了 Store<String>
    @Bean
    public StringStore stringStore() {
        return new StringStore();
    }

    // 实例化一个 IntegerStore 对象,IntegerStore 实现了 Store<Integer>
    @Bean
    public IntegerStore integerStore() {
        return new IntegerStore();
    }
}

@Autowired
private Store<String> s1; // 注入StringStore
@Autowired
private Store<Integer> s2; // 注入 IntegerStore
  1. 使用@Resource进行自动注入

@Resource 是JSR-250定义的注解,与@Autowired都是用于属性注入,可以作用于类、成员变量和方法上。常用的属性有name和type。

  • 未指定name和type

  • 同时指定name和type

  • 仅指定name

  • 仅指定type

    public class SimpleMovieLister {
    private MovieFinder movieFinder;

      // 仅按 myMovieFinder 名称匹配,失败则抛异常
      @Resource(name="myMovieFinder")
      public void setMovieFinder(MovieFinder movieFinder) {
          this.movieFinder = movieFinder;
      }
      
      // 优先按 movieFinder 名称匹配,失败则按类型匹配
      @Resource
      public void setMovieFinder(MovieFinder movieFinder) {
          this.movieFinder = movieFinder;
      }
    

    }

注意事项

数据注入注解@Autowired、@Inject、@Resource和@Value由BeanPostProcessor处理。这意味着您不能在自己的BeanPostProcessor或BeanFactoryPostProcessor类型(如果有)中应用这些注解。必须使用 XML 或 Spring @Bean方法显式“连接”这些类型。

  1. 用于注入数据的其他注解
  • 使用@Value注入字面量

@Value注解一般用于字面量的注入,并且会解析${}从环境中取值进行替换。

@Configuration
@ImportResource("classpath:beans.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);
    }
}

注意
@Value注解会优先去容器中查找名称一致的Bean进行注入,只有未找到合适的Bean时,才会进行字面量注入。

  • 使用@Required标记属性的必需性

@Required 通常作用于 setter 方法之上,标记某个属性在实例化 Bean 时必须被注入,否则就会抛出 BeanInitializationException 异常,具体请参考 RequiredAnnotationBeanPostProcessor 的实现。

public class Student {
    private String name;

    // 标记 name 属性必须被注入(注意:即使注入 NULL 也算被注入了,@Required 不做空指针检测)
    @Required
    public void setName(String name) {
        this.name = name;
    }
}

等效的XML配置如下。

<!-- student bean的定义,必须对 name 属性进行注入 -->
<bean id = "student" class="com.how2java.w3cschool.required.Student">
    <property name = "name" value="派大星"/>
</bean>
  • 使用@Primary调整自动注入优先级

使用自动装配可能存在多个适合注入的候选对象,@Primary 注解可以指定某个 Bean 作为主 Bean,如果候选对象列表中存在唯一的主 Bean,则使用该值进行注入。

@Configuration
public class MovieConfiguration {
    @Bean
    @Primary // 指定该Bean为主Bean,优先进行注入
    public MovieCatalog firstMovieCatalog() { ... }

    @Bean
    public MovieCatalog secondMovieCatalog() { ... }
}

等效的XML配置如下。

<bean id="firstMovieCatalog"  class="example.SimpleMovieCatalog" primary="true"/>
<bean id="secondMovieCatalog" class="example.SimpleMovieCatalog"/>
  • 使用@Qualifier进行匹配限定

@Qualifier 用于指定一个限定符,以缩小自动装配的匹配范围,可作用于成员变量、构造方法参数或其它方法参数之上。

public class MovieRecommender {
    @Autowired
    @Qualifier("main") // 成员变量上使用@Qualifier(指定匹配的限定符为main)
    private MovieCatalog movieCatalog;
    
    private CustomerPreferenceDao customerPreferenceDao;
    
    // 方法参数上使用@Qualifier
    @Autowired
    public void prepare(@Qualifier("main") CustomerPreferenceDao customerPreferenceDao) {
        this.customerPreferenceDao = customerPreferenceDao;
    }
}

假设 MovieCatalog 类型的 Bean 配置如下,则会与 SimpleMovieCatalog1 进行连接。

@Component
@Qualifier("main")
public class MovieCatalog extends SimpleMovieCatalog1{
}

@Component
@Qualifier("action")
public class MovieCatalog extends SimpleMovieCatalog2{
}

同等效果的XML配置如下。

<bean id="simpleMovieCatalog1" class="example.SimpleMovieCatalog1">
    <qualifier value="main"/>
</bean>

<bean id="simpleMovieCatalog2" class="example.SimpleMovieCatalog2">
    <qualifier value="action"/> 
</bean>

提示:

  1. 每个Bean都会将 id 属性作为默认的限定符值,如上例也可以使用@Qualifier(“simpleMovieCatalog2”)来连接另一个实例。
  2. @Qualifier对集合类型变量也有效,将会在注入前限定匹配的范围(注意:Bean的限定符值并不是唯一的)。
  • 自定义限定符

可以基于@Qualifier自定义一些组合注解,并对属性做一些修改,只有当全部属性都匹配时,才会加入候选列表。

// 定义组合注解 @MovieQualifier
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface MovieQualifier {
    String genre();
    Format format();
}

// 限定自动注入的匹配范围,只有当format=Format.VHS, genre="Action"时才进行匹配
@Autowired
@MovieQualifier(format=Format.VHS, genre="Action")
private MovieCatalog actionVhsCatalog;

<bean class="example.SimpleMovieCatalog">
    <qualifier type="MovieQualifier">
        <attribute key="format" value="VHS"/>
        <attribute key="genre" value="Comedy"/>
    </qualifier>
</bean>

<bean class="example.SimpleMovieCatalog">
    <meta key="format" value="DVD"/>
    <meta key="genre" value="Action"/>
</bean>

也可以使用CustomAutowireConfigurer来声明自定义的限定符注解,而不使用组合注解的方式。

<bean id="customAutowireConfigurer" class="org.springframework.beans.factory.annotation.CustomAutowireConfigurer">
    <property name="customQualifierTypes">
        <set>
            <value>example.CustomQualifier</value>
        </set>
	</property>
</bean>
  1. 使用JSR-330注解(了解)

JSR-330注解是Java标准定义的依赖注入注解,Spring从3.0版本开始提供对这些注解的支持。

<!--引入JSR-330注解的相关依赖-->
<dependency>
    <groupId>javax.inject</groupId>
    <artifactId>javax.inject</artifactId>
    <version>1</version>
</dependency>
  • @ManagedBean/@Named: 组件标记

javax.annotation.ManagedBean或javax.inject.Named可用于组件扫描,作用与@Component类似。

import javax.inject.Inject;
import javax.inject.Named;

@Named("movieListener")  // @ManagedBean("movieListener") could be used as well
public class SimpleMovieLister {

    private MovieFinder movieFinder;

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

    // ...
}

并且可以使用与Spring注解完全相同的方式来配置组件扫描,如以下示例所示:

@Configuration
@ComponentScan(basePackages = "org.example")
public class AppConfig  {
    ...
}
  • @Inject/@Named: 依赖注入

可以使用javax.inject.Inject进行属性注入,作用与@Autowired类似。

import javax.inject.Inject;

public class SimpleMovieLister {

    private MovieFinder movieFinder;

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

    public void listMovies() {
        this.movieFinder.findMovies(...);
        ...
    }
}

如果要为注入的依赖项使用限定名称,还可以使用@Named注解。

import javax.inject.Inject;
import javax.inject.Named;

public class SimpleMovieLister {

    private MovieFinder movieFinder;

    @Inject
    public void setMovieFinder(@Named("main") MovieFinder movieFinder) {
        this.movieFinder = movieFinder;
    }

    // ...
}

@Inject没有required属性,但可以和java.util.Optional或@Nullable一起使用设置属性是否为必输。

public class SimpleMovieLister {

    @Inject
    public void setMovieFinder(Optional<MovieFinder> movieFinder) {
        ...
    }
}
    
public class SimpleMovieLister {

    @Inject
    public void setMovieFinder(@Nullable MovieFinder movieFinder) {
        ...
    }
}
  • JSR-330 标准注解的局限性

当使用标准注解时,您应该知道某些重要功能不可用,如下表所示:

Spring javax.inject.* javax.inject 限制/注解
@Autowired @Inject @Inject没有“必需”属性。可以与 Java 8 的Optional一起使用。
@Component @Named/@ManagedBean JSR-330 不提供可组合的模型,仅提供一种识别命名组件的方法。
@Scope(“singleton”) @Singleton JSR-330 的默认范围类似于 Spring 的prototype。但是,为了使其与 Spring 的常规默认设置保持一致,默认情况下,在 Spring 容器中声明的 JSR-330 bean 为singleton。为了使用singleton以外的范围,您应该使用 Spring 的@Scope注解。 javax.inject还提供@Scope注解。但是,此仅用于创建自己的 注解。
@Qualifier @ Qualifier/@ Named javax.inject.Qualifier只是用于构建自定义限定符的元 注解。可以通过javax.inject.Named关联具体的String限定词(如带有值的 Spring 的@Qualifier)。
@Value - no equivalent
@Required - no equivalent
@Lazy - no equivalent
ObjectFactory Provider javax.inject.Provider是 Spring 的ObjectFactory的直接替代方法,只是使用较短的get()方法名。它也可以与 Spring 的@Autowired或未注解的构造函数和 setter 方法结合使用。

第七节 基于 Java 的容器配置

基于Java的容器配置指的是在 Java 代码中使用注解来配置 Spring 容器,其中最核心的两个注解为@Configuration和@Bean。其中@Configuration 标记某个类为 Spring 的配置类组件,作为 Bean 定义的来源,而 @Bean 通过标记类中的公共方法进行 Bean 的定义。

  1. 使用@Bean定义Bean

默认情况下,bean 名称为方法名称,value 为返回的对象,类型为返回值类型。

@Configuration
public class AppConfig {
	// 定义一个Bean:transferService -> com.acme.TransferServiceImpl
    @Bean
    public TransferServiceImpl transferService() {
        return new TransferServiceImpl();
    }
}

等效的 XML 配置如下:

<beans>
    <bean id="transferService" class="com.acme.TransferServiceImpl"/>
</beans>
  1. 配置Bean的属性
  • 修改Bean的名称和描述

可以使用name属性来指定bean的名称,或通过指定多个名称来设置别名。

@Configuration
public class AppConfig {

    // 定义一个Bean指定名称为:myThing
    @Bean(name = "myThing")
    public Thing thing() {
        return new Thing();
    }
}

@Configuration
public class AppConfig {
	// 定义一个Bean,并指定多个名称
    @Bean({"dataSource", "subsystemA-dataSource", "subsystemB-dataSource"})
    public DataSource dataSource() {
        // instantiate, configure and return DataSource bean...
    }
}

在必要的时候,也可以通过@Description提供更详细的文本描述。

@Configuration
public class AppConfig {

    // 定义一个Bean,并添加描述信息
    @Bean
    @Description("Provides a basic example of a bean")
    public Thing thing() {
        return new Thing();
    }
}
  • 设置生命周期回调

@Bean 注解支持指定任意的初始化和销毁回调方法,就像 XML 配置中的init-method和destroy-method属性一样。

public class BeanOne {

    public void init() {
        // initialization logic
    }
}

public class BeanTwo {

    public void cleanup() {
        // destruction logic
    }
}

@Configuration
public class AppConfig {

    // 指定初始化方法
    @Bean(initMethod = "init")
    public BeanOne beanOne() {
        return new BeanOne();
    }

    // 指定销毁方法
    @Bean(destroyMethod = "cleanup")
    public BeanTwo beanTwo() {
        return new BeanTwo();
    }
}

你也可以在构造期间直接调用 init() 方法同样有效。

@Configuration
public class AppConfig {
    
    @Bean
    public BeanOne beanOne() {
        BeanOne beanOne = new BeanOne();
        beanOne.init();
        return beanOne;
    }
}

除上之外,任何使用 @Bean 注解定义的类都支持常规的生命周期回调,并且可以使用 JSR-250 中的@PostConstruct和@PreDestroy注解。同样的,如果 bean 实现了InitializingBean,DisposableBean或Lifecycle,则容器将调用它们各自的方法。

注意:默认情况下,使用Java 配置时会自动将公共的close或shutdown方法注册为销毁回调,可以使用下面方式去除。

@Bean(destroyMethod="")
public DataSource dataSource() throws NamingException {
	return (DataSource) jndiTemplate.lookup("MyDS");
}
  • 修改Bean的范围

使用 Java 配置默认生成的 Bean 范围是 singleton,你可以使用 @Scope 注解来修改它。

@Configuration
public class MyConfiguration {

    @Bean
    @Scope("prototype")
    public Encryptor encryptor() {
        // ...
    }
}

提示:有关scoped-proxy属性的使用请参考官方文档!

  1. Bean的依赖注入
  • 方法参数方式

创建Bean的方法可以具有任意数量的参数,这些参数描述构建该 bean 所需的依赖关系,解析机制与基于构造函数的依赖注入几乎相同。

@Configuration
public class AppConfig {

    // transferService -> com.acme.TransferServiceImpl
    @Bean
    public TransferService transferService(AccountRepository accountRepository) {
        return new TransferServiceImpl(accountRepository);
    }
}
  • 方法调用方式

除了使用方法参数来定义依赖外,还可以通过方法调用来定义 Bean 之间的依赖。

@Configuration
public class AppConfig {

    // 定义一个 beanOne,并通过方法调用依赖 beanTwo
    @Bean
    public BeanOne beanOne() {
        return new BeanOne(beanTwo());
    }

    // 定义一个 beanTwo
    @Bean
    public BeanTwo beanTwo() {
        return new BeanTwo();
    }
}

等效的 XML 配置如下:

<beans>
    <bean id="beanOne" class="com.acme.services.BeanOne">
    	<constructor-arg name="beanTwo" ref="beanTwo"/>
    </bean>
    
    <bean id="beanTwo" class="com.acme.services.BeanTwo"></bean>
</beans>

注意:仅当在@Configuration类中声明@Bean方法时,此声明 bean 间依赖性的方法才有效。您不能通过使用普通@Component类来声明 Bean 间的依赖关系。

  • 关于方法调用定义 Bean 依赖的进一步说明

请看如下示例,clientDao()在clientService1()中被调用过一次,在clientService2()中被调用过一次,但是这两次调用返回的却是同一个实例。

@Configuration
public class AppConfig {

    // 定义一个Bean:clientService1
    @Bean
    public ClientService clientService1() {
        ClientServiceImpl clientService = new ClientServiceImpl();
        // 第一次调用 clientDao() 
        clientService.setClientDao(clientDao());
        return clientService;
    }

    // 定义一个Bean:clientService2
    @Bean
    public ClientService clientService2() {
        ClientServiceImpl clientService = new ClientServiceImpl();
        // 第二次调用 clientDao() 
        clientService.setClientDao(clientDao());
        return clientService;
    }

    // 定义一个单例Bean:clientDao
    @Bean
    public ClientDao clientDao() {
        return new ClientDaoImpl();
    }
}

因为所有@Configuration类在启动时都使用CGLIB子类化。在子类中,子方法在调用父方法并创建新实例之前,首先检查容器中是否有任何缓存(作用域)的 bean。

  • 类成员注入

Configuration类也是一个Bean,因此可以使用@Autowired或@Value注入需要的依赖,在方法中使用。

@Configuration
public class ServiceConfig {

    // 注入依赖:accountRepository
    @Autowired
    private AccountRepository accountRepository;

    // 定义一个bean:transferService
    @Bean
    public TransferService transferService() {
        // 使用注入的依赖
        return new TransferServiceImpl(accountRepository);
    }
}

@Configuration
public class RepositoryConfig {
    private final DataSource dataSource;

    // 注入依赖:dataSource
    @Autowired
    public RepositoryConfig(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    // 定义一个Bean:accountRepository
    @Bean
    public AccountRepository accountRepository() {
        // 使用注入的依赖
        return new JdbcAccountRepository(dataSource);
    }
}

// 总配置类
@Configuration
@Import({ServiceConfig.class, RepositoryConfig.class})
public class SystemTestConfig {

    @Bean
    public DataSource dataSource() {
        // return new DataSource
    }
}

// 测试
public static void main(String[] args) {
    ApplicationContext ctx = new AnnotationConfigApplicationContext(SystemTestConfig.class);
    TransferService transferService = ctx.getBean(TransferService.class);
    transferService.transfer(100.00, "A123", "C456");
}

但是这种方式是不被推荐的,因为Configuration类在上下文初始化期间非常早地处理的,并且强制以这种方式注入的依赖项(如dataSource和accountRepository)可能导致意外的早期初始化。

注意:与此类似的是,如果创建的Bean为BeanPostProcessor或BeanFactoryPostProcessor,应该将方法定义为 static方法,从而防止Configuration类被过早实例化。

  • 查找方法注入

在单例作用域的 bean 依赖于原型作用域的 bean 的情况下,通过使用 Java 配置,您可以创建CommandManager的子类,在该子类中,抽象createCommand()方法被覆盖,从而可以查找新的(原型)命令对象。

public abstract class CommandManager {
    public Object process(Object commandState) {
        // 获取一个新实例,并初始化
        Command command = createCommand();
        command.setState(commandState);
        // 使用新实例来执行业务逻辑
        return command.execute();
    }

    // okay... but where is the implementation of this method?
    protected abstract Command createCommand();
}

// 被依赖的原型Bean:asyncCommand
@Bean
@Scope("prototype")
public AsyncCommand asyncCommand() {
    AsyncCommand command = new AsyncCommand();
    return command;
}

// 依赖原型Bean的单例Bean:commandManager
@Bean
public CommandManager commandManager() {
    // 返回单例Bean实例,并实现查找方法:createCommand()
    return new CommandManager() {
        protected Command createCommand() {
            return asyncCommand();
        }
    }
}
  1. 导入其它配置类

与 XML 配置中的 import 标签一样,Java 配置中可以使用 @import 注解来导入其它配置类(或常规组件类)。

@Configuration
public class ConfigA {

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

@Configuration
@Import({ConfigA.class,UserService.class})
public class ConfigB {

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

@Component
public class UserService {
}

如果需要导入的类数量比较多,还可以使用ImportSelector或ImportBeanDefinitionRegistrar接口来辅助导入。

public class Myclass implements ImportSelector {
    @Override
    public String[] selectImports(AnnotationMetadata annotationMetadata) {
        // 返回全类名数组(注意不能返回null)
        return new String[]{"com.yc.Test.TestDemo3"};
    }
}

如果想对某些配置类或Bean进行选择性导入,即在dev环境导入指定类,在test环境导入另外的类,可以使用@Profile注解,具体用法可参考:https://blog.csdn.net/ysl19910806/article/details/91646554。

  1. Java和XML配置混合使用
  • 以XML为中心

即使用 ClassPathXmlApplicationContext 来创建容器,注解中的配置在XML中引入,主要思路是将配置类作为一个 Bean 注册到容器中,容器会识别@Configuration注解并正确处理配置类中声明的@Bean方法。

<beans>
    <!-- 打开注解配置开关 -->
    <context:annotation-config/>

    <!-- 定义配置类作为一个Bean -->
    <bean class="com.acme.AppConfig"/>
</beans>

也可以使用注解扫描来进行配置类的Bean定义,因为@Configuration使用了@Component进行元注解。

<beans>
    <!-- 配置注解扫描(隐式打开注解配置开关) -->
    <context:component-scan base-package="com.acme"/>
</beans>
  • 以注解为中心的配置

即使用 AnnotationConfigApplicationContext 来创建容器,通过在@Configuration类上使用@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>
  1. 关于在组件中定义Bean的说明

可以在 @Component 中使用 @Bean 注解来定义其它的 Bean,但这些Bean是有限制的。

@Component
public class FactoryMethodComponent {
    private static int i;

    @Bean
    @Qualifier("public")
    public TestBean publicInstance() {
        return new TestBean("publicInstance");
    }

    // use of a custom qualifier and autowiring of method parameters
    @Bean
    protected TestBean protectedInstance( @Qualifier("public") TestBean spouse, 
                                          @Value("#{privateInstance.age}") String country) {
        TestBean tb = new TestBean("protectedInstance", 1);
        tb.setSpouse(spouse);
        tb.setCountry(country);
        return tb;
    }

    @Bean
    private TestBean privateInstance() {
        return new TestBean("privateInstance", i++);
    }

    @Bean
    @RequestScope
    public TestBean requestScopedInstance() {
        return new TestBean("requestScopedInstance", 3);
    }
}

常规 Spring 组件中的@Bean方法的处理方式与 Spring @Configuration类中相应方法的处理方式不同。不同之处在于,CGLIB 并未增强@Component类来拦截方法和字段的调用。 CGLIB 代理是调用@Configuration类中@Bean方法中的方法或字段中的字段的方法,用于创建 Bean 元数据引用以协作对象。此类方法不是用普通的 Java 语义调用的,而是通过容器进行的,以提供通常的生命周期 Management 和 Spring bean 的代理,即使通过编程调用@Bean方法引用其他 bean 时也是如此。相反,在普通@Component类内的@Bean方法中调用方法或字段具有标准 Java 语义,而无需特殊的 CGLIB 处理或其他约束。

您可以将@Bean方法声明为static,从而允许在不将其包含的配置类创建为实例的情况下调用它们。在定义后处理器 Bean(例如,类型BeanFactoryPostProcessor或BeanPostProcessor)时,这特别有意义,因为此类 Bean 在容器生命周期的早期进行了初始化,并且应避免在那时触发配置的其他部分。

由于技术限制,对静态@Bean方法的调用永远不会被容器拦截,即使在@Configuration类中也是如此(如本节前面所述),由于技术限制:CGLIB 子类只能覆盖非静态方法。因此,直接调用另一个@Bean方法具有标准的 Java 语义,从而导致直接从工厂方法本身直接返回一个独立的实例。

@Bean方法的 Java 语言可见性不会对 Spring 容器中的最终 bean 定义产生直接影响。您可以随意声明自己的工厂方法,以适合非@Configuration类,也可以随处声明静态方法。但是,@Configuration类中的常规@Bean方法必须是可重写的—即,不得将它们声明为private或final。

还可以在给定组件或配置类的 Base Class 上以及在由组件或配置类实现的接口中声明的 Java 8 默认方法上找到@Bean方法。这为组合复杂的配置安排提供了很大的灵 Active,从 Spring 4.2 开始,通过 Java 8 默认方法甚至可以实现多重继承。

最后,单个类可以为同一个 bean 保留多个@Bean方法,这取决于在运行时可用的依赖关系,从而可以使用多个工厂方法。这与在其他配置方案中选择“最贪婪”的构造函数或工厂方法的算法相同:在构造时将选择具有最大可满足依赖关系数量的变量,类似于容器在多个@Autowired构造函数之间进行选择的方式。

您还可以声明类型为InjectionPoint(或其更具体的子类:DependencyDescriptor)的工厂方法参数,以访问触发当前 bean 创建的请求注入点。注意,这仅适用于实际创建 bean 实例,而不适用于注入现有实例。因此,此功能对原型范围的 bean 最有意义。

@Component
public class FactoryMethodComponent {

    @Bean @Scope("prototype")
    public TestBean prototypeInstance(InjectionPoint injectionPoint) {
        return new TestBean("prototypeInstance for " + injectionPoint.getMember());
    }
}

第八节 环境抽象

Environment接口用来表示整个应用运行时的环境,是当前Bean集合及相关属性在容器中的一个抽象,定义了下面两个重要的概念。

  1. Profile

Profile用于控制哪些Bean被注册,哪些Bean不被注册,只有处于活动状态的 Bean 才会被注册。@Profile 是 Spring 为我们提供的可以根据当前环境,动态的激活和切换一系列组件的功能。

  • 为Bean配置环境

@Profile 注解可以作用于配置类或方法之上,表示只有对应的环境被激活时,被注解的Bean才会被注册到Spring容器。

@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");
    }
}


@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");
    }
}

提示

  1. 可以使用一些基本运算符来配置Bean的环境。如 production & (us-east | eu-central)。
  2. 可以同时配置多个环境。如@Profile({“p1”, “!p2”})表示在p1活动状态或p2未活动状态进行注册。
  3. 如果 @Profile 注解作用于同名的@Bean方法(方法重载)之上,则它们之间的配置最好相同。

在XML配置中,可以使用 beans 标签的profile属性来配置Bean的环境,但可能会有一些限制。

<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>
  • 激活环境

我们在启动容器时,需要指定激活的环境,否则会抛出NoSuchBeanDefinitionException。最直接的方法是通过 Environment API 以编程方式进行配置。

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

当然,也可以使用声明式的方式,通过系统环境变量、JVM系统属性等方式设置 spring.profiles.active 属性的值。

-Dspring.profiles.active="profile1,profile2"

在特定环境,如WEB开发中也可以设置web.xml中的servlet 上下文参数,或测试环境中,通过 @ActiveProfiles 注解来声明。

  • 默认环境

当spring.profiles.active没有被设置时,那么Spring会根据spring.profiles.default属性的对应值来进行Profile进行激活。spring.profiles.default属性的默认值为 default,可以使用setDefaultProfiles()方法或spring.profiles.default属性来修改。

@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();
    }
}
  1. @PropertySource

Property 表示当前环境中的属性配置,属性可能来源于properties文件、JVM properties、system环境变量、JNDI、servlet context parameters上下文参数、专门的properties对象,Maps等。@PropertySource 注解用于加载指定的属性文件(properties/xml/yml)到 Spring 的 Environment 中。

  • 从环境中取出属性

我们可以使用${}或env.getProperty("")从 Environment 中取出这些值。

@Configuration
@PropertySource("classpath:/com/${my.placeholder:default/path}/app.properties")
public class AppConfig {

    @Autowired
    Environment env;

    @Bean
    public TestBean testBean() {
        TestBean testBean = new TestBean();
        testBean.setName(env.getProperty("testbean.name"));
        return testBean;
    }
}
  • 与@Value组合使用

@PropertySource与@Value组合使用,可以将自定义属性文件中的属性变量值注入到当前类的使用@Value注解的成员变量中。

@Component
@PropertySource(value = {"demo/props/demo.properties"})
public class ReadByPropertySourceAndValue {

    @Value("${demo.name}")
    private String name;

    @Value("${demo.sex}")
    private int sex;

    @Value("${demo.type}")
    private String type;

    @Override
    public String toString() {
        return "ReadByPropertySourceAndValue{" +
                "name='" + name + '\'' +
                ", sex=" + sex +
                ", type='" + type + '\'' +
                '}';
    }
}
  • 与@ConfigurationProperties组合使用(SpringBoot)

和 @ConfigurationProperties 组合使用,可以将属性文件与一个Java类绑定,将属性文件中的变量值注入到该Java类的成员变量中。

@Component
@PropertySource(value = {"demo/props/demo.properties"})
@ConfigurationProperties(prefix = "demo")
public class ReadByPropertySourceAndConfProperties {

    private String name;

    private int sex;

    private String type;

    public void setName(String name) {
        this.name = name;
    }

    public void setSex(int sex) {
        this.sex = sex;
    }

    public void setType(String type) {
        this.type = type;
    }

    public String getName() {
        return name;
    }

    public int getSex() {
        return sex;
    }

    public String getType() {
        return type;
    }

    @Override
    public String toString() {
        return "ReadByPropertySourceAndConfProperties{" +
                "name='" + name + '\'' +
                ", sex=" + sex +
                ", type='" + type + '\'' +
                '}';
    }
}

第九节 其它功能

  1. 加载时编织(LoadTimeWeaver)

如果需要在将类加载到 Java 虚拟机(JVM)中时对其进行动态转换,则可使用下面方式开启加载时编织。

@Configuration
@EnableLoadTimeWeaving
public class AppConfig {
}

等效的XML配置如下:

<beans>
    <context:load-time-weaver/>
</beans>

一旦为ApplicationContext配置,该ApplicationContext内的任何 bean 都可以实现LoadTimeWeaverAware,从而接收到对加载时编织器实例的引用。

  1. 使用 MessageSource 进行国际化

  2. 标准和自定义事件

ApplicationContext中的事件处理是通过ApplicationEvent类和ApplicationListener接口提供的。如果将实现ApplicationListener接口的 bean 部署到上下文中,则每次将ApplicationEvent发布到ApplicationContext时,都会通知该 bean。本质上,这是标准的 Observer 设计模式。
下表描述了 Spring 提供的标准事件:

Event Explanation
ContextRefreshedEvent 在初始化或刷新ApplicationContext时发布(例如,通过使用ConfigurableApplicationContext接口上的refresh()方法)。在这里,“已初始化”是指所有 Bean 都已加载,检测到并激活了后处理器 Bean,已预先实例化单例,并且已准备好使用ApplicationContext对象。只要尚未关闭上下文,只要选定的ApplicationContext实际上支持这种“热”刷新,就可以多次触发刷新。例如,XmlWebApplicationContext支持热刷新,但GenericApplicationContext不支持。
ContextStartedEvent 在ConfigurableApplicationContext界面上使用start()方法启动ApplicationContext时发布。在这里,“启动”是指所有Lifecycle bean 都收到一个明确的启动 signal。通常,此 signal 用于在显式停止后重新启动 Bean,但也可以用于启动尚未配置为自动启动的组件(例如,尚未在初始化时启动的组件)。
ContextStoppedEvent 在ConfigurableApplicationContext接口上使用stop()方法停止ApplicationContext时发布。此处,“已停止”表示所有Lifecycle bean 都收到一个明确的停止 signal。停止的上下文可以通过start()调用重新启动。
ContextClosedEvent 在ConfigurableApplicationContext接口上使用close()方法关闭ApplicationContext时发布。此处,“封闭”表示所有单例 bean 都被破坏。封闭的情境到了生命的尽头。无法刷新或重新启动。
RequestHandledEvent 一个特定于 Web 的事件,告诉所有 Bean HTTP 请求已得到服务。请求完成后,将发布此事件。此事件仅适用于使用 Spring 的DispatcherServlet的 Web 应用程序。

您还可以创建和发布自己的自定义事件。以下示例显示了一个简单的类,该类扩展了 Spring 的ApplicationEventBase Class:

public class BlackListEvent extends ApplicationEvent {

    private final String address;
    private final String content;

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

    // accessor and other methods...
}

要发布自定义ApplicationEvent,请在ApplicationEventPublisher上调用publishEvent()方法。通常,这是通过创建一个实现ApplicationEventPublisherAware的类并将其注册为 Spring bean 来完成的。以下示例显示了此类:

public class EmailService implements ApplicationEventPublisherAware {

    private List<String> blackList;
    private ApplicationEventPublisher publisher;

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

    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;
        }
        // send email...
    }
}

在配置时,Spring 容器检测到EmailService实现ApplicationEventPublisherAware并自动调用setApplicationEventPublisher()。实际上,传入的参数是 Spring 容器本身。您正在通过其ApplicationEventPublisher接口与应用程序上下文进行交互。

要接收自定义ApplicationEvent,您可以创建一个实现ApplicationListener的类并将其注册为 Spring Bean。以下示例显示了此类:

public class BlackListNotifier implements ApplicationListener<BlackListEvent> {

    private String notificationAddress;

    public void setNotificationAddress(String notificationAddress) {
        this.notificationAddress = notificationAddress;
    }

    public void onApplicationEvent(BlackListEvent event) {
        // notify appropriate parties via notificationAddress...
    }
}

注意,ApplicationListener通常用您的自定义事件的类型(上一示例中的BlackListEvent)进行参数化。这意味着onApplicationEvent()方法可以保持类型安全,从而避免了向下转换的任何需要。您可以根据需要注册任意数量的事件侦听器,但是请注意,默认情况下,事件侦听器会同步接收事件。这意味着publishEvent()方法将阻塞,直到所有侦听器都已完成对事件的处理为止。这种同步和单线程方法的一个优点是,当侦听器收到事件时,如果有可用的事务上下文,它将在发布者的事务上下文内部进行操作。

以下示例显示了用于注册和配置上述每个类的 Bean 定义:

<bean id="emailService" class="example.EmailService">
    <property name="blackList">
        <list>
            <value>[emailprotected]</value>
            <value>[emailprotected]</value>
            <value>[emailprotected]</value>
        </list>
    </property>
</bean>

<bean id="blackListNotifier" class="example.BlackListNotifier">
    <property name="notificationAddress" value="[emailprotected]"/>
</bean>

将所有内容放在一起,当调用emailService bean 的sendEmail()方法时,如果有任何电子邮件消息应列入黑名单,则会发布BlackListEvent类型的自定义事件。 blackListNotifier bean 注册为ApplicationListener并接收BlackListEvent,此时它可以通知适当的参与者。

Spring 的事件机制旨在在同一应用程序上下文内在 Spring bean 之间进行简单的通信。但是,对于更复杂的企业集成需求,单独维护的Spring Integration项目为构建基于众所周知的 Spring 编程模型的事件驱动的轻量级pattern-oriented体系结构提供了完整的支持。

从 Spring 4.2 开始,您可以使用EventListener注解在托管 Bean 的任何公共方法上注册事件侦听器。

public class BlackListNotifier {

    private String notificationAddress;

    public void setNotificationAddress(String notificationAddress) {
        this.notificationAddress = notificationAddress;
    }

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

方法签名再次声明其侦听的事件类型,但是这次使用灵活的名称并且没有实现特定的侦听器接口。只要实际事件类型在其实现层次结构中解析您的通用参数,也可以通过通用类型来缩小事件类型。

如果您的方法应该侦听多个事件,或者您要完全不使用任何参数来定义它,则事件类型也可以在注解本身上指定。以下示例显示了如何执行此操作:

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

也可以通过使用定义SpEL expression的注解的condition属性来添加其他运行时过滤,该属性应匹配以针对特定事件实际调用该方法。

以下示例显示了仅当事件的content属性等于my-event时,才可以重写我们的通知程序以进行调用:

@EventListener(condition = "#blEvent.content == 'my-event'")
public void processBlackListEvent(BlackListEvent blEvent) {
    // notify appropriate parties via notificationAddress...
}

每个SpEL表达式都针对专用上下文进行评估。下表列出了可用于上下文的项目,以便您可以将它们用于条件事件处理:

Name Location Description Example
Event root object 实际的ApplicationEvent。 #root.event
Arguments array root object 用于调用目标的参数(作为数组)。 #root.args[0]
Argument name evaluation context 任何方法参数的名称。如果由于某种原因名称不可用(例如,因为没有调试信息),则参数名称也可以在#a<#arg>下获得,其中#arg代表参数索引(从 0 开始)。 #blEvent或#a0(您也可以使用#p0或#p<#arg>表示法作为别名)

请注意,即使您的方法签名实际上引用了已发布的任意对象,#root.event也使您可以访问基础事件。

如果由于处理另一个事件而需要发布一个事件,则可以更改方法签名以返回应发布的事件,如以下示例所示:

@EventListener
public ListUpdateEvent handleBlackListEvent(BlackListEvent event) {
    // notify appropriate parties via notificationAddress and
    // then publish a ListUpdateEvent...
}

asynchronous listeners不支持此功能

此新方法为上述方法处理的每个BlackListEvent发布一个新的ListUpdateEvent。如果您需要发布多个事件,则可以返回Collection事件。

  • 异步事件监听
    如果希望特定的侦听器异步处理事件,则可以重用常规@Async 支持。以下示例显示了如何执行此操作:

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

使用异步事件时,请注意以下限制:

  1. 如果事件监听器抛出Exception,则它不会传播到调用者。有关更多详细信息,请参见AsyncUncaughtExceptionHandler。
  2. 此类事件侦听器无法发送答复。如果您需要发送另一个事件作为处理结果,请注入ApplicationEventPublisher以手动发送事件。
  • 监听器调用顺序
    如果需要先调用一个侦听器,则可以将@Order注解添加到方法声明中,如以下示例所示:

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

  • 监听器泛型
    您还可以使用泛型来进一步定义事件的结构。考虑使用EntityCreatedEvent,其中T是已创建的实际实体的类型。例如,您可以创建以下侦听器定义以仅接收_4 的EntityCreatedEvent:

    @EventListener
    public void onPersonCreated(EntityCreatedEvent event) {

    }

由于类型擦除,只有在触发的事件解析了事件侦听器所依据的通用参数(即诸如class PersonCreatedEvent extends EntityCreatedEvent { … })时,此方法才起作用。

在某些情况下,如果所有事件都遵循相同的结构,这可能会变得很乏味(就像前面示例中的事件一样)。在这种情况下,您可以实现ResolvableTypeProvider以指导框架超出运行时环境提供的范围。以下事件显示了如何执行此操作:

public class EntityCreatedEvent<T> extends ApplicationEvent implements ResolvableTypeProvider {

    public EntityCreatedEvent(T entity) {
        super(entity);
    }

    @Override
    public ResolvableType getResolvableType() {
        return ResolvableType.forClassWithGenerics(getClass(), ResolvableType.forInstance(getSource()));
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值