@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 { ... }
代码是有顺序的,从上至下执行的。
最终结果为:
就这样,我们导入的属性值们,最终也放进了环境Environment
里面。