Java9のStackWalkerを試す

Java9のStackWalkerを試してみたメモです。


Java9で追加されたStack Walking APIの実装であるStackWalkerを試してみたメモです。
StackWalkerを使用すると、スタックトレースの情報をフィルタリングしたり遅延アクセスすることが出来ます。
https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/lang/StackWalker.html
https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/lang/StackWalker.StackFrame.html


下記バージョンで試してみます。

getInstance(), getCallerClass()

下記のようにメソッド呼び出しがネストしているケースで試してみます。

public class Main {
    public static void main(String[] args) throws Exception {
        // call via Caller
        Caller.callWhoIsCallingMe("via caller");
    }
}

public class Caller {
    public static void callWhoIsCallingMe(String str) {
        MyService.whoIsCallingMe(str);
    }
}

public class MyService {
    public static void whoIsCallingMe(String str) {
        System.out.println(str);

        StackWalker stackWalker = StackWalker.getInstance(Option.RETAIN_CLASS_REFERENCE);
        System.out.println(stackWalker.getCallerClass());
    }
}

StackWalker.getInstance()でインスタンスを取得します。
引数でStackWalker.Option.RETAIN_CLASS_REFERENCE
を指定することでclassの情報を保持することが出来ます。

StackWalker.getCallerClass()で呼び出し元のClassを取得します。
getCallerClass()を使用するためには、StackWalker.Option.RETAIN_CLASS_REFERENCE が指定されている必要があります。


実行結果。
直前の呼び出し元のCallerが表示されています。

via caller
class com.example.stackwalker.service.Caller


walk()

StackWalker.walk()はStreamに対して指定したFunctionを順に適用していきます。
Streamはwalk()が実行されたポイントから順に呼び出し元を辿ったストリームです。

public class Main {
    public static void main(String[] args) throws Exception {
        // walking
        Caller.callWalking("walking");
    }
}

public class Caller {
    public static void callWalking(String str) {
        MyService.walking(str);
    }
}

public class MyService {
    public static void walking(String str) {
        System.out.println(str);

        StackWalker stackWalker = StackWalker.getInstance(Option.RETAIN_CLASS_REFERENCE);

        List<StackFrame> frames = stackWalker.walk(frame -> frame.collect(Collectors.toList()));
        frames.forEach(f -> System.out.println(f.getDeclaringClass()));
    }
}

実行してみると、呼び出し元のクラスが順に表示されています。

walking
class com.example.stackwalker.service.MyService
class com.example.stackwalker.service.Caller
class com.example.stackwalker.Main


skip

StackWalker.walk()のStreamを取得すれば、自由にストリーム処理で操作出来ます。

例えば、呼び出し先の自クラスの情報が不要な場合、skip(1)して飛ばせます。

public class Main {
    public static void main(String[] args) throws Exception {
        // skip
        Caller.skip("skip");
    }
}

public class Caller {
    public static void skip(String str) {
        MyService.skipItself(str);
    }
}

public class MyService {
    public static void skipItself(String str) {
        System.out.println(str);

        StackWalker stackWalker = StackWalker.getInstance(Option.RETAIN_CLASS_REFERENCE);
        stackWalker.walk(frame -> frame.collect(Collectors.toList()))
                .stream()
                .skip(1)
                .forEach(f -> System.out.println(f.getDeclaringClass()));
    }
}

実行すると呼び出し先のクラスがスキップされました。

skip
class com.example.stackwalker.service.Caller
class com.example.stackwalker.Main


filter

service packageに属するクラスを除外したい場合は、filter()で除外すればよいです。

public class Main {
    public static void main(String[] args) throws Exception {
        // filter
        Caller.filter("filter");
    }
}

public class Caller {
    public static void filter(String str) {
        FooBarService.walk(str);
    }
}

