目次


Groovy を DSL として用いてプラグイン機能を実現する

Groovy を DSL として利用することで、柔軟な設定が可能となります

Comments

プラグイン設計パターンについて

プラグイン設計パターン [1] は、複数のコンポーネント同士の関連を、コンパイル時ではなく実行時に解決します。プラグインの基本的な動作は以下のようになります。

  1. アプリケーションコードを、特定のインターフェースに依存するように実装しておく。
  2. 1 のインターフェースを実装するクラスとして何を使用するかを、外部の設定ファイルに記述する。
  3. 2 の設定ファイルを実行時に読み込んで、実装クラスのインスタンスを生成し、アプリケーションの実行時に動的にリンクする。

これによりアプリケーションを構築し直したり、配備し直したりすることなく、実装クラスを入れ替えることが可能となります。なお、一般にプラグインと言った場合、上記に加えて、特定の場所に置かれたプラグインを自動的に発見、組み込む仕組みが提供されているケースがほとんどです。例えば特定のディレクトリ (例:アプリケーションインストールディレクトリの下の plugins ディレクトリ) に jar ファイルを置いておくだけで、アプリケーションが起動した際に、そこにある全 jar ファイルが自動的に読み込まれて、その中にある設定ファイルに従ってアプリケーションに組み込まれるわけです。これにより、ユーザは、簡単にアプリケーションに機能追加を行うことが可能となります。

XML を設定ファイルとして用いた場合の問題点

プラグインの設定ファイルとして良く利用されるのが XML ファイルでしょう。XML ファイルは書式が厳格に決まっており、その構造を定義するための DTD やスキーマがあり、また XML を扱うためのパーサが整備されていることから、プログラムで扱うのが比較的容易です。例えば以下は Struts (バージョン 1) にタイルプラグインを設定する例です。

<plug-in className="org.apache.struts.tiles.TilesPlugin" >
  <set-property property="definitionConfigFiles" value="/WEB-INF/tiles-defs.xml" />
  <set-property property="moduleAware" value="true" />
  <set-property property="parserDetails" value="1" />
  <set-property property="parserValidate" value="true" />
</plug-in>

plug-in 要素の className 属性でタイルプラグインのクラス名を指定しています。そしてネストした set-property 要素によって、このプラグインへの設定を与えていることが分かります。特定のドメインでの用途に特化した言語のことを DSL (Domain Specific Language) と呼びますが、この例では Struts の設定という用途に XML を DSL として使用していることになります。しかし、XML は時として冗長になりすぎる嫌いがあり、またプログラムでの扱いにも手がかかる場合があります。

アプリケーションが、このファイルを扱う場合、大きく分けて 2 つのステップが必要になります。まず最初に XML パーサによる読み込み処理が、そしてその後、各値を解釈するステップが必要となります。XML の構造のチェックは、XML をパースする際に発見されます。例えばタグの閉め忘れや、対応間違い、要素、属性名の間違いなどは、このステップで発見されます。XML の構造定義では値について、ある程度の束縛を行うことができます。例えば、上記の例であれば parserDetails 属性には整数しか許さないように制限することが可能です。この場合、値の妥当性は XML パーサで行うことが可能です。しかし、これらの機能にも限界があります。例えば当然ですが、plug-in 要素の className 属性に与えたクラス名が妥当であるかどうかは、XML パーサでチェックすることは不可能です。このため一般には、その後アプリケーション側で値のチェックを行わなければなりません。

ある程度、設定内容が複雑化、巨大化してくると、定数を定義して複数の設定で共有したり、共通の初期化処理をメソッドにまとめて、共用したいといった要求が出てくるでしょう。残念ながら XML は、プログラミング言語ほどうまくは、これらの問題に対処できません。そもそも上記のような設定は、Java のコードに密接に関係しています。それならば、こうした定義は Java 自身で行うのが最もストレートなのではないでしょうか。

プログラミング言語を設定ファイルとして用いる

プログラミング言語を DSL として使用するという動きもあります。以下は Google の Guice という DI コンテナでの設定内容の例で、Java で記述されています。

        bind(TransactionDao.class).to(TransactionDaoImpl.class);
        bind(QueryService.class).to(QueryServiceImpl.class);

DI コンテナは、特定のインターフェースを、特定の実装に結び付けます。上記の例は、TransactionDao インターフェースを TransactionDaoImpl クラスに、QueryService インターフェースを、QueryServiceImpl クラスに結び付けています。

