16-21 IOC进阶-功能特性

149 阅读7分钟

16、事件机制&监听器

16.1 观察者模式

观察者模式 的三大核心是:观察者、被观察主题、订阅者。某一个对象被修改 / 做出某些反应 / 发布一个信息等,会自动通知依赖它的对象(订阅者)。

  1. 观察者模式中与 Spring 中角色的对应关系:

    • 观察者 - 广播器 - IOC容器;
    • 订阅者 - 监听器。
  2. 广播器 是事件真正广播给监听器的对象,即 ApplicationContext。 其中:

ApplicationEventPublisher 接口,具备 发布事件的能力

ApplicationEventMulticaster 组合 (建立依赖关系?) 了所有的监听器,具备 广播事件的能力

16.2 事件与监听器

事件ContextRefreshedEventContextClosedEvent

分别代表 容器刷新完毕即将关闭

监听器必须要注册到 Spring 的 IOC 容器才能生效,所以要用 @Component 注解标注监听器,以便包扫描。

自定义监听器 两种方法:

  1. 实现 Spring 中内置的监听器接口 ApplicationListener ,带有的 泛型 代表要监听的具体事件。
  2. 使用注解式监听器,直接在需要作出事件反应的方法上标注 @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 容器中。

通用抽取

问题: 观察到上面的例子,把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)