コールスタックを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しようと思ったら、ネイティブなコードでがんばることになるんでしょうか?
と思ったら、やってる方がいらっしゃいました。(上のコード片のクラス名は、畏れ多くもこちらから拝借)
- 普通のやつらの下を行け: assert_caller() http://0xcc.net/blog/archives/000066.html
なるほど。。。
易しく書いてくださっているので、内容は2割くらい分かるけど(おーい)、こんなコード書けないなぁ。すごい。