虎の穴開発室ブログ

虎の穴ラボ株式会社所属のエンジニアが書く技術ブログです

MENU

エクセルデータのマッピングにJavaの動的プロキシを使ってみる

こんにちは。虎の穴ラボのS.Aです。

今回はJavaリフレクション周りのマイナーな機能「動的プロキシ(Dynamic Proxy)」を触ってみます。 動的プロキシとは、あるインターフェースの実装クラスを実行時に生成する機能で、Javaの標準ライブラリの java.lang.reflect.Proxy クラスの newProxyInstance メソッドを用いることで簡単に利用できます。

ただし、動的プロキシが必要になる場面はライブラリやフレームワーク開発などごく一部に限られるため、 フレームワークの上でWebアプリケーションをつくることを主にしていると、あまり表立って使わない機能かと思われます。 一方で、Javaリフレクションではこんなこともできるんだ...ということを知っておくと、フレームワークを利用する際などに、内部でどのような処理が動いているか想像する助けになるかもしれません。

実際に試してみる

動的プロキシを試すにあたって、何か適当なインターフェースを用意します。

今回は、戻り値のない greet (挨拶)メソッドが定義された Greeter (挨拶する人)インターフェースを作成しました。

interface Greeter{

    void greet();
}

動的プロキシではなく通常どおりこのインターフェースを利用するとしたら、このインターフェースを実装(implements)したクラスを用意する必要があります。

class HelloGreeter implements Greeter{

    @Override
    public void greet(){
        System.out.println("hello!")
    }
}
Greeter g = new HelloGreeter();
g.greet(); //hello!と出力される

一方で、動的プロキシを利用すれば上記のように実装クラスをソースコード上で用意せずともインスタンスを生成できます。

