Shoeisha Technology Media

CodeZine(コードジン)

記事種別から探す

Oracle JDBCドライバにオブジェクトの自動クローズ処理を追加する

Proxyによる既存クラスへの機能追加とスタブを利用したユニットテスト

  • LINEで送る
  • このエントリーをはてなブックマークに追加
2005/08/26 12:00

java.lang.reflectパッケージのProxyを利用すると、指定したインターフェイスに対して、そのインターフェイスを実装したクラスとインスタンスを動的に作ることができます。本稿では、Proxyによって既存のクラスへ処理を追加する方法、およびそれによって得られる効果について説明します。

はじめに

 ReflectionパッケージのProxyを利用すると、指定したインターフェイスに対して、そのインターフェイスを実装したクラスとインスタンスを動的に作ることができます。

 本稿では、Oracle JDBCドライバの制約をProxyを利用して回避する方法を示すことで、Proxyの具体的な利用方法を説明します。

 Oracle JDBCドライバはJ2SEのAPI規定と異なり、GCによるConnectionStatementの自動クローズを行いません。そのため、これらのJDBCオブジェクトを自動的にクローズする他のJDBC実装用のコードを流用するとリソースリークの原因となります。これを回避するには、アプリケーションがすべてのStatementなどのオブジェクトをクローズするか、またはミドルウェアなどでアプリケーションが作成したすべてのJDBCオブジェクトを保持しておき、なんらかのタイミングでクローズするように実装しなければなりません。

 JDBCのConnectionStatementはインターフェイスですので、Proxyの適用が可能です。このため、簡単なハンドラクラス(Proxyからの呼び出しを処理するクラス)を用意することで、後者の方法を実装できます。この場合、アプリケーションにとってはインターフェイスが透過なため呼び出し時の特別な考慮を必要としません。

 本記事では、上記のシナリオに沿ってJDBCのインターフェイスに対するProxyを生成し、Connectionオブジェクトのクローズ時にそのConnectionオブジェクトが生成した未クローズのJDBCオブジェクトを自動的にクローズする、Oracle JDBCドライバの制約回避処理の実装方法を示します。

対象読者

 本記事は、Javaプログラミングの初級者から中級者を対象に、java.lang.reflect.Proxyの利用方法、Proxyによって既存のクラスへ処理を追加する方法、およびそれによって得られる効果について説明します。

 また、スタブを利用したユニットテストプログラミングについても解説します。

必要な環境

 本記事のソースをビルド/実行するにはJ2SE 5.0を利用してください。また、antのビルドスクリプトを添付しているので、一括してビルドするにはant 1.6以上を用意すると良いでしょう。

 参考までに筆者が利用した本記事のテスト環境は以下のものです。

OS OS X 10.4.2
J2SE 1.5.0_02
Ant 1.6.2
JUnit 3.8.1
IDE NetBeans4.1J

 なお、元々存在するインターフェイスの実装などを行っているため、ビルド時にdeprecatedメッセージなどが出力されますが、実行には問題ありません。

NetBeans 4.1からの利用

 メニューから[ファイル]→[新規プロジェクト]→[プロジェクト―既存のAntスクリプトを使用するJavaプロジェクト]を順に選択し、[次へ]ボタンをクリックします。次に[場所]の[ブラウズ]ボタンをクリックし、ファイルオープンダイアログを表示します。アーカイブを展開したディレクトリの「proxy」ディレクトリを選択し、[開く]ボタン(OS Xの場合は[選択]ボタン)をクリックします。この操作で、[構築スクリプト][プロジェクトフォルダ]が自動的に設定されるため、残る[プロジェクト名]に適当な名前(「proxy」など)を入力し、[次へ]ボタンをクリックしてください。

 一般的なメニュー項目のうち、[プロジェクトをテスト]に「junit」を設定します。[次へ]ボタンをクリックし、[ソースパッケージフォルダ]に「src」、[テストパッケージフォルダ]に「test」をそれぞれ追加し、[次へ]ボタンをクリックして完了します。この時、[ソースレベル]は[JDK 1.5]を選択します(デフォルト)。

 以後、「build.xml」を右クリックすると表示されるコンテキストメニューで、[ターゲット実行]→[その他のターゲット]をクリックすると、「build.xml」に定義されている各種ターゲットをNetBeans上で実行できるようになります。

