一般我們會把常用的屬性放在工程的classpath文件夾中,以property,yaml或json的格式進行文件存儲,便於Spring-boot在初始化時獲取。
@Value則是Spring一個非常有用的注解,可以在初始化時很方便的對Bean的入參變量進行賦值,例如:
@Bean public BusinessClient businessClient (@Value("http://baseUrl/") String baseUrl) { Retrofit retrofit = new Retrofit.Builder() .baseUrl(baseUrl) .addConverterFactory(GsonConverterFactory.create()) .build(); return retrofit.create(BusinessClient .class);
於是,初始化好的Business Client進行http請求時,默認的baseurl都是”http://baseUrl“。
實際上,@Value還支持一種特殊的寫法:”${some.proptery.key}”,即將property的key值寫在花括號中。例如,我有一個property為aerexu.basurl=http://baseUrl2,將上例中的@Value改寫成@Value("${aerexu.basurl}")
,baseUrl實際獲取到的值是http://baseUrl2。
這個特性是利用Spring的bean PropertySourcesPlaceholder實現的,Spring boot已經在初始化時幫我們自動實例化了該bean。若是傳統的Spring工程,則需要主動實例化,如下:
@Bean public static PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() { PropertySourcesPlaceholderConfigurer configurer = new PropertySourcesPlaceholderConfigurer(); configurer.setPlaceholderPrefix(PlaceholderConfigurerSupport.DEFAULT_PLACEHOLDER_PREFIX); configurer.setPlaceholderSuffix(PlaceholderConfigurerSupport.DEFAULT_PLACEHOLDER_SUFFIX); configurer.setValueSeparator(PlaceholderConfigurerSupport.DEFAULT_VALUE_SEPARATOR); return configurer; }
一般情況下,property存在工程中的文件就可以了,但帶來的壞處是如果屬性需要改變,必須重新發布工程。比如,對接上例中的url,可能會變為https,可能端口會變化。所以,這種類型的屬性放在數據庫中更合適。
然而將屬性存儲在數據庫中后,@Value對應的值就無法正常解析了。因此,這里提供一種hack的方法,使得@Value可以正常解析。
PropertySourcesPlaceholder在解析屬性時,都是從ConfigurableEnvironment中進行尋找的。當ConfigurableEnvironment沒有存在的屬性時,${}寫法的@Value就無法解析了。因此,需要通過特殊的處理,將存儲在數據庫中的屬性注入到ConfigurableEnvironment中。本文定義了一個LoadFromDatabasePropertyConfig類實現該功能,其代碼如下:
@Configuration @Slf4j public class LoadFromDatabasePropertyConfig { @Autowired private ConfigurableEnvironment env; @Autowired private SysPropertyResourceMapper propertyResourceMapper; @PostConstruct public void initializeDatabasePropertySourceUsage() { MutablePropertySources propertySources = env.getPropertySources(); try { Map<String, Object> propertyMap = propertyResourceMapper.selectAll().stream() .collect(Collectors.toMap(SysPropertyResource::getPropertyName, SysPropertyResource::getPropertyValue)); Properties properties = new Properties(); properties.putAll(propertyMap); PropertiesPropertySource dbPropertySource = new PropertiesPropertySource("dbPropertySource", properties); Pattern p = Pattern.compile("^applicationConfig.*"); String name = null; boolean flag = false; for (PropertySource<?> source : propertySources) { if (p.matcher(source.getName()).matches()) { name = source.getName(); flag = true; log.info("Find propertySources ".concat(name)); break; } } log.info("========================================================================="); if(flag) { propertySources.addBefore(name, dbPropertySource); } else { propertySources.addFirst(dbPropertySource); } } catch (Exception e) { log.error("Error during database properties setup", e); throw new RuntimeException(e); } } }
上述代碼的具體思路是將數據庫中的所有需要的屬性讀出,通過Properties類轉換為Spring可用的PropertiesPropertySource,並取名為dbPropertySource。隨后利用正則匹配,從已有的所有屬性中找到名稱以applicationConfig開頭的屬性(該屬性即是所有配置在文件中的property所解析成的對象),並將dbPropertySource存儲在其之前。這樣當文件和數據庫中同時存在key相等的屬性時,會優先使用數據庫中存儲的value。
需要注意的是,上述方案提供的屬性解析,必須在數據庫相關的bean都實例化完成后才可進行。且為了保證bean在實例化時,數據庫屬性已經被加入到ConfigurableEnvironment中去了,必須添加@DependsOn注解。上面的BusinessClient的實例化就需更新成:
@Bean @DependsOn("loadFromDatabasePropertyConfig") public BusinessClient businessClient (@Value("${aerexu.basurl}") String baseUrl) { Retrofit retrofit = new Retrofit.Builder() .baseUrl(baseUrl) .addConverterFactory(GsonConverterFactory.create()) .build(); return retrofit.create(BusinessClient .class);