1. 程式人生 > >曹工談Spring Boot:Spring boot中怎麼進行外部化配置,一不留神摔一跤;一路debug,原來是我太年輕了

曹工談Spring Boot:Spring boot中怎麼進行外部化配置,一不留神摔一跤;一路debug,原來是我太年輕了

# spring boot中怎麼進行外部化配置,一不留神摔一跤;一路debug,原來是我太年輕了 # 背景 我們公司這邊,目前都是spring boot專案,沒有引入spring cloud config,也就是說,配置檔案,還是放在resources下面的,為了區分多環境,是採用了profile這種方式,大致如下: ![](https://img2020.cnblogs.com/blog/519126/202005/519126-20200520142734804-1378443516.png) 上面這裡,就定義了3個profile,實際還不止這點,對應了3個環境。 每次啟動的時候,只需要(省略無關 jvm 引數): ```java java -Dspring.profiles.active=dev -jar xxx.jar ``` 這樣來指定要使用的profile即可。 然後每次發測試版本,我們這邊就得加1個profile,所以導致我們工作量也是巨大,因為我們這邊環境比較多,地址總變。後來,經過開發和測試那邊的協調,變成了我們只管jar包,不管測試環境的維護。每次發版本,只發個jar包過去,配置檔案裡的地址,由測試同學自己配置。 大致變成了如下的樣子: ```shell -rw-r--r--. 1 root root 111978406 May 19 13:24 xxx.jar drwxr-xr-x. 2 root root 120 May 20 13:25 config [root@localhost cad]# ll config/ total 16 -rw-r--r--. 1 root root 498 May 20 13:31 application.properties -rw-r--r--. 1 root root 601 May 20 13:31 application.yml ``` 即,在jar包旁邊,放上一個config目錄,然後在config目錄裡,放我們的配置檔案,至於配置檔案裡的各種配置,比如資料庫ip、redis等等,就由測試同學自己配置了,這樣呢,我們的工作量,大大減小。 看起來很棒了,然而,前一陣,測試同學發現一個問題,即,只能在和config同級目錄下,執行java -jar,這種情況下,config裡面的配置才生效,換個目錄執行,config裡面的配置就不生效了。 ```shell [root@localhost cad]# ll // 這裡啟動jar包,ok,沒問題;換個目錄執行,不行! total 109356 -rw-r--r--. 1 root root 111978406 May 19 13:24 xxx.jar drwxr-xr-x. 2 root root 120 May 20 13:25 config ``` 還有這種事?我們看看到底怎麼回事。 # 官方文件 參考: > ## 24.3 Application Property Files > > `SpringApplication` loads properties from `application.properties` files in the following locations and adds them to the Spring `Environment`: > > 1. A `/config` subdirectory of the current directory > 2. The current directory > 3. A classpath `/config` package >
4. The classpath root 這裡說,SpringApplication載入`application.properties`配置檔案,從如下位置: 1. 當前目錄的config子目錄下 2. 當前目錄 3. classpath下的config包 4. classpath的根路徑 我們這裡,就是利用了第一點。但是,這個當前目錄下的config目錄,不是很清楚。當前目錄,怎麼才算當前目錄,我在jar包同級目錄算當前目錄;換個目錄用絕對路徑,啟動jar包,就不算當前目錄了嗎? 再往下翻一下看看。 > Config locations are searched in reverse order. By default, the configured locations are `classpath:/,classpath:/config/,file:./,file:./config/`. The resulting search order is the following: >
> 1. `file:./config/` > 2. `file:./` > 3. `classpath:/config/` > 4. `classpath:/` 配置地址被以相反的順序搜尋,預設情況下,地址包括了:`classpath:/,classpath:/config/,file:./,file:./config/`,因此,被搜尋的順序如下: 1. `file:./config/` 2. `file:./` 3. `classpath:/config/` 4. `classpath:/` 這裡的第一項,`file: ./config`,應該就是我們目前的那種情況。 然後文件裡,沒提到我的問題,可能是太低階。。只能從原始碼找答案了。 # 原始碼分析 ##通過關鍵字查詢,大致定位原始碼 我們直接用前面的關鍵字,搜尋一波(記得把maven裡設定為下載原始碼) ![](https://img2020.cnblogs.com/blog/519126/202005/519126-20200520144655601-1096379820.png) 果然看到了一處地方: ```java org.springframework.boot.context.config.ConfigFileApplicationListener#DEFAULT_SEARCH_LOCATIONS private static final String DEFAULT_SEARCH_LOCATIONS = "classpath:/,classpath:/config/,file:./,file:./config/"; ``` 查詢這個變數被引用的地方: ```java org.springframework.boot.context.config.ConfigFileApplicationListener.Loader#getSearchLocations() private Set getSearchLocations() { if (this.environment.containsProperty(CONFIG_LOCATION_PROPERTY)) { return getSearchLocations(CONFIG_LOCATION_PROPERTY); } Set locations = getSearchLocations(CONFIG_ADDITIONAL_LOCATION_PROPERTY); // 1 locations.addAll( asResolvedSet(ConfigFileApplicationListener.this.searchLocations, DEFAULT_SEARCH_LOCATIONS)); return locations; } ``` 這裡1處,就用到了前面的`DEFAULT_SEARCH_LOCATIONS`。 接著看看,上面這個函式被呼叫的地方: ```java private void load(Profile profile, DocumentFilterFactory filterFactory, DocumentConsumer consumer) { // 1 getSearchLocations().forEach((location) -> { boolean isFolder = location.endsWith("/"); Set names = isFolder ? getSearchNames() : NO_SEARCH_NAMES; // 2 names.forEach((name) -> load(location, name, profile, filterFactory, consumer)); }); } ``` 這裡的1處,就是前面的獲取config location;這裡1處,獲取到了集合後,對其進行foreach處理。 2處,這裡即會呼叫一個load函式,看名字就是載入,差不多可以猜到,是載入我們的那幾個目錄: ```java private static final String DEFAULT_SEARCH_LOCATIONS = "classpath:/,classpath:/config/,file:./,file:./config/"; ``` 好了,可以在這裡打個斷點,看看到底怎麼載入,因為前面的`file:./config/`是一個相對路徑,我們要看看,怎麼被解析為絕對路徑的。 ## 斷點debug,探求謎底 斷點我們打在了load方法,執行專案,然後斷點果然停在了我們想要的地方: ![](https://img2020.cnblogs.com/blog/519126/202005/519126-20200520150006376-2094627787.png) 這個圖就不多解釋了,直接看圈出來的地方,我們接著要看下面的函式: ```java private void loadForFileExtension(PropertySourceLoader loader, String prefix, String fileExtension, Profile profile, DocumentFilterFactory filterFactory, DocumentConsumer consumer) { DocumentFilter defaultFilter = filterFactory.getDocumentFilter(null); DocumentFilter profileFilter = filterFactory.getDocumentFilter(profile); if (profile != null) { // 1 ... } //2 Also try the profile-specific section (if any) of the normal file load(loader, prefix + fileExtension, profile, profileFilter, consumer); } ``` * 1處,省略了profile相關內容,我們本次啟動,沒指定profile * 2處,繼續load load處程式碼: ```java private void load(PropertySourceLoader loader, String location, Profile profile, DocumentFilter filter, DocumentConsumer consumer) { // 1 Resource resource = this.resourceLoader.getResource(location); // 2 if (resource == null || !resource.exists()) { if (this.logger.isTraceEnabled()) { StringBuilder description = getDescription("Skipped missing config ", location, resource, profile); this.logger.trace(description); } return; } ... } ``` * 1處,將location轉換為Resource,這裡傳入的location為:`file:./config/application.properties` * 2處,判斷resource是否存在 ## resourceLoader怎麼getResource 這裡的resourceLoader,為 `org.springframework.core.io.DefaultResourceLoader`。這個類,直接實現了`org.springframework.core.io.ResourceLoader`介面。 這個類,位於spring-core.jar中,基本是核心類了。 其註釋寫道: > ``` > * Default implementation of the {@link ResourceLoader} interface. > * Used by {@link ResourceEditor}, and serves as base class for > * {@link org.springframework.context.support.AbstractApplicationContext}. > * Can also be used standalone. > * > *

