自作DI①

SpringやCDIで提供されるDIの仕組だが、いろいろ原理主義的で扱いにくいところがあるため、やりやすいようにDIを自作してみる。
最終形として
アノテーションベース
XMLの設定ファイルでインジェクションするクラスを指定
を想定して作っていこうと思っている。

DIの仕組を考える前に、今日は設定ファイルに使おうと思っているXMLJavaからの扱い方を復習してみたい。
有名どころのDOMのライブラリにて、かつてハマったところも鑑みて使い方をみていく。

test.xml

<?xml version="1.0" encoding="UTF-8"?>
<factor1>
  <factor2>
    <factor3>value3</factor3>
    <factor4>value4</factor4>
  </factor2>
</factor1>

このxmlファイルをパースしてDocumentクラスのインスタンスに展開。

package xmltest;

import java.io.File;
import javax.xml.parsers.DocumentBuilderFactory;
import org.w3c.dom.Document;

public class XmlTest {
    public static void main(String args[]){
        try{
            Document doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().
                parse(new File("C:\\test.xml"));
        }catch(Exception e){
            e.printStackTrace();
        }
    }
}

XMLドキュメントの根幹であるDocumentのインスタンスは、内部でDOMツリーの構造を成している。
ドキュメント内の各要素をNodeとして取り出すことが出来、子要素は各NodeからNodeとして取り出すことが出来る。
複数Nodeを取り出す際にはNodeList型というのもある。ちなみにIterableではない。
Nodeを辿って目的の子ノードを探すことも出来るが、getElementsByTagNameで特定のtag名を持つNodeを再帰的に取ってくることも出来る。

ちょっとハマるのが、タグだけでなくタグ外のテキストもNodeとして扱われることだ。test.xmlの例で言えば、factor2からgetChildNodes()でNodeListを取得した場合、中のNodeは
・factor2の後ろの改行コード+インデント
・factor3
・factor3の後ろの改行コード+インデント
・factor4
・factor4の後ろの改行コード+インデント
の5つになる。なので無考慮にgetFirstChild()で最初のタグを処理しようとすると失敗する。かと言ってtext上タグを完全に隣接させれば最初のNodeがタグになるため、2番目のNodeを最初のタグとすることも出来ない。
NodeNameを確認して処理するなど、きちんとチェックを行うことが必要になる。

package xmltest;

import java.io.File;
import javax.xml.parsers.DocumentBuilderFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

public class XmlTest {
    public static void main(String args[]){
        try{
            Document doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().
                parse(new File("C:\\test.xml"));
            Node nodeTest1 = doc.getFirstChild();
            System.out.println("--test1---");
            System.out.print("name:" + nodeTest1.getNodeName());
            System.out.print(",type:" + nodeTest1.getNodeType());
            System.out.println(",value:" + nodeTest1.getNodeValue());
            System.out.println("----------");

            Node nodeTest2 = doc.getFirstChild().getChildNodes().item(0);
            System.out.println("--test2---");
            System.out.print("name:" + nodeTest2.getNodeName());
            System.out.print(",type:" + nodeTest2.getNodeType());
            System.out.println(",value:" + nodeTest2.getNodeValue());
            System.out.println("----------");

            NodeList parentNl = doc.getElementsByTagName("factor2");
            NodeList childNl = parentNl.item(0).getChildNodes();
            System.out.println("--test3---");
            for(int i = 0;i < childNl.getLength();i++){
            	Node nodeTest3 = childNl.item(i);
            	System.out.print("name:" + nodeTest3.getNodeName());
            	System.out.print(",type:" + nodeTest3.getNodeType());
            	System.out.println(",value:" + nodeTest3.getNodeValue());
            }
            System.out.println("----------");
        }catch(Exception e){
            e.printStackTrace();
        }
    }
}

実行結果

--test1---
name:factor1,type:1,value:null
----------
--test2---
name:#text,type:3,value:
  
----------
--test3---
name:#text,type:3,value:
    
