16、事件机制&监听器
16.1 观察者模式
观察者模式 的三大核心是:观察者、被观察主题、订阅者。某一个对象被修改 / 做出某些反应 / 发布一个信息等,会自动通知依赖它的对象(订阅者)。
-
观察者模式中与 Spring 中角色的对应关系:
- 观察者 - 广播器 - IOC容器;
- 订阅者 - 监听器。
-
广播器 是事件真正广播给监听器的对象,即 ApplicationContext。 其中:
ApplicationEventPublisher
接口,具备 发布事件的能力
ApplicationEventMulticaster
组合 (建立依赖关系?) 了所有的监听器,具备 广播事件的能力
16.2 事件与监听器
事件:ContextRefreshedEvent
和 ContextClosedEvent
;
分别代表 容器刷新完毕 和 即将关闭。
监听器必须要注册到 Spring 的 IOC 容器才能生效,所以要用 @Component
注解标注监听器,以便包扫描。
自定义监听器 两种方法:
- 实现 Spring 中内置的监听器接口
ApplicationListener
,带有的 泛型 代表要监听的具体事件。 - 使用注解式监听器,直接在需要作出事件反应的方法上标注
@EventListener
注解。
16.3 Spring内置事件
- ApplicationEvent,由所有事件继承的抽象类。继承自 jdk 原生的观察者模式的事件模型。
- ApplicationContextEvent,在构造时,会把 IOC 容器一起传进去,这意味着事件发生时,可以通过监听器直接取到
ApplicationContext
而不需要做额外的操作。是下面几个类的父类: - ContextRefreshedEvent & ContextClosedEvent
-
ContextStartedEvent & ContextStoppedEvent
ContextRefreshedEvent
事件的触发是所有单实例 Bean 刚创建完成后,就发布的事件,此时那些实现了Lifecycle
接口的 Bean 还没有被回调start
方法。 当这些start
方法被调用后,ContextStartedEvent
才会被触发。
ContextStoppedEvent
事件也是在ContextClosedEvent
触发之后才会触发,此时单实例 Bean 还没有被销毁,要先把它们都停掉才可以释放资源,销毁 Bean 。
16.4 自定义事件
使用场景少,暂时不看
17、模块装配
SpringBoot 的自动装配,基础就是模块装配 + 条件装配。
-
原生手动装配:
- 常用方式:
@Configuration
+@Bean
注解组合 /@Component
+@ComponentScan
注解组合 - 弊端:当要注册的Bean很多时,前面介绍了比@Bean注解更好的@Component注解,但后者需要选好包进行组件扫描,而且每个类还要标注好
@Component
。还是比较麻烦。
- 常用方式:
-
模块装配:使用一个注解,把一个模块需要的核心功能组件都装配好。
模块装配的核心原则:自定义注解 +
@Import
导入组件。 自定义注解上需要标注@Import
,而@Import
注解内可以传入四类值:-
普通类
此时Bean类上
不需标注@Component
等注解;需要在配置类上标注
自定义注解
。 -
配置类
可以和普通类等共同传入
@Import
-
ImportSelector
接口的实现类ImportSelector
可以导入配置类/普通类,实现接口时需要覆写selectImports
方法,这个方法返回String[]类型的一组全限定类名(使用 类型.class.getName 获取),我们需要手动输入返回数组:public String[] selectImports(AnnotationMetadata importingClassMetadata) { return new String[] {Bar.class.getName(),BarConfiguration.class.getName()}; }
注意:
ImportSelector
接口的实现类,没有注册到 IOC 容器! -
ImportBeanDefinitionRegistrar
接口的实现类它导入的实际是
BeanDefinition
( Bean 的定义信息),实现接口时需要覆写registerBeanDefinitions(两个参数)
方法。例如:public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) { registry.registerBeanDefinition( "waiter", new RootBeanDefinition(Waiter.class) ); }
registerBeanDefinition
方法的第一个参数是 Bean 的名称(id),第二个参数中传入的RootBeanDefinition
要指定 Bean 的字节码(.class
)。
-
注意:上面四类方式都需要一个启动类(xxxApplication),都需要在自定义注解上方的 @Import注解
中加入需要传入的类。
18、条件装配
18.1 @Profile
使用方法:在注册Bean的配置类上标注 @Profile
,例如:
@Profile("city")
public class BartenderConfiguration {}
默认情况下,ApplicationContext
中的 profile 为 “default” ,需要手动将 ApplicationContext
中的 profile设置为配置类中的 @Profile( "city" ) 。这有两类方法:
编程式设置运行时环境
修改启动类中的main方法。
错误示例:
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(TavernConfiguration.class); ctx.getEnvironment().setActiveProfiles("city");
这样也是无法将 @Profile 注释的Bean加载到 IOC 容器中的,因为——在
new AnnotationConfigApplicationContext
的时候,如果传入了配置类,它内部就自动初始化完成了,那些 Bean 也就都创建好了(refresh方法,15章2.2.3)。
正确方法:
创建 IOC 容器时,先new一个空的容器,然后设置了 profile 之后再将配置类注册到容器中。
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.getEnvironment().setActiveProfiles("city");
ctx.register(TavernConfiguration.class);
ctx.refresh();
声明式设置运行时环境
上面编程式配置的缺点:把 profile 硬编码在 .java 里了,若切换环境,还要重新编译。
可以采用命令行参数配置方式,在启动类中点右上角的 Configuration,在 VM options
一栏中填入:
-Dspring.profiles.active=city
同样可以注册正确的 Bean。
in action
以数据源为例,在开发环境、测试环境、生产环境中,项目连接的数据库都是不一样的。如果每切换一个环境都要重新改一遍配置文件,那真的是太麻烦了,所以咱就可以采用 @Profile 的方式来解决。
@Configuration
public class DataSourceConfiguration {
@Bean
@Profile("dev")
public DataSource devDataSource() {
return null;
}
@Bean
@Profile("test")
public DataSource testDataSource() {
return null;
}
...
}
这样写完之后,通过 @PropertySource
注解 + 外部配置文件,就可以做到只切换 profile 即可切换不同的数据源。
总结
-
@Profile
注解可以标注在组件上,当一个配置属性激活时,它才会起作用,而激活这个属性的方式有很多种:- 可以通过
ConfigurableEnvironment.setActiveProfiles
以编程方式激活; - 也可以通过将
spring.profiles.active
属性设置为 JVM 系统属性,环境变量或web.xml
中用于 Web 应用的ServletContext
参数来声明性地激活; - 还可以通过
@ActiveProfiles
注解在集成测试中声明性地激活配置文件。
- 可以通过
-
profile 提供了一种可以理解成“基于环境的配置”:根据当前项目的运行时环境不同,可以动态的注册当前运行环境匹配的组件。
-
缺点:profile控制的是整个项目的运行环境,无法根据单个 Bean 的因素决定是否装配。
18.2 @Conditional
基本使用方法
被 @Conditional
注解标注的组件,只有所有指定条件都匹配时,才有资格注册。条件是可以在要注册 BeanDefinition
之前以编程式确定的任何状态。
-
@Conditional
注解可以通过以下任何一种方式使用:-
作为任何直接或间接用
@Component
注解的类的 类型级别注解,包括@Configuration
类- 传递性:如果
@Configuration
配置类被@Conditional
标记,则与该类关联的所有@Bean
的工厂方法、@Import
注解和@ComponentScan
注解也将受条件限制。
- 传递性:如果
-
作为 元注解 ,以组成自定义注解
-
作为任何
@Bean
方法上的 方法级注解
-
-
@Conditional
使用方法例如,某个类 Bar 需要依赖于 Boss类 才能存在,而 Bar类 是通过配置类中 @Bean方法注册的,则需要:
- 在@Bean方法上添加
@Conditional
注解,注解需要传入Condition
接口的实现类数组作为匹配依据——> 需要自己编写条件匹配类 - 编写匹配类,类要
implements Condition
接口,并且要覆写matches
方法。
此时若把 Boss类 从IOC容器中去除,则通过@Bean方法注册的 Bar 也不会注册到 IOC 容器中。
- 在@Bean方法上添加
通用抽取
问题: 观察到上面的例子,把IOC容器中是否包含依赖类的判断过程放到了 Condition
实现类中。 如果一个项目中,有比较多的组件需要依赖另一些不同的组件,如果每个组件都写一个 Condition
实现类作为匹配条件,那工程量真的太大了。
解决方法: 可以把匹配规则抽取为通用的方式。
-
抽取传入的beanName(判断条件):
-
把
@Conditional
当做元注解,自定义一个注解:@Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE, ElementType.METHOD}) @Conditional(OnBeanCondition.class) public @interface ConditionalOnBean { String[] beanNames() default {}; }
public class OnBeanCondition implements Condition { @Override public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { // 先获取目标自定义注解ConditionalOnBean上的beanNames属性 String[] beanNames = (String[]) metadata.getAnnotationAttributes(ConditionalOnBean.class.getName()).get("beanNames"); // 逐个校验IOC容器中是否包含传入的bean名称 for (String beanName : beanNames) { if (!context.getBeanFactory().containsBeanDefinition(beanName)) { return false; } } return true; } }
随后,将方法级注解
@Conditional
替换为自定义注解@ConditionalOnBean
即可:@Bean @ConditionalOnBean(beanNames = "com.linkedbear.spring.configuration.c_conditional.component.Boss") public Bar bbbar() { return new Bar(); }
-
-
上面自定义注解中需要传入全限定名,改造
OnBeanCondition
,对value
的属性进行解析,并加入类型匹配。 最后效果:@ConditionalOnBean(Boss.class)