ファイル構成

 ダウンロードしたファイルはzipで圧縮してあります。展開すると「proxy」というディレクトリを頂点としたディレクトリ階層ができます。すぐに実行できるようにコンパイル済みのクラスファイルも添付してあります。また、ソースファイルはすべてシフトJISでエンコードしています。

proxyディレクトリ

  • 「build.xml」
    antのビルドスクリプトです。junit.homeプロパティの値は「junit.jar」が配置されたディレクトリ名に変更してください。
    以下のターゲットがあります。
    • デフォルト(build):クリーンアップ後に「src」ディレクトリのプログラムをビルド
    • prepare:ビルドに必要なディレクトリを作成
    • clean:ビルドしたクラスファイルを削除
    • compile:srcディレクトリ下のソールファイルをコンパイル
    • junitProxyConnectionTestStubGeneratorTestをビルドして実行

proxy\src\com\example\proxyディレクトリ

  • 「ProxyConnection.java」
    Proxyを利用してConnectionclose呼び出し時に、StatementResultSetclose漏れを検証するユーティリティ。もし未クローズならば代わりにcloseメソッドを呼び出す。

proxy\src\com\example\toolディレクトリ

  • 「StubGenerator.java」
    ユニットテスト用のスタブクラスを生成するユーティリティプログラム。

proxy\test\com\example\proxyディレクトリ

  • 「ProxyConnectionTest.java」
    StatementResultSetcloseテストプログラム。

proxy\test\com\example\toolディレクトリ

  • 「StubGeneratorTest.java」
    多次元配列の型情報の生成テストプログラム。

解説

Proxy

 proxy(プロクシ)とは、「代理人」や「代用品」、あるいは「……の代理」という意味の言葉です。コンピュータの世界だと、HTTPサーバーへのリクエスト/レスポンスの中継を行うプロクシサーバーが一番有名かも知れません。

 java.lang.reflect.Proxyは名前の通り、指定したインターフェイスを持つオブジェクトを偽装することで、ユーザーが指定したハンドラへクライアントからの呼び出しを中継します。

インターフェイス
インターフェイス

 図は、インターフェイスを介したクライアントとオブジェクトの関連を示したものです。コードで示すと、たとえば次のようになります。

インターフェイスを介した呼び出し
public void foo(Map map) {
    map.put("a", "b");
}

 上のリストで、クライアント(fooメソッド)は、Mapインターフェイスを実装したオブジェクトを直接利用しているわけではなく、あくまでもMapインターフェイスを介して利用しています。ただし、インターフェイスはオブジェクトではなく、あくまでもメソッド定義の集合なので実体があるわけではありません。したがって、クライアントのメソッド呼び出しは、実行時には直接オブジェクトに対して行われます。その意味では、インターフェイスは設計時の文字通りインターフェイスの定義と、コンパイル時のスタブの役割しかないと言っても良いでしょう。

 Proxyはインターフェイスに対して実体を提供します。

Proxyの利用
Proxyの利用

 図のようにProxyクラスは、特定のインターフェイスを偽装して(あるいは実行時に動的に実装して)クライアントからの呼び出しをユーザー定義のハンドラオブジェクトへデリゲートします。

 Proxyクラスに特定のインターフェイスを実装したオブジェクトを生成させるには、次のリストのようにProxy.newProxyInstance staticメソッドを利用します。

