byte-buddy icon indicating copy to clipboard operation
byte-buddy copied to clipboard

How to do a single proxy sub-class for several methods which have different annotations

Open azewiusz opened this issue 3 years ago • 3 comments

Hi, I looked briefly for similar Issue but could not find match. My problem is that I want to do a proxy/wrapped around JUnit5 BeforeAll and Test annotations.

       AnnotationDescription annotationDescriptionTest = AnnotationDescription.Builder.ofType( Test.class ).build();
       AnnotationDescription annotationDescriptionBeforeAll = AnnotationDescription.Builder.ofType( BeforeAll.class ).build();

        final Class<?> proxyClass = new ByteBuddy()
                .subclass( TestSet.class )
                .modifiers( Modifier.PUBLIC )
                .name( "TestSetProxy1" )
                .method( ElementMatchers.isAnnotatedWith( BeforeAll.class ) )
                .intercept( MethodDelegation.to( InterceptorForBeforeAllAnnotation.class ) )
                .annotateMethod( annotationDescriptionBeforeAll )
                .method( ElementMatchers.isAnnotatedWith( Test.class ) )
                .intercept( MethodDelegation.to( InterceptorForTestAnnotation.class ) )
                .annotateMethod( annotationDescriptionTest )
                .make()
                .load( ClassLoader.getSystemClassLoader(), ClassLoadingStrategy.Default.INJECTION )
                .getLoaded();

        new RunJUnit5TestsFromJava().runOne( proxyClass.getName() + "#test1B()" );

Interceptor for Before All looks like this :

public class InterceptorForBeforeAllAnnotation
{
    @RuntimeType
    public static void intercept(@This Object self,
                                   @Origin Method method,
                                   @AllArguments Object[] args,
                                   @SuperMethod Method superMethod) throws Throwable {


        System.out.println("A beforeAll proxy "+method.getName() + " to method "+superMethod.getName());
        superMethod.invoke(self, args);
    }
}

Interceptor class for Test annotation:

public class InterceptorForTestAnnotation
{
    @RuntimeType
    public static void intercept(@This Object self,
                                   @Origin Method method,
                                   @AllArguments Object[] args,
                                   @SuperMethod Method superMethod) throws Throwable {
        System.out.println("A test proxy "+method.getName() + " to method "+superMethod.getName());
        superMethod.invoke(self, args);
    }
}

For this code and a simple test1B method without parameters and with just system out print I get following call order (exact). I looks that Test.call annotated method get proxy call execution.

BeforeAll from class tests.TestSet
Before Each from tests.TestSet
A test proxy test1B to method test1B$accessor$rgDvkSdV
Test 1B - executing 
After Each from tests.TestSet
AfterAll from class tests.TestSet

The question is why I do not get call from the BeforeAll proxy ?

azewiusz avatar May 21 '22 10:05 azewiusz

The latter interception overrides the former. You can only have one Implementation per method. With method delegation, you can however stack multiple delegations via:

MethodDelegation.to(delegationA).andThen(MethodDelegation.to(delegationB)).andThen(SuperMethodCall.INSTANCE)

This way, the super method call comes at the end implicitly. I assume you do not want to invoke the super method multiple times?

Alternatively, use implementation to implement SuperMethodCall for all relevant methods and then use Advice to also register code that should be executed before (or after) any (implemented) method.

raphw avatar May 22 '22 09:05 raphw

My goal appears to be reachable using ByteBuddy (that is intercepting all call types from JUnit5) in order to enable/disable them at runtime. I added some code here

but briefly I'm using staging (slightly different way than in you example):

 public static Class stagedTypeTransform( Class originalClass, DynamicType.Unloaded transformedClass )
    {
        String rootPathOfClassLoader = originalClass.getClassLoader().getResource( "." ).getPath();
        try
        {
            transformedClass.saveIn( new File( rootPathOfClassLoader ) );
            Class c = originalClass.getClassLoader().loadClass( transformedClass.getTypeDescription().getName() );
            return c;
        }
        catch ( IOException e )
        {
            e.printStackTrace();
        }
        catch ( ClassNotFoundException e )
        {
            e.printStackTrace();
        }
        return null;
    }

and the used like this:

 Class beforeAll = stagedTypeTransform( strippedOffExtendWithAnnotation,
                new ByteBuddy().rebase( strippedOffExtendWithAnnotation,
                                ClassFileLocator.ForClassLoader.of( classLoader ) )
                        .name( testClass.getName() + "BeforeAll" )
                        .method(  ElementMatchers.isAnnotatedWith( BeforeAll.class )  )
                        .intercept( MethodDelegation.withDefaultConfiguration()
                                .to( InterceptorForBeforeAllAnnotation.class ) ).make() );

 Class afterAll = stagedTypeTransform( beforeAll,
                new ByteBuddy().rebase( beforeAll, ClassFileLocator.ForClassLoader.of( classLoader ) )
                        .name( testClass.getName() + "AfterAll" )
                        .method( ElementMatchers.isAnnotatedWith( AfterAll.class ) )
                        .intercept( MethodDelegation.withDefaultConfiguration()
                                .to( InterceptorForAfterAllAnnotation.class ) ).make() );

sorry for this temporary suffixing of class names (I'm still learning how to do this properly)

@raphw - I'm trying to figure out how to process Inner Classes of a Test Class (the JUnit5 Nested annotation). Currently this is failing hard but, you gave already some useful clues here : https://github.com/raphw/byte-buddy/issues/247

azewiusz avatar May 30 '22 15:05 azewiusz

Inner classes are just syntactic sugar. You can intercept them by intercepting by their name that also the reflection API provides.

raphw avatar May 30 '22 17:05 raphw