コールスタックをassertする(※ただしクラスローダを越えられない)

先日、メソッドの事前条件/事後条件の話をしていたところ、うちのししょーが「そもそもJavaコントラクトの表現があまりに貧弱」と言っていました。

たとえば、

  • 引数の値の範囲チェックができない
  • メソッドの呼び出し順を保証してくれない

といった話です。自分は気にしたことがなかったのですが、そういわれればそうですね。

コールスタックをassertするコードを、手持ちの方法で書いたらどうなるだろう?とふと思い、書いてみました。

しかし、クラスローダを越える方法は、自分の「手持ちの方法」の範疇外だったので、以下略。。。(そこが大事なのに)

package sample.ac;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

public class AssertCaller {
	static List<String> callers = new ArrayList<>();

	protected static void add(String fullName) {
		callers.add(fullName);
	}

	/**
	 * 呼び出し順をassertする.呼び出す関数に近い順に、「完全修飾クラス名.メソッド名」の形式で名前を与えて使う.
	 * 
	 * @param fullName
	 */
	public static void assertCaller(String... fullName) {
		for (String string : fullName) {
			add(string);
		}
		StackTraceElement[] stackTrace = new Throwable().getStackTrace();
		checkCount(stackTrace);
		checkName(stackTrace);
	}

	protected static void checkName(StackTraceElement[] stackTrace) {
		Iterator<String> iterator = callers.iterator();
		for (int i = 1; i < stackTrace.length && iterator.hasNext(); i++) {
			String expected = iterator.next();
			String actual = stackTrace[i].getClassName() + "."
					+ stackTrace[i].getMethodName();

			if (actual.equals(expected) == false) {
				String message = "[" + i + "] expected: <" + expected
						+ "> but was: <" + actual + ">";
				throw new AssertionError(message);
			}
		}
	}

	protected static void checkCount(StackTraceElement[] stackTrace) {
		if ((stackTrace.length - 1) != callers.size()) {
			String message = "expected: <" + String.valueOf(callers.size())
					+ " caller> but was: <"
					+ String.valueOf(stackTrace.length - 1) + " caller>";
			throw new AssertionError(message);
		}
	}
}

こんな感じで使います。

package sample.ac;

class Bar {
    // このメソッドに至る呼び出し順を検証する
	public void execute(String str) {
		AssertCaller.assertCaller("sample.ac.Bar.execute",
				"sample.ac.Foo.execute", "sample.ac.Main.main");
		System.out.println(str);
	}
}

class Foo {
	Bar bar;
	String str;

	public Foo(String input) {
		bar = new Bar();
		str = input;
	}

	public void execute() {
		bar.execute(str);
	}
}

public class Main {
	public static void main(String[] args) {
		Foo foo = new Foo("Hello, Caller!");
		foo.execute();
	}
}

上のコードだと問題なく実行されますが、assertの中身を変えて試すと、

		AssertCaller.assertCaller("sample.ac.Bar.execute",
				"sample.ac.Hoge.execute", "sample.ac.Main.main");

エラーになる、と。対象のメソッドに至るまでに通ったメソッドが、違っているためです。

Exception in thread "main" java.lang.AssertionError: [2] expected: <sample.ac.Hoge.execute> but was: <sample.ac.Foo.execute>
	at sample.ac.AssertCaller.checkName(AssertCaller.java:38)
	at sample.ac.AssertCaller.assertCaller(AssertCaller.java:25)
	at sample.ac.Bar.execute(Main.java:5)
	at sample.ac.Foo.execute(Main.java:21)
	at sample.ac.Main.main(Main.java:28)

また、呼出しの深度が異なる場合も、

		AssertCaller.assertCaller("sample.ac.Bar.execute",
				"sample.ac.Main.main");

エラーになります。横着して、メッセージが不親切だけれども。

Exception in thread "main" java.lang.AssertionError: expected: <2 caller> but was: <3 caller>
	at sample.ac.AssertCaller.checkCount(AssertCaller.java:48)
	at sample.ac.AssertCaller.assertCaller(AssertCaller.java:24)
	at sample.ac.Bar.execute(Main.java:5)
	at sample.ac.Foo.execute(Main.java:21)
	at sample.ac.Main.main(Main.java:28)

真面目にコールスタックをassertしようと思ったら、ネイティブなコードでがんばることになるんでしょうか?

と思ったら、やってる方がいらっしゃいました。(上のコード片のクラス名は、畏れ多くもこちらから拝借)

なるほど。。。

易しく書いてくださっているので、内容は2割くらい分かるけど(おーい)、こんなコード書けないなぁ。すごい。