name:factor3,type:1,value:null
name:#text,type:3,value:
    
name:factor4,type:1,value:null
name:#text,type:3,value:
  
----------

AOPアスペクト指向プログラミング)といって、多くの業務機能で共通する同じ処理を「アスペクト(横断的関心ごと)」として切り出してモジュール化しようという考え方がある。

これだけだとサブルーチンと同じだが、サブルーチンの場合各機能の中で明示的にサブルーチン呼び出しの処理を書かなければならない。AOPではそれすらせずに共通処理を埋め込みたいのだ。

アスペクトモジュールへの依存が残ると、万が一引数を変えなければなんてことがあったときに、全ての機能に変更が必要になる。そうでなくても、業務機能だけ外注に出そうなんてときにちゃんとアスペクトの呼び出しを書いているかもチェック・試験が必要になり、地味に大変だ。

今回はリフレクションのお仲間のProxyクラスを使って、業務機能側には呼び出し処理を一切書かずにアスペクトの処理を差し込んでAOPを実現してみる。

元のつくりにもよるが、これによって現行の業務機能に手を加えず、アスペクトの処理を差し込むことが出来る場合もある。

業務処理インタフェース

package aopTest;

public interface Yobidasee {
    public void yobidaseeOne();
}
業務処理クラス

package aopTest;

public class YobidaseeImpl implements Yobidasee{
    public void yobidaseeOne(){
        System.out.println("------------");
        System.out.println("呼び出され1");
        System.out.println("------------");
    }
}

これをFactoryでプロキシインスタンス化する。

Factory

package aopTest;

import java.lang.reflect.Proxy;

public class TestProxyFactory {
    public static Yobidasee createNewInstance(){
        Yobidasee obj = new YobidaseeImpl();
        TestHandler handler = new TestHandler(obj);
        return (Yobidasee)Proxy.newProxyInstance(Yobidasee.class.getClassLoader(),
        		obj.getClass().getInterfaces(), handler);
    }
}
実行ハンドラ

package aopTest;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

public class TestHandler implements InvocationHandler {
    private Yobidasee yobidasee;
    public TestHandler(Yobidasee yobidasee){
        this.yobidasee = yobidasee;
    }
    public Object invoke(Object obj, Method method, Object[] args){
        System.out.println("before");
        try{
            method.invoke(yobidasee,args);
        }catch(Exception e){}
        System.out.println("after");
        return obj;
    }
}

Factoryで業務処理のインスタンスからプロキシインスタンスを生成する。
Proxy.newProxyInstanceにクラスローダとクラスオブジェクト、そして実行ハンドラを渡すことで、プロキシインスタンスをObject型で返してくれる。
実行ハンドラはjava.lang.reflect.InvocationHandlerを実装したものを作成する。invokeメソッドをオーバーライドして記述した処理がインスタンスのメソッド実行時に代わりに実行される。
invokeメソッドの第3引数はインスタンスのメソッドの引数である。
とりあえず実行ハンドラには開始ログとして"before"、終了ログとして"after"と標準出力する処理を入れた。

呼び出し側クラス

package aopTest;

public class Yobidaser {
    public static void main(String args[]){
        Yobidasee yobidasee = new YobidaseeImpl();
        yobidasee.yobidaseeOne();
        System.out.println();
        Yobidasee yobidaseeP = TestProxyFactory.createNewInstance();
        yobidaseeP.yobidaseeOne();
    }
}

直接newした場合と、Factoryで生成した場合とでそれぞれ実行してみる。

実行結果

------------
呼び出され1
------------

before
------------
呼び出され1
------------
after

プロキシインスタンスは、生成後は通常のインスタンスと同じように扱えるため、呼び出し側の業務処理はアスペクトの存在を全く意識せず処理が出来る。アスペクトの追加も、業務処理には手を加えず可能だ。
既存システムにも、Factoryへの処理追加だけでログ機能とかを新たに追加することも出来る。まあFactoryを使わず全部直newしていたらいかんともし難そうだが。