設定ファイルの内容をAOPでオブジェクトに適用する実験

開発環境と本サイトなど、異なる環境でプログラムを動作させる場合、環境に応じてソースコードの変更をおこなわなくても済むように設定値を設定ファイルに外出しするというのは割と一般的だと思う。しかし、その設定値を使うクラスにとって設定値(例えば、メール送信をおこなうクラスにとってのSMTPの接続情報等)の取得処理は、明らかにCore Concernではない(そしておそらくCrosscutting Concernな)ことだ。ということで設定ファイルの内容をオブジェクトに自動で読み込むようにし、その実現にAOPを使ってみようと思う。

方針

オブジェクトの生成・初期化のタイミングで設定ファイルの値を適用したいということなら、ConstructorInterceptorが用途には向いていると思う。設定ファイルをクラス/インタフェース毎に用意し、対象となるオブジェクトのクラスがそのクラスまたはサブクラスである、あるいはインタフェースを実装している場合にそのファイルの内容を適用する。


設定クラスの読み込み・保持には前回のConfigurationクラスを使用する。クラス/インタフェース毎にファイルを用意し、ファイル名=クラス名とする(中身はXMLだけど拡張子は無しということで。) Configurationを保持してクラス名/インタフェース名で問い合わせて取得できるようにConfigurationManagerクラスというのを作成する。


設定ファイルに登録されているキー=プロパティ名に対するsetterメソッドが対象となるクラスに実装されていれば、そのsetterメソッドを使って値をセットする。

ソースコード

ConfigurationManagerクラス

package sample.config;

import java.io.File;
import java.net.MalformedURLException;
import java.util.HashMap;
import java.util.Map;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * クラス毎の設定オブジェクトを管理するクラス
 */
public class ConfigurationManager {
    private Log logger = LogFactory.getLog(getClass());
    private File baseDir;
    private Map<String, Configuration> configurations = new HashMap<String, Configuration>();

    /**
     * 設定ファイルを含むディレクトリをConfigurationManagerを生成するコンストラクタ
     * @param basePath 設定ファイルを含むディレクトリのパス
     */
    public ConfigurationManager(String basePath) {
        if (basePath == null) {
            throw new IllegalArgumentException("basePath is null.");
        }
        File baseDir = new File(basePath);
        if (!baseDir.isDirectory()) {
            throw new IllegalArgumentException("Bad basePath specified. Not directory");
        }
        this.baseDir = baseDir;
    }
    
    /**
     * aClassで使用する設定オブジェクトを取得
     * @param aClass Class
     * @return 設定ファイルが見つかればその内容を読み込んで戻す。ファイルが無ければnullを戻す。
     */
    public Configuration getConfiguration(Class aClass) {
        if (aClass == null) {
            throw new IllegalArgumentException("aClass is null.");
        }
        String className = aClass.getName();
        synchronized (configurations) {
            Configuration configuration = this.configurations.get(className);
            if (configuration == null) {
                configuration = createConfiguration(className);
                if (configuration != null) {
                    this.configurations.put(className, configuration);
                }
            }
            return configuration;
        }
    }

    /**
     * クラス名classNameに対応するファイルをConfigurationオブジェクトに読み込む
     * @param className クラス名
     * @return クラス名に対応するファイルから読み込んだConfigurationオブジェクト。
     * ファイルが存在しないか読み込めない場合nullを戻す。
     */
    private Configuration createConfiguration(String className) {
        Configuration configuration = null;
        File configFile = new File(this.baseDir, className);
        if (!configFile.isFile() ||  !configFile.canRead()) {
            return null;
        }
        try {
            configuration = new Configuration(configFile.toURL());
        } catch (MalformedURLException e) {
            logger.error("Failed to get config url:" + configFile.getPath(), e);
        }
        return configuration;
    }
    
}


インスタンス生成後に、実装する全てのクラス/インタフェースに対するConfigurationを適用するConstractorInterceptor

package sample.config;

import java.beans.PropertyDescriptor;
import java.lang.reflect.Method;
import java.util.List;

import org.aopalliance.intercept.ConstructorInterceptor;
import org.aopalliance.intercept.ConstructorInvocation;
import org.apache.commons.beanutils.PropertyUtils;
import org.apache.commons.lang.ClassUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

@SuppressWarnings("unchecked")
public class ConfigurationInterceptor implements ConstructorInterceptor {
    private ConfigurationManager configurationManager;
    
    private Log logger = LogFactory.getLog(getClass());

    public ConfigurationInterceptor(ConfigurationManager configurationManager) {
        this.configurationManager = configurationManager;
    }
    public Object construct(ConstructorInvocation invocation) throws Throwable {
        Object instance = invocation.proceed();
        
        configure(instance);
        
        return instance;
    }

    /**
     * オブジェクトinstanceが実装する全クラス/インスタンスを取得し、それぞれに対する設定オブジェクトを取得して適用する
     * @param instance 設定をおこなうインスタンス
     */
    private void configure(Object instance) {
        Class classOfInstance = instance.getClass();
        List<Class> classes = ClassUtils.getAllSuperclasses(classOfInstance);
        classes.addAll(ClassUtils.getAllInterfaces(classOfInstance));
        classes.add(classOfInstance);
      
        for (Class aClass: classes) {
            Configuration configuration = this.configurationManager.getConfiguration(aClass);
            if (configuration != null) {
                applyConfiguration(instance, aClass, configuration);
            }
        }
    }
    /**
     * オブジェクトinstanceにconfiguration設定オブジェクトを適用する
     * aClassに含まれるプロパティ全てについて、該当する設定値を設定オブジェクトから取得し、
     * aClassに実装されたsetterを使用してinstanceにセットする。
     * 設定値が空値の場合、またはsetterが無い場合は設定をおこなわない
     * @param instance 設定を適用するオブジェクト
     * @param aClass 設定を適用するクラス/インタフェース
     * @param configuration 設定
     */
    private void applyConfiguration(Object instance, Class aClass, Configuration configuration) {
        PropertyDescriptor[] descriptors =
            PropertyUtils.getPropertyDescriptors(aClass);
        for (PropertyDescriptor propertyDescriptor: descriptors) {
            String propertyName = propertyDescriptor.getName();
            Object value = configuration.get(propertyName);
            Method method = propertyDescriptor.getWriteMethod();
            if (value != null && method != null) {
                try {
                    method.invoke(instance, new Object[] {value});
                } catch (Exception e) {
                    this.logger.debug("Failed to invoke setter.", e);
                }
            }
       }
    }
}

環境

  • J2SE 5.0
  • commons-digester-1.8.jar
  • commons-logging-1.0.4.jar
  • commons-beanutils-core-1.7.0.jar
  • commons-lang-2.3.jar
  • aopalliance-1.0.jar

使用方法

S2を使って実際に設定をおこなってみよう。


と思ったらS2はMethodInterceptorしか対応していないっぽい……ということで、次はその辺をなんとかしてみたい。