【小家Spring】Spring中@PropertySource和@ImportResource的区别,以及各自的实现原理解析(中)

本文涉及的产品
云数据库 RDS MySQL Serverless,0.5-2RCU 50GB
简介: 【小家Spring】Spring中@PropertySource和@ImportResource的区别,以及各自的实现原理解析(中)

@PropertySource


Spring框架提供了PropertySource注解,目的是加载指定的属性文件。

这个注解是非常具有实际意义的,特别是在SpringBoot环境下,意义重大


由于SpringBoot默认情况下它会去加载classpath下的application.properties文件,所以我看大绝大多数开发者是这么干的:把所有的配置项都写在这一个配置文件里

这是非常不好的习惯,非常容易造成配置文件的臃肿,不好维护到最后的不能维护。


比如我们常见的一些配置:jdbc的、redis的、feign的、elasticsearch的等等他们的边界都是十分清晰的,因此Spring提供给我们这个注解,能让我们很好的实现隔离性~~

备注:此注解是Spring3.1后提供的,并不属于Spring Boot


使用Demo

我有一个数据库的配置文件:jdbc.properties


## 配置db数据库相关信息
datasource.drivername=com.mysql.jdbc.Driver
datasource.username=vipkid_xb
datasource.password=jmdneyh4m2UT
datasource.url=jdbc:mysql://localhost:3316/test?zeroDateTimeBehavior=convertToNull
#### 连接池相关
datasource.maximum-pool-size=10
datasource.auto-commit=true
datasource.connection-test-query=SELECT 1
datasource.connectionTimeout=20000
datasource.maxLifetime=180000


我们可以这么使用它:采用Spring支持的@Value获取值


@Configuration
@PropertySource(value = "classpath:jdbc.properties", name = "jdbc-config", ignoreResourceNotFound = false, encoding = "UTF-8")
public class JdbcConfig implements TransactionManagementConfigurer {
    @Value("${datasource.username}")
    private String userName;
    @Value("${datasource.password}")
    private String password;
    @Value("${datasource.url}")
    private String url;
    // 此处只是为了演示 所以不用连接池了===========生产环境禁止这么使用==========
    @Bean
    public DataSource dataSource() {
        MysqlDataSource dataSource = new MysqlDataSource();
        dataSource.setUser(userName);
        dataSource.setPassword(password);
        dataSource.setURL(url);
        return dataSource;
    }
}


单元测试:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {JdbcConfig.class})
public class TestSpringBean {
    @Autowired
    private DataSource dataSource;
    @Test
    public void test1() throws SQLException {
        Connection connection = dataSource.getConnection();
        System.out.println(connection); com.mysql.jdbc.JDBC4Connection@6db66836
    }
}


能够正常获取到链接,说明配置生效~~~

其实大多数时候如果你是SpringBoot环境,我建议采用下面这种更优雅的方式,来处理某一类(请保证这一类拥有共同的前缀)属性值:@ConfigurationProperties


@Configuration
@PropertySource(value = "classpath:jdbc.properties", name = "jdbc-config", ignoreResourceNotFound = false, encoding = "UTF-8")
@ConfigurationProperties(prefix = "datasource")
public class JdbcConfig implements TransactionManagementConfigurer {
    private String username;
    private String password;
    private String url;
    public void setUsername(String username) {
        this.username = username;
    }
    public void setPassword(String password) {
        this.password = password;
    }
    public void setUrl(String url) {
        this.url = url;
    }
    // 此处只是为了演示 所以不用连接池了===========生产环境禁止这么使用==========
    @Bean
    public DataSource dataSource() {
        MysqlDataSource dataSource = new MysqlDataSource();
        dataSource.setUser(username);
        dataSource.setPassword(password);
        dataSource.setURL(url);
        return dataSource;
    }
}


这样也是ok的。需要的注意的是:各个属性名和配置文件里的需要对应上。并且需要提供set方法。

另外还可以这么使用,直接把@ConfigurationProperties注解放在@Bean上,赋值极其方便


@Configuration
@PropertySource(value = "classpath:jdbc.properties", name = "jdbc-config", ignoreResourceNotFound = false, encoding = "UTF-8")
public class JdbcConfig implements TransactionManagementConfigurer {
    @ConfigurationProperties(prefix = "datasource")
    @Bean
    public DataSource dataSource() {
        //dataSource.setUser(username);
        //dataSource.setPassword(password);
        //dataSource.setURL(url);
        return new MysqlDataSource();
    }
}