Greeter g = (Greeter) Proxy.newProxyInstance(
                Greeter.class.getClassLoader(),
                new Class[]{Greeter.class},
                (proxy, method, args)->{
                    if(method.equals(Greeter.class.getMethod("greet")){
                        System.out.println("hello!");
                        return null;
                    }
                    return method.invoke(proxy, args);
                });
g.greet();  //hello! と出力される

上のコードでは newProxyInstance メソッドを用いることで動的に Greeter インターフェースの実装クラスを生成しています。 newProxyInstance では、第二引数でクラス型の配列としてインターフェースの型を指定することで、そのインターフェースのプロキシを生成してくれます。

プロキシの実際の振る舞いは、第三引数のラムダ式(InvocationHandler インターフェース)で指定できます。 プロキシのどのメソッドが呼ばれてもこのラムダ式が呼ばれるので、 上のコードでは、呼ばれたメソッドがgreetであれば hello! と出力する といった条件文をラムダ式の中に書いています。 それ以外のメソッド(例えば equals メソッド)であれば、大元のプロキシインスタンスに処理を移譲するといった指定になっています。

動的プロキシのスゴい点

Java ではオブジェクトを単純にキャストしただけでは実行時に ClassCastException 例外が発生してしまいますが、 newProxyInstance を使うとキャスト可能で、実際にそのインターフェースとして振る舞うことができるという点が驚きでした。

Greeter g = (Greeter) new Object(); //実行時に ClassCastException

また、先ほども述べた通り、ソースコードとして存在しないクラスのインスタンスを生成できるという点も驚きです。

ちなみに newProxyInstance で生成したインスタンスについてクラス名を getClass() で取得して出力したところ $Proxy0 といったクラス名(ソースコードに存在しないクラス名!)になっていました。

より実践的な例を考える

先ほどの例では、わざわざ動的プロキシを利用しなくとも、HelloGreeter などの実装クラスを用意すれば済むだけでしたが、 ジェネリクスを使って汎用性のある形に落とし込めば、複数の実装クラスを用意する手間が省けるという点で実用性が生まれるかもしれません。

また、動的プロキシは InvocationHandler で呼び出される処理を一元的に記述することになるので、 インターフェースに定義される複数のメソッドが全て似たような処理を持つパターンで使えそうな気がします。

エクセルを読み込むクラスに動的プロキシを使ってみる

ひとまず、エクセルファイルを読み込む共通クラス ExcelReader というものを考えてみました。 例えば以下のようなユーザの名前、年齢、出身地がまとめられたシートがあるとします。

A B C
名前 年齢 出身地
あああ 25 東京都
いいい 30 石川県
ううう 35 福岡県

この各行のユーザーデータを以下のようにJava上でUser型として利用したいとします。

//ExcelReader から何らかの形で user インスタンスを取得
System.out.println("名前: " + user.getName());
System.out.println("年齢: " + user.getAge());
System.out.println("出身地: " + user.getBirthplace());

これを実現するには、ユーザの各データのフィールドを持ったDTOクラスを作り、 エクセルファイルから順に値をDTOクラスにsetしていく方法が最もシンプルですが、 せっかくなので動的プロキシを使い、getterを記述したインターフェースだけ用意すれば済むようにしてみます。

インターフェースのイメージは以下の通りです。 getterメソッドがどのカラムに対応しているか、記述できるように ExcelColumn という独自のアノテーションを作って、 名前はA列、年齢はB列、出身地はC列...といった形で記述しています。

interface User {

    @ExcelColumn("A")
    String getName(); //名前を取得

    @ExcelColumn("B")
    int getAge(); //年齢を取得

    @ExcelColumn("C")
    String getBirthPlace(); //出身地を取得
}

以下がエクセルファイルを読み込む ExcelReaderクラスです。(org.apache.poiライブラリを使用しています。) コンストラクタで、エクセルファイルのパスと各行に対応するインターフェース(今回はUser.class)を指定し、 getRowを呼び出すことで、任意の行番号の行のインスタンス(今回はUserの実装クラス)を取得できます。

public class ExcelReader <T> implements Closeable {
    private final XSSFWorkbook book;
    private final XSSFSheet sheet;
    private final Class<T> rowInterface;

    //コンストラクタ
    public ExcelReader(Class<T> rowInterface, String filePath) throws IOException {
        this.rowInterface = rowInterface;
        this.book = new XSSFWorkbook(new FileInputStream(filePath));
        this.sheet = book.getSheetAt(0); //今回は1シートのみを想定
    }

    //指定された番号の行のインスタンスを取得する
    public T getRow(int rowNumber){
        @SuppressWarnings("unchecked")
        //プロキシインスタンスを生成
        T instance = (T) Proxy.newProxyInstance(getClass().getClassLoader(), new Class[]{rowInterface}, ((proxy, method, args) -> {
            if (method.getDeclaringClass()==rowInterface){
                //アノテーションを取得
                ExcelColumn annotation = method.getAnnotation(ExcelColumn.class);
                if (annotation==null){
                    return null;
                }
                String column = annotation.value();
                //セルを取得
                XSSFCell cell = sheet.getRow(rowNumber).getCell(CellReference.convertColStringToIndex(column));
                if (cell==null){
                    return null;
                }
                //戻り値が文字列であれば文字列、整数であれば整数を返す(今回はStringとintのみ対応)
                if (method.getReturnType()==String.class) {
                    return cell.getStringCellValue();
                }else if (method.getReturnType()==int.class){
                    return (int)cell.getNumericCellValue();
                }
                return null;
            }
            return method.invoke(proxy, args);
        }));
        return instance;
    }

    @Override
    public void close() throws IOException {
        book.close();
    }
}

上のコードでは getRow の部分で、動的プロキシによるインスタンス生成を行っています。 プロキシインスタンスはメソッドが呼び出されると、そのメソッドのアノテーションで指定した列の値を返すような動きをします。 例えば、getName を呼び出すと @ExcelColumn("A") とアノテーションが指定されているので、A列の名前が取得される形になります。

呼び出し側はこのような形になります。

try(ExcelReader<User> reader = new ExcelReader<>(User.class, filePath)){
    User user = reader.getRow(1); //エクセルシートの1行目をUser型として取得
    System.out.println(user.getName() + "さん"); //あああさん と出力される
    System.out.println(user.getAge() + "歳"); //25歳 と出力される
    System.out.println(user.getBirthPlace() + "出身"); //東京都出身 と出力される
}
振り返り

今回書いてみた ExcelReader ではジェネリクスを使っているので、 例えば次のような別のシートがあったとしても使い回しができます。

A B C
商品名 単価 在庫
XXX 800 50
YYY 1200 40
ZZZ 1600 30

この場合、以下のようなインターフェースを作れば良いです。

interface Item{

    @ExcelCoulmn("A")
    String getName(); //商品名を取得

    @ExcelCoulmn("B")
    int getPrice(); //単価を取得

    @ExcelCoulmn("C")
    int getStock(); //在庫を取得
}

このようにインターフェースだけの記述であれば、フィールド/getter/setter のあるDTOを作るよりシンプルになりました。 一方で、コード記述量を減らすために、わざわざ動的プロキシを用いた奇抜な書き方をするかと言われるとやっぱり使わないかな...という印象です。

シリアライズやプロキシパターンなどの場面でもしかしたらより有用なケースがあるかもしれませんが、今回はこれらの実用例は想定しきれませんでした。 ただ、冒頭でも述べた通り、リフレクションでこんなこともできるということが頭の隅にあれば、フレームワークの仕組みの理解などに役立ちそうです。

みなさんもぜひ試してみてください!

(あと、動的プロキシを利用することが最適であるユースケースを見つけたらぜひ教えてください!)

次回はT.Hさんの「kotlinからWP REST APIを利用してWordPressに自動投稿してみる」です。お楽しみに。

P.S.

採用

虎の穴では一緒に働く仲間を募集中です!
この記事を読んで、興味を持っていただけた方はぜひ弊社の採用情報をご覧下さい。
yumenosora.co.jp

LINEスタンプ

エンジニア専用のメイドちゃんスタンプが完成しました!
「あの場面」で思わず使いたくなるようなスタンプから、日常で役立つスタンプを合計40個用意しました。
エンジニアの皆さん、エンジニアでない方もぜひスタンプを確認してみてください。 store.line.me