Spring系列-04-事件机制,监听器,模块/条件装配

事件机制&监听器

SpringFramework中设计的观察者模式-掌握

SpringFramework 中, 体现观察者模式的特性就是事件驱动监听器监听器充当订阅者, 监听特定的事件;事件源充当被观察的主题, 用来发布事件;IOC 容器本身也是事件广播器, 可以理解成观察者

  • 事件源:发布事件的对象
  • 事件:事件源发布的信息 / 作出的动作
  • 广播器:事件真正广播给监听器的对象【即ApplicationContext
    • ApplicationContext 接口有实现 ApplicationEventPublisher 接口, 具备事件广播器的发布事件的能力
    • ApplicationEventMulticaster 组合了所有的监听器, 具备事件广播器的广播事件的能力
  • 监听器:监听事件的对象
    在这里插入图片描述

事件与监听器使用

两种方式, 一种是实现``ApplicationListener接口, 一种是注解@EventListener`

ApplicationListener接口

SpringFramework 中内置的监听器接口是 ApplicationListener , 它还带了一个泛型, 代表要监听的具体事件:

@FunctionalInterface // 函数式接口
public interface ApplicationListener<E extends ApplicationEvent> extends EventListener {
	void onApplicationEvent(E event);
}

事件ContextRefreshedEventContextClosedEvent , 它们分别代表容器刷新完毕即将关闭, 这里以监听ContextRefreshedEvent事件为例子

@Component // 监听器注册到IOC容器当中
public class ContextRefreshedApplicationListener implements ApplicationListener<ContextRefreshedEvent> { // 监听 ContextRefreshedEvent 事件
    
    @Override 
    public void onApplicationEvent(ContextRefreshedEvent event) {
        System.out.println("ContextRefreshedApplicationListener监听到ContextRefreshedEvent事件!");
    }
}

@EventListener注解式监听器

使用注解式监听器, 组件不再需要实现任何接口, 而是直接在需要作出事件反应的方法上标注 @EventListener 注解即可

@Component

public class ContextClosedApplicationListener {
    @EventListener
    public void onContextClosedEvent(ContextClosedEvent event) {
        System.out.println("ContextClosedApplicationListener监听到ContextClosedEvent事件!");
    }
}
public class QuickstartListenerApplication {    
    public static void main(String[] args) throws Exception {
        System.out.println("准备初始化IOC容器。。。");
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(
            "com.linkedbear.spring.event.a_quickstart");
        System.out.println("IOC容器初始化完成。。。");
        ctx.close();
        System.out.println("IOC容器关闭。。。");
        /*
        控制台输出结果如下: 
        ContextRefreshedApplicationListener监听到ContextRefreshedEvent事件!
        IOC容器初始化完成。。。
        ContextClosedApplicationListener监听到ContextClosedEvent事件!
        IOC容器关闭。。。
        */
    }
}
  • ApplicationListener 会在容器初始化阶段就准备好, 在容器销毁时一起销毁
  • ApplicationListener 也是 IOC 容器中的普通 Bean

SpringFramework中的内置事件-熟悉

在 SpringFramework 中, 已经有事件的默认抽象, 以及4个默认的内置事件

ApplicationEvent

ApplicationEvent是事件模型的抽象, 它是一个抽象类, 里面也没有定义什么东西, 只有事件发生时的时间戳。

public abstract class ApplicationEvent extends EventObject // 继承自 jdk 原生的观察者模式的事件模型, 并且把它声明为抽象类

Class to be extended by all application events. Abstract as it doesn’t make sense for generic events to be published directly.

翻译:

由所有应用程序事件扩展的类。它被设计为抽象的, 因为直接发布一般事件没有意义

ApplicationContextEvent

public abstract class ApplicationContextEvent extends ApplicationEvent { // 继承ApplicationEvent
    public ApplicationContextEvent(ApplicationContext source) {
        super(source);
    }

    public final ApplicationContext getApplicationContext() {
        return (ApplicationContext) getSource();
    }
}

构造方法将IOC 容器一起传进去, 这意味着事件发生时, 可以通过监听器直接取到 ApplicationContext 而不需要做额外的操作, 这才是 SpringFramework 中事件模型扩展最值得的地方。下面列举的几个内置的事件, 都是基于这个 ApplicationContextEvent 扩展的

ContextRefreshedEvent&ContextClosedEvent

这两个是一对, 分别对应着 IOC 容器刷新完毕但尚未启动, 以及 IOC 容器已经关闭但尚未销毁所有 Bean 。这个时机可能记起来有点小困难, 小伙伴们可以不用记很多, 只通过字面意思能知道就 OK , 至于这些事件触发的真正时机, 在我的 SpringBoot 源码小册第 16 章中有提到, 感兴趣的小伙伴可以去看一看。在后面的 IOC 原理篇中, 这部分也会略有涉及。

ContextStartedEvent&ContextStoppedEvent

这一对跟上面的时机不太一样了。ContextRefreshedEvent 事件的触发是所有单实例 Bean 刚创建完成后, 就发布的事件, 此时那些实现了 Lifecycle 接口的 Bean 还没有被回调 start 方法。当这些 start 方法被调用后, ContextStartedEvent 才会被触发。同样的, ContextStoppedEvent 事件也是在 ContextClosedEvent 触发之后才会触发, 此时单实例 Bean 还没有被销毁, 要先把它们都停掉才可以释放资源, 销毁 Bean 。

自定义事件开发

什么时候需要自定义?

想自己在合适的时机发布一些事件, 让指定的监听器来以此作出反应, 执行特定的逻辑

自定义事件到底有什么刚需吗?讲道理, 真的非常少。很多场景下, 使用自定义事件可以处理的逻辑, 完全可以通过一些其它的方案来替代, 这样真的会显得自定义事件很鸡肋

运行示例

论坛应用, 当新用户注册成功后, 会同时发送短信、邮件、站内信, 通知用户注册成功, 并且发放积分。

在这个场景中, 用户注册成功后, 广播一个“用户注册成功”的事件, 将用户信息带入事件广播出去, 发送短信、邮件、站内信的监听器监听到注册成功的事件后, 会分别执行不同形式的通知动作。

自定义事件

仿Spring内置的事件, 继承ApplicationEvent

/**
 * 注册成功的事件
 */
public class RegisterSuccessEvent extends ApplicationEvent {
    public RegisterSuccessEvent(Object source) {
        super(source);
    }
}

监听器

使用实现ApplicationListener接口, 添加@EventListener注解两种

实现ApplicationListener接口
@Component
public class SmsSenderListener implements ApplicationListener<RegisterSuccessEvent> {
    @Override
    public void onApplicationEvent(RegisterSuccessEvent event) {
        System.out.println("监听到用户注册成功, 发送短信。。。");
    }
}
添加EmailSenderListener注解
@Component
public class EmailSenderListener {
    @EventListener
    public void onRegisterSuccess(RegisterSuccessEvent event) {
        System.out.println("监听到用户注册成功!发送邮件中。。。");
    }
}
@Component
public class MessageSenderListener {
    @EventListener
    public void onRegisterSuccess(RegisterSuccessEvent event) {
        System.out.println("监听到用户注册成功, 发送站内信。。。");
    }
}

注册逻辑业务层(事件发布器)

只有事件和监听器还不够, 还需要有一个事件源来持有事件发布器, 在应用上下文中发布事件

Service 层中, 需要注入 ApplicationEventPublisher 来发布事件, 此处选择使用回调注入的方式

@Service
public class RegisterService implements ApplicationEventPublisherAware {
    ApplicationEventPublisher publisher;
    
    /**
    * 用户注册: 注册后会发布事件, 也就是说它是事件源
    */
    public void register(String username) {
        // 用户注册的动作。。。
        System.out.println(username + "注册成功。。。");
        // 发布事件, 将我们自定义的事件进行发布
        publisher.publishEvent(new RegisterSuccessEvent(username));
    }
    
    @Override
    public void setApplicationEventPublisher(ApplicationEventPublisher publisher) {
        this.publisher = publisher;
    }
}

测试启动类

public class RegisterEventApplication {
    public static void main(String[] args) throws Exception {
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(
                "com.linkedbear.spring.event.b_registerevent");
        RegisterService registerService = ctx.getBean(RegisterService.class);
        // 调用方法, 进行发布
        registerService.register("张大三");
        /*
        控制台打印结果如下: 
        张大三注册成功。。。
        监听到用户注册成功, 发送邮件中。。。
        监听到用户注册成功, 发送站内信。。。
        监听到用户注册成功, 发送短信。。。
        */
    }
}

注解式监听器的触发时机比接口式监听器早

调整监听器的触发顺序

监听器上标**@Order可以调整触发顺序, 默认的排序值为 Integer.MAX_VALUE , 代表最靠后**

使用如下

@Order(0)
@Component
public class MessageSenderListener {
    @EventListener
    public void onRegisterSuccess(RegisterSuccessEvent event) {
        System.out.println("监听到用户注册成功, 发送站内信。。。");
    }
}

模块装配&条件装配(理解)

SpringBoot 的自动装配, 基础就是模块装配 + 条件装配

模块装配

原生手动装配

最原始的 Spring不支持注解驱动开发, 后续逐渐引入注解驱动开发, 现在常用@Configuration + @Bean 注解组合, 或者 @Component + @ComponentScan 注解组合, 可以实现编程式/声明式的手动装配

存在的问题: 如果要注册的 Bean 很多, 要么一个一个的 @Bean 编程式写, 要么就得选好包进行组件扫描, 而且这种情况还得每个类都标注好 @Component 或者它的衍生注解才行。面对数量很多的Bean , 这种装配方式很明显会比较麻烦

模块概念和模块装配

模块可以理解成一个一个的可以分解、组合、更换的独立的单元, 模块与模块之间可能存在一定的依赖, 模块的内部通常是高内聚的, 一个模块通常都是解决一个独立的问题

模块特征

  • 独立的
  • 功能高内聚
  • 可相互依赖
  • 目标明确

模块构成

在这里插入图片描述

模块装配可以理解为把一个模块需要的核心功能组件都装配好(注意, 这里强调了核心功能)

Spring的模块装配

SpringFramework中的模块装配, 是在3.1之后引入大量**@EnableXXX注解, 来快速整合激活**相对应的模块(这里用的是词语是激活, 也就是一个开关的意思, 不需要我们再手动将这些模块所需的Bean挨个注册了)

@EnableXXX注解的使用例子

  • EnableTransactionManagement :开启注解事务驱动
  • EnableWebMvc :激活 SpringWebMvc
  • EnableAspectJAutoProxy :开启注解 AOP 编程
  • EnableScheduling :开启调度功能(定时任务)
@Import注解解析

模块装配的核心原则:自定义注解 + @Import 导入组件

@Import注解解析

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Import {

    /**
	 * {@link Configuration @Configuration}, {@link ImportSelector},
	 * {@link ImportBeanDefinitionRegistrar}, or regular component classes to import.
	 */
    Class<?>[] value();
}

文档中写道"允许导入 @ Configuration类、ImportSelector和ImportBeanDefinitionRegistrar实现以及常规组件类", 这里的组件类就被Spring管理的普通类

@Import使用案例

模仿Spring的@EnableXxx注解, 实现一个自己定义的注解

定义老板和调酒师模型

老板

@Data
public class Boss {
    
}

调酒师

@Data
public class Bartender {   
    private String name;
}
注册调酒师对象(配置类统一管理)
@Configuration
public class BartenderConfiguration {
    @Bean
    public Bartender zhangxiaosan() {
        return new Bartender("张小三");
    }
    
    @Bean
    public Bartender zhangdasan() {
        return new Bartender("张大三");
    }
    
}
定义注解
@Import({Boss.class, BartenderConfiguration.class}) // 说明要导入
public @interface EnableTavern {}

启动类里或者配置类上用了包扫描, 恰好把这个类扫描到了, 导致即使没有 @Import 这个 BartenderConfiguration , Bartender 调酒师也被注册进 IOC 容器了

酒馆配置类
@EnableTavern // Tavern配置类中添加上这个注解, 表明使用这个配置时将其相关bean激活, 并自动注册到Spring中 
@Configuration
public class TavernConfiguration {

}
测试
public class TavernApplication {
    public static void main(String[] args) throws Exception {
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(TavernConfiguration.class);
        Stream.of(ctx.getBeanDefinitionNames())
            .forEach(System.out::println);
        System.out.println("--------------------------");
        // 一次性取出IOC容器指定类型的所有Bean
        Map<String, Bartender> bartenders = ctx.getBeansOfType(Bartender.class);
        // 会打印出两个调酒师, 分别为zhangxiaosan和zhangdasan的内存地址
        bartenders.forEach((name, bartender) -> System.out.println(bartender));
    }
}
ImportSelector接口解析
public interface ImportSelector {
	/**
	 * 根据导入@Configuration类的AnnotationMetadata选择并返回应导入的类的名称。即是全限定类名
	 * 返回值: 类名, 如果没有, 则为空数组
	 */
	String[] selectImports(AnnotationMetadata importingClassMetadata);

	/**
	 * 返回一个谓词, 用于从导入候选项中排除类, 以传递方式应用于通过此选择器的导入找到的所有类。
如果此谓词对于给定的完全限定类名称返回true , 则该类将不会被视为导入的配置类, 从而绕过类文件加载和元数据内省
	 * 返回值: 可传递导入的配置类的完全限定候选类名称的筛选谓词, 如果没有, 则为null
	 */
	@Nullable
	default Predicate<String> getExclusionFilter() {
		return null;
	}

}

Interface to be implemented by types that determine which @Configuration class(es) should be imported based on a given selection criteria, usually one or more annotation attributes.

它是一个接口, 它的实现类可以根据指定的筛选标准(通常是一个或者多个注解)来决定导入哪些配置类

ImportSelector使用案例

注意了, 这是在@Import的案例上继续进行的

定义吧台类
@Data
public class Bar {}
吧台配置类
@Configuration
public class BarConfiguration {   
    @Bean
    public Bar myBar() {
        return new Bar();
    }
}
实现ImportSelector接口
public class BarImportSelector implements ImportSelector {   
    @Override
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        return new String[] {Bar.class.getName(), BarConfiguration.class.getName()};
    }
}
@EnableTavern的@Import添加BarImportSelector全类名
@Import({Boss.class, BartenderConfiguration.class, BarImportSelector.class})
public @interface EnableTavern {
}
重新运行TavernApplication

最终结果会打印出bar, 也就说明ImportSelector是可以导入普通和配置类的

ImportBeanDefinitionRegistrar接口

如果说 ImportSelector 更像声明式导入的话, 那 ImportBeanDefinitionRegistrar 就可以解释为编程式向 IOC 容器中导入 Bean 。不过由于它导入的实际是 BeanDefinition ( Bean 的定义信息)

关于BeanDefinition的, 后续细说, 目前暂时不对ImportBeanDefinitionRegistrar进行过多性的解析, 相关用到的方法会简单说明

ImportBeanDefinitionRegistrar使用案例

注意了, 这是在ImportSelector的案例上继续进行的

定义服务员模型
@Data
public class Waiter {}
实现ImportBeanDefinitionRegistrar接口
public class WaiterRegistrar implements ImportBeanDefinitionRegistrar {

    @Override
    public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
        /*
        registerBeanDefinition传入两个参数
        第一个参数: Bean的id
        第二个参数: RootBeanDefinition要指定Bean字节码对象(即是class对象)
        */
        registry.registerBeanDefinition("waiter", new RootBeanDefinition(Waiter.class));
    }
}
把WaiterRegistrar标注在@EnableTavern的 @Import
@Import({Boss.class, BartenderConfiguration.class, BarImportSelector.class, WaiterRegistrar.class})
public @interface EnableTavern {}
运行TavernApplication

运行结果会发现waiter出现在打印结果当中

模块装配总结

什么是模块装配?

  • 将每个独立的模块所需要的核心功能组件进行装配

模块装配的核心是什么?

  • 通过@EnableXxx注解快速激活相应的模块

模块装配方式有几种

四种, 分别如下

  1. @Import + @Bean
  2. @Import + @Configuration
  3. @Import + ImportSelector实现类
  4. @Import + ImportBeanDefinitionRegistrar实现类

条件装配

有了模块装配为什么还需要有条件装配?

模块装配的主要目的是将应用程序划分为一系列的功能模块, 每个模块内部管理自己的 Bean 定义

上述的模块装配颗粒度太粗了, 没法做更加细致的控制, 比如什么情况要使用哪个配置, 而条件装配则是根据条件装配是指根据特定的条件来决定一个配置是否生效。例如, 可以根据系统属性、Bean 的存在与否、类路径上的特定资源等条件来决定某个配置是否应该被加载

Profile

Spring3.1引入进来profile, profile翻译过来就有配置文件的意思, 作用就是基于项目运行环境动态注册所需要的组件, 通常用于区分测试, 预发布, 生产环境的配置

Javadoc解释如下

Indicates that a component is eligible for registration when one or more specified profiles are active. A profile is a named logical grouping that may be activated programmatically via ConfigurableEnvironment.setActiveProfiles or declaratively by setting the spring.profiles.active property as a JVM system property, as an environment variable, or as a Servlet context parameter in web.xml for web applications. Profiles may also be activated declaratively in integration tests via the @ActiveProfiles annotation.

@Profile 注解可以标注一些组件, 当应用上下文的一个或多个指定配置文件处于活动状态时, 这些组件允许被注册。

配置文件是一个命名的逻辑组, 可以通过 ConfigurableEnvironment.setActiveProfiles 以编程方式激活, 也可以通过将 spring.profiles.active 属性设置为 JVM 系统属性, 环境变量或 web.xml 中用于 Web 应用的 ServletContext 参数来声明性地激活, 还可以通过 @ActiveProfiles 注解在集成测试中声明性地激活配置文件。

profile仍然存在颗粒度粗的问题, 因为profile控制的是整个项目的运行环境, 无法根据单个bean是否装配, 这里得依靠@Conditional实现更加细的粒度

@Profile使用

下述代码是基于模块装配代码上继续操作

调酒师工作的环境应该是啤酒店而不是其它随意环境, 例如菜市场

伪代码如下

if(工作环境 == 啤酒店) {
	调酒师工作
} else {
	调酒师不来=}

现在通过@Profile进行环境选择

@Profile("beer-shop") // 这里是@Porfile指定环境, 表明当运行环境为beer-shop, 那么就会将以下bean注册到Spring中
@Configuration
public class BartenderConfiguration {
    @Bean
    public Bartender zhangxiaosan() {
        return new Bartender("张小三");
    }
    
    @Bean
    public Bartender zhangdasan() {
        return new Bartender("张大三");
    }
}

之前Javadoc中不是指出了, 可以通过以下方式指定运行环境吗?

  • 编程式
  • Spring的active属性(最常用)
  • @ActiveProfiles
编程式指定

需要说明的是, ApplicationContext默认profile是"default", 也就说我们的@Profile(“beer-shop”)是不匹配的, 那么该配置下的两个bean是不会注册到IOC容器中的(自己可以运行)

可以通过以下方式指定运行环境

public class TavernApplication {
    public static void main(String[] args) throws Exception {
        // 注意了! 这里没有在构造方法指定配置文件, 由于AnnotationConfigApplicationContext传入配置类, 内部会进行自动初始化, 到时候打印不出bean, 所以这里选择手动将配置类注册到ctx中
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
        // 给ApplicationContext的环境设置正在激活的profile
        // PS: environment是一个Spring中环境对象(后续会说)
        ctx.getEnvironment().setActiveProfiles("beer-shop");
        // 注册配置, 注意了这个register动作必须在setActiveProfiles之后
        ctx.register(TavernConfiguration.class);
        // 改变了环境(即是配置发生了变化, 得我们通知Spring), 那么这里需要调用refresh进行刷新
        ctx.refresh();
        // 此时运行控制台就会出现zhangxiaosan和zhangdasan
        Stream.of(ctx.getBeanDefinitionNames())
            .forEach(System.out::println);
    }
}
声明式指定

在IDEA的启动配置的VM Option中指定下述参数

-Dspring.profiles.active=beer-shop

指定完后如下
在这里插入图片描述

当然也可以通过Spring的配置文件声明

spring:
  prifiles:
    active: beer-shop

Conditional

condition翻译过来就是条件, @Conditional 是在 SpringFramework 4.0 版本正式推出, 目的就是将满足@Conditiaonal上指定所有条件的bean进行装配(看到没有, 这里就实现了更加细粒度的装配)

Javadoc

Indicates that a component is only eligible for registration when all specified conditions match.

A condition is any state that can be determined programmatically before the bean definition is due to be registered (see Condition for details).

The @Conditional annotation may be used in any of the following ways:

as a type-level annotation on any class directly or indirectly annotated with @Component, including @Configuration classes
as a meta-annotation, for the purpose of composing custom stereotype annotations
as a method-level annotation on any @Bean method
If a @Configuration class is marked with @Conditional, all of the @Bean methods, @Import annotations, and @ComponentScan annotations associated with that class will be subject to the conditions.

被 @Conditional 注解标注的组件, 只有所有指定条件都匹配时, 才有资格注册。条件是可以在要注册 BeanDefinition 之前以编程式确定的任何状态。

@Conditional 注解可以通过以下任何一种方式使用:

  • 作为任何直接或间接用 @Component 注解的类的类型级别注解, 包括@Configuration 类
  • 作为元注解, 目的是组成自定义注解
  • 作为任何 @Bean 方法上的方法级注解

如果@Configuration配置类被@Conditional 标记, 则与该类关联的所有@Bean 的工厂方法, @Import 注解和 @ComponentScan 注解也将受条件限制。

唯一需要解释就是最后一句话, 它想表达的是@Conditional 注解标注的 组件类 / 配置类 / 组件工厂方法 必须满足 @Conditional 中指定的所有条件, 才会被创建 / 解析

@Conditional使用
@Conditional普通使用
@Conditianal注解解析以及Condition实现类
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Conditional {

	/**
	 * 要注册组件必须匹配的所有条件
	 */
	Class<? extends Condition>[] value();
}

查看@Conditional 注解源码, 返现中需要传入一个 Condition 接口的实现类数组, 说明咱还需要编写条件匹配类做匹配依据。那咱就先写一个匹配条件

public class ExistBossCondition implements Condition {
    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        // 这里用的是BeanDefinition做判断而不是Bean, 考虑的是当条件匹配时, 可能Boss还没被创建, 导致条件匹配出现偏差
        return context
            .getBeanFactory()
            .containsBeanDefinition(Boss.class.getName());
    }
}
@Conditional添加规则类

在BarConfiguration中指定bar创建需要有Boss存在

@Configuration
public class BarConfiguration { 
    @Bean
    @Conditional(ExistBossCondition.class)
    public Bar bbbar() {
        return new Bar();
    }
}
注释@Import, 减少干扰项

为了不干扰结果, 现在将EnableTavern上的@Import注释掉

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
// @Import({Boss.class, BartenderConfiguration.class, BarImportSelector.class, WaiterRegistrar.class})
public @interface EnableTavern {
    
}
运行main

运行下面的main方法, 那么就会发现确实 Bossbbbar 都没了

public static void main(String[] args) throws Exception {
    AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(TavernConfiguration.class);
    Stream
        .of(ctx.getBeanDefinitionNames())
        .forEach(System.out::println);
}
@Conditional派生派生

Javadoc中说了, @Conditional可以派生, 那么派生一个新注解@ConditionalOnBean, 即是当指定Bean存在的时候为之匹配

定义条件匹配规则类

首先明确需要一个条件匹配规则类, 虽然是派生的, 但是@Cdonditianal最终还是需要传入这个规则类的

public class OnBeanCondition implements Condition {
    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        // 从ConditionalOnBean注解中获取到需要匹配的Bean类型和Bean名
        Map<String, Object> attributes = metadata.getAnnotationAttributes(ConditionalOnBean.class.getName());
        // 遍历需要匹配的Bean类型, 检查Bean工厂中是否包含对应的Bean定义, 如果不包含则返回false
        Class<?>[] classes = (Class<?>[]) attributes.get("value");
        for (Class<?> clazz : classes) {
            if (!context.getBeanFactory().containsBeanDefinition(clazz.getName())) {
                return false;
            }
        }
        // 遍历需要匹配的Bean名称, 检查Bean工厂中是否包含对应的Bean定义, 如果不包含则返回false
        String[] beanNames = (String[]) attributes.get("beanNames");
        for (String beanName : beanNames) {
            if (!context.getBeanFactory().containsBeanDefinition(beanName)) {
                return false;
            }
        }
        // 执行到这里说明所有的Bean类型和Bean名称都匹配, 则返回true
        return true;
    }
}
定义注解
@Documented
@Conditional(OnBeanCondition.class) // 这里指定需要条件匹配规则类
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD}) // 这里指明了, 字段可用, 方法可用
public @interface ConditionalOnBean {
    /**
    * class对象
    */        
    Class<?>[] value() default {};

    /**
    * beanNames指定bean
    */
    String[] beanNames() default {};
}

替换原生的@Condional
@Bean
// @ConditionalOnBean(beanNames = "xxx.xxxx.xxx.Boss") // 全类名可以
@ConditionalOnBean(Boss.class) // class对象也行
public Bar bbbar() {
    return new Bar();
}

此时重新运行, 发现也确实是一样的效果

参考资料

从 0 开始深入学习 Spring

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/843178.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

ArcGIS Pro不能编辑ArcGIS10.X的注记的解决办法

​ 点击下方全系列课程学习 点击学习—>ArcGIS全系列实战视频教程——9个单一课程组合系列直播回放 点击学习——>遥感影像综合处理4大遥感软件ArcGISENVIErdaseCognition 一、问题 我们利用ArcGIS Pro编辑ArcGIS10.X系列软件生成的注记要素类的时候&#xff0c;会提示不…

FinClip 率先入驻 AWS Marketplace,加速全球市场布局

近日&#xff0c;凡泰极客旗下的小程序数字管理平台 FinClip 已成功上线亚马逊云科技&#xff08;AWS&#xff09;Marketplace。未来&#xff0c;FinClip 将主要服务于海外市场的开放银行、超级钱包、财富管理、社交电商、智慧城市解决方案等领域。 在全球市场的多样性需求推动…

KAFKA搭建教程

KAFKA搭建教程 期待您的关注 KAFKA学习笔记 帮助更多人 目录 KAFKA搭建教程 1.下载Kafka并解压 2.添加环境变量 3.修改 server.properties 文件 4.将kafka复制到其它节点 5.修改node1、node2节点的broker.id 6.将master的环境变量同步到node1、 node2 7.启动zookeeper…

Elasticsearch:评估搜索相关性 - 第 1 部分

作者&#xff1a;来自 Elastic Thanos Papaoikonomou, Thomas Veasey 这是一系列博客文章中的第一篇&#xff0c;讨论如何在更好地理解 BEIR 基准的背景下考虑评估你自己的搜索系统。我们将介绍具体的技巧和技术&#xff0c;以便在更好地理解 BEIR 的背景下改进你的搜索评估流程…

17_高级进程间通信 UNIX域套接字1

非命名的UNIX域套接字 第1个参数domain&#xff0c;表示协议族&#xff0c;只能为AF_LOCAL或者AF_UNIX&#xff1b; 第2个参数type&#xff0c;表示类型&#xff0c;只能为0。 第3个参数protocol&#xff0c;表示协议&#xff0c;可以是SOCK_STREAM或者SOCK_DGRAM。用SOCK_STR…

视觉巡线小车——STM32+OpenMV(三)

目录 前言 一、OpenMV代码 二、STM32端接收数据 1.配置串口 2.接收数据并解析 总结 前言 通过视觉巡线小车——STM32OpenMV&#xff08;二&#xff09;&#xff0c;已基本实现了减速电机的速度闭环控制。要使小车能够自主巡线&#xff0c;除了能够精准的控制速度之外&#xff0…

【BUG】已解决:raise KeyError(key) from err KeyError: (‘name‘, ‘age‘)

已解决&#xff1a;raise KeyError(key) from err KeyError: (‘name‘, ‘age‘) 欢迎来到英杰社区https://bbs.csdn.net/topics/617804998 欢迎来到我的主页&#xff0c;我是博主英杰&#xff0c;211科班出身&#xff0c;就职于医疗科技公司&#xff0c;热衷分享知识&#xf…

第十课:telnet(远程登入)

如何远程管理网络设备&#xff1f; 只要保证PC和路由器的ip是互通的&#xff0c;那么PC就可以远程管理路由器&#xff08;用telnet技术管理&#xff09;。 我们搭建一个下面这样的简单的拓扑图进行介绍 首先我们点击云&#xff0c;把云打开&#xff0c;点击增加 我们绑定vmn…

idea如何让包结构分层

文章目录 前言1.选中前项目包结构2.取消后项目包结构3.情况二 前言 在大型项目中&#xff0c;代码的分层管理至关重要。IDEA编辑器提供了强大的package分层结构功能&#xff0c;帮助开发者更好地组织和管理代码。通过合理配置&#xff0c;我们可以清晰地看到各个package之间的…

【BUG】已解决:java.lang.reflect.InvocationTargetException

已解决&#xff1a;java.lang.reflect.InvocationTargetException 欢迎来到英杰社区https://bbs.csdn.net/topics/617804998 欢迎来到我的主页&#xff0c;我是博主英杰&#xff0c;211科班出身&#xff0c;就职于医疗科技公司&#xff0c;热衷分享知识&#xff0c;武汉城市开发…

Mysql-索引结构

一.什么是索引&#xff1f; 索引(index)是帮助MySQL高效获取数据的数据结构(有序)。在数据之外,数据库系统还维护着满足特定查找算法的数据结构,这些数据结构以某种方式引用(指向)数据,这样就可以在这些数据结构上实现高级查找算法,这种数据结构就是索引 二.无索引的情况 找到…

【效率提升】程序员常用Shell脚本

文章目录 常用Shell脚本一. 定期更新分区数据二、获取系统资源的使用情况 常用Shell脚本 一. 定期更新分区数据 在某些场景下&#xff0c;我们需要对N年前某一分区的数据进行删除&#xff0c;并添加今年该对应分区的数据&#xff0c;实现数据的流动式存储。 #!/bin/bash dt$…

NFT革命:数字资产的确权、营销与元宇宙的未来

目录 1、NFT&#xff1a;数字社会的数据确权制度 2、基于低成本及永久产权的文化发现 3、PFP&#xff1a;从“小图片”到“身份表达”&#xff0c;再到社区筛选 4、透明表达&#xff1a;NFT 在数字化营销中的商业价值 5、可编程性&#xff1a;赋予 NFT 无限可能的应用 5.…

微信被好友屏蔽朋友圈/拉黑/删除?教你几招悄悄验证

微信这一国民级的社交软件&#xff0c;基本上渗入了大家日常生活的方方面面&#xff0c;沟通、支付、购物、娱乐都可以在上面一站式解决。微信功能虽然很全面&#xff0c;但某些功能细节设计也会让人感到困惑&#xff0c;比如我们被朋友拉黑或者删除&#xff0c;微信是不会通知…

C程序优化与指针传址

最近在写程序时遇到了一些问题&#xff0c;记录一下&#xff1a; 开始程序使用全局变量&#xff0c;程序如下&#xff1a;程序的缺点是全局变量的泛滥。 笔者觉得有些不妥&#xff0c;于是将它修改成这样&#xff1a; 使用结构体进行封装&#xff0c;避免了全局变量&#xff0…

Gitee使用教程2-克隆仓库(下载项目)并推送更新项目

一、下载 Gitee 仓库 1、点击克隆-复制代码 2、打开Git Bash 并输入复制的代码 下载好后&#xff0c;找不到文件在哪的可以输入 pwd 找到仓库路径 二、推送更新 Gitee 项目 1、打开 Git Bash 用 cd 命令进入你的仓库&#xff08;我的仓库名为book&#xff09; 2、添加文件到 …

转移C盘中的conda环境(包括.condarc文件修改,environment.txt文件修改,conda报错)

conda环境一般是默认安装到C盘的&#xff0c;若建立多个虚拟环境&#xff0c;时间长了&#xff0c;容易让本不富裕的C盘更加雪上加霜&#xff0c;下面给出将conda环境从C盘转移到D盘的方法。 目录 电脑软硬件转移方法查看当前conda目录转移操作第一步&#xff1a;.condarc文件修…

C语言迷宫

目录 开头程序程序的流程图程序输入与输出的效果结尾 开头 大家好&#xff0c;我叫这是我58。今天&#xff0c;我要来看我用C语言编译出来的迷宫游戏。 程序 #define _CRT_SECURE_NO_WARNINGS 1 #include <stdio.h> #include <Windows.h> void printmaze(char s…

鸿蒙Navigation路由能力汇总

基本使用步骤&#xff1a; 1、新增配置文件router_map&#xff1a; 2、在moudle.json5中添加刚才新增的router_map配置&#xff1a; 3、使用方法&#xff1a; 属性汇总&#xff1a; https://developer.huawei.com/consumer/cn/doc/harmonyos-references/ts-basic-compone…

在RK3568上如何烧录MAC?

这里我们用RKDevInfoWriteTool 1.1.4版本 下载地址&#xff1a;https://pan.baidu.com/s/1Y5uNhkyn7D_CjdT98GrlWA?pwdhm30 提 取 码&#xff1a;hm30 烧录过程&#xff1a; 1. 解压RKDevInfoWriteTool_Setup_V1.4_210527.7z 进入解压目录&#xff0c;双击运行RKDevInfo…