LoginSignup
23
18

More than 5 years have passed since last update.

java.lang.reflect.Proxyの使い方(2)

Last updated at Posted at 2014-11-09

前回の内容

前回はjava.lang.reflect.Proxyの使い方を簡単なサンプルコードで示しました。
今回はこんな場面で使えるのではないかという、少し実践的な内容をサンプルコードを使って実際に動かしてみたいと思います。

はじめに

SQLのログ出力をプログラム中に埋め込むことがあると思いますが、私はそのようなログ出力用コードでプログラム全体の流れを乱されるのが嫌いです。しかも、SQLのログなんてものは開発/テスト環境でしか出力しないと思うので、なおさら邪魔だと思うわけです。なので、プロキシを使ってプロダクションコードから追い出してみましょう。

事前準備

以下を参考にローカルのMySQLにテーブルを用意してください。

create_table.sql
CREATE DATEBASE sample;
CREATE TABLE sample_table (
   id    INT  PRIMARY KEY
  ,name  VARCHAR(10)
);

pom.xmlに依存ライブラリを追加してください。

pom.xml
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>5.1.34</version>
</dependency>

サンプルコード

簡単のためにエラー処理などは省略してありますので、適宜補ってください。

まずはエントリポイントを含むクラスです。
検索結果であるResultSetの内容を出力するコードはありますが、Statement実行時のSQLを出力するコードは一切ありません。

Main.java
public class Main {

    /**
     * エントリポイント.
     * @param args
     * @throws SQLException
     */
    public static void main(String[] args) throws SQLException {
        // DB接続時のユーザ名、パスワードを変更してください
        String connectString = "jdbc:mysql://localhost/sample?user=<your user>&password=<your password>";

        // Connectionのプロキシを取得します.
        Connection con = ConnectionProxy.createProxy(DriverManager.getConnection(connectString));

        // Clear table
        Statement st = con.createStatement();
        st.executeUpdate("TRUNCATE TABLE sample_table");
        st.close();

        // Insert
        PreparedStatement pstmInsert = con.prepareStatement("INSERT INTO sample_table (id, name) VALUES (?, ?)");
        pstmInsert.setInt(1, 100);
        pstmInsert.setString(2, "abc");
        pstmInsert.executeUpdate();
        pstmInsert.close();

        // Select and Print
        PreparedStatement pstmSelect = con.prepareStatement("SELECT id, name FROM sample_table WHERE name = ?");
        pstmSelect.setString(1, "abc");

        ResultSet rs = pstmSelect.executeQuery();
        while (rs.next()) {
            int id = rs.getInt("id");
            String name = rs.getString("name");

            System.out.printf("id=%d, name=%s\n", id, name);
        }
        rs.close();
        pstmSelect.close();

        con.close();
    }
}

次にプロキシクラスは以下のようになります。
ポイントはConnectionからStatementやPreparedStatementが生成されたら、そのプロキシを生成して返すことです。そうするとStatement、PreparedStatementの呼び出しをフックして、SQLのログを出力することができるようになります。

ConnectionProxy.java
public class ConnectionProxy {

    private Connection connection;
    private Object proxy;

    private ConnectionProxy(Connection con) {
        this.connection = con;
        this.proxy = Proxy.newProxyInstance(
            Connection.class.getClassLoader(),
            new Class[]{ Connection.class },
            new ConnectionHandler());
    }

    /**
     * 外部からはこのメソッドを通じてプロキシを取得します.
     */
    public static Connection createProxy(Connection con) {
        ConnectionProxy obj = new ConnectionProxy(con);
        return Connection.class.cast(obj.proxy);
    }


    /**
     * Connectionのプロキシ.
     */
    private class ConnectionHandler implements InvocationHandler {

        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            Object obj = method.invoke(connection, args);

            if (obj instanceof PreparedStatement) {
                obj = new Delegate<PreparedStatement>(PreparedStatement.class.cast(obj), PreparedStatement.class).proxy;
            } else if (obj instanceof Statement) {
                obj = new Delegate<Statement>(Statement.class.cast(obj), Statement.class).proxy;
            }

            return obj;
        }
    }

    /**
     * Statement/PreparedStatementのデリゲート.
     *
     */
    private class Delegate<T extends Statement> implements InvocationHandler {

        private T original;
        private Object proxy;

        private Delegate(T original, Class<T> clazz) {
            this.original = original;
            this.proxy = Proxy.newProxyInstance(
                clazz.getClassLoader(),
                new Class[]{ clazz },
                this);
        }

        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            Class<?>[] paramTypes = method.getParameterTypes();

            if ("executeUpdate".equals(method.getName())) {
                if (paramTypes.length > 0) {
                    System.out.println(original + ": " + args[0]);
                } else {
                    System.out.println(original);
                }
            }

            if ("executeQuery".equals(method.getName())) {
                if (paramTypes.length > 0) {
                    System.out.println(original + ": " + args[0]);
                } else {
                    System.out.println(original);
                }
            }

            return method.invoke(original, args);
        }
    }
}

実行結果

com.mysql.jdbc.StatementImpl@a9b68c: TRUNCATE TABLE sample_table
com.mysql.jdbc.JDBC4PreparedStatement@18429d6: INSERT INTO sample_table (id, name) VALUES (100, 'abc')
com.mysql.jdbc.JDBC4PreparedStatement@13d59d4: SELECT id, name FROM sample_table WHERE name = 'abc'
id=100, name=abc

実行されたSQLが出力されました。但し、JDBCドライバによってはSQL文を出力するのにもう少し工夫が必要になる場合があるかもしれません。(今回はPreparedStatementをそのまま出力するだけで表示できましたが・・)
また、開発時のみプロキシを返すように作っておけば、プロダクション環境への影響は全くありません。

補足

もう少し作りこんで、ConnectionやStatement、ResultSetのクローズ忘れを検出する機能、SQLの実行時間を計測する機能を追加してみるのも良いと思います。

23
18
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
23
18