靈魂拷問:在不重啟服務的前提下,如何讓配置修改生效的呢?有什麼奇技淫巧嗎? 靈魂拷問:在 Java 項目中,總能看到以 .properties 為尾碼的文件蹤影,這類配置文件是怎麼載入的呢? 項目研發過程中,總會遇到一些經常改變的參數,比如要連接的資料庫的連接地址、名稱、用戶名、密碼;再比如訪問三方 ...
靈魂拷問:在不重啟服務的前提下,如何讓配置修改生效的呢?有什麼奇技淫巧嗎?
靈魂拷問:在 Java 項目中,總能看到以 .properties 為尾碼的文件蹤影,這類配置文件是怎麼載入的呢?
項目研發過程中,總會遇到一些經常改變的參數,比如要連接的資料庫的連接地址、名稱、用戶名、密碼;再比如訪問三方服務的 URL 等等。考慮到程式的通用性,這些參數往往不能直接寫死在程式里,通常藉助配置文件來優雅處理。
在 Java 項目中,properties 文件當屬使用較簡單一類,不過雖然簡單,還是要好好說說項目中都是怎麼使用的,嘗試通過源碼解讀,讓你真正懂它,並帶你深刻體會 Java 中重載的意義。
1. 雖說簡單,格式還是要看一看。
相比上次談及的 ini 配置文件,properties 文件格式沒有 Section (節)的概念,反而是簡單了不少。
上圖是一個 jdbc 連接所需要的配置,其中以 # 開始的每一行是註釋信息,而以等號分割的每行配置,就是常說的鍵-值對,等號左邊的為 key(代碼中的變數),等號右邊的為 value(是依據實際場景而配置的值)。
2. 雖說簡單,Java 源碼還是去要看看。
在 Java 中提供了 java.util.Properties 類,主要用於對配置文件的讀寫操作。
一圖掌握血緣關係,很顯然 Properties 繼承自 Hashtable,歸根結底是個 Map,而 Properties 最特殊的地方,就是它的鍵和值都是字元串類型。
從全局瞭解梗概,然後走進 JDK 源碼,按照思路,步步去深入。
首先,要看看寫好的配置文件是怎麼載入的?
源碼很清晰,提供字元流 Reader、位元組流 InputStream兩種方式載入配置文件(方法重載的目的:讓使用者更方便),再深入去看最終會調用 Hashtable 的 put(key, value) 來設置鍵值對,最終完成配置文件中的載入。
然後,要看看怎麼根據 key 獲得對應的 value(放進去了,還要考慮拿出來)?
源碼很清晰,通過參數 key 獲得對應的 value,考慮到使用者的方便,對 getProperty 方法進行了重載,其中標註 2 的方法,當根據 key 獲得值是 null 時,會返回一個調用方法時傳入的預設值(很多場景下,確實很有用)。
知道了怎麼載入配置文件,知道了怎麼獲取 key 對應的值,按照常理說,項目中已經夠用了,但是有些時候項目啟動後,還真需要再額外設置一下參數的值,不過沒關係,因為 Java 已經想到了這一點,對外提供了 setProperty 的方法,讓額外設置參數成為可能。
若能在項目研發中,熟練使用上面提到的這些 API,已經足矣。既然打開了源碼,索性把雜七雜八的都提提,說不定某些 API 也能解決你碰到的其它場景的問題呢。
Properties 類不僅提供了 load 進行載入配置,而且還提供了把鍵值對寫到文件中的能力。如上面源碼所示,考慮到使用者的方便,對 store 方法進行重載,提供了面向位元組流 OutputStream、字元流 Writer 兩種方式的 store。
如上面源碼所示,Properties 除了提供對常規配置文件讀寫能力支撐,對 xml 配置文件載入、寫入也提供了支撐。
如上圖源碼所示,Properties 類提供了重載的 list 方法,為了方便調試,可以把鍵值對列表給整齊的列印出來。
3. 雖說簡單,不能賦予實踐一切都是扯淡。
場景一:在 APM 性能監控時,獲取 Java 應用畫像信息常用 API。
import java.util.Properties; /** * @author 一猿小講 */ public class JVMDetails { public static void main(String[] args) { // 獲取系統信息(JDK信息、Java虛擬機信息、Java提供商信息、當前電腦用戶信息) Properties properties = System.getProperties(); // 把系統信息列印一下 properties.list(System.out); } }
程式跑起來,部分輸出截圖示意如下。
場景二:業務開發中,讓配置替代硬編碼,並考慮配置更新時,程式能夠讀到最新的值。
import java.io.*; import java.util.Hashtable; import java.util.Properties; /** * 配置文件工具類 * 1. 支持載入 .properties文件、.ini文件 * 2. 支持配置文件更新 * @author 一猿小講 */ public class PropertiesUtil { // private static final Log4j LOG = .....; private static Hashtable<String, PropCache> propCache = new Hashtable<String, PropCache>(); public static String getString(String propFile, String key) { return getString(propFile, key, null); } public static String getString(String propFile, String key, String defaultValue) { if ((!propFile.endsWith(".properties")) && (!propFile.endsWith(".ini"))) { propFile = propFile + ".properties"; } PropCache prop = propCache.get(propFile); if (prop == null) { try { prop = new PropCache(propFile); } catch (IOException e) { // LOG.warn(e); System.out.println(String.format("讀取 %s 出現異常%s", propFile,e)); return defaultValue; } propCache.put(propFile, prop); } String value = prop.getProperty(key, defaultValue); if (value != null) { value = value.trim(); } return value; } public static void setString(String propFile, String key, String value) throws IOException { if ((!propFile.endsWith(".properties")) && (!propFile.endsWith(".ini"))) { propFile = propFile + ".properties"; } File file = new File(propFile); Properties prop = new Properties(); try { FileInputStream fis = new FileInputStream(file); prop.load(fis); fis.close(); } catch (FileNotFoundException e) { // LOG.warn(e); System.out.println(String.format("文件 %s 不存在", propFile)); } FileOutputStream fos = new FileOutputStream(file); prop.put(key, value); prop.store(fos, null); fos.close(); PropCache localPropCache = propCache.get(propFile); if (localPropCache != null) { localPropCache.reload(); } } private static class PropCache { private String fileName; private long lastLoad; private Properties prop; public PropCache(String propFileName) throws IOException { File file = new File(propFileName); FileInputStream fis = new FileInputStream(file); this.prop = new Properties(); this.prop.load(fis); fis.close(); this.fileName = file.getAbsolutePath(); this.lastLoad = file.lastModified(); } public String getProperty(String key, String defaultValue) { File file = new File(this.fileName); if (this.lastLoad < file.lastModified()) { reload(); } return this.prop.getProperty(key, defaultValue); } public void reload() { File file = new File(this.fileName); try { Properties prop = new Properties(); FileInputStream fis = new FileInputStream(file); prop.load(fis); fis.close(); this.prop.clear(); this.prop.putAll(prop); this.lastLoad = file.lastModified(); } catch (IOException e) { // PropertiesUtil.LOG.warn(e); System.out.println(String.format("文件 %s 重新載入出現異常%s", this.fileName, e)); } // PropertiesUtil.LOG.all(new Object[]{this.fileName, " reloaded."}); System.out.println(String.format("文件 %s 重新載入完畢", this.fileName)); } } }
藉助開篇提到的 jdbc 配置文件,進行驗證。嘗試獲取資料庫類型,預設配置為 db2,中途修改參數的值為 mysql,看看效果如何?
/** * 測試類 * @author 一猿小講 */ public class M { public static void main(String[] args) { while(true) { System.out.println(PropertiesUtil.getString("db", "jdbc.type")); try { Thread.sleep(10000); } catch (InterruptedException e) { } } } }
程式跑起來,效果還是讓人很滿意。
4. 雖說簡單,洋洋灑灑分享一大篇。
有關配置文件的分享網上有很多,而我們的分享卻顯得不太一樣。
我們的初衷是:結合實際項目及源碼,說說這些年用過的那些有關配置的奇技淫巧,幫你提高研發能力(那怕是提高一丟丟,就算成功)。
它山之石可以攻玉,相信會對你有所幫助。
為了能夠幫你提高研發能力(那怕是提高一丟丟呢),後續將繼續結合實際項目,看看用到的其它形式的配置文件,敬請期待。