Will return a {@link UrlResource} if the location value is a URL, > * and a {@link ClassPathResource} if it is a non-URL path or a > * "classpath:" pseudo-URL. > ``` 大體翻譯: > ResourceLoader介面的預設實現,被ResourceEditor使用,同時,是AbstractApplicationContext的基類。 > > 也能被單獨使用。 > > 當傳入的value,是一個URL,則封裝為一個UrlResource並返回; > > 當傳入的是一個非URL,或者是一個類似於"classpath:"這樣的,則返回一個ClassPathResource 對其的介紹到此打住。繼續前面的程式碼: ```java @Override public Resource getResource(String location) { Assert.notNull(location, "Location must not be null"); ... // 1 if (location.startsWith("/")) { return getResourceByPath(location); } // 2 else if (location.startsWith(CLASSPATH_URL_PREFIX)) { return new ClassPathResource(location.substring(CLASSPATH_URL_PREFIX.length()), getClassLoader()); } else { try { // 3 Try to parse the location as a URL... URL url = new URL(location); return (ResourceUtils.isFileURL(url) ? new FileUrlResource(url) : new UrlResource(url)); } catch (MalformedURLException ex) { // No URL -> resolve as resource path. return getResourceByPath(location); } } } ``` * 1,判斷是否以 `/`開頭 * 2,判斷是否以classpath開頭 * 3,作為引數,看看能不能 被解析為一個URL 然後3處這裡,URL,是 jdk 的核心類,裡面 debug 進去挺深的,直接執行完這一句之後,我們看看url這個引數的值: ![](https://img2020.cnblogs.com/blog/519126/202005/519126-20200520153922495-2033478849.png) 總的來說,這裡就是:你給一個字串,URL按照它的格式,來解析為各個欄位:比如,協議,host,port,query等等。但是,不代表這個URL就是可以訪問的,如果是file,不代表這個檔案就存在。這裡只是按照URL的格式去解析而已。 我們繼續下一句: ```java URL url = new URL(location); // 1 return (ResourceUtils.isFileURL(url) ? new FileUrlResource(url) : new UrlResource(url)); ``` 這裡1處,判斷是否為file,如果是,則會new一個FileUrlResource。 該類的類結構如下: ![](https://img2020.cnblogs.com/blog/519126/202005/519126-20200520154430214-152385115.png) 前面呼叫了new ,我們看看: ```java public FileUrlResource(URL url) { super(url); } ``` 呼叫了父類: ```java /** * Original URI, if available; used for URI and File access. */ @Nullable private final URI uri; /** * Original URL, used for actual access. */ private final URL url; /** * Cleaned URL (with normalized path), used for comparisons. */ private final URL cleanedUrl; public UrlResource(URL url) { this.url = url; this.cleanedUrl = getCleanedUrl(this.url, url.toString()); this.uri = null; } ``` 總的來說,就是利用你傳入的URL,進行clean,然後儲存到了cleanedUrl。 我們這裡,經過clean後, * clean後,cleanedUrl的值為:`file:config/application.properties` * 原始的:`file:./config/application.properties` 差別不大,主要是去掉了開頭的`./`。 至此,我們的`FileUrlResource`就構造結束了,至此,我們完成了下面這行的解析。 ```java Resource resource = this.resourceLoader.getResource(location); ``` ## 判斷resource是否存在 前面我們看到,FileUrlResource,繼承了`org.springframework.core.io.AbstractFileResolvingResource`介面。 而我們這裡呼叫: ```java resource.exists() ``` 就會進入其父類的exists方法 ```java org.springframework.core.io.AbstractFileResolvingResource#exists public boolean exists() { try { // 1 URL url = getURL(); if (ResourceUtils.isFileURL(url)) { //2 Proceed with file system resolution return getFile().exists(); } ... } ``` * 1,獲取url * 2,獲取file * 3,判斷是否存在。 其中2處,繼續: ```java @Override public File getFile() throws IOException { // 1 File file = this.file; if (file != null) { return file; } // 2 file = super.getFile(); // 3 this.file = file; return file; } ``` * 1,查詢本地是否快取 * 2,沒快取,則呼叫super類的getFile去獲取 * 3,快取。 繼續進入2處, ```java org.springframework.core.io.UrlResource#getFile public File getFile() throws IOException { // 1 if (this.uri != null) { return super.getFile(this.uri); } else { // 2 return super.getFile(); } } ``` * 1,我們這裡uri是null,會進入2處 * 2,呼叫父類。 ```java org.springframework.core.io.AbstractFileResolvingResource#getFile() @Override public File getFile() throws IOException { // 1 URL url = getURL(); // 2 return ResourceUtils.getFile(url, getDescription()); } ``` * 1處,獲取url * 2處,獲取file。 繼續進入2處: ```java org.springframework.util.ResourceUtils#getFile(java.net.URL, java.lang.String) public static File getFile(URL resourceUrl, String description) throws FileNotFoundException { try { // 1 return new File(toURI(resourceUrl).getSchemeSpecificPart()); } catch (URISyntaxException ex) { // Fallback for URLs that are not valid URIs (should hardly ever happen). return new File(resourceUrl.getFile()); } } ``` 這裡,傳入的resourceURL,型別為URL, 在idea中顯示為: ```java file:./config/application.properties ``` toURI,大家可以大致看下, ```java org.springframework.util.ResourceUtils#toURI(java.net.URL) public static URI toURI(URL url) throws URISyntaxException { return toURI(url.toString()); } ``` ```java public static URI toURI(String location) throws URISyntaxException { return new URI(StringUtils.replace(location, " ", "%20")); } ``` 上面幹了啥,就是把路徑裡的" "換成了"%20"。然後new了一個URI。 ## URI、URL的差別簡述 這兩個東西,太學術了,簡單理解,就是URI,指代的東西更多,包含的範圍更廣,URI表示中國的話,URL可能只能表示臺灣省。(我他麼一顆紅心) 總的來說,uri 不一定可以訪問,url基本是可以的。 ![](https://img2020.cnblogs.com/blog/519126/202005/519126-20200520160618133-391325442.png) 參考:

## 最終是如何new file,判斷file是否存在的 經過前面的步驟後, ```java return new File(toURI(resourceUrl).getSchemeSpecificPart()); ``` 我們獲取了一個URI,然後呼叫其getSchemeSpecificPart,最終拿到一個String,其值為: ```java ./config/application.properties ``` 然後傳入了 File,用於構造一個file。 然後接著呼叫 ```java java.io.File#exists public boolean exists() { // 1 return ((fs.getBooleanAttributes(this) & FileSystem.BA_EXISTS) != 0); } ``` 然後這裡,1處呼叫了一個native方法: ```java java.io.WinNTFileSystem#getBooleanAttributes public native int getBooleanAttributes(File f); ``` 都到native方法了,沒法繼續了。 但是,最終呢,我們知道,現在的問題,變成了: ```java File file = new file("./config/application.properties"); file.exists(); ``` # new file,傳入相對路徑,這個相對路徑,到底相對於哪裡 經過我一番探索,最終寫了下面這個測試類,注意,該類使用預設包: ```java public class Test { public static void main(String[] args) throws IOException{ // 1 File file = new File("a.txt"); // 2 if (file.exists()) { System.out.println("file exists.path:" + file.getAbsolutePath()); } else { // 3 boolean newFile = file.createNewFile(); if (newFile) { System.out.println("create new file"); } else { System.out.println("create failed. file exists.path:" + file.getAbsolutePath()); } } } } ``` * 1,new file,使用了相對路徑,即當前路徑,模擬之前我們的問題 * 2,判斷是否存在 * 3,如果不存在,建立檔案。 ## idea中執行 我目前的idea中,project路徑為: ```java F:\workproject_codes\xxxx ``` 第一次執行,結果: ```java create new file ``` 說明檔案不存在,進行了檔案建立。然後我用everything搜尋了下該檔案,發現: ![](https://img2020.cnblogs.com/blog/519126/202005/519126-20200520162158278-146922517.png) 就在我的project路徑下。 然後我在想,為啥會建立到這個地方去? 然後我加了一段程式碼: ```java Properties properties = System.getProperties(); for (Map.Entry entry : properties.entrySet()) { System.out.println(entry.getKey() + ", " + entry.getValue()); } ``` 發現打印出來的properties中,有一個屬性: ```java user.dir, F:\workproject_codes\saltillo ``` 說明這個地址,就是user.dir搞出來的。 在idea中,user.dir,就是project的路徑。 ## 直接和class同級目錄下,java執行class 直接執行那個class檔案,我放到了centos下的/home/test目錄下: ```shell [root@localhost test]# ll -rw-r--r--. 1 root root 2013 May 20 13:40 Test.class [root@localhost test]# java Test ``` 這種情況下,建立的file,就是這個目錄下。 而且,看了下user.dir,就是當前目錄: ```java [root@localhost test]# java Test|grep user.dir user.dir, /home/test ``` ## 和class不在同級目錄下,java執行class 切換到上層目錄,即home下: ```shell [root@localhost home]# pwd /home [root@localhost home]# java -cp test/ Test |grep user.dir user.dir, /home ...會在本目錄下生產a.txt,刪除後再次執行: [root@localhost home]# java -cp test/ Test |grep create create new file result ``` 看上面,此時的user.dir,就變成了/home目錄。 同時,建立了新的檔案a.txt,就在當前home目錄下。 ## 打成spring boot jar後,在centos執行,結果如何 在spring boot jar包裡的main,註釋了原來的啟動程式碼,我加了這段程式碼: ```java @SpringBootApplication @EnableTransactionManagement @EnableAspectJAutoProxy(exposeProxy = true) @EnableFeignClients //@Slf4j @Controller @EnableScheduling public class xxx { private static Logger log= null; static { public static void main(String[] args) throws IOException { Properties properties = System.getProperties(); for (Map.Entry entry : properties.entrySet()) { System.out.println(entry.getKey() + ", " + entry.getValue()); } File file = new File("a.txt"); if (file.exists()) { System.out.println("file exists.path:" + file.getAbsolutePath()); } else { boolean newFile = file.createNewFile(); if (newFile) { System.out.println("create new file"); } else { System.out.println("create failed. file exists.path:" + file.getAbsolutePath()); } } // new SpringApplicationBuilder(xxx.class).web(WebApplicationType.SERVLET).run(args); } } ``` 在`/root/tt`下執行,用java -jar xxx.jar執行後, ```shell user.dir, /root/tt ... create new file ``` 然後,果然,在/root/tt下,就建了一個a.txt檔案。 ```shell [root@localhost tt]# ll total 109412 -rw-r--r--. 1 root root 0 May 20 16:41 a.txt -rw-r--r--. 1 root root 112035602 May 20 16:40 xxx.jar [root@localhost tt]# pwd /root/tt ``` ## user.dir是個什麼東西 為此,我專門把那個class,拷貝到了root目錄下,執行: ```shell [root@localhost ~]# java Test |grep user.dir user.dir, /root ``` 這,看起來,在哪裡執行java,user.dir就是哪兒啊,類似於pwd了。 大家如果直接去網上搜user.dir,基本都是很混亂,各說各的,大家按照上面這樣實踐下就知道了。 # 總結 我們已經找到了問題原因了,總的來說,就是spring boot外部化配置時, ```java file:./config/ ``` 這個路徑,相對路徑,相對的是user.dir。 而user.dir怎麼來,就是你在哪個目錄下執行java,哪個目錄就是user.dir。 題目中這個問題怎麼解決,可以直接在java -jar xxx.jar中,加一個引數: ```shell java -jar -Dspring.config.location=D:\config\config.properties springbootrestdemo-0.0.1-SNAPSHOT.jar ``` 可參考: https://www.cnblogs.com/xiaoqi/p/6955288.html 我這邊的作業系統,pc是win7,centos是: ```shell [root@localhost tt]# cat /etc/centos-release CentOS Linux release 7.6.1810 (Core) ``` 謝謝