DSL として Java のような静的型付け言語を用いた場合のメリットを見てみましょう。TransactionDao.class のようなクラスの指定は、Java の Class 型であり、もしもスペルミスをしていれば即座にコンパイルエラーで検出されます。これはアプリケーションを実行してみるまでもなく発見可能です。また、IDE (統合開発環境) を使用していれば、

bind(TransactionDao.class).

まで入力しただけで、この後にメソッドとして何が利用可能なのか、そのメソッドの引数は何が指定できるのかが、IDE によって示されます。その他の単純な記述上の間違いもほとんどがコンパイルエラーで発見できるでしょう。しかし Java のソースコードは、どちらかと言えば記述が冗長になる傾向があります。これは Java の文法の設計思想がコードを簡潔に書くことよりも、分かり易さの方を重視しているためです。

DSL にもっと向いた言語があります。それは一般に「軽量言語」と呼ばれるプログラミング言語です。今回のプラグインライブラリのサンプルで使用している Groovy も、そうした言語の 1 つです。Groovy の文法は Java に非常に似通っており、Java のクラスをシームレスに呼び出したり、逆に Java から Groovy のコードを呼び出すこともできます。そして、Java には無い幾つかの言語仕様(クロージャや、演算子オーバーロード、メタクラスなど)を持っており、Java よりも簡潔に記述することが可能です。例えば Java で Map のインスタンスを生成して、内容を設定する場合、以下のように記述する必要がありますが、

Map map = new HashMap();
map.put("key1", "value1");
map.put("key2", "value2");
map.put("key3", "value3");

Groovy なら、以下のように一行書くだけですみます。

def map = [key1:"value1", key2:"value", key3:"value3"]

なお軽量言語の多くは、ソースコードを直接インタプリタ形式で実行するため、Java を DSL に使用した時のような実行前チェックのメリットは無くなってしまいます。しかし Groovy の場合、groovyc コマンドで Java のクラスファイルにコンパイルすることも可能なので、あらかじめコンパイルを行って、文法上の間違いを発見することも可能です。

プログラミング言語を DSL として用いると、XML を使用した時に存在した様々な問題が解決できます。型付けを活用することで、強力な値の妥当性チェックが可能です。定数定義や、メソッドへの括り出し、クラス継承などを用いて柔軟に設定を記述することが可能です。本稿の残りを使って、実際に Groovy を利用して設定ファイルを記述する例を見てみましょう。

今回のプラグインライブラリのアーキテクチャ

今回は、Groovy を DSL に適用した例として、簡単なプラグインライブラリをご紹介したいと思います。今回使用するサンプルのプラグインライブラリとサンプルアプリケーションは、筆者の Web サイトからダウンロード可能です。このプラグインライブラリは、指定されたディレクトリの下に存在するプラグインを自動的に読み込み、アプリケーションから利用可能にします。プラグインは jar ファイルの形式(以後プラグインパッケージと呼びます)で、中に plugins.conf という名前の設定ファイルを含みます (図 1)。

図 1
図 1
図 1

今回登場するコンポーネントとしては以下のようなものが挙げられます (図 2)。

図 2
図 2
図 2
  1. アプリケーション
    プラグインを利用するアプリケーションです。プラグインライブラリを使用することで、アプリケーションに簡単にプラグイン機能を組み込むことができます。
  2. プラグインクラス
    プラグインの機能は、Java のクラスとして提供します。このクラスの最低限の要件は Plugin インターフェースを実装することです。
  3. プラグイン設定ファイル
    プラグインの初期化設定を記述したファイルです。Groovy のスクリプトになっており、Groovy でプラグインの初期化処理を記述することができます。
  4. プラグインレポジトリ
    プラグインを管理するクラスです。読み込まれたプラグインは、プラグインレポジトリに登録されます。アプリケーションは、プラグインレポジトリに問い合わせることで、プラグインを取得できます。
  5. プラグインクラスローダ
    プラグインは、動的にプラグインパッケージを発見して読み込むため、専用のクラスローダを使用して読み込みます。これを利用しプラグインクラスに、特別なセキュリティ制限をかけることも可能でしょう。

