事件机制&监听器
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);
}
事件ContextRefreshedEvent
和 ContextClosedEvent
, 它们分别代表容器刷新完毕和即将关闭, 这里以监听ContextRefreshedEvent
事件为例子
@Component // 监听器注册到IOC容器当中
public class ContextRefreshedApplicationListener implements ApplicationListener<ContextRefreshedEvent> { // 监听 ContextRefreshedEvent 事件@Override public void onApplicationEvent(ContextRefreshedEvent event) {System.out.println("ContextRefreshedApplicationListener监听到ContextRefreshedEvent事件!");}
}
@EventListener注解式监听器
使用注解式监听器, 组件不再需要实现任何接口, 而是直接在需要作出事件反应的方法上标注 @EventListener
注解即可
@Componentpublic class ContextClosedApplicationListener {@EventListenerpublic 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 { // 继承ApplicationEventpublic 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> {@Overridepublic void onApplicationEvent(RegisterSuccessEvent event) {System.out.println("监听到用户注册成功, 发送短信。。。");}
}
添加EmailSenderListener注解
@Component
public class EmailSenderListener {@EventListenerpublic void onRegisterSuccess(RegisterSuccessEvent event) {System.out.println("监听到用户注册成功!发送邮件中。。。");}
}
@Component
public class MessageSenderListener {@EventListenerpublic 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));}@Overridepublic 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 {@EventListenerpublic 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
:激活 SpringWebMvcEnableAspectJAutoProxy
:开启注解 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 {@Beanpublic Bartender zhangxiaosan() {return new Bartender("张小三");}@Beanpublic 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容器指定类型的所有BeanMap<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*/@Nullabledefault 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 { @Beanpublic Bar myBar() {return new Bar();}
}
实现ImportSelector接口
public class BarImportSelector implements ImportSelector { @Overridepublic 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 {@Overridepublic 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注解快速激活相应的模块
模块装配方式有几种
四种, 分别如下
- @Import + @Bean
- @Import + @Configuration
- @Import + ImportSelector实现类
- @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 {@Beanpublic Bartender zhangxiaosan() {return new Bartender("张小三");}@Beanpublic 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和zhangdasanStream.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 {@Overridepublic 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方法, 那么就会发现确实 Boss
和 bbbar
都没了
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 {@Overridepublic boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {// 从ConditionalOnBean注解中获取到需要匹配的Bean类型和Bean名Map<String, Object> attributes = metadata.getAnnotationAttributes(ConditionalOnBean.class.getName());// 遍历需要匹配的Bean类型, 检查Bean工厂中是否包含对应的Bean定义, 如果不包含则返回falseClass<?>[] classes = (Class<?>[]) attributes.get("value");for (Class<?> clazz : classes) {if (!context.getBeanFactory().containsBeanDefinition(clazz.getName())) {return false;}}// 遍历需要匹配的Bean名称, 检查Bean工厂中是否包含对应的Bean定义, 如果不包含则返回falseString[] beanNames = (String[]) attributes.get("beanNames");for (String beanName : beanNames) {if (!context.getBeanFactory().containsBeanDefinition(beanName)) {return false;}}// 执行到这里说明所有的Bean类型和Bean名称都匹配, 则返回truereturn 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