> For the complete documentation index, see [llms.txt](https://ldbmcs.gitbook.io/java/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://ldbmcs.gitbook.io/java/java-frameworks-60/spring/spring-ioc/ioc-mo-kuai-zhuang-pei-he-tiao-jian-zhuang-pei.md).

# IOC - 模块装配和条件装配

## 1. 原生手动装配

在最原始的 SpringFramework 中，是不支持注解驱动开发的（当时最低支持版本是 1.3 、1.4 ），直到 SpringFramework 2.0 版本，才初步出现了模式注解（ `@Repository` ），到了 SpringFramework 2.5 出现了 `@Component` 和它的几个派生注解，到了 SpringFramework 3.0 才完全的支持注解驱动开发（当时最低支持版本已经升级到 1.5）。

使用 `@Configuration` + `@Bean` 注解组合，或者 `@Component` + `@ComponentScan` 注解组合，可以实现编程式 / 声明式的手动装配。这两种方式咱前面已经写过很多了，不再赘述。

不过，咱思考一个问题：如果使用这两种方式，如果要注册的 Bean 很多，要么一个一个的 `@Bean` 编程式写，要么就得选好包进行组件扫描，而且这种情况还得每个类都标注好 `@Component` 或者它的衍生注解才行。面对数量很多的 Bean ，这种装配方式很明显会比较麻烦，需要有一个新的解决方案。

## 2. 模块装配

SpringFramework 3.0 的发布，全面支持了注解驱动开发，随之而来的就是快速方便的模块装配。在正式了解模块装配之前，咱先思考一个问题。

### 2.1 什么是模块

通常理解下，模块可以理解成一个一个的可以分解、组合、更换的独立的单元，模块与模块之间可能存在一定的依赖，模块的内部通常是高内聚的，一个模块通常都是解决一个独立的问题（如引入事务模块是为了解决数据库操作的最终一致性）。其实按照这个理解来看，我们平时写的一个一个的功能，也可以看成一个个的模块；封装的一个个组件，可以看做是模块。

简单总结下，模块通常具有以下几个特征：

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

### 2.2 什么是模块装配

明确了模块的定义，下面就可以思考下一个问题了：什么是模块装配？

既然模块是功能单元，那模块装配，就可以理解为**把一个模块需要的核心功能组件都装配好**，当然如果能有尽可能简便的方式那最好。

### 2.3 SpringFramework中的模块装配

SpringFramework 中的模块装配，是在 **3.1** 之后引入大量 `**@EnableXXX**` 注解，来快速整合激活相对应的模块。

从现在 5.x 的官方文档中已经很难找到 `@EnableXXX` 的介绍了，小伙伴们可以回溯到 SpringFramework 3.1.0 的官方文档：

[docs.spring.io/spring/docs…](https://link.juejin.cn/?target=https%3A%2F%2Fdocs.spring.io%2Fspring%2Fdocs%2F3.1.0.RELEASE%2Freference%2Fhtmlsingle%2F)

在 3.1.5 节中，它有介绍 `@EnableXXX` 注解的使用，并且它还举了不少例子，这里面不乏有咱可能熟悉的：

* `EnableTransactionManagement` ：开启注解事务驱动
* `EnableWebMvc` ：激活 SpringWebMvc
* `EnableAspectJAutoProxy` ：开启注解 AOP 编程
* `EnableScheduling` ：开启调度功能（定时任务）

这些内容，咱在后面的学习中都会慢慢遇到的，小伙伴们没有必要现在就搞明白这些注解都是干嘛用的，一步一个脚印学习就好。

下面，咱先来体会一下最简单的模块装配。

### 2.4 快速体会模块装配

先记住使用模块装配的核心原则：**自定义注解 +** `**@Import**` **导入组件。**

#### 2.4.1 模块装配场景概述

下面咱构建一个场景：使用代码模拟构建出一个**酒馆**，酒馆里得有**吧台**，得有**调酒师**，得有**服务员**，还得有**老板**。这里面具体的设计咱不过多深入，小伙伴自己练习时可以自由发挥。

在这个场景中，`ApplicationContext` 看作一个酒馆，酒馆里的吧台、调酒师、服务员、老板，这些元素统统看作一个一个的**组件**。咱用代码模拟实现的最终目的，是可以**通过一个注解，同时把这些元素都填充到酒馆中**。

目的明确了，下面就开始动手吧。一开始咱先实现最简单的装配方式。

#### 2.4.2 声明自定义注解

既然是酒馆，那咱仿照着 SpringFramework 的写法，咱就来一个 `**@EnableTavern**` 吧！

```java
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface EnableTavern {
    
}
```

注意注解上面要标注三个元注解，代表它在运行时起效，并且只能标注在类上。

还没完事，模块装配需要一个最核心的注解是 `**@Import**` ，它要标注在 `@EnableTavern` 上。不过这个 `@Import` 中需要传入 `value` 值，点开看一眼它的源码吧：

```java
@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();
}
```

看，文档注释已经写得非常明白了：它可以导入**配置类**、`**ImportSelector**` **的实现类**，`**ImportBeanDefinitionRegistrar**` **的实现类**，或者**普通类**。咱这里先来快速上手，所以咱先选择使用**普通类**导入。

#### 2.4.3 声明老板类

既然先导入普通类，那咱就来整一个老板的类吧，毕竟酒馆必须有老板经营才是呀！

```java
public class Boss {

}
```

没了，这点代码就够了，连 `@Component` 注解都不用标注。

然后！咱在上面 `@EnableTavern` 的 `@Import` 注解中，填入 Boss 的类：

```java
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Import(Boss.class)
public @interface EnableTavern {
    
}
```

这样就代表，如果**标注了** `**@EnableTavern**` **注解，就会触发** `**@Import**` **的效果，向容器中导入一个** `**Boss**` **类型的 Bean** 。

#### 2.4.4 创建配置类

注解驱动，自然少不了配置类。咱声明一个 `TavernConfiguration` 的配置类，并在类上标注 `@Configuration` 和 `@EnableTavern` 注解：

```java
@Configuration
@EnableTavern
public class TavernConfiguration {
    
}
```

配置类中什么都不用写，只要标注好注解即可。

***

这样我们就完成了最简单的模块装配。

这个时候可能有小伙伴开始不耐烦了：我去原本我可以用 `@Configuration` + `@Bean` 就能完事的，你非得给我整这么一堆，这不是**徒增功耗**吗？别着急，往上翻一翻 `@Import` 可以传入的东西，是不是发现普通类是最简单的呀？下面咱就来学习剩下几种更复杂的方式。

### 2.5 模块装配的四种方式

#### 2.5.1 导入普通类

上面的方式就是导入普通类。

#### 2.5.2 导入配置类

如果需要直接导入一些现有的配置类，使用 `@Import` 也可以直接加载进来。下面咱来把调酒师搞定。

**2.5.2.1 声明调酒师类**

调酒师的模型，咱加一个 `**name**` 的属性吧，暗示着咱要搞不止一个调酒师咯：

```java
public class Bartender {
    
    private String name;
    
    public Bartender(String name) {
        this.name = name;
    }
    
    public String getName() {
        return name;
    }
}
```

**2.5.2.2 注册调酒师的对象**

如果要注册多个相同类型的 Bean ，现在咱能想到的办法就是通过配置类了。下面咱编写一个 `BartenderConfiguration` ：

```java
@Configuration
public class BartenderConfiguration {
    
    @Bean
    public Bartender zhangxiaosan() {
        return new Bartender("张小三");
    }
    
    @Bean
    public Bartender zhangdasan() {
        return new Bartender("张大三");
    }
    
}
```

注意哦，如果小伙伴用 **IDEA** 开发的话，此时这个类会报黄，提示这个配置类还没有被用到过，事实上也确实是这样，咱在驱动 IOC 容器初始化时，用的是只传入一个配置类的方式，所以它肯定不会用到。那想让它起作用，只需要在 `@EnableTavern` 的 `@Import` 中把这个配置类加上即可：

```java
@Import({Boss.class, BartenderConfiguration.class})
public @interface EnableTavern {
    
}
```

注意这里有一个小细节，有小伙伴在学习的时候，启动类里或者配置类上用了**包扫描**，恰好把这个类扫描到了，导致即使没有 `@Import` 这个 `BartenderConfiguration` ，`Bartender` 调酒师也被注册进 IOC 容器了。这里一定要细心哈，包扫描本身就会扫描配置类，并且让其生效的。如果既想用包扫描，又不想扫到这个类，很简单，把这些配置类拿到别的包里，让包扫描找不到它就好啦。

#### 2.5.3 导入ImportSelector

借助 IDE 打开 `ImportSelector` ，会发现它是一个**接口**，它的功能可以从文档注释中读到一些信息：

Interface to be implemented by types that determine which [@Configuration ](/java/java-frameworks-60/spring/spring-ioc/ioc-mo-kuai-zhuang-pei-he-tiao-jian-zhuang-pei.md)class(es) should be imported based on a given selection criteria, usually one or more annotation attributes.

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

文档注释中想表达的是可以导入配置类，但其实 `ImportSelector` 也可以导入普通类。下面咱先演示如何使用。

**2.5.3.1 声明吧台类**

吧台的模型类咱就不搞花里胡哨了，最简单的类模型即可：

```java
public class Bar {
    
}
```

**2.5.3.2 声明注册吧台的配置类**

咱为了说明 `ImportSelector` 不止可以导入配置类，也可以导入普通类，所以这里咱也造一个配置类，来演示两种类型皆可的效果。

```java
@Configuration
public class BarConfiguration {
    
    @Bean
    public Bar bbbar() {
        return new Bar();
    }
}
```

**2.5.3.3 编写ImportSelector的实现类**

咱编写一个 `BarImportSelector` ，来实现 `ImportSelector` 接口，实现 `selectImports` 方法：

```java
public class BarImportSelector implements ImportSelector {
    
    @Override
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        return new String[0];
    }
}
```

注意，`selectImports` 方法的返回值是一个 String 类型的数组，它这样设计的目的是什么呢？咱来看看 selectImports 方法的文档注释：

Select and return the names of which class(es) should be imported based on the AnnotationMetadata of the importing [@Configuration ](/java/java-frameworks-60/spring/spring-ioc/ioc-mo-kuai-zhuang-pei-he-tiao-jian-zhuang-pei.md)class.

根据导入的 `@Configuration` 类的 `AnnotationMetadata` 选择并返回要导入的类的类名。

哦，合着它要的是一组类名呀，自然肯定是**全限定类名**咯（没有全限定类名没办法定位具体的类）。那既然这样，咱就在这里面把上面的 `Bar` 和 `BarConfiguration` 的类名写进去：

```java
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        return new String[] {Bar.class.getName(), BarConfiguration.class.getName()};
    }
```

最后，把 `@EnableTavern` 的 `@Import` 中把这个 `BarImportSelector` 导入进去即可。

```java
@Import({Boss.class, BartenderConfiguration.class, BarImportSelector.class})
public @interface EnableTavern {
    
}
```

#### 2.5.4 导入ImportBeanDefinitionRegistrar

如果说 `ImportSelector` 更像声明式导入的话，那 `ImportBeanDefinitionRegistrar` 就可以解释为编程式向 IOC 容器中导入 Bean 。不过由于它导入的实际是 `BeanDefinition` （ Bean 的定义信息），这部分咱还没有接触到，就先不展开大篇幅解释了（如果要解释，那可真的是大篇幅的）。咱先对 `ImportBeanDefinitionRegistrar` 有一个快速的使用入门即可，后面在讲到 IOC 高级和原理部分，会回过头来详细解析 `ImportBeanDefinitionRegistrar` 的使用和原理。

**2.5.4.1 声明服务员类**

离最后的酒馆只剩服务员了：

```java
public class Waiter {
    
}
```

**2.5.4.2 编写ImportBeanDefinitionRegistrar的实现类**

咱编写一个 `WaiterRegistrar` ，实现 `ImportBeanDefinitionRegistrar` 接口：

```java
public class WaiterRegistrar implements ImportBeanDefinitionRegistrar {
    
    @Override
    public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
        registry.registerBeanDefinition("waiter", new RootBeanDefinition(Waiter.class));
    }
}
```

这里面的写法小伙伴们先不要过度纠结，跟着写就完事了。简单解释下，这个 `registerBeanDefinition` 方法传入的两个参数，第一个参数是 Bean 的名称（id），第二个参数中传入的 `RootBeanDefinition` 要指定 Bean 的字节码（ `**.class**` ）。

最后，把 `WaiterRegistrar` 标注在 `@EnableTavern` 的 `@Import` 中：

```java
@Import({Boss.class, BartenderConfiguration.class, BarImportSelector.class, WaiterRegistrar.class})
public @interface EnableTavern {
    
}
```

***

到这里，`@Import` 的四种导入的方式也就全部过了一遍，**模块装配**说白了**就是这四种方式的综合使用**。

学完这几种方式后，可能小伙伴对模块装配的概念和重要性不是很能感知到，没有关系，后面咱学到 AOP 、事务等章节时，会以这些模块的激活，了解一下模块装配在 SpringFramework 内部的体现。

## 3. 条件装配

还是拿上一章的酒馆为例。如果这套代码模拟的环境放到**一片荒野**，那这个时候可能吧台还在，老板还在，但是**调酒师**肯定就**不干活了**（荒郊野外哪来那些闲情雅致的人去喝酒呢），所以这个时候调酒师就不应该注册到 IOC 容器了。这种情况下，如果只是模块装配，那就没办法搞定了：只要配置类中声明了 `@Bean` 注解的方法，那这个方法的返回值就一定会被注册到 IOC 容器成为一个 Bean 。

所以，有没有办法解决这个问题呢？当然是有（不然咱这一章讲个啥呢），先来学习第一种方式：**Profile** 。

### 3.1 Profile

SpringFramework 3.1 中就已经引入 Profile 的概念了，可它是什么意思呢？咱先了解一下。

#### 3.1.1 什么是Profile

SpringFramework 的官方文档中并没有对 Profile 进行过多的描述，而是借助了一篇官网的博客来详细介绍 Profile 的使用：[spring.io/blog/2011/0…](https://link.juejin.cn/?target=https%3A%2F%2Fspring.io%2Fblog%2F2011%2F02%2F14%2Fspring-3-1-m1-introducing-profile%2F) ，咱这里的讲解也会参考这篇博客的内容。另外的，javadoc 中有对 `@Profile` 注解的介绍，这个介绍可以说是把 Profile 的设计思想介绍的很到位了：

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 ](/java/java-frameworks-60/spring/spring-ioc/ioc-mo-kuai-zhuang-pei-he-tiao-jian-zhuang-pei.md)annotation.

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

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

简单理解下这段文档注释的意思：`@Profile` 注解可以标注在组件上，当一个配置属性（并不是文件）激活时，它才会起作用，而激活这个属性的方式有很多种（启动参数、环境变量、`web.xml` 配置等）。

如果小伙伴看完这段话开始有点感觉了，那说明你可能已经知道它的作用了。说白了，profile 提供了一种可以理解成“**基于环境的配置**”：**根据当前项目的运行时环境不同，可以动态的注册当前运行环境匹配的组件**。

下面咱就上一章的场景，为酒馆添加外置的环境因素。

#### 3.1.2 @Profile的使用

**3.1.2.1 Bartender添加**[**@Profile**](/java/java-frameworks-60/spring/spring-ioc/ioc-mo-kuai-zhuang-pei-he-tiao-jian-zhuang-pei.md)

刚在上面说了，荒郊野外下，调酒师率先不干了，跑路了，此时调酒师就不会在荒郊野外的环境下存在，只会在城市存在。用代码来表达，就是在注册调酒师的配置类上标注 `@Profile` ：

```java
@Configuration
@Profile("city")
public class BartenderConfiguration {
    @Bean
    public Bartender zhangxiaosan() {
        return new Bartender("张小三");
    }
    
    @Bean
    public Bartender zhangdasan() {
        return new Bartender("张大三");
    }
}
```

**3.1.2.2 编程式设置运行时环境**

如果现在直接运行 `TavernProfileApplication` 的 `main` 方法，控制台中不会打印 `zhangxiaosan` 和 `zhangdasan` ：

```
tavernConfiguration
com.linkedbear.spring.configuration.b_profile.component.Boss
com.linkedbear.spring.configuration.b_profile.component.Bar
com.linkedbear.spring.configuration.b_profile.config.BarConfiguration
bbbar
waiter
```

默认情况下，`ApplicationContext` 中的 profile 为 **“default”**，那上面 `@Profile("city")` 不匹配，`BartenderConfiguration` 不会生效，那这两个调酒师也不会被注册到 IOC 容器中。要想让调酒师注册进 IOC 容器，就需要给 `ApplicationContext` 中设置一下：

```java
public static void main(String[] args) throws Exception {
    AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(TavernConfiguration.class);
    // 给ApplicationContext的环境设置正在激活的profile
    ctx.getEnvironment().setActiveProfiles("city");
    Stream.of(ctx.getBeanDefinitionNames()).forEach(System.out::println);
}
```

重新运行 `main` 方法，发现控制台还是只打印上面那些，两个调酒师还是没有被注册到 IOC 容器中。

这个时候可能有小伙伴要一脸问号了：我去你这不是逗我玩吗？你告诉我用 `setActiveProfiles` 激活，我好不容易写上，结果不好使？？？你在骗我吗？

不要着急嘛，这都是节目效果而已（狗头保命）。在生气之余，我希望小伙伴们能停下来思考一下：既然这样写不好使，但我又告诉你这么写，那是不是哪里出了问题呢？结合前面 15 章，咱对 `ApplicationContext` 的认识，是不是突然意识到了点什么？如果你还记得有个 `**refresh**` 方法的话，那这个地方就可以大胆猜测了：\*\*是不是在 `new AnnotationConfigApplicationContext` 的时候，如果传入了配置类，它内部就自动初始化完成了，那些 Bean 也就都创建好了？\*\*如果小伙伴能意识到这一点，说明对前面 `ApplicationContext` 的学习足够的认真了！（不记得的小伙伴可以回头看 15 章的 2.2.3 章节）

那应该怎么写才行呢？既然在构造方法中传入配置类就自动初始化完成了，那我不传呢？

```java
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
```

诶？这也不报错啊！那我先这样 new 一个空的呗？然后再设置 profile 是不是就好使了呢？赶紧来试试：

```java
public static void main(String[] args) throws Exception {
    AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
    ctx.getEnvironment().setActiveProfiles("city");
    ctx.register(TavernConfiguration.class);
    ctx.refresh();
    Stream.of(ctx.getBeanDefinitionNames()).forEach(System.out::println);
}
```

这样子一写，再重新运行 `main` 方法，果然控制台就打印 zhangxiaosan 和 zhangdasan 了！

```
tavernConfiguration
com.linkedbear.spring.configuration.b_profile.component.Boss
com.linkedbear.spring.configuration.b_profile.config.BartenderConfiguration
zhangxiaosan
zhangdasan
com.linkedbear.spring.configuration.b_profile.component.Bar
com.linkedbear.spring.configuration.b_profile.config.BarConfiguration
bbbar
waiter
```

**3.1.2.3 声明式设置运行时环境**

上面编程式配置虽然可以用了，但仔细思考一下，这种方式似乎不实用吧！我都把 profile 硬编码在 .java 里了，那要是切换环境，我还得重新编译来，那图个啥呢？所以肯定还有更好的办法。上面的文档注释中也说了，它可以使用的方法很多，下面咱来演示最容易演示的一种：命令行参数配置。

测试命令行参数的环境变量，需要在 IDEA 中配置启动选项：

![img](https://cdn.nlark.com/yuque/0/2022/png/229542/1669127448301-d87f2342-184e-46f8-844c-f204a06c4cee.png)

这样配置好之后，在 `main` 方法中改回原来的构造方法传入配置类的形式，运行，控制台仍然会打印 zhangxiaosan 和 zhangdasan 。

修改传入的 jvm 参数，将 city 改成 **wilderness** ，重新运行 `main` 方法，发现控制台不再打印 zhangxiaosan 和 zhangdasan ，说明使用 jvm 命令行参数也可以控制 profile 。

除了 jvm 命令行参数，通过 web.xml 的方式也可以设置，不过咱还没有学习到集成 web 开发环境，所以这部分先放一放，后续讲 SpringWebMvc 时会提到它的。

#### 3.1.3 @Profile在实际开发的用途

以数据源为例，在开发环境、测试环境、生产环境中，项目连接的数据库都是不一样的。如果每切换一个环境都要重新改一遍配置文件，那真的是太麻烦了，所以咱就可以采用 [@Profile ](/java/java-frameworks-60/spring/spring-ioc/ioc-mo-kuai-zhuang-pei-he-tiao-jian-zhuang-pei.md)的方式来解决。下面咱来模拟演示这种配置。

声明一个 `DataSourceConfiguration` 类，并一次性声明 3 个 `DataSource` ：

```java
@Configuration
public class DataSourceConfiguration {

    @Bean
    @Profile("dev")
    public DataSource devDataSource() {
        return null;
    }

    @Bean
    @Profile("test")
    public DataSource testDataSource() {
        return null;
    }

    @Bean
    @Profile("prod")
    public DataSource prodDataSource() {
        return null;
    }
}
```

这样写完之后，通过 `@PropertySource` 注解 + 外部配置文件，就可以做到**只切换 profile 即可切换不同的数据源**。

#### 3.1.4 profile控制不到的地方

profile 强大吗？当然很强大，但它还有一些无法控制的地方。下面咱把场景进一步复杂化：

**吧台应该是由老板安置好的，如果酒馆中连老板都没有，那吧台也不应该存在。**

这种情况下，用 profile 就不好使了：因为 **profile 控制的是**整个项目的运行**环境**，无法根据单个 Bean 的因素决定是否装配。也是因为这个问题，出现了第二种条件装配的方式：`**@Conditional**` **注解**。

### 3.2 Conditional

看这个注解的名，condition ，很明显就是条件的意思啊，这也太直白明了了。按照惯例，咱先对 Conditional 有个清楚的认识。

#### 3.2.1 什么是Conditional

`@Conditional` 是在 SpringFramework 4.0 版本正式推出的，它可以让 Bean 的装载基于一些指定的条件，换句话说，被标注 `@Conditional` 注解的 Bean 要注册到 IOC 容器时，必须全部满足 `@Conditional` 上指定的所有条件才可以。

在 SpringFramework 的官方文档中，并没有花什么篇幅介绍 `@Conditional` ，而是让咱们直接去看 javadoc ，不过有一说一，javadoc 里基本上把 `@Conditional` 的作用都描述明白了：

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 ](/java/java-frameworks-60/spring/spring-ioc/ioc-mo-kuai-zhuang-pei-he-tiao-jian-zhuang-pei.md)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 ](/java/java-frameworks-60/spring/spring-ioc/ioc-mo-kuai-zhuang-pei-he-tiao-jian-zhuang-pei.md)classes
* as a meta-annotation, for the purpose of composing custom stereotype annotations
* as a method-level annotation on any [@Bean ](/java/java-frameworks-60/spring/spring-ioc/ioc-mo-kuai-zhuang-pei-he-tiao-jian-zhuang-pei.md)method

If a [@Configuration ](/java/java-frameworks-60/spring/spring-ioc/ioc-mo-kuai-zhuang-pei-he-tiao-jian-zhuang-pei.md)class is marked with @Conditional, all of the [@Bean ](/java/java-frameworks-60/spring/spring-ioc/ioc-mo-kuai-zhuang-pei-he-tiao-jian-zhuang-pei.md)methods, [@Import ](/java/java-frameworks-60/spring/spring-ioc/ioc-mo-kuai-zhuang-pei-he-tiao-jian-zhuang-pei.md)annotations, and [@ComponentScan ](/java/java-frameworks-60/spring/spring-ioc/ioc-mo-kuai-zhuang-pei-he-tiao-jian-zhuang-pei.md)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` 条件装配的实际使用。

#### 3.2.2 @Conditional的使用

**3.2.2.1 Bar的创建要基于Boss**

在 `BarConfiguration` 的 Bar 注册中，要指定 Bar 的创建需要 Boss 的存在，反映到代码上就是在 bbbar 方法上标注 `@Conditional` ：

```java
    @Bean
    @Conditional(???)
    public Bar bbbar() {
        return new Bar();
    }
```

发现 `@Conditional` 注解中需要传入一个 `Condition` 接口的实现类数组，说明咱还需要编写条件匹配类做匹配依据。那咱就先写一个匹配条件：

**3.2.2.2 条件匹配规则类的编写**

声明一个 `ExistBossCondition` 类，表示它用来判断 IOC 容器中是否存在 `Boss` 的对象：

```java
public class ExistBossCondition implements Condition {

    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        return context.getBeanFactory().containsBeanDefinition(Boss.class.getName());
    }
}
```

注意这个地方用 `**BeanDefinition**` 做判断而不是 **Bean** ，考虑的是当条件匹配时，可能 `Boss` 还没被创建，导致条件匹配出现偏差。

然后，把这个 `ExistBossCondition` 规则类放入 `@Conditional` 注解中。

#### 3.2.2 通用抽取

思考一个问题：如果一个项目中，有比较多的组件需要依赖另一些不同的组件，如果每个组件都写一个 `Condition` 条件，那工程量真的太大了。这个时候咱就要想想了：如果能把这个匹配的规则抽取为通用的方式，那岂不是让条件装配变得容易得多？抱着这个想法，咱来试着修改一下现有的代码。

**3.2.2.1 抽取传入的beanName**

由于上面咱在文档注释中看到了 `@Conditional` 可以派生，那就来写一个新的注解吧：`@ConditionalOnBean` ，意为存在指定的 Bean 时匹配：

```java
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
@Conditional(OnBeanCondition.class)
public @interface ConditionalOnBean {