プラグインライブラリを利用したアプリケーションの動作を見てみましょう。

  1. アプリケーションは、プラグインレポジトリのインスタンスを生成します。この時、プラグインパッケージが格納されているディレクトリを指定します。
  2. プラグインレポジトリは、指定されたディレクトリの下の jar ファイルを読み込み、プラグイン設定ファイルを含んだものを探します。
  3. 見つかったプラグイン設定ファイルを読み込み、Groovy で実行します。
  4. プラグインクラスは、プラグインクラスローダでロードされます。
  5. プラグイン設定ファイルの中には、プラグインレジトリへの登録処理が記述されているため、これが実行されることでプラグインがプラグインレポジトリに登録されることになります。
  6. アプリケーションは、プラグインレポジトリから、登録されているプラグインの一覧を取得することができます。
  7. アプリケーションは、プラグインレポジトリから得たプラグインを利用します。

次節でサンプルアプリケーションを使用して、具体的な仕組みについて解説することにします。

Hello World アプリケーション

今回用意したサンプルアプリケーションは、"Hello xxx" と表示するだけの単純なものです。配布ファイルには、pluginexample.zip という名前で格納されています。まずプラグイン側から見てみます。Hello メッセージを表示するプラグインのための共通インターフェースとして、HelloPlugin を宣言しています。

public interface HelloPlugin extends Plugin {
    void hello(String name);
}

ここで継承している Plugin インターフェースは、今回のプラグインライブラリで読み込むことのできるプラグインが最低限実装しなければならないインターフェースで、getName() というメソッドだけを持った単純なものです。そして、このインターフェースを実装するクラスとして、MyPlugin を用意しています。

public class MyPlugin implements HelloPlugin {
    final String greeting;

    public MyPlugin(String greeting) {
        if (greeting == null) throw new NullPointerException();
        this.greeting = greeting;
    }

    public void hello(String name) {
        System.out.printf(greeting, name);    // (1)
    }

    public String getName() {
        return "Plugin example.";
    }
}

hello() メソッドを実装し、コンストラクタで受け取った書式化文字列を使用して Hello メッセージを表示していることが分かります。次にアプリケーションの main() メソッドを見てみましょう。

    public static void main(String[] args) throws IOException {
        PluginRepository<HelloPlugin>pluginRepository
            = new PluginRepository<HelloPlugin>(new File("plugins"));  // (1)

        for (HelloPlugin plugin:pluginRepository.plugins()) {  // (2)
            plugin.hello(args[0]);
        }
    }

最初にプラグインレポジトリオブジェクトを生成しています。この時、引数に渡しているのはプラグインパッケージが格納されているディレクトリです (1)。

登録されたプラグインは、プラグインレポジトリの plugins() メソッドを呼び出すことで取り出すことができます (2)。プラグインレポジトリは型パラメータが付加できるようになっており、これはプラグインの型を表します。これにより、そのアプリケーションで利用可能なプラグインの基底型を指定することができます。

それでは、この 2 つを結び付けているライブラリの中身を見てみましょう。PluginRepository は、プラグインディレクトリの中からプラグインを探し出して、その中のプラグイン構成ファイルを Groovy のファイルとして実行します。Groovy のファイルを実行する場合、GroovyShell というクラスを使用しますが、そこで実行するプログラムが使用するクラスを読み込むためのクラスローダを指定する必要があります。このため PluginRepository は最初にクラスローダを作成します。

ClassLoader classLoader = AccessController.doPrivileged
    (new PrivilegedAction<ClassLoader>() {
        public ClassLoader run() {
            try {
                return new PluginClassLoader(getClass().getClassLoader(), pluginDir);
            }
            catch (IOException ex) {
                throw new RuntimeException(ex);
            }
        }
    });

クラスローダの生成にあたっては、createClassLoader パーミッションが必要なので、ここでは AccessController クラスの doPrivileged() メソッドを使用してクラスローダを生成しています。PluginClassLoader は、今回の目的のために作成したもので、指定されたディレクトリの下にある jar ファイルをクラスのロード先として使用するものです。

    PluginClassLoader(ClassLoader parent, File pluginDirectory) throws IOException {
        super (parent);
        if (pluginDirectory == null) throw new NullPointerException();
        this.pluginDirectory = pluginDirectory;
        File[] jarFiles = pluginDirectory.listFiles(new FilenameFilter() {
            public boolean accept(File dir, String name) {
                return name.toLowerCase().endsWith(".jar");
            }
        });
        jars = new JarFile[jarFiles.length];
        for (int i = 0; i < jars.length; ++i) {
            jars[i] = new JarFile(jarFiles[i]);
        }
    }

    protected Class<?> findClass(String name) throws ClassNotFoundException {
        String classFileName = name.replace('.', '/') + ".class";
        for (JarFile jar:jars) {
            JarEntry je = jar.getJarEntry(classFileName);
            if (je != null) {
                byte[] classData = readClassFile(jar, je, classFileName);
                return defineClass(name, classData, 0, classData.length);
            }
        }
        throw new ClassNotFoundException(name);
    }
