Spring Cloud的PropertySourceLocator扩展点太棒了!

前言

在我新入职公司后熟悉公司项目时,想用dbeaver连接他的数据库看看,就得知道这个项目连接的数据库ip、账户密码,通常情况都是在本地配置,所以我索性就找,但是找了半天都什么都没有,不禁的就开始好奇,这玩意是怎么配置数据库信息的,最后实在找不到配置文件,所以想到在SpringBoot创建数据库连接时一定会创建一个DataSource,所以只要在其实现类先打下断点,就可以通过传递的参数看到连接信息,果不其然,在一个DriverDataSource的构造方法下传递了数据库的连接信息。

但出于好奇,所以一层层的追踪从什么地方取到的参数并传递给DriverDataSource的构造方法,最后发现还是在DataSourceProperties下获取的,这个类是SpringBoot用来表示在配置文件中spring.datasource开头的配置,但整个工程都没有配置过这个属性,那从什么地方配置的呢?

最后顺藤摸瓜找到了关键点,即NacosPropertySourceLocator,我们知道,在引入spring-boot-starter-jdbc后,如果不在配置文件中设置spring.datasource信息,那么Spring Boot将拒绝启动,很明显,Spring Boot表示你既然要引入我,那么肯定要使用数据库,既然要使用数据库,那么你就得要告诉我连接信息,这很合理吧。

通常情况我们都是在配置文件中写入,但这样就比较死了,最灵活的方式就是通过SpringBoot留下来的扩展方式,在代码中动态添加。

NacosPropertySourceLocator做的事就是从Nacos服务中拉取配置,并添加到系统的属性源集合中,他的实现核心是PropertySourceLocator扩展,但这个扩展是Spring Cloud家族的,但在往上说,他还是使用了SpringBoot的扩展接口,只是在这基础上封装了一个专门针对加载属性列表的这样一个接口。

ApplicationContextInitializer

我们从最上层开始说,Spring Cloud中的PropertySourceLocator实现核心是ApplicationContextInitializer,这是Spring留下来为我们在应用程序上下文初始化前做的一些工作,比如拿到系统的PropertySources,向其中添加自定义的配置,添加后即可通过@Value这种方式取得。

代码如下。

@Configuration
class TestApplicationContextInitializer :
    ApplicationContextInitializer<ConfigurableApplicationContext>, CommandLineRunner {
    @Autowired
    lateinit var dataSource: DataSource
    override fun run(vararg args: String?) {
        println(dataSource as HikariDataSource)
    }

    override fun initialize(applicationContext: ConfigurableApplicationContext) {
        val mapOf = mapOf<String, String>(
            "spring.datasource.url" to "jdbc:mysql://localhost:3306/db_blog",
            "spring.datasource.username" to "hxl",
            "spring.datasource.driver-class-name" to "com.mysql.cj.jdbc.Driver",
        )
        applicationContext.environment.propertySources.addFirst(MapPropertySource("datasource", mapOf))

    }
}

注意的是,@Configuration不能使ApplicationContextInitializer生效,他是为了让CommandLineRunner生效。

而我们所写的这个类会被实例化两次,一次是在应用程序上下文初始化前回调initialize方法是所创建,另一次是被Spring扫描到@Configuration注解后将其加入到Bean集合中最后实例化。

而要想ApplicationContextInitializer生效,有三种办法。

  1. 系统属性配置中加入context.initializer.classes
context.initializer.classes=com.example.springclouddemo.loader.TestApplicationContextInitializer
  1. 新建META-INF/spring.factories,内容如下
org.springframework.context.ApplicationContextInitializer=com.example.springclouddemo.loader.TestPropertySourceLocator
  1. 通过addInitializers 方法
@EnableDiscoveryClient
@SpringBootApplication
@EnableConfigurationProperties
class SpringCloudDemoApplication

fun main(args: Array<String>) {
    val  application =  SpringApplication(SpringCloudDemoApplication::class.java)
    application.addInitializers(TestApplicationContextInitializer())
    application.run(*args)
}

运行后,将会看到DataSource 会被注入成功,也证实了在代码中加入的属性集合生效了。

PropertySourceLocator

这个扩展是Spring Cloud中的东西,使用起来也很简单,只需要实现locate方法返回一个PropertySource即可。

class TestPropertySourceLocator : PropertySourceLocator {
    override fun locate(environment: Environment?): PropertySource<*> {
        val  mapOf = mapOf(
            "spring.datasource.url" to "jdbc:mysql://rm-bp1v2a7fy0qq145e5qo.mysql.rds.aliyuncs.com:3306/db_blog",
            "spring.datasource.username" to "hxl",
            "spring.datasource.driver-class-name" to "com.mysql.cj.jdbc.Driver",
            "spring.datasource.url" to " jdbc:mysql://rm-bp1v2a7fy0qq145e5qo.mysql.rds.aliyuncs.com:3306/db_blog"
        )
        return MapPropertySource("test", mapOf)
    }
}

让其生效同样需要在META-INF/spring.factories下加入下面配置。

org.springframework.cloud.bootstrap.BootstrapConfiguration=com.example.springclouddemo.loader.TestPropertySourceLocator

源码

源码一共分为两步。

  1. BootstrapImportSelectorConfiguration通过@Import注解从某地提取一些class,最终将这些class实例化后放入系统Bean容器中。

    而这里的某地就是从spring.factories文件,属性名是org.springframework.cloud.bootstrap.BootstrapConfiguration,到这里,我们所指定的类就可以通过@Autowired注解注入。

    如下是提取类名的源码

BootstrapImportSelector#selectImports

List<String> names = new ArrayList<>(
  SpringFactoriesLoader.loadFactoryNames(BootstrapConfiguration.class, classLoader));
  1. 获取所有PropertySourceLocator实现类,并收集属性源

    从下面这个类中可以看到,他通过@Autowired注入了所有实现了PropertySourceLocator接口的类,而这个实现类就是上面我们编写的,如果使用了Nacos的话,这里有一个NacosPropertySourceLocator,他负责从远程拉取配置。

@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(PropertySourceBootstrapProperties.class)
public class PropertySourceBootstrapConfiguration
  implements ApplicationContextInitializer<ConfigurableApplicationContext>, Ordered {

 @Autowired(required = false)
 private List<PropertySourceLocator> propertySourceLocators = new ArrayList<>();
 }

而在其initialize方法下会遍历上面集合,调用locate进行属性收集,最后同样调用addLast等方法向系统的属性原集合中添加新属性。

@Override
public void initialize(ConfigurableApplicationContext applicationContext) {
 List<PropertySource<?>> composite = new ArrayList<>();
 ConfigurableEnvironment environment = applicationContext.getEnvironment();
 for (PropertySourceLocator locator : this.propertySourceLocators) {
  Collection<PropertySource<?>> source = locator.locateCollection(environment);
  
}

猜你喜欢

转载自juejin.im/post/7126423320773263391