    String[] beanNames() default {};
}
```

传入的 `Condition` 类型为自己声明的 `OnBeanCondition` ：

```java
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;
    }
}
```

**3.3.3.2 替换上面的原生@Conditional注解**

在 `BarConfiguration` 中，将 `bbbar` 方法上的 `@Conditional(ExistBossCondition.class)` 去掉，换用 `@ConditionalOnBean` 注解：

```java
@Bean
@ConditionalOnBean(beanNames = "com.linkedbear.spring.configuration.c_conditional.component.Boss")
public Bar bbbar() {
    return new Bar();
}
```

重新运行 `main` 方法，发现 `bbbar` 依然没有创建（此时 `@EnableTavern` 中已经没有导入 Boss 类了），证明自定义注解已经生效。

**3.3.3.3 加入类型匹配**

上面只能是抽取 `beanName` ，传整个类的全限定名真的很费劲。如果当前类路径下本来就有这个类，那直接写进去就好呀。我们希望代码最终改造成这个样子：

```java
@Bean
@ConditionalOnBean(Boss.class)
public Bar bbbar() {
    return new Bar();
}
```

这样子多简洁啊，因为我已经有 `Boss` 类了，所以直接写进去就好嘛。那下面咱来改造这个效果。

给 `@ConditionalOnBean` 注解上添加默认的 `value` 属性，类型为 `Class[]` ，这样就可以传入类型了：

```java
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
@Conditional(OnBeanCondition.class)
public @interface ConditionalOnBean {
    