这样做极其优雅。但是需要注意的是MysqlDataSource里面对应的属性名称是啥。比如此处为user、password、URL,因此为了对应上你需要做出如下修改才能生效。(如何修改此处省略)

建议:这种方式一般还是用于框架内部(比如我自己写的框架就用到了它,挺方便也好懂),而不是外部使用(因为约定得太多了,不太好太强的约束到使用者,当然我觉得也没啥,规范应该人人遵守)


备注:SpringBoot下此种写法不区分大小写,驼峰,-,_等书写形式都是兼容的。但是你的字母必须对应上啊,比如上面的user你不能写成username了。比如我这样写:datasource.u-r-l=xxx也是能够被正常识别的~~~ 具体参照SpringBoot的黑科技类:RelaxedNames


另外,本文重点是@PropertySource而非@ConfigurationProperties~~~~~~


实现原理剖析


上面已经贴出了入口,此处直接分析方法(该注解的解析时机还是非常早的)processPropertySource

class ConfigurationClassParser {
  ...
  private void processPropertySource(AnnotationAttributes propertySource) throws IOException {
    String name = propertySource.getString("name");
    if (!StringUtils.hasLength(name)) {
      name = null;
    }
    String encoding = propertySource.getString("encoding");
    if (!StringUtils.hasLength(encoding)) {
      encoding = null;
    }
    // 这里value代表这locations  我个人感觉  语义可以优化一下
    String[] locations = propertySource.getStringArray("value");
    Assert.isTrue(locations.length > 0, "At least one @PropertySource(value) location is required");
    boolean ignoreResourceNotFound = propertySource.getBoolean("ignoreResourceNotFound");
    Class<? extends PropertySourceFactory> factoryClass = propertySource.getClass("factory");
    // PropertySourceFactory接口,就是createPropertySource的工厂,Spring内部只有一个实现:DefaultPropertySourceFactory 
    // 若你不指定默认就是DefaultPropertySourceFactory,否则给你new一个对象出来~(请保证有空的构造函数~)
    PropertySourceFactory factory = (factoryClass == PropertySourceFactory.class ?
        DEFAULT_PROPERTY_SOURCE_FACTORY : BeanUtils.instantiateClass(factoryClass));
    for (String location : locations) {
      try {
        // 显然它也支持占位符,支持classpath*
        String resolvedLocation = this.environment.resolveRequiredPlaceholders(location);
        Resource resource = this.resourceLoader.getResource(resolvedLocation);
        // 调用factory的createPropertySource方法根据名字、编码、资源创建出一个PropertySource出来(实际是一个ResourcePropertySource)
        addPropertySource(factory.createPropertySource(name, new EncodedResource(resource, encoding)));
      }
      catch (IllegalArgumentException | FileNotFoundException | UnknownHostException ex) {
        // Placeholders not resolvable or resource not found when trying to open it
        // 若它为true,那没找着就没找着,不会抛异常阻断程序的启动,需要注意~
        if (ignoreResourceNotFound) {
          if (logger.isInfoEnabled()) {
            logger.info("Properties location [" + location + "] not resolvable: " + ex.getMessage());
          }
        }
        else {
          throw ex;
        }
      }
    }
  }
  // 把属性资源添加进来,最终全部要放进MutablePropertySources 里  这点非常重要~~~~ 这个时机
  private void addPropertySource(PropertySource<?> propertySource) {
    String name = propertySource.getName();
    // 这个特别的重要,这个其实就是Spring处理配置文件优先级的原理,下面有个截图可以看到
    // 因为这块特别重要,后面还会有专门章节分析~~~
    // MutablePropertySources它维护着一个List<PropertySource<?>> 并且是有序的~~~
    MutablePropertySources propertySources = ((ConfigurableEnvironment) this.environment).getPropertySources();
    // 此处若发现你的同名PropertySource已经有了,还是要继续处理的~~~而不是直接略过
    if (this.propertySourceNames.contains(name)) {
      // We've already added a version, we need to extend it
      // 根据此name拿出这个PropertySource~~~~若不为null
      // 下面就是做一些属性合并的工作~~~~~
      PropertySource<?> existing = propertySources.get(name);
      if (existing != null) {
        PropertySource<?> newSource = (propertySource instanceof ResourcePropertySource ?
            ((ResourcePropertySource) propertySource).withResourceName() : propertySource);
        if (existing instanceof CompositePropertySource) {
          ((CompositePropertySource) existing).addFirstPropertySource(newSource);
        }
        else {
          if (existing instanceof ResourcePropertySource) {
            existing = ((ResourcePropertySource) existing).withResourceName();
          }
          CompositePropertySource composite = new CompositePropertySource(name);
          composite.addPropertySource(newSource);
          composite.addPropertySource(existing);
          propertySources.replace(name, composite);
        }
        return;
      }
    }
    // 这段代码处理的意思是:若你是第一个自己导入进来的,那就放在最末尾
    // 若你不是第一个,那就把你放在已经导入过的最后一个的前一个里面~~~
    if (this.propertySourceNames.isEmpty()) {
      propertySources.addLast(propertySource);
    } else {
      String firstProcessed = this.propertySourceNames.get(this.propertySourceNames.size() - 1);
      propertySources.addBefore(firstProcessed, propertySource);
    }
    this.propertySourceNames.add(name);
  }
  ...
}


