Aspect call ending before the method is subscribed

1k views Asked by At

I have a method which has reactive code in it (RxJava).

I have an Aspect @around wrapped around it.

The setup is fine, it does the print out as follows.

But it happens before the method is even subscribed to.

Is there a way we could set it up such that the Aspect is going to only kick off after the method gets subscribed to?

My Aspect class

@Aspect
public class AspectClass {

        @Around("@annotation(someLogger) && execution(* *(..))")
        public Object getMockedData(ProceedingJoinPoint pjp, SomeLogger someLogger) throws Throwable {
    
            System.out.println("from aspect start: " + Thread.currentThread().getName());
    
            Object actualResponse = pjp.proceed(methodArguments);
    
            System.out.println("from aspect close: " + Thread.currentThread().getName());
    
            return actualResponse;
    
        }
    }

The custom annotation

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SomeLogger {

    String name() default "";
}

The method that gets annotated to use aspect.

@SomeLogger(name = "name")
public Single<Response>  add(Request request) {

    return sample(request)
            .flatMapObservable(resultSet -> Observable.from(resultSet.getRows()))
            .map(row -> Response.builder()
                    .sampleTimestamp(localDateTime(row.getInstant("sample")))
                    .build()
            )
            .first()
            .toSingle()
            .doOnSubscribe(() -> System.out.println("method is subbed: add " + Thread.currentThread().getName()))
            .doOnEach(n -> System.out.println("method ends and doOnEach: add " + Thread.currentThread().getName()));

}

As mentioned the print lines in the aspect class gets printed even before subscription which is wrong.

Thus this is the current wrong order for the print.

from aspect start: eventloop-thread-3
from aspect close: eventloop-thread-3
method is subbed: add eventloop-thread-3
method ends and doOnEach: add eventloop-thread-3

I was expecting the following order.

method is subbed: add eventloop-thread-3
from aspect start: eventloop-thread-3
method ends and doOnEach: add eventloop-thread-3
from aspect close: eventloop-thread-3

Is this possible?

This is the closest question I found with regards to this. Writing Aspects For Reactive Pipelines

But that question is based off Spring. Also there is a line in that answer:

You do need to make the aspect aware of the asynchronous situation.

Sounds like this is what I need to do, but how can I do it? Thanks for any advice.

--- UPDATE after suggestion ---

To note, am using AspectJ not Spring.

This doesn't work cos variable proceed is null before subscription.

Thus I added a null check. But we are only going to enter here once.

Thus proceed.doOnSubscribe() never happens.

@Before("@annotation(someLogger) && execution(* *(..))")
public void before(JoinPoint jp, SomeLogger someLogger) throws Throwable {

    Object[] args = jp.getArgs();

    Single<?> proceed = ((Single<?>) ((ProceedingJoinPoint) jp).proceed(args));

    if(proceed != null) {
        proceed.doOnSubscribe(() -> System.out.println("doOnSubscribe in before"));
    }

    // this will print before a subscription
    // as expected before which is not what I want. 
    System.out.println("inside before");

}

Further attempt:

At least in theory was expecting this to work. But throws AJC Compiler errors.

@Around("@annotation(someLogger) && execution(* *(..))")
    public Object around(ProceedingJoinPoint pjp, SomeLogger someLogger) {

        // return pjp.proceed(methodArguments); // compiles fine if no operations on method 

        return pjp.proceed(methodArguments)
        .doOnSubscribe(() -> System.out.println("method is subbed: add " + Thread.currentThread().getName()))
        .doOnEach(n -> System.out.println("method ends and doOnEach: add " + Thread.currentThread().getName()));
}
1

There are 1 answers

2
kriegaex On

What is the problem?

The problem is not the aspect, it is your understanding of how and when the code you are trying to intercept is getting executed. The aspect does exactly what you tell it to: It logs something before and after the annotated method is executed. So far, so good.

What you should to do instead

What you want to intercept are the asynchronous callbacks you register in the add method configuring the behaviour for later execution. If you want to do that, you should rather intercept the code provided to doOnSubscribe and doOnEach with @Before or @After advices, depending on when you want the information printed (in your example log you seem to prefer after).

A) If you use Spring AOP

In order to do that, you cannot use lambdas in combination with Spring AOP because Spring AOP can only intercept code in Spring components. So you would have to extract both lambdas into classes (which essentially they are, just anonymous ones), make those classes Spring beans and then intercept their methods. I hope you know that a lambda is basically an anonymous class implementing an interface with a single method (not really internally in JVM byte code, but for simple understanding). So if you create separate Spring components from RxJava subclasses of Consumer, Observer or whatever your callback implements, you can intercept them via Spring AOP. But that would mean to modify your code so as to accommodate to and facilitate AOP usage. Your code would also get less terse and expressive. But you asked the question, I am just answering it.

B) If you use AspectJ

If you switch to from Spring AOP to AspectJ or have been using AspectJ already, you can try to directly target the lambdas, but this is also tricky. I would have to write a very long answer to explain why, you you can read this answer and AspectJ issue #471347 for more information.

It gets easier if you convert the lambdas to classical anonymous subclasses (a good IDE like IntelliJ IDEA should help you do that automatically for you with two mouse clicks when you ask it to), which then you can intercept via AspectJ. But again, you are then catering your programming style to AOP usage because at the moment AspectJ does not have explicit support for lambdas.


Update after question was changed

You edited your question ~1.5 years ago, about 1 week after my answer, providing additional sample code snippets (but sadly, still no MCVE. However, people who commented on or answered questions do not get notifications about question or answer edits. If you want their attention, you need to notify them in an additional comment. I just happened to open this question again today by chance, searching for something else. So even though I hope that you have solved your problem long ago, let me provide some feedback for the record, as other people might also stumble upon this question.

@Before("...")
public void before(JoinPoint jp, SomeLogger someLogger) throws Throwable {
    Object[] args = jp.getArgs();
    Single<?> proceed = ((Single<?>) ((ProceedingJoinPoint) jp).proceed(args));
    // ...
}

You are mixing a @Before advice with ProceedingJoinPoint, which is a JoinPoint subclass used only in @Around advice, by trying to down-cast and proceed() on it. This is doomed to fail. It is the same as trying to cast an instance of Ellipse to Circle (a specific ellipse with excentricity 0) or Rectangle to Square (a specific rectangle with four equal sides). In order to be able to use a Square, Circle or ProceedingJoinPoint, the object your are trying to cast actually needs to be one such instance, which is not the case here. The joinpoint object in a before-advice is not a proceeding joinpoint.

As for your second example, it is an @Around advice, which is fine if you wish to change the original return value. But here...

    return pjp.proceed(methodArguments)

You are trying to proceed with arguments you never defined via pjp.getArgs() before. Just like the first problem, this one is not even an AOP problem but simply using an undefined variable in Java. Of course, it cannot compile. Actually, if you do not change the arguments, there is no need to do that. Simply call proceed() without arguments, and it will use the original ones.