Java實現配置載入機制

segmentfault發表於2016-01-22

前言

現如今幾乎大多數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 Jobs
port2=Bill Gates
port3=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,以及我最近在研究並被氣到吐血的某國產開源資料庫中介軟體軟體,如果沒有一套完整的配置載入機制,那麼程式碼會比較散亂,可維護性不高。容易使人吐血。

相關文章