public class FooBarService {
    public static void walk(String str) {
        System.out.println(str);

        StackWalker stackWalker = StackWalker.getInstance(Option.RETAIN_CLASS_REFERENCE);
        stackWalker.walk(frame -> frame.collect(Collectors.toList()))
                .stream()
                .filter(frame -> !frame.getClassName().contains("service"))
                .forEach(f -> System.out.println(f.getDeclaringClass()));
    }
}

実行するとservice packageのクラスは除外されています。

filter
class com.example.stackwalker.foo.bar.FooBarService
class com.example.stackwalker.Main


reflection

デフォルトでは、リフレクションによるメソッド呼び出しは表示されません。
リフレクションも表示するためには、Option.SHOW_REFLECT_FRAMESを指定します。

下記のようなリフレクションを使用したメソッド呼び出しを試してみます。
まずデフォルトの状態で試してみます。

public class Main {
    public static void main(String[] args) throws Exception {
        // not use reflect frame
        Caller.callNotUseShowReflectFrames("not use reflect frame");
    }
}

public class Caller {
    public static void callNotUseShowReflectFrames(String str) throws NoSuchMethodException,
            IllegalAccessException, InstantiationException, InvocationTargetException {
        UseReflection clazz = UseReflection.class.getDeclaredConstructor().newInstance();
        Method method = UseReflection.class.getMethod("notUseShowReflectFrames", String.class);
        method.invoke(clazz, str);
    }
}

public class UseReflection {
    public void notUseShowReflectFrames(String str) {
        System.out.println(str);

        StackWalker stackWalker = StackWalker.getInstance(Option.RETAIN_CLASS_REFERENCE);
        List<StackFrame> frames = stackWalker.walk(frame -> frame.collect(Collectors.toList()));
        frames.forEach(f -> System.out.println(f.getClassName() + "#" + f.getMethodName()));
    }
}

実行すると下記のように、リフレクションのメソッド呼び出しは含まれません。

not use reflect frame
com.example.stackwalker.reflection.UseReflection#notUseShowReflectFrames
com.example.stackwalker.service.Caller#callNotUseShowReflectFrames
com.example.stackwalker.Main#main

今度は下記のように、
Option.SHOW_REFLECT_FRAMESを指定してStackWalkerインスタンスを生成し、実行してみます。

public class Main {
    public static void main(String[] args) throws Exception {
        // use reflect frame
        Caller.callUseShowReflectFrames("use reflect frame");
    }
}

public class Caller {
    public static void callUseShowReflectFrames(String str) throws NoSuchMethodException,
            IllegalAccessException, InstantiationException, InvocationTargetException {
        UseReflection clazz = UseReflection.class.getDeclaredConstructor().newInstance();
        Method method = UseReflection.class.getMethod("useShowReflectFrames", String.class);
        method.invoke(clazz, str);
    }
}

public class UseReflection {
    public void useShowReflectFrames(String str) {
        System.out.println(str);

        StackWalker stackWalker = StackWalker.getInstance(Option.SHOW_REFLECT_FRAMES);
        List<StackFrame> frames = stackWalker.walk(frame -> frame.collect(Collectors.toList()));
        frames.forEach(f -> System.out.println(f.getClassName() + "#" + f.getMethodName()));
    }
}

実行すると下記のように、Method.invoke()などのリフレクションの呼び出しが含まれています。

use reflect frame
com.example.stackwalker.reflection.UseReflection#useShowReflectFrames
jdk.internal.reflect.NativeMethodAccessorImpl#invoke0
jdk.internal.reflect.NativeMethodAccessorImpl#invoke
jdk.internal.reflect.DelegatingMethodAccessorImpl#invoke
java.lang.reflect.Method#invoke
com.example.stackwalker.service.Caller#callUseShowReflectFrames
com.example.stackwalker.Main#main


【参考】
https://alidg.me/blog/2017/9/8/stack-walking-api-java9
https://www.baeldung.com/java-9-stackwalking-api



サンプルコードは下記にあげました。

github.com


おわり。