Search code examples
javaandroidbyte-buddy

ByteBuddy Android - Create and call a method with a certain name that directly calls another method


This is a follow up (though different) question to this thread: Java - Create anonymous Exception subclass with a certain name

I'm trying to send non-crash error reports to Crashlytics on Android and thanks to Rafael Winterhalter I now achieved that they get displayed in different rows in the Crashlytics UI. The requirement for this was to send different Exception subclasses for every issueType.

It looks like in the screenshot below now.

enter image description here The next issue is that it still doesn't really show on one glance what the actual exception-type is. Instead it only shows the function-name that the error was sent from. Having now learnt about the magic of Byte Buddy, I thought - maybe it's even possible to create a static function in the custom Exception subclass, that directly calls the Crashlytics.logException function. I've seen that it's possible to use an interceptor class (and then call code from there), but I guess Crashlytics would then just always show the function name inside this interceptor.

Is it possible to create a static method with name issueType and have it directly call Crashlytics.logException?

Here's my updated code so far:

public static void craslyticsRecordError(String issueType, String errorMsg)
{
    // byte buddy needs a private directory
    File filesDir = GameApplication.getAppContext().getFilesDir();
    String dirPath = filesDir.getAbsolutePath() + File.separator + "ByteBuddy";
    File byteBuddyPrivateDirectory = new File(dirPath);
    if (!byteBuddyPrivateDirectory.exists())
    {
        byteBuddyPrivateDirectory.mkdirs();
    }
    GameLog.d("ByteBuddyDir:" + byteBuddyPrivateDirectory);

    try
    {
        // dynamically create an exception with 'issueType' as its class name and also create method
        // that has this name, so that Crashlytics will show that name
        Class<? extends Exception> dynamicType = new ByteBuddy()
                .subclass(Exception.class)
                .name(issueType)
                .defineMethod(issueType, void.class, Ownership.STATIC, Visibility.PUBLIC)
                .withParameters(Throwable.class)
                .intercept(MethodCall.invoke(Crashlytics.class.getMethod("logException", Throwable.class)))
                .make() // line of crash
                .load(Fabric.class.getClassLoader(), new AndroidClassLoadingStrategy.Wrapping(byteBuddyPrivateDirectory))
                .getLoaded();

        Exception e = dynamicType.getConstructor(String.class).newInstance(errorMsg);

        Method method = Crashlytics.class.getMethod(issueType, Exception.class);
        method.invoke(null, e);
    }
    catch (Exception e1)
    {
        Exception e = new Exception(issueType + "-" + errorMsg);
        Crashlytics.logException(e);
        e1.printStackTrace();
    }
}

Crash Stacktrace:

java.lang.IllegalStateException: public static void com.crashlytics.android.Crashlytics.logException(java.lang.Throwable) does not take 0 arguments
    at net.bytebuddy.implementation.MethodCall$Appender.apply(MethodCall.java:1998)
    at net.bytebuddy.dynamic.scaffold.TypeWriter$MethodPool$Record$ForDefinedMethod$WithBody.applyCode(TypeWriter.java:620)
    at net.bytebuddy.dynamic.scaffold.TypeWriter$MethodPool$Record$ForDefinedMethod$WithBody.applyBody(TypeWriter.java:609)
    at net.bytebuddy.dynamic.scaffold.TypeWriter$MethodPool$Record$ForDefinedMethod.apply(TypeWriter.java:526)
    at net.bytebuddy.dynamic.scaffold.TypeWriter$Default$ForCreation.create(TypeWriter.java:4159)
    at net.bytebuddy.dynamic.scaffold.TypeWriter$Default.make(TypeWriter.java:1633)
    at net.bytebuddy.dynamic.scaffold.subclass.SubclassDynamicTypeBuilder.make(SubclassDynamicTypeBuilder.java:174)
    at net.bytebuddy.dynamic.scaffold.subclass.SubclassDynamicTypeBuilder.make(SubclassDynamicTypeBuilder.java:155)
    at net.bytebuddy.dynamic.DynamicType$Builder$AbstractBase.make(DynamicType.java:2559)
    at net.bytebuddy.dynamic.DynamicType$Builder$AbstractBase$Delegator.make(DynamicType.java:2661)
    at org.utils.Fabric.craslyticsRecordError(Fabric.java:69)
    at android.os.MessageQueue.nativePollOnce(Native Method)
    at android.os.MessageQueue.next(MessageQueue.java:323)
    at android.os.Looper.loop(Looper.java:143)
    at android.app.ActivityThread.main(ActivityThread.java:7224)
    at java.lang.reflect.Method.invoke(Native Method)
    at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1230)
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1120)

Solution

  • Yes, of course this is possible and the documentation even contains an example. You can define the method as follows:

    new ByteBuddy()
      .subclass(Exception.class)
      .defineMethod("issueType", void.class, Ownership.STATIC, Visibility.PUBLIC)
      .withParameters(Exception.class)
      .intercept(MethodCall.invoke(Crashalytics.getMethod("logException"))
                           .withArgument(0));
    

    This would define a method

    public static void issueType(Exception e) {
      Crashalytics.logException(e);
    }
    

    within your generated class.