Proxyオブジェクトの生成
foo((Map)Proxy.newProxyInstance(Map.class.getClassLoader(),
                                new Class[] { Map.class },
                                new InvocationHandler() {
  public Object invoke(Object proxy, Method method, Object[] args) {
    if (method.getName().equals("put")) {
      System.out.println("key=" + args[0] + ", value=" + args[1]);
      return null;
    }
    throw new UnsupportedOperationException("only put can handle");
  }
});

 リストはMapインターフェイスを実装したProxyオブジェクトの生成方法を示したものです。

 Proxy.newProxyInstanceメソッドの引数は

  • Proxyオブジェクトを定義するのに利用するクラスローダ
    通常はこの例のように偽装するインターフェイスのクラスローダを指定すれば良いでしょう。
  • 偽装するインターフェイスの配列
    Proxyは同時に複数のインターフェイスを実装することができます。
  • InvocationHandlerのインスタンス
    クライアントからの呼び出しを受け取るユーザー供与のオブジェクトです。

 の3つです。

 このうち3番目の引数として与えるInvocationHandlerインターフェイス(invocationはメソッドの呼び出しの意味)は、3引数のinvokeメソッドを定義したイベント受信用インターフェイスです。Proxyオブジェクトはクライアントからの呼び出しを受けるとInvocationHandler#invokeメソッドを呼び出します(図)。

Proxyを介した呼び出し
Proxyを介した呼び出し

 InvocationHandler#invokeメソッドの引数は

  • Proxyオブジェクト
    同一ハンドラで複数のProxyオブジェクトの通知を受ける時の識別に利用できます
  • Methodオブジェクト
    クライアントが呼び出したメソッド
  • Object配列
    クライアントがメソッドに与えた引数の配列

 の3つです。また戻り値は、クライアントが呼び出したメソッドの戻り値と、型を一致させる必要があります。もしvoid型メソッドの場合にはnullを返します。この時、戻り値の型が一致しないとClassCastExceptionがスローされます。また、invokeメソッドからスローした例外が呼び出されたメソッドで宣言していないチェック例外の場合にはUndeclaredThrowableExceptionがスローされます。

Proxyの利用用途

 Proxyを利用すると、比較的少ない手数で以下のデザインパターンのインスタンスを生成することができます。

  • decorator パターンの実装
    Proxyオブジェクトからのメソッド呼び出し通知を内包するオブジェクトへデリゲートする前後で、新たな機能を追加します(図を参照)。
  • adapter パターンの実装
    クライアントが利用したいインターフェイスのProxyオブジェクトを生成し、InvocationHandler内でクライアントからの呼び出しを実際のオブジェクトの呼び出しへ変換します。
  • proxy パターンの実装
    Proxyオブジェクトからのメソッド呼び出し通知を内包するオブジェクトへデリゲートする前後で、必要なアクセス制御を行います。
Proxyを利用したデコレータパターン
Proxyを利用したデコレータパターン

 なお、筆者には正直なところ本記事の目的の実装がdecorator パターンなのかproxy パターンなのか、はっきりわかりません。新たな機能を追加しているわけではないのでproxy パターンのインスタンスだと思いますが、プロクシと言うとリモートオブジェクトの転送の受け口のイメージが強いため、decorator パターンのインスタンスのようにも感じるからです。もし、おわかりであればフォーラムの本記事のスレッドにご投稿いただけたらと思います。

 なお、Proxyはデザインパターンの実装以外にも、Mapに対するProxyオブジェクトの例で示したputされた内容を標準出力へ出力するダミーのMapのように、スタブの作成にも利用できるでしょう。

Proxyを利用したOracle JDBC Connectionの作成

 はじめに書いたように、OracleのJDBC実装には、生成したJDBCオブジェクトを明示的にクローズする必要があるという制限があります。そのため、きちんとcloseメソッドを呼ばないとWebアプリケーションのように長期間に渡って実行するアプリケーションではResultSetなどが解放されないまま蓄積されていき、最終的にJVMのヒープが食いつぶされてしまうという問題があります。

 「ProxyConnection.java」は、この制限に対応できるように、Connection#closeで、そのConnectionから返されたStatement、およびそのStatementから返されたResultSetで未クローズのオブジェクトのcloseメソッドを呼び出すクラスです。このクラスはProxyを利用することでクライアントに対してはConnectionとして振る舞います。

ProxyConnection.java
package com.example.proxy;

import java.lang.reflect.*;
import java.sql.*;
import java.util.*;

public class ProxyConnection {
    Connection connection;
    Set<Statement> openedStatements;
    Set<ResultSet> openedResultSets;
    Object proxy;
    ProxyConnection(Connection org) {
        connection = org;
        openedStatements = new HashSet<Statement>();
        openedResultSets = new HashSet<ResultSet>();
        proxy = Proxy.newProxyInstance
            (Connection.class.getClassLoader(),
             new Class[] { Connection.class },
             new ConnectionHandler());
    }
    /**
     * 指定されたコネクションのプロクシを生成する。
     * @param c 生成対象のコネクション
     * @return PreparedStatementとResultSetのclose検証処理を
含んだコネクション
     */
    public static Connection createProxy(Connection c) {
        ProxyConnection pc = new ProxyConnection(c);
        return (Connection)pc.proxy
    }
    Object send(Object target, Method m, Object[] args) 
        throws Throwable {
        try {
            return m.invoke(target, args);
        } catch (InvocationTargetException e) {
            if (e.getCause() != null) {
                throw e.getCause();
            }
            throw e;
        }
    }
    void remove(Object o) {
        if (o instanceof ResultSet) {
            openedResultSets.remove(o);
        } else if (o instanceof Statement) {
            openedStatements.remove(o);
        } else {
            throw new IllegalArgumentException("bad class:" + o);
        }
    }
    void closeAll() {
        for (ResultSet rs : openedResultSets) {
            try {
                rs.close();
            } catch (SQLException e) {
            }
        }
        for (Statement ps : openedStatements) {
            try {
                ps.close();
            } catch (SQLException e) {
            }
        }
    }
    class ConnectionHandler implements InvocationHandler {
        public Object invoke(Object proxy, Method method,
            Object[] args) throws Throwable {
            if (method.getName().equals("close")) {
                closeAll();
            }
            Object o = send(connection, method, args);
            if (o instanceof Statement) {
                openedStatements.add((Statement)o);
                o = new Delegate(o, PreparedStatement.class).proxy;
            }
            return o;
        }
    }
    class Delegate implements InvocationHandler {
        Object proxy;
        Object original;
        Delegate(Object o, Class c) {
            original = o;
            proxy = Proxy.newProxyInstance(c.getClassLoader(),
                                           new Class[] { c }, this);
        }
        public Object invoke(Object proxy, Method method,
            Object[] args) throws Throwable {
            if (method.getName().equals("close")) {
                remove(original);
            }
            Object o = send(original, method, args);
            if (o instanceof ResultSet) {
                openedResultSets.add((ResultSet)o);
                o = new Delegate(o, ResultSet.class).proxy;
            }
            return o;
        }
    }
}
 アーカイブのソースのimportなどの冗長な記述は簡素化してあります。
 

フィールド

 ProxyConnectionは以下のフィールドを持ちます。

  • Connection connection;
    元のConnectionのインスタンスを保持します。
  • Set<Statement> openedStatements;
    Connectionから返されたStatementのうち、未クローズのオブジェクトを保持します。
  • Set<ResultSet> openedResultSets;
    Statementから返されたResultSetのうち、未クローズのオブジェクトを保持します。
  • Object proxy;
    作成したProxyのインスタンスを保持します。

コンストラクタ

 ProxyConnectionは、デコレーションの対象となるConnectionのインスタンスを受け取り、Connectionインターフェイスに対するProxyを生成します。

createProxy

 外部へ公開するstaticメソッドです。ProxyConnectionを利用するプログラムは、たとえば以下のように当メソッドを呼び出すことを想定します。

利用例
Connection connection;
...
try {
    connection = 
        ProxyConnection.createProxy(dataSource.getConnection());
} catch (SQLException e) {
    log("connectionの取得に失敗", e);
}

 これはProxyConnectionクラスそのものを直接クライアントに見せる必要が無いからです。なお、一見するとせっかく生成したProxyConnectionのインスタンスが誰からも参照されないため(クライアントへ返しているのはProxyのインスタンスです)すぐにGCされてしまいそうですが、実際にはインナークラスのConnectionHandlerのインスタンスがthisを参照しているためConnectionHandlerが有効な間はGCされることはありません。また、ConnectionHandlerの寿命はProxyと一致するため、結局、クライアントがConnectionオブジェクト(のプロクシ)の参照を保持している限り有効となります。

 なお、本記事でJDBCオブジェクトのクローズのトリガーをConnection#closeにしている理由は、ここで意図的にProxyConnectionの呼び出しが必要な以上、Connectionを特別扱いするプログラムの存在を前提できるからです。たとえば、Webアプリケーションであれば、DataSourceProxyを作り、getConnectionの呼び出し時にこのメソッドを実行し、リクエストの終了時に作成したConnectioncloseを呼ばせるように実装することが可能です。

send

 内部処理用のメソッドです。Proxyから与えられたMethodオブジェクトと引数を利用して、内包するConnectionStatementなどのオブジェクトへ処理をデリゲートします。

 ここでキャッチしているInvocationTargetExceptionは、Method#invokeの呼び出し先でスローされた例外を保持する例外です。したがって、Connectionオブジェクトなどがスローした例外はMethod#invoke内でInvocationTargetExceptionにラップされることになります。クライアントにProxyを意識させないために、ここでInvocationTargetException#getCauseにより元の例外を取得し再スローしています。リスト上はgetCauseの内容がnullかチェックしていますが、本来nullになることはありません。

 なお、InvocationTargetException以外の例外は、Method#invokeの処理そのものがスローしたものとなります。このような例外としては、たとえばargsが呼び出し対象のメソッドとマッチしないなどが考えられます。しかし、クライアントはConnectionインターフェイスのメソッドを呼び出すソースをコンパイルしたと想定できるため、このような状況はバグ以外にはあり得ません。したがって、ここでは特にキャッチせずにそのままクライアントへスローされるようにしています。

remove

 登録したResultSetStatementオブジェクトのclose呼び出し時に利用します。closeが呼ばれた場合、該当オブジェクトをそれ以上保持する必要はないため、当メソッドで登録解除します。

closeAll

 Connection#closeの呼び出しを受けて、すべての未クローズのStatementおよびResultSetオブジェクトをクローズします。なお、この時点でSQLExceptionをキャッチしても処理できたないため単純に無視しています。意味的にはファイナライザでのクローズと同様の扱いだからです。

ConnectionHandler(InvocationHandler実装)

 Connectionオブジェクトのデコレータとして以下の処理を実行します。

  • close呼び出し時にcloseAllを呼び出して未クローズのリソースを解放する。
  • メソッド呼び出しを元のConnectionオブジェクトへデリゲートする。
  • メソッドの結果がStatementのインスタンス(PreparedStatementなどを含みます)であれば、未クローズオブジェクトとして登録します。また、新たなProxyを生成します。

 なお、StatementPreoparedStatementCallableStatementの区別なくStatementインターフェイスおよびその継承インターフェイスをPreparedStatementインターフェイスとして、Proxyオブジェクトを生成しています。ここは必要に応じて細分化を行うなどしても良いでしょう。たとえば、クライアントが何らかの処理で、PreparedStatementインターフェイスかStatementインターフェイスかによって処理を分岐するような場合には、この実装では正しく処理できないことに注意してください。

Delegate(InvocationHandler実装)

 Connectionオブジェクト専用のConnectionHandlerと異なり、Delegateクラスは、StatementResultSetの両方を扱うハンドラです。なお、ここでは個々のリソース単位に新たなDelegateのインスタンスを割り当てていますが(ConnectionHandler#invoke内)、InvocationHandler#invokeメソッドの第1引数で説明したように、実際には単一のインスタンスで複数のProxyオブジェクトを扱うことなども可能です。しかし、ここでは処理の単純さを重視してオブジェクトごとに作成しています。

 コンストラクタでは、デリゲート先のオブジェクト(元のオブジェクト)をフィールドへ保持し、Proxyオブジェクトを生成しています。ここで生成したProxyオブジェクトは、Delegateのインスタンスの生成と同時にメソッドの戻り値としてそのままクライアントへ返します(各InvocationHandlerDelegateオブジェクトを生成している個所を参照)。

 invokeメソッドでは、ResultSetStatement(および派生したPreparedStatement)に対するデリゲートを実行します。ただし、closeメソッドが呼び出された場合はremoveソッドを呼び出して該当するオブジェクトの登録を解除します。また戻り値の型がResultSetの場合には新規に対応するDelegateオブジェクトを生成しクライアントへはその結果を返します。

別の選択肢――Stubの生成

スタブを利用したユニットテスト

 次のリストは、ProxyConnectionのテストプログラムです。

「ProxyConnectionTest.java」
package com.example.proxy;

import com.example.tool.*;
import java.lang.reflect.*;
import java.sql.*;
import junit.framework.*;

public class ProxyConnectionTest extends TestCase {
    int close;
    protected void setUp() throws Exception {
        close = 0;
    }
    // PreparedStatement, ResultSetがクローズされるか検証
    public void testClose() throws Exception {
        Connection c = new StubConnection() {
            public void close() {
                // 最初のclose呼び出しか確認
                assertEquals(2, close);
                close++;
            }
            public PreparedStatement prepareStatement(String s) {
                return new TestStubPreparedStatement();
            }
        };
        c = ProxyConnection.createProxy(c);
        PreparedStatement ps = c.prepareStatement("select");
        ResultSet rs = ps.executeQuery();
        c.close();
        assertEquals(3, close);
    }
    // PreparedStatement, ResultSetがクローズされている場合に
    // 何も行わないことを検証
    public void testClosed() throws Exception {
        Connection c = new StubConnection() {
            public void close() {
                // 既にclose呼び出し済みか確認
                assertEquals(2, close);
                close++;
            }
            public PreparedStatement prepareStatement(String s) {
                return new TestStubPreparedStatement();
            }
        };
        c = ProxyConnection.createProxy(c);
        PreparedStatement ps = c.prepareStatement("select");
        ResultSet rs = ps.executeQuery();
        rs.close();
        ps.close();
        c.close();
        assertEquals(3, close);
    }
    // 例外が透過に受け取れるか検証
    public void testTransparent() throws Exception {
        Connection c = new StubConnection() {
                public PreparedStatement prepareStatement(String s) 
                    throws SQLException {
                    throw new SQLException("test exception");
                }
            };
        c = ProxyConnection.createProxy(c);
        try {
            c.prepareStatement("select");
            fail("no exception");
        } catch (SQLException e) {
            assertEquals("test exception", e.getMessage());
        }
    }
    class TestStubPreparedStatement extends StubPreparedStatement {
        public void close() {
            close++;
        }
        public ResultSet executeQuery() {
            /*
             * スタブを利用した実装
             return new TestStubResultSet();
            */
            /*
             * Proxyを利用した実装
             */
            return (ResultSet)Proxy.newProxyInstance(
                            ResultSet.class.getClassLoader(),
                            new Class[] { ResultSet.class },
                            new InvocationHandler() {
        public Object invoke(Object proxy, Method method, 
            Object[] args) throws Throwable {
            if (method.getName().equals("close")) {
                close++;
                return null;
            } else if (method.getName().equals("next")) {
                return Boolean.FALSE;
            } else if (method.getName().equals("equals")
                       || method.getName().equals("hashCode")) {
                return method.invoke(this, args);
            }
            throw new UnsupportedOperationException
                ("only close can handle");
        }
                            });
        }
    }
    class TestStubResultSet extends StubResultSet {
        public void close() {
            close++;
        }
    }
    public static void main(String[] args) {
        junit.textui.TestRunner.run(ProxyConnectionTest.class);
    }
}

 JDBCドライバを直接利用してテストをすることは環境の用意の問題があることや、単純に実装するとテストケースごとにコネクションが必要となりテスト実行に時間がかかることから、ロジックのテストについてはスタブやモックを利用したユニットテストプログラムを記述します。ProxyConnectionTestもそのようなスタブを利用したプログラムの例です。

 このプログラムでは、StubConnectionおよびStubPreparedStatementという2つのスタブクラス(いずれもソースは、アーカイブの「proxy\test\com\example\tool」ディレクトリに格納してあります)および、ResultSetについてはProxyオブジェクトを使ったスタブを利用しています(スタブクラスを利用するためのコードも含んでいます)。

 スタブクラスはすべてのメソッドのデフォルト処理(大抵はnull0を返します)を実装しただけのクラスで、Proxyと異なり、継承可能であればインターフェイスだけではなくクラスについても作成可能だという長所があります。スタブクラスを利用したテストの場合、テストの都合に合わせて呼び出しの対象となるメソッドをオーバーライドした無名クラスを、その都度生成することも(StubConnectionの利用方法)、オーバーライドしたメソッドがテスト全体で共用されるのであれば、テストプログラム内でスタブクラスを継承したクラスを定義することも(StubPreparedStatementの利用方法)可能です。

 一方、Proxyオブジェクトを利用する場合(TestStubPreparedStatement#executeQueryメソッドでResultSetへ適用しています)は、InvocationHandler#invokeでテストに必要なメソッドについて処理を実装します。なお、ProxyConnectionTestの例では、HashSetに格納されるため、hashCodeメソッドおよびequalsメソッドについても処理していますが、スタブクラスの場合はスタブクラス自体が実装を持つためそのまま利用可能です。そのため同じくHashSetに格納されるTestStubPreparedStatementではequalsメソッドやhashCodeメソッドを特にオーバーライドせずに利用しています。

 このような点からテスト用途にはProxyクラスの利用は手軽ですが、再利用性を考えるとスタブクラスを用意したほうがより望ましいと言えます。

 また、Proxyよりも直接内包するオブジェクトへデリゲートするフィルタのほうが、処理効率は(呼び出し頻度にも依存しますが)一般的に良好です。このようなフィルタの例として、J2SEにはjava.io.FilterReaderFilterWriterなどが存在します。これらのクラスはコンストラクタで指定したオブジェクトを内包しすべてのメソッドをデリゲートします。プロクシパターンやデコレータパターンをインスタンス化したい場合には、必要なメソッドをオーバーライドして利用することになります。

スタブジェネレータ

 アーカイブに添付したJDBCのスタブクラスは以下のような機械的なプログラムです。

「StubConnection.java」(抜粋)
package com.example.tool;

/**
 * java.sql.Connectionのスタブクラス。
 * @author StubGenerator by arton.
 */
public class StubConnection implements java.sql.Connection {
    public StubConnection() {
    }
    public java.sql.Statement createStatement
         (int a0, int a1, int a2) throws java.sql.SQLException {
        return null;
    }
    public java.sql.Statement createStatement(int a0, int a1)
         throws java.sql.SQLException {
        return null;
    }
    public java.sql.Statement createStatement()
         throws java.sql.SQLException {
        return null;
    }

 このようなプログラムは一度作れば何度でも利用できるとは言え、手で打ち込むのはあまり賢い方法ではありません。

 本記事のおまけとして、「proxy\src\com\example\tool」ディレクトリにreflectionを利用して指定されたインターフェイスや、クラスのスタブを生成するユーティリティを添付してありますので、興味があったら利用してください。アーカイブの「proxy\test\com\example\tool」ディレクトリ下には、当クラスの一部機能のテストプログラムであるStubGeneratorTestを除いて、StubGeneratorによって生成したクラスを格納してあります。

 なお、StubGeneratorについては、アーカイブ内の他のソースと異なり、フェアライセンス日本語訳)を適用します。ただし、生成したソースにまでは効力は及ばないものとします。

まとめ

 Proxyクラスを利用して、decoratorパターンやproxyパターンを比較的簡単に実装することができます。また、テスト用のスタブが必要な機能が少なければProxyクラスを利用してスタブを作ることも可能です。特にメソッドが多数含まれているインターフェイスのごく1部のメソッドのみテストで利用したい場合にはProxyクラスの利用は効果的です。

 本記事のサンプルソースは自由に使ってかまいません(ただし、StubGeneratorについてはフェアライセンスを適用します)。いろいろ試してJavaの学習などに利用してみてください。

 なお、ProxyConnectionおよびその派生物を実際の運用に利用する場合については、無保証であること、筆者および翔泳社は、利用したことによって生じたいかなる問題についても一切の責を追わないことを認めることを利用許諾の条件とします。利用される場合にはProxyConnectionのソースコードについては特にライセンスを定めません(元の著作権者の表示を不要とするライセンスが見つからなかったのが理由です)ので、むしろ改変して利用するようにしてください。

参考資料

  • LINEで送る
  • このエントリーをはてなブックマークに追加

著者プロフィール

  • arton(アートン)

    専門は業界特化型のミドルウェアやフレームワークとそれを利用するアプリケーションの開発。需要に応じてメインフレームクラスから携帯端末までダウンサイジングしたりアップサイジングしたりしながらオブジェクトを連携させていくという変化に富んだ開発者人生を歩んでいる。著書に『Ruby③ オブジェクト指向とはじめ...

おすすめ記事

All contents copyright © 2006-2016 Shoeisha Co., Ltd. All rights reserved. ver.1.5