1、模块装配与条件装配如何能同时兼容多种场景的需求?

首先谈到模块装配,我们可以利用自定义注解和 @Import 来完成某个模块所有所需的组件的导入,即将模块需要的所有组件都注入到IOC容器中,例如我们要对数据库模块进行统一装配:


  • 自定义一个注解,例如 @EnableJdbc
  • 接着可以编写一个 Configuration 类,里面包含所有组件的 Bean 实例;
  • 接着在自定义注解处使用 @Import 注解将步骤2中的 Java配置类 注入到IOC容器中。
  • 最后在启动类上加入自定义注解,即可将整个模块进行装配了

条件装配:条件装配可以理解为,我们可根据当前环境信息、配置信息或组件之间的依赖来决定加载哪些组件。


Spring 提供了 @Profile 支持环境变量条件装配、提供了 @Condition 支持自定义条件装配,如组件之间的依赖、组件依赖配置、组件依赖环境等,更加丰富。


那么模块装配和条件装配的混合使用必定能支持非常多的场景。

2、如何理解 SPI ?JDK 原生的 SPI 与 Spring 的 SPI 有什么区别?

SPI 全程叫 Service Provider Interface 服务提供接口,它可以通过一个指定的接口 / 抽象类,寻找到预先配置好的实现类(并创建实现类对象)。

jdk1.6 中有 SPI 的具体实现,SpringFramework 3.2 也引入了 SPI 的实现,而且比 jdk 的实现更加强大,下面我们分别来讲解这两种 SPI 的实现。

JDK 原生 SPI 和 Spring SPI 的区别:


  • JDK SPI 扫描的路径是:META-INF/services/ 下的文件,文件名为接口/抽象类的全路径;而 Spring 的扫描路径是:META-INF 下的所有名为 spring.factories 的文件
  • JDK SPI 文件内容为实现类全路径、支持多个换行、文件名和文件内容需是继承或实现关系;而 Spring SPI 的文件内容为 key/value 对,key 支持注解、接口、类,value 支持为全路径名,key 和 value 不需是继承或实现关系。
  • JDK 的 SPI 实现类为 ServiceLoader,支持根据接口/抽象类获取 Iterator,然后遍历获取所有实现类实例(load);而 Spring SPI 的实现类为 SpringFactoriesLoader,支持根据注解、接口、类获取全路径名列表(loadFactoryNames)或实例列表(loadFactories)。

3、模块装配&条件装配综合例子

代码github地址:​​code demo​

目标:


  • 支持数据库链接的模块装配
  • 支持DataSource的条件装配
  • 支持配置外部化

3.1 依赖

我们这里用 MySQL 做例子

<!-- db -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.20</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.22</version>
</dependency>
<dependency>
<groupId>commons-dbutils</groupId>
<artifactId>commons-dbutils</artifactId>
<version>1.7</version>
</dependency>

3.2 自定义注解:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(JdbcBeanDefinitionRegistryPostProcessor.class)
@PropertySource("enablejdbc/jdbc.properties")
public @interface EnableJdbc {
}

我们可以看到,我们在自定义注解中利用 @Import 注入 JdbcBeanDefinitionRegistryPostProcessor 到容器中,并且利用 @PropertySource 注解读取外部配置文件 enablejdbc/jdbc.properties。

3.3 文件

spring.factories:

com.github.howinfun.demo.ioc.conditon_demo.version3.EnableJdbc=\
com.mysql.cj.jdbc.Driver,\
oracle.jdbc.driver.OracleDriver,\
org.h2.Driver

jdbc.properties:

jdbc.url=jdbc:mysql://localhost:3306/test?characterEncoding=utf8
jdbc.username=root
jdbc.password=123456

3.4 动态注册 BeanDefinition

逻辑:


  1. 我们利用 Spring 提供的 SPI 机制,读取 spring.factories 文件的支持的数据库Driver全路径名
  2. 利用 Class.forName 来判断是否引入了 Driver 的依赖
  3. 如果引入了对应的依赖,注册 DataSource 对应的 BeanDefinition 到 IOC 容器中
  4. 利用 @PropertySource 和 Environment 支持读取外部配置文件

/**
* 动态注入数据库链接
* @author winfun
* @date 2021/7/19 8:53 上午
**/
public class JdbcBeanDefinitionRegistryPostProcessor implements BeanDefinitionRegistryPostProcessor, EnvironmentAware {

private Environment environment;

@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
// 利用Spring的SPI读取支持的数据库的Driver,这里我们利用的是 loadFactoryNames 方法来读取 spring.factories 的配置
List<String> driverClassNames = SpringFactoriesLoader.loadFactoryNames(EnableJdbc.class, this.getClass().getClassLoader());
if (!CollectionUtils.isEmpty(driverClassNames)){
AtomicReference<String> finalDriverClassName = new AtomicReference<>();
driverClassNames.forEach(driverClassName ->{
try {
// 判断是否引入了对应的 Driver
Class.forName(driverClassName);
finalDriverClassName.set(driverClassName);
}catch (Exception e){
System.out.println("不支持:"+driverClassName);
}
});
BeanDefinition beanDefinition = BeanDefinitionBuilder.rootBeanDefinition(DruidDataSource.class)
.addPropertyValue("url",environment.getProperty("jdbc.url"))
.addPropertyValue("username",environment.getProperty("jdbc.username"))
.addPropertyValue("password",environment.getProperty("jdbc.password"))
.addPropertyValue("driverClassName",finalDriverClassName.get())
.getBeanDefinition();
registry.registerBeanDefinition("dataSource",beanDefinition);
}
}

@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {

}

/**
* Set the {@code Environment} that this component runs in.
* @param environment
*/
@Override
public void setEnvironment(Environment environment) {
this.environment = environment;
}
}

3.5 启动类

最后我们在启动类上加上自定义注解 @EnableJdbc 即可

@EnableJdbc
@Configuration
public class ApplicationV3 {

public static void main(String[] args) throws SQLException {
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(ApplicationV3.class);
for (String beanDefinitionName : applicationContext.getBeanDefinitionNames()) {
System.out.println(beanDefinitionName);
}
DataSource mysqlDataSource = applicationContext.getBean(DataSource.class);
query(mysqlDataSource).forEach(System.out::println);

}

private static List<Object> query(DataSource dataSource) throws SQLException {
QueryRunner queryRunner = new QueryRunner();
return queryRunner.execute(dataSource.getConnection(),"select * from user", new BeanListHandler(Object.class));
}
}