前言
現如今幾乎大多數Java應用,例如我們耳熟能詳的tomcat, struts2, netty...等等數都數不過來的軟體,
要滿足通用性,都會提供設定檔供使用者定製功能。
甚至有一些例如Netty這樣的網路架構,幾乎完全就是由配置驅動,這樣的軟體我們也通常稱之為"微核心架構"的軟體。
你把它配置成什麼,它就是什麼。
It is what you configure it to be.
最常見的設定檔格式是XML, Properties等等檔案。
本文探討載入配置中最通用也是最常見的情境,那就是把一個設定檔映射成Java裡的POJO對象.
並探討如何?不同方式的載入,例如,有一些配置是從本地XML檔案裡面載入的,而有一些配置需要從本地Properties檔案載入,更有甚者,有一些配置需要通過網路載入配置。
如何?這樣一個配置載入機制,讓我們擁有這個機制後,不會讓載入配置的代碼散布得到處都是,並且可擴充,可管理。
配置載入器
首先,我們需要一個配置載入器,而這個配置載入器是可以有多種不同的載入方式的,因此,我們用一個介面來描述它,如下所示:
/** * * * @author Bean * @date 2016年1月21日 上午11:47:12 * @version 1.0 * */public interface IConfigLoader<T> { /** * load the config typed by T * * @return * @throws ConfigException */ public T load() throws ConfigException;}
可是,為什麼我們需要在這個介面上聲明泛型<T> ?
很明顯,當我們要使用一個配置載入器時,你得告訴這個配置載入器你需要載入後得到什麼結果。
例如,你希望載入配置後得到一個AppleConfig對象,那麼你就可以這麼去使用上述定義的介面:
IConfigLoader<AppleConfig> loader = new AppleConfigLoader<AppleConfig>(); AppleConfig config = loader.load();
於是你將設定檔裡的資訊轉化成了一個AppleConfig對象,並且你能得到這個AppleConfig對象執行個體。
到目前,貌似只要我們的AppleConfigLoader裡面實現了怎麼載入設定檔的具體勞動,我們就可以輕易載入配置了。
可以這麼說,但是不是還沒有考慮到,配置可能通過不同的方式載入呢,比如通過Properties載入,通過dom方式載入,通過sax方式載入,或者通過某些第三方的開源庫來載入。
因此,除了配置載入器,我們還需要另外一種角色,配置載入方式的提供者。暫且,我們就叫它IConfigProvider。
配置載入方式的提供者
配置載入方式的提供者可以提供一種載入方式給配置載入器,換言之,提供一個對象給配置載入器。
如果通過dom方式載入,那麼提供者提供一個Document對象給載入器。
如果通過Properties方式載入,那麼提供者提供一個Properties對象給載入器
如果通過第三方類庫提供的方式載入,比如apache-commons-digester3(tomcat的配置載入),那麼提供者提供一個Digester對象給載入器
提供者的職責就是提供,僅此而已,只提供配置載入器所需要的對象,但它本身並不參與配置載入的勞動。
我們用一個介面IConfigProvider來定義這個提供者
/** * * * @author Bean * @date 2016年1月21日 上午11:54:28 * @version 1.0 * */public interface IConfigProvider<T> { /** * provide a config source used for loading config * * @return * @throws ConfigException */ public T provide() throws ConfigException;}
這裡為什麼又會有<T>來聲明泛型呢?
如果需要一個提供者,那麼至少得告訴這個提供者它該提供什麼吧。
因此,一個提供者會提供什麼,由這個來決定。
同時,到這裡,我們可以先建造一個工廠,讓它來生產特定的提供者:
/** * * * @author Bean * @date 2016年1月21日 上午11:56:28 * @version 1.0 * */public class ConfigProviderFactory { private ConfigProviderFactory() { throw new UnsupportedOperationException("Unable to initialize a factory class : " + getClass().getSimpleName()); } public static IConfigProvider<Document> createDocumentProvider(String filePath) { return new DocumentProvider(filePath); } public static IConfigProvider<Properties> createPropertiesProvider(String filePath) { return new PropertiesProvider(filePath); } public static IConfigProvider<Digester> createDigesterProvider(String filePath) { return new DigesterProvider(filePath); }}
可以開始實現具體配置載入器了?
還不行!
到這裡,假設我們有一個設定檔,叫apple.xml。而且我們要通過DOM方式把這一份apple.xml載入後變成AppleConfig對象。
那麼,首先我要通過提供者工廠給我製造一個能提供Document的提供者。然後拿到這個提供者,我就可以調用它的provide方法來獲得Document對象,
有了document對象,那麼我就可以開始來載入配置了。
可是,如果要載入BananaConfig、PearConfig.......呢,其步驟都是一樣的。因此我們還要有一個抽象類別,來實現一些預設的共同行為。
/** * * * @author Bean * @date 2016年1月21日 上午11:59:19 * @version 1.0 * */public abstract class AbstractConfigLoader <T, U> implements IConfigLoader<T>{ protected IConfigProvider<U> provider; protected AbstractConfigLoader(IConfigProvider<U> provider) { this.provider = provider; } /* * @see IConfigLoader#load() */ @Override public T load() throws ConfigException { return load(getProvider().provide()); } public abstract T load(U loaderSource) throws ConfigException; protected IConfigProvider<U> getProvider() { return this.provider; }}
每個配置載入器都有一個帶參數構造器,接收一個Provider。
泛型指明了我要載入的是AppleConfig還是BananConfig,泛型<U>指明了要用什麼載入方式載入,是Document呢,還是Properties,或者其他。
實戰運用執行個體
有一份菜市場設定檔market.xml,配置了菜市場的商品,裡面有兩種商品,分別是蘋果和雞蛋。
<market> <apple> <color>red</color> <price>100</price> </apple> <egg> <weight>200</weight> </egg></market>
另外還有一份關於各個檔口老闆名字的設定檔,owner.properties
port1=Steve Jobsport2=Bill Gatesport3=Kobe Bryant
我們先定義好如下類:
MarketConfig.java
/** * * * @author Bean * @date 2016年1月21日 下午11:03:37 * @version 1.0 * */public class MarketConfig { private AppleConfig appleConfig; private EggConfig eggConfig; private OwnerConfig ownerConfig; public AppleConfig getAppleConfig() { return appleConfig; } public void setAppleConfig(AppleConfig appleConfig) { this.appleConfig = appleConfig; } public EggConfig getEggConfig() { return eggConfig; } public void setEggConfig(EggConfig eggConfig) { this.eggConfig = eggConfig; } public OwnerConfig getOwnerConfig() { return ownerConfig; } public void setOwnerConfig(OwnerConfig ownerConfig) { this.ownerConfig = ownerConfig; }}
AppleConfig.java
/** * * * @author Bean * @date 2016年1月21日 下午11:03:45 * @version 1.0 * */public class AppleConfig { private int price; private String color; public void setPrice(int price) { this.price = price; } public int getPrice() { return this.price; } public void setColor(String color) { this.color = color; } public String getColor() { return this.color; }}
EggConfig.java
/** * * * @author Bean * @date 2016年1月21日 下午11:03:58 * @version 1.0 * */public class EggConfig { private int weight; public void setWeight(int weight) { this.weight = weight; } public int getWeight() { return this.weight; }}
OwnerConfig.java
/** * * * @author Bean * @date 2016年1月21日 下午11:04:06 * @version 1.0 * */public class OwnerConfig { private Map<String, String> owner = new HashMap<String, String>(); public void addOwner(String portName, String owner) { this.owner.put(portName, owner); } public String getOwnerByPortName(String portName) { return this.owner.get(portName); } public Map<String, String> getOwners() { return Collections.unmodifiableMap(this.owner); }}
這個例子有兩種配置載入方式,分別是Dom和Properties載入方式。
所以我們的提供者建造工廠需要製造兩種提供者provider.
而且需要定義2個配置載入器,分別是:
OwnerConfigLoader
/** * * * @author Bean * @date 2016年1月21日 下午11:24:50 * @version 1.0 * */public class OwnerConfigLoader extends AbstractConfigLoader<OwnerConfig, Properties>{ /** * @param provider */ protected OwnerConfigLoader(IConfigProvider<Properties> provider) { super(provider); } /* * @see AbstractConfigLoader#load(java.lang.Object) */ @Override public OwnerConfig load(Properties props) throws ConfigException { OwnerConfig ownerConfig = new OwnerConfig(); /** * 利用props,設定ownerConfig的屬性值 * * 此處代碼省略 */ return ownerConfig; }}
然後是MarketConfigLoader
import org.w3c.dom.Document;/** * * * @author Bean * @date 2016年1月21日 下午11:18:56 * @version 1.0 * */public class MarketConfigLoader extends AbstractConfigLoader<MarketConfig, Document> { /** * @param provider */ protected MarketConfigLoader(IConfigProvider<Document> provider) { super(provider); } /* * AbstractConfigLoader#load(java.lang.Object) */ @Override public MarketConfig load(Document document) throws ConfigException { MarketConfig marketConfig = new MarketConfig(); AppleConfig appleConfig = new AppleConfig(); EggConfig eggConfig = new EggConfig(); /** * 在這裡處理document,然後就能得到 * AppleConfig和EggConfg * * 此處代碼省略 */ marketConfig.setAppleConfig(appleConfig); marketConfig.setEggConfig(eggConfig); /** * 由於OwnerConfig是需要properties方式來載入,不是xml * 所以這裡要建立一個OwnerConfigLoader,委託它來載入OwnerConfig */ OwnerConfigLoader ownerConfigLoader = new OwnerConfigLoader(ConfigProviderFactory.createPropertiesProvider(YOUR_FILE_PATH)); OwnerConfig ownerConfig = ownerConfigLoader.load(); marketConfig.setOwnerConfig(ownerConfig); return marketConfig; }}
然後,我們在應用程式層面如何擷取到MarketConfig呢
MarketConfigLoader marketConfigLoader = new MarketConfigLoader(ConfigProviderFactory.createDocumentProvider(YOUR_FILE_PATH));
MarketConfig marketConfig = marketConfigLoader.load();
也許有個地方會人奇怪,明明有四個配置類,為什麼只有2個配置載入器呢。
因為MarketConfig、EggConfig和AppleConfig,都是從同一個xml設定檔裡面載入,所以只要一個Document對象,通過MarketConfigLoader就可以全部載入。
而OwnerConfig是不同的載入方式,所以需要另外一個載入器。
尾聲
本文提出的配置載入機制,並不能夠實際幫忙載入配置,這事應該留給DOM,SAX,以及其他一些開源庫如dom4j,Digester去做。
但本文提出的配置載入機制能夠讓配置載入機制更靈活,容易擴充,並且能夠整合多種配置載入方式,融合到一個機制進來,發揮各自有點。
實際上,有些軟體經常需要同時從多種不同格式的設定檔裡面載入配置,例如struts2,以及我最近在研究並被氣到吐血的某國產開來源資料庫中介軟體軟體,
如果沒有一套完整的配置載入機制,那麼代碼會比較散亂,可維護性不高。容易使人吐血。