...

readClassFile() メソッドは、指定されたクラスをバイト列として読み出すだけのメソッドなので、ここでは解説を省略します。

もう一度 PluginRepository クラスに戻りましょう。以下は、PluginRepository の中で実際にプラグインを読み込んでいる箇所です。

    void loadPlugin(ClassLoader classLoader, JarFile jf) throws IOException {
        JarEntry je = jf.getJarEntry(PLUGIN_CONFIG_FILE_NAME);  // (1)
        if (je == null) return;
        Map<String, Object> map = new HashMap<String, Object>();
        map.put("plugins", this);
        Binding binding = new Binding(map);
        GroovyShell shell = new GroovyShell(classLoader, binding); // (2)
        InputStream is = null;
        try {
            is = jf.getInputStream(je);
            shell.evaluate(is, je.getName() + " in " + jf.getName());  // (3)
        }
        finally {
...

引数の jf は、プラグインディレクトリの下に見つかった JAR ファイルです。最初にプラグイン構成ファイルが含まれていることを確認し (1)、もしも存在すれば、それを読み込んで GroovyShell で実行しています (2), (3)。GroovyShell に渡している引数はクラスローダとバインディングです。クラスローダは、GroovyShell が使用するクラスローダを指定します。ここでは、先ほど作成したクラスローダを指定することで、プラグインディレクトリ内の JAR ファイルに含まれているクラスは、何も指定しなくてもプラグインから利用できるようになります。バインディングを利用することで、"plugins" という名前と、プラグインレポジトリオブジェクトとを関連付けます。これにより Groovy のスクリプトでは plugins という名前でプラグインレポジトリにアクセス可能となります。今回利用するプラグイン設定ファイル (plugins.conf) を見てみましょう。

plugins.register(new MyPlugin("Hello %s!"))

MyPlugin に "Hello %s!" という引数を渡して初期化を行い、プラグインレポジトリの register() メソッドを利用して、プラグインを登録しているのが分かります。今回のサンプルには Gant のビルドファイルが含まれているので、Gant を実行すれば、コンパイルが行われて実行結果が表示されます。

shanai@shanai-laptop:~/tools/pluginexample$ gant
   [delete] Deleting directory /home/shanai/tools/pluginexample/plugins
    [mkdir] Created dir: /home/shanai/tools/pluginexample/plugins
      [jar] Building jar: /home/shanai/tools/pluginexample/plugins/myplugin.jar
     [java] Hello Ruimo!

結論

これまでアプリケーションの設定を保持するファイルとして、XML ファイルが良く利用されてきましたが、XML を利用するとプログラムコードに密接に関連する設定内容を記述したい場合に、柔軟性が犠牲になる場合があります。本稿では、アプリケーションにプラグイン機能を組み込む例を用いて、プログラミング言語を DSL として用いてプラグインの設定を行う例を紹介しました。使用する言語としては Java と親和性の高い軽量言語である Groovy を用い、XML を用いた場合と比べた場合の優位点について論じました。本稿が Groovy を DSL として Java アプリケーションから利用する方法の理解の一助となれば幸いです。


ダウンロード可能なリソース


関連トピック

  • [1] 『Patterns of Enterprise Application Architecture』 Martin Fowler 著 (Addison-Wesley、2003 年)を読み、プラグイン設計パターンについて学んでください。
  • [2] JSR 223 で、Java から様々なスクリプト言語を利用できるようにする仕組みについて学んでください。
  • [3] scripting プロジェクトホームで、現時点で Java から利用可能な様々なスクリプティング言語について情報を入手してください。
  • 筆者の Web サイトから今回紹介したプラグインライブラリとサンプルアプリケーションを入手してください。

コメント

コメントを登録するにはサインインあるいは登録してください。

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Java technology, Open source, XML
ArticleID=297135
ArticleTitle=Groovy を DSL として用いてプラグイン機能を実現する
publish-date=03282008