1. 程式人生 > >【小家Spring】老專案遷移問題:@ImportResource匯入的xml配置裡的Bean能夠使用@PropertySource匯入的屬性值嗎?

【小家Spring】老專案遷移問題:@ImportResource匯入的xml配置裡的Bean能夠使用@PropertySource匯入的屬性值嗎?

#### 每篇一句 > 大師都是偏執的,偏執才能產生力量,妥協是沒有力量的。你對全世界妥協了你就是空氣。所以若沒有偏見,哪來的大師呢 #### 相關閱讀 [【小家Spring】詳解PropertyPlaceholderConfigurer、PropertyOverrideConfigurer等對屬性配置檔案Properties的載入和使用](https://blog.csdn.net/f641385712/article/details/91444601) [【小家Spring】Spring中@PropertySource和@ImportResource的區別,以及各自的實現原理解析](https://blog.csdn.net/f641385712/article/details/90636649) [【小家Spring】Spring中@Value註解有多強大?從原理層面去剖析為何它有如此大的“能耐“](https://blog.csdn.net/f641385712/article/details/91043955) ---
對Spring感興趣可掃碼加入wx群:`Java高工、架構師3群`(文末有二維碼)
--- #### 前言 寫這篇文章的原動力是由於**昨晚深夜**一個小夥伴諮詢我的一個問題(這位小夥伴這麼晚了還在折騰,也是給個大大的贊),涉及到了如題方面的知識。 當然促使我書寫本文最重要原因的是:這種從傳統`Spring`專案向`SpringBoot`遷移進階的case,我個人認為在現階段的環境下還是有**較大概率**出現的,**因此推薦收藏本文,對你後續或許有所幫助~** ### 情景描述 為了更直觀的說明問題所在,截圖部分聊天記錄如下: ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20190716222844516.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2Y2NDEzODU3MTI=,size_16,color_FFFFFF,t_70) **這位小夥伴描述的問題還是蠻清晰,所以我還是很願意跟他一起探討的~** 勾起興趣還有一個原因:`Spring`對佔位符提供了非常強大的支援,**但基本上新手**都還不能好好利用它和利用好它,更區分不清使用的規範和區別,本文也希望做點努力,能夠起到稍微一點的作用~ 對此部分內容若需要`熱場`,推薦可以先瀏覽一下這篇文章:[【小家Spring】Spring中@PropertySource和@ImportResource的區別,以及各自的實現原理解析](https://blog.csdn.net/f641385712/article/details/90636649) 可以看到,早在我這篇文章裡我就說了這麼一句話: ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20190716223321427.png) 而剛好這個小夥伴的場景(其實我自己還並沒有遇到過此場景),就類屬於`老專案`到`SpringBoot新專案`的一個遷移case,這時不結合分析,更待何時呢。 >
看到聊天記錄,部分小夥伴可能會想:把Bean拿出來配置不就行了嗎?或者key就寫在原來的屬性檔案裡唄? > 其實對於職場老兵都能對此種現象給與理解和接受,畢竟有種叫**理想化**,有種叫是叫**實際化**~ --- --- --- 因為我不可能貼出該小夥伴的原始碼(畢竟人家是生產環境的程式碼,咋可能貼出給大夥看呢?)so,接下來旨在說明這個問題,我就只好採用我的**模擬大法**嘍: #### 傳統Spring工程下使用 本處以一個傳統的Spring工程為例,模擬這種使用case。`classpath`下有如下兩個檔案: **spring-beans.xml:** ```xml
``` 可以看到此xml配置Bean中使用了佔位符:`${diy.name}`來引用下面屬性檔案的屬性值~ **my.properties:** ```java diy.name = fsx-fsx ``` 使用`@ImportResource`和`@PropertySource`分別把它哥倆匯入: ```java @ImportResource(locations = "classpath:spring-beans.xml") @PropertySource("classpath:my.properties") @Configuration public class RootConfig { } ``` 執行如下測試用例: ```java @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = {RootConfig.class}) public class TestSpringBean { @Autowired private ApplicationContext applicationContext; @Autowired private Environment environment; @Test public void test1() { Person bean = (Person) applicationContext.getBean("myPerson"); System.out.println(bean); System.out.println(environment.getProperty("diy.name")); } } ``` 列印結果為: ```java Person{name='${diy.name}', age=18} fsx-fsx ``` 從結果中可以至少知道如下兩點: 1. 環境變數裡是存在`diy.name`這個屬性k-v的。並且此處我附上截圖如下: ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20190717202659570.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2Y2NDEzODU3MTI=,size_16,color_FFFFFF,t_70) 2. xml中的佔位符**並沒有**被解析 > 若你對技術有敏感性的話,你會疑問**為何佔位符沒被解析但並沒有報錯呢?** > 這個問題我在這篇文章:[【小家Spring】Spring中@Value註解有多強大?從原理層面去剖析為何它有如此大的“能耐“](https://blog.csdn.net/f641385712/article/details/91043955) 裡有過解釋,有興趣的可以點開看看(沒興趣的可以略過) **存在但又沒被解析,看似有點矛盾,難道Spring工程不支援這麼用,作為職場老兵的你,答案肯定是否定的,那如何破呢?** 其實解決起來非常簡單,我們只需要配置上一個`PropertyResourceConfigurer`即可: ```java @Bean public PropertyResourceConfigurer propertyResourceConfigurer() { PropertyResourceConfigurer configurer = new PropertySourcesPlaceholderConfigurer(); // 使用的PropertySourcesPlaceholderConfigurer,不用自己再手動指定亦可處理佔位符~~~ // configurer.setLocation(new ClassPathResource("my.properties")); // 載入指定的屬性檔案 return configurer; } ``` 再次執行,列印如下: ```java Person{name='fsx-fsx', age=18} fsx-fsx ``` **完美~** 關於xml配置Bean處理佔位符問題,為了加深理解,亦可參考:[【小家Spring】Spring IoC是如何使用BeanWrapper和Java內省結合起來給Bean屬性賦值的](https://blog.csdn.net/f641385712/article/details/91444601) > 我想說:此處介紹的是註解版怎麼處理佔位符問題,若你仍舊是傳統的xml配置專案,至於具體使用哪個標籤,小夥伴自行尋找咯~ --- 我們知道`PropertyResourceConfigurer`它是個抽象類,它的三大實現子類除了上例使用的,還有其餘兩大實現類:`PropertyOverrideConfigurer`和`PropertyPlaceholderConfigurer`,**若註冊它哥倆可行嗎???** 行不行試試唄 ###### 使用PropertyOverrideConfigurer `PropertyOverrideConfigurer` 利用屬性檔案的相關資訊,覆蓋XML 配置檔案中`Bean定義`。它要求配置的屬性檔案第一個`.`前面是`beanName`來匹配,所以這個子類我看都不用看,它肯定不行(因為它改變了k-v的結構)。 > **其實上面說配置`PropertyResourceConfigurer`的實現類來處理是不太準確的。 > 準確的說應該是配置`PlaceholderConfigurerSupport`的實現子類來處理`Placeholder佔位符`更精確,特此糾正哈~** ###### 使用PropertyPlaceholderConfigurer 其實大多數小夥伴對`PropertyPlaceholderConfigurer`比對`PropertySourcesPlaceholderConfigurer`熟悉多了,畢竟前者的`年紀`可大多了~ 它哥倆都是`PlaceholderConfigurerSupport`的實現子類有能力能夠處理佔位符 > `PropertySourcesPlaceholderConfigurer`是Spring 3.1後提供的,希望用來取代`PropertyPlaceholderConfigurer` ```java @Bean public PropertyResourceConfigurer propertyResourceConfigurer() { PropertyResourceConfigurer configurer = new PropertyPlaceholderConfigurer(); //configurer.setLocation(new ClassPathResource("my.properties")); // 載入指定的屬性檔案 return configurer; } ``` 執行上面用例就報錯了: ```java Caused by: java.lang.IllegalArgumentException: Could not resolve placeholder 'diy.name' in value "${diy.name}" at org.springframework.util.PropertyPlaceholderHelper.parseStringValue(PropertyPlaceholderHelper.java:172) at org.springframework.util.PropertyPlaceholderHelper.replacePlaceholders(PropertyPlaceholderHelper.java:124) ``` what?看列印結果,明明`environment.getProperty("diy.name")`從環境裡能拿到這個key呀,怎麼會報錯呢??? 這就是為何`Spring3.1`要提供一個`PropertySourcesPlaceholderConfigurer`旨在想代替掉此類的原因了。 **但是,但是,但是把上配置中注掉的那行程式碼放開(也就是說自己設定location從而把屬性檔案載入進來),就能正常work了。** 關於使用這種方式我還**有必要再說明一點**:若自己設定了location載入屬性檔案,`@PropertySource("classpath:my.properties")`這句程式碼對此種場景就沒有必要了,`xml`配置的佔位符也是能夠讀取到的。**但是但是但是**,若注掉這句`@PropertySource...`,此時執行輸出如下: ```java Person{name='fsx-fsx', age=18} null ``` 會發現`environment.getProperty("diy.name")`為null,也就是說該屬性值並不會存在應用的環境內了(但是xml的佔位符已被成功解析)。從我的這個截圖中也能看出來環境裡已經沒它了: ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20190717214337802.png) 至於這**深處**到底是什麼原因,有興趣的可以輕點這裡:[【小家Spring】詳解PropertyPlaceholderConfigurer、PropertyOverrideConfigurer等對屬性配置檔案Properties的載入和使用](https://blog.csdn.net/f641385712/article/details/91444601) ==只new一個PropertyPlaceholderConfigurer報錯原因分析:== 其實從原始碼處一眼就能看出來原因: ```java public class PropertyPlaceholderConfigurer extends PlaceholderConfigurerSupport { ... // 是否能被解析到值,重點在於入參的這個Properties props是否有這個key,而這個引數需要追溯它的父類~ @Override protected void processProperties(ConfigurableListableBeanFactory beanFactoryToProcess, Properties props) throws BeansException { StringValueResolver valueResolver = new PlaceholderResolvingStringValueResolver(props); doProcessProperties(beanFactoryToProcess, valueResolver); } ... } // 從父類中看看props的傳值是啥~~~ public abstract class PropertyResourceConfigurer extends PropertiesLoaderSupport implements BeanFactoryPostProcessor, PriorityOrdered { ... @Override public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { try { Properties mergedProps = mergeProperties(); // Convert the merged properties, if necessary. convertProperties(mergedProps); // Let the subclass process the properties. // 抽象方法,交給子類~~~這裡傳入的mergedProps,全部來自location~~~ processProperties(beanFactory, mergedProps); } catch (IOException ex) { throw new BeanInitializationException("Could not load properties", ex); } } protected Properties mergeProperties() throws IOException { ... loadProperties(result); ... } // 從配置裡的location裡把屬性值都讀出來~~~~~ protected void loadProperties(Properties props) throws IOException { if (this.locations != null) { for (Resource location : this.locations) { ... PropertiesLoaderUtils.fillProperties(props, new EncodedResource(location, this.fileEncoding), this.propertiesPersister); ... } } } ... } ``` 由此可見,若上述`@Bean`配置使用的是`PropertyPlaceholderConfigurer`,那必須手動的把屬性檔案設定location載入進去才行,**否則是讀取不到滴~** ==那麼問題來了,為何使用PropertySourcesPlaceholderConfigurer,只需要簡單的new一個就成了勒(並不需要手動設定location)?== 一樣的,從原始碼處一看便知,非常非常簡單: ```java // @since 3.1 直接實現了EnvironmentAware,說明此Bean可以拿到當前環境Environment public class PropertySourcesPlaceholderConfigurer extends PlaceholderConfigurerSupport implements EnvironmentAware { ... @Override public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { ... // 把環境屬性都放進來~ if (this.environment != null) { this.propertySources.addLast( new PropertySource(ENVIRONMENT_PROPERTIES_PROPERTY_SOURCE_NAME, this.environment) { @Override @Nullable public String getProperty(String key) { return this.source.getProperty(key); } } ); } // 把配置的屬性放進來~~~ PropertySource localPropertySource = new PropertiesPropertySource(LOCAL_PROPERTIES_PROPERTY_SOURCE_NAME, mergeProperties()); this.propertySources.addFirst(localPropertySource); ... } } ``` 相信不用我做過多的解釋,就知道為何不用自己設定location,直接使用註解`@PropertySource("classpath:my.properties")`就好使了吧。這個時候環境截圖如下(**注意:此處我截圖是基於`已經set了location`的截圖哦**): ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20190717215854966.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2Y2NDEzODU3MTI=,size_16,color_FFFFFF,t_70) what?雖然配置時候set了location去載入屬性檔案,但是上面程式碼中add進去的屬性源`environmentProperties`和`localProperties` ```java public static final String LOCAL_PROPERTIES_PROPERTY_SOURCE_NAME = "localProperties"; public static final String ENVIRONMENT_PROPERTIES_PROPERTY_SOURCE_NAME = "environmentProperties"; ``` 兩個`PropertySource`都並沒有出現? 關於此,我這裡就不再解釋了,哈哈。還是那句話,**留給小夥伴們自己思考**,若思考不明白的亦可掃碼入群探討哦~(當然參照上面文章也是可行的) --- #### SpringBoot工程下使用 關於在`SpringBoot`中使用,簡單到令人髮指,畢竟`SpringBoot`的使命也是讓你使用它能夠簡單到木有朋友。 so,在`SpringBoot`工程下使用`@ImportResource`和`@PropertySource`啥都不用配,它是能夠天然的直接work的~ ==原因分析如下:== 一切得益於`SpringBoot`強悍的自動化配置能力,它提供了這樣一個配置類: ```java @Configuration @AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE) // 最高優先順序 public class PropertyPlaceholderAutoConfiguration { // 注意此處使用的是PropertySourcesPlaceholderConfigurer // 並且你可以在本容器內覆蓋它的預設行為喲~~~~ @Bean @ConditionalOnMissingBean(search = SearchStrategy.CURRENT) public static PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() { return new PropertySourcesPlaceholderConfigurer(); } } ``` `PropertyPlaceholderAutoConfiguration`它被配置在了自動配置包下的`spring.factories`檔案裡: ```java # Auto Configure org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ ... org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration,\ ... ``` 因此它會隨著工程啟動而自動生效。有了上面對`Spring`工程下的使用分析,此處就**不用再**花筆墨解釋了~ **另外附加說明一點:哪怕你的屬性不使用`@PropertySource`匯入,而是寫在`SB`自帶的`application.properties`檔案裡,依舊是沒有問題的。** ==原因分析如下:== 其實這個涉及到的是`SpringBoot`對`application.properties`的載入時機問題,因為本文對`SB`的介紹並不是重點,因此此處我直接**簡單的說出結論**即止: - `SpringBoot`通過事件監聽機制載入很多東西,載入此屬性檔案用的是`ConfigFileApplicationListener`這個監聽器 - 它監聽到`ApplicationEnvironmentPreparedEvent`(環境準備好後)事件後開始載入`application.properties`等檔案 - `ApplicationEnvironmentPreparedEvent`的事件發出是發生在`createApplicationContext()`之前~~~ 部分程式碼如下: ```java public class SpringApplication { ... public ConfigurableApplicationContext run(String... args) { ... ApplicationArguments applicationArguments = new DefaultApplicationArguments(args); // 此步驟 會發出ApplicationEnvironmentPreparedEvent事件 ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments); Banner printedBanner = printBanner(environment); // 開始建立,初始化容器~ context = createApplicationContext(); ... } ... } ``` so,在`SB`環境下已經早早把屬性都放進環境內了,藉助它預設配置好的`PropertySourcesPlaceholderConfigurer`來處理的,那可不能正常work嗎。一切都是這麼自然,**這或許就是`SB`的魅力所在吧~** #### 關於小夥伴問題的解決 開頭提出了問題,肯定得解決問題嘛~~~如下圖 ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20190717222424348.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2Y2NDEzODU3MTI=,size_16,color_FFFFFF,t_70) 哈哈,雖然最終我並沒有**直接的**幫助解決問題,但是此問題給了我寫本文的動力,總體還是不錯的~ #### 總結 本文通過一個小夥伴諮詢的`小問題(真是小問題嗎?)`引申比較詳細的說了`Spring`在處理佔位符這塊的內容(其實本並沒打算寫這麼多的,尷尬~) 寫本文的目的開頭也說了,我認為在`SpringBoot`還並非100%滲透的當下,肯定有人會遇到從傳統`Spring`專案向`SpringBoot`過度的一個階段,而本文的描述或許能給你的遷移提供一種新的思路(特別是時間緊、任務重的時候),希望小夥伴們能有所收穫,peace~ #### 知識交流 > **`若文章格式混亂,可點選`**:[原文連結-原文連結-原文連結-原文連結-原文連結](https://blog.csdn.net/f641385712/article/details/96195401) ==The last:如果覺得本文對你有幫助,不妨點個讚唄。當然分享到你的朋友圈讓更多小夥伴看到也是被`作者本人許可的~`== **若對技術內容感興趣可以加入wx群交流:`Java高工、架構師3群`。 若群二維碼失效,請加wx號:`fsx641385712`(或者掃描下方wx二維碼)。並且備註:`"java入群"` 字樣,會手動邀請入群** ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20190703175020107.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2Y2NDEzODU3MTI=,size_16,color_FFFFFF,t_70,pic_cente