    Class<?>[] value() default {};

    String[] beanNames() default {};
}
```

之后，在 `OnBeanCondition` 中添加 `value` 的属性解析：

```java
public class OnBeanCondition implements Condition {

    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        Map<String, Object> attributes = metadata.getAnnotationAttributes(ConditionalOnBean.class.getName());
        // 匹配类型
        Class<?>[] classes = (Class<?>[]) attributes.get("value");
        for (Class<?> clazz : classes) {
            if (!context.getBeanFactory().containsBeanDefinition(clazz.getName())) {
                return false;
            }
        }
        // 匹配beanName
        String[] beanNames = (String[]) attributes.get("beanNames");
        for (String beanName : beanNames) {
            if (!context.getBeanFactory().containsBeanDefinition(beanName)) {
                return false;
            }
        }
        return true;
    }
}
```

这样，就可以匹配 bean 的名称为默认的全限定名的情况了。

最后多说一句，小伙伴们在自己动手练习这部分内容时不要过于纠结这里面的内容，其实咱写的这个 `@ConditionalOnBean` 是参考 SpringBoot 中的 `@ConditionalOnBean` 注解，人家 SpringBoot 官方实现的功能更严密完善，后续你在项目中用到了 SpringBoot ，那这些 `@Conditional` 派生的注解尽情用就好。


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://ldbmcs.gitbook.io/java/java-frameworks-60/spring/spring-ioc/ioc-mo-kuai-zhuang-pei-he-tiao-jian-zhuang-pei.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