比如这样子导入两次,但是名字不同,比如这样子导入:


@PropertySource(value = "classpath:jdbc.properties", name = "jdbc-config", ignoreResourceNotFound = false, encoding = "UTF-8")
@PropertySource(value = "classpath:jdbc.properties", name = "jdbc-config2", ignoreResourceNotFound = false, encoding = "UTF-8")
public class JdbcConfig implements TransactionManagementConfigurer {
  ...
}


代码是有顺序的,从上至下执行的。


最终结果为:

image.png


就这样,我们导入的属性值们,最终也放进了环境Environment里面。




相关文章
|
3天前
|
Java
并发编程之线程池的底层原理的详细解析
并发编程之线程池的底层原理的详细解析
13 0
|
28天前
|
XML 存储 Java
Spring重要类解析
Spring重要类解析
20 0
|
27天前
|
安全 Java 数据安全/隐私保护
【深入浅出Spring原理及实战】「EL表达式开发系列」深入解析SpringEL表达式理论详解与实际应用
【深入浅出Spring原理及实战】「EL表达式开发系列」深入解析SpringEL表达式理论详解与实际应用
62 1
|
14天前
|
存储 关系型数据库 MySQL
MySQL引擎对决:深入解析MyISAM和InnoDB的区别
MySQL引擎对决:深入解析MyISAM和InnoDB的区别
29 0
|
27天前
|
存储 XML 缓存
【深入浅出Spring原理及实战】「缓存Cache开发系列」带你深入分析Spring所提供的缓存Cache功能的开发实战指南(一)
【深入浅出Spring原理及实战】「缓存Cache开发系列」带你深入分析Spring所提供的缓存Cache功能的开发实战指南
62 0
|
22小时前
|
安全 Java API
Spring工厂API与原理
Spring工厂API与原理
22 10
|
2天前
|
缓存 JavaScript 前端开发
|
3天前
|
SQL 分布式计算 资源调度
一文解析 ODPS SQL 任务优化方法原理
本文重点尝试从ODPS SQL的逻辑执行计划和Logview中的执行计划出发,分析日常数据研发过程中各种优化方法背后的原理,覆盖了部分调优方法的分析,从知道怎么优化,到为什么这样优化,以及还能怎样优化。
|
3天前
|
JSON Java Maven
Javaweb之SpringBootWeb案例之 SpringBoot原理的详细解析
Javaweb之SpringBootWeb案例之 SpringBoot原理的详细解析
8 0
Javaweb之SpringBootWeb案例之 SpringBoot原理的详细解析
|
3天前
|
前端开发 JavaScript 编译器
深入解析JavaScript中的异步编程:Promises与async/await的使用与原理
【4月更文挑战第22天】本文深入解析JavaScript异步编程,重点讨论Promises和async/await。Promises用于管理异步操作,有pending、fulfilled和rejected三种状态。通过.then()和.catch()处理结果,但可能导致回调地狱。async/await是ES2017的语法糖,使异步编程更直观,类似同步代码,通过事件循环和微任务队列实现。两者各有优势,适用于不同场景,能有效提升代码可读性和维护性。

推荐镜像

更多