メソッドの前後にAOPのように一律で処理を入れたい

メソッドの前後にAOP(アスペクト指向プログラミング)のように一律で処理を入れたいが、プロジェクトの極少数の箇所のみ必要で、AOPを持ち出すのは大げさなとき、Javaのメソッド参照やラムダ式を使って解決したい。

環境: Java 16

ユースケース例

一律で処理を入れたい例をあげる。

処理の追加

APIを提供してくれる外部サービスがあり、APIの呼び出し方法をSDKで提供してくれているとする。SDKにはリクエスト・レスポンスのロギング処理がないが、自プロジェクトでは外部サービスとの通信はロギングしたい。

SDKを拡張するのは保守性が悪化するので対応したくないので、SDKのAPI呼び出し実行メソッドの前後にロギングを仕込みたい。

APIが一個だけであればいいが、APIが複数あり、複数箇所でロギングのコードを書くのは微妙なので、AOPのように一律で処理ができるようにしたい。ただ外部APIを呼び出す箇所は一クラスに閉じていて、AOPを持ち出すのは大げさになってしまう。

検査例外のラップ

また、このSDKのAPI呼び出し実行メソッドが検査例外を投げるとする。

自プロジェクトでは、検査例外ではなく独自の非検査例外にした方が、既存コードやフレームワーク等の都合がいいとする。

API呼び出しのメソッドごとにtry catchを書いたりするのは嫌なので、try catchする箇所を一つにまとめたい。

メソッド参照・ラムダ式を引数に渡して解決する

まずは実装例を書く。

何かしらの処理を行ってくれるメソッドを定義する。(先ほどのユースケースでいうと、これが外部SDKの提供するクラスとなる)

public class SampleMethodsContainer {

    public String joinA(String x) {
        return "A" + x;
    }

    public String joinB(String x) {
        return "B" + x;
    }

    public String joinManyParams(String x, String y, String z) throws Exception {
        if (new Random().nextInt() % 2 == 0) {
            throw new Exception();
        }
        return x + y + z;
    }
}

この3つのメソッドを呼び出すときに、前後処理を入れたい。ユースケース例の一つ目に対応する前後処理は単純に標準出力にする。ユースケース例の二つ目に対応する前後処理はtry catchと非検査例外でのラップになる。

これをAOPではなくメソッド参照あるいはラムダ式で解決したい。一文で説明すると以下になる。

呼び出したいメソッドをメソッド参照、もしくは() -> object.method()の形式でラムダ式にして、前後処理を施した共通メソッドに渡す。

実装は以下になる。

public class Main {

    public static void main(String[] args) {
        new Test().test();
    }

    public static class Test {

        public void test() {
            var c = new SampleMethodsContainer();
            String r1 = exec(c::joinA, "aaa");
            String r2 = exec(c::joinB, "bbb");

            // 直接newしてもOK
            String r3 = exec(new SampleMethodsContainer()::joinA, null);

            // Function<T, R>以外に自己定義した@FunctionalInterfaceも使えるし、
            // さらにメソッド参照ではなくてラムダ式でも問題ない。
            // 自己定義した@FunctionalInterfaceが使えるので、検査例外と相性の悪いラムダ式の問題も解決できる。
            String r4 = exec(() -> c.joinManyParams("x", "y", "z"));
        }

        private <T, R> R exec(Function<T, R> f, T t) {
            System.out.println("[start] param=" + t);
            R r = f.apply(t);
            System.out.println("[end] param=" + t + ", return=" + r);
            return r;
        }

        private <R> R exec(MyFunction<R> f) {
            try {
                System.out.println("[start]");
                R r = f.exec();
                System.out.println("[end] return=" + r);
                return r;
            } catch (Exception e) {
                System.out.println("error");
                throw new RuntimeException(e);
            }
        }

        @FunctionalInterface
        public interface MyFunction<R> {
            R exec() throws Exception;
        }
    }
}

実行すると以下の通り出力される。

# 例外発生しない場合
[start] param=aaa
[end] param=aaa, return=Aaaa
[start] param=bbb
[end] param=bbb, return=Bbbb
[start] param=null
[end] param=null, return=Anull
[start]
[end] return=xyz

# 例外発生する場合
[start] param=aaa
[end] param=aaa, return=Aaaa
[start] param=bbb
[end] param=bbb, return=Bbbb
[start] param=null
[end] param=null, return=Anull
[start]
error
# 標準エラー出力
Exception in thread "main" java.lang.RuntimeException: java.lang.Exception