7

I want to intercept some method calls on one of my classes but those classes dont have a default constructor.

Given the following class, how would I setup Byte Buddy to also create a public no-argument constructor to be able to create the generated class?

public class GetLoggedInUsersSaga extends AbstractSpaceSingleEventSaga {
    private final UserSessionRepository userSessionRepository;

    @Inject
    public GetLoggedInUsersSaga(final UserSessionRepository userSessionRepository) {
        this.userSessionRepository = userSessionRepository;
    }

    @StartsSaga
    public void handle(final GetLoggedInUsersRequest request) {
       // this is the method in want to intercept
    }
}

EDIT: The concrete use case for this is to simplify unit test setup.
Currently we always have to write something like this:

@Test
public void someTest() {
   // Given

   // When
   GetLoggedInUsersRequest request = new GetLoggedInUsersRequest();
   setMessageForContext(request); // <-- always do this before calling handle
   sut.handle(request);

   // Then
}

I thought it would be nice to create a proxy in the @Before method which automatically sets up the context for you.

@Before
public void before()  {
    sut = new GetLoggedInUsersSaga(someDependency);
    sut = intercept(sut);
}

@Test
public void someTest() {
   // Given

   // When
   GetLoggedInUsersRequest request = new GetLoggedInUsersRequest();
   sut.handle(request);

   // Then
}

I played around a bit but unfortunately I didnt get it working..

public <SAGA extends Saga> SAGA intercept(final SAGA sagaUnderTest) throws NoSuchMethodException, IllegalAccessException, InstantiationException {
    return (SAGA) new ByteBuddy()
            .subclass(sagaUnderTest.getClass())
            .defineConstructor(Collections.<Class<?>>emptyList(), Visibility.PUBLIC)
            .intercept(MethodCall.invokeSuper())
            .method(ElementMatchers.isAnnotatedWith(StartsSaga.class))
            .intercept(
                    MethodDelegation.to(
                            new Object() {
                                @RuntimeType
                                public Object intercept(
                                        @SuperCall Callable<?> c,
                                        @Origin Method m,
                                        @AllArguments Object[] a) throws Exception {
                                    setMessageForContext((Message) a[0]);
                                    return c.call();
                                }
                            }))
            .make()
            .load(getClass().getClassLoader(), ClassLoadingStrategy.Default.WRAPPER)
            .getLoaded()
            .newInstance();
}

Unfortunately now i get (probably because the ctor invocation is still not correctly setup)

java.lang.IllegalStateException: Cannot invoke public com.frequentis.ps.account.service.audit.GetLoggedInUsersSaga$ByteBuddy$zSZuwhtR() as a super method

Is this even the correct approach?
Should I even use byte buddy here or is there an easier/other way?

Rafael Winterhalter
  • 42,759
  • 13
  • 108
  • 192
leozilla
  • 1,296
  • 2
  • 19
  • 29

1 Answers1

3

You cannot define a constructor without any byte code. This would be an abstract constructor what is illegal in Java. I am going to add a more precise description to the javadoc for a future version. Thanks for bringing this to my attention.

You need to define a super method call which is required for any constructor:

DynamicType.Builder builder = ...
builder = builder
  .defineConstructor(Collections.<Class<?>>emptyList(), Visibility.PUBLIC)
  .intercept(MethodCall
               .invoke(superClass.getDeclaredConstructor())
               .onSuper())

As for wheather you should use Byte Buddy here: I cannot tell you from the little code I saw. The question you should ask: Does it make my code easier, both considering the amount of code and the complexity of following it? If Byte Buddy makes your code easier to use (and to run), use it. If not, don't use it.

Rafael Winterhalter
  • 42,759
  • 13
  • 108
  • 192
  • Thanks for your answer, I edited my question to also show the concrete use case for this. Maybe you have a couple of minutes left to explain what I am doing wrong. – leozilla Jul 08 '15 at 08:31
  • Jikes. Should read my own *javadoc*. You need to specify the constructor to be called and provide arguments if they are required. This way, Byte Buddy does not need to guess. You can also have a look at the `MethodCallTest` for examples. – Rafael Winterhalter Jul 09 '15 at 08:41
  • I don't understand. You have the ability to call the default classloader if the bytebuddy classloaders are not suitable. It seems to me there should be absolutely no reason he can't just write his own class, targeting the same package where he wants to build a new class using those methods, and just make his new class call them. Then he could build a constructor in his new class and do all kinds of fun things. The purpose of a codegen should be flexibility if you need to change a class, but it should not be limited so much it can not do default java thing.s. – Xype Aug 06 '17 at 09:35