方法订阅前方面调用结束

huangapple go评论93阅读模式
英文:

Aspect call ending before the method is subscribed

问题

我有一个方法,在其中有反应式代码(RxJava)。

我有一个环绕通知 @Around 包围着它。

设置是正确的,它按以下方式进行打印。

但是它发生在方法甚至被订阅之前。

有没有办法使它的设置仅在方法被订阅后才启动?

我的环绕通知类:

@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;

    }
}

自定义注解:

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

    String name() default "";
}

被注解以使用切面的方法:

@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()));

}

如前所述,切面类中的打印行甚至在订阅之前就被打印出来,这是错误的。

因此,这是打印的当前错误顺序。

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

我期望以下顺序。

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

这种可能吗?

这是我找到的与此相关的最接近的问题。

https://stackoverflow.com/questions/60630848/writing-aspects-for-reactive-pipelines

但是那个问题是基于 Spring 的。而且那个答案中有一句话:

您确实需要让切面意识到异步情况。

听起来这正是我需要做的,但我该如何做呢?感谢任何建议。

--- 在建议后更新 ---

注意,我使用的是 AspectJ 而不是 Spring。

这并不起作用,因为在订阅之前,变量 proceed 为 null。

因此我添加了一个空检查。但我们只会进入一次这里。

因此,proceed.doOnSubscribe() 永远不会发生。

@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"));
    }

    // 这会在订阅之前打印,不符合我的预期。
    System.out.println("inside before");

}

进一步尝试:

至少在理论上,我希望这能起作用。但会抛出 AJC 编译器错误。

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

    // return pjp.proceed(methodArguments); // 如果方法上没有操作,则编译正常

    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()));
}
英文:

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(&quot;@annotation(someLogger) &amp;&amp; execution(* *(..))&quot;)
        public Object getMockedData(ProceedingJoinPoint pjp, SomeLogger someLogger) throws Throwable {
    
            System.out.println(&quot;from aspect start: &quot; + Thread.currentThread().getName());
    
            Object actualResponse = pjp.proceed(methodArguments);
    
            System.out.println(&quot;from aspect close: &quot; + Thread.currentThread().getName());
    
            return actualResponse;
    
        }
    }

The custom annotation

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

    String name() default &quot;&quot;;
}

The method that gets annotated to use aspect.

@SomeLogger(name = &quot;name&quot;)
public Single&lt;Response&gt;  add(Request request) {

    return sample(request)
            .flatMapObservable(resultSet -&gt; Observable.from(resultSet.getRows()))
            .map(row -&gt; Response.builder()
                    .sampleTimestamp(localDateTime(row.getInstant(&quot;sample&quot;)))
                    .build()
            )
            .first()
            .toSingle()
            .doOnSubscribe(() -&gt; System.out.println(&quot;method is subbed: add &quot; + Thread.currentThread().getName()))
            .doOnEach(n -&gt; System.out.println(&quot;method ends and doOnEach: add &quot; + 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.
https://stackoverflow.com/questions/60630848/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(&quot;@annotation(someLogger) &amp;&amp; execution(* *(..))&quot;)
public void before(JoinPoint jp, SomeLogger someLogger) throws Throwable {

    Object[] args = jp.getArgs();

    Single&lt;?&gt; proceed = ((Single&lt;?&gt;) ((ProceedingJoinPoint) jp).proceed(args));

    if(proceed != null) {
        proceed.doOnSubscribe(() -&gt; System.out.println(&quot;doOnSubscribe in before&quot;));
    }

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

}

Further attempt:

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

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

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

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

答案1

得分: 2

问题是什么?

问题并不在于方面(Aspect),而在于您对试图拦截的代码何时以及如何执行的理解。方面(Aspect)完全按照您告诉它的方式运行:它会在注解方法执行之前和之后记录一些内容。到目前为止,一切都很好。

您应该做什么

您希望拦截的是在add方法中注册的异步回调,用于配置稍后执行的行为。如果您想要这样做,您应该使用@Before@After通知来拦截提供给doOnSubscribedoOnEach的代码,具体取决于您希望在何时打印信息(根据您的示例日志,似乎更喜欢在执行后打印)。

A) 如果您使用Spring AOP

为了实现这一点,您不能在Spring AOP中使用lambda,因为Spring AOP只能拦截Spring组件中的代码。因此,您需要将这两个lambda提取为类(从本质上讲,它们只是匿名类),将这些类创建为Spring bean,然后拦截它们的方法。我希望您知道,lambda基本上是实现具有单个方法的接口的匿名类(实际上并不是JVM字节码内部的方式,但有助于简单理解)。因此,如果您从RxJava的ConsumerObserver或其他回调实现的子类中创建单独的Spring组件,就可以通过Spring AOP拦截它们。但这意味着您需要修改代码以适应并促进AOP的使用。您的代码也会变得不那么简洁和表达性强。但这是您提出的问题,我只是在回答。

B) 如果您使用AspectJ

如果您从Spring AOP切换到AspectJ,或者已经在使用AspectJ,您可以尝试直接针对lambda,但这也有些棘手。我需要写一个非常长的答案来解释为什么,您可以阅读这个答案AspectJ问题#471347获取更多信息。

如果您将lambda转换为传统的匿名子类(像IntelliJ IDEA这样的好的IDE应该在您请求时自动帮助您执行两次鼠标点击,将它们转换为传统的匿名子类),然后您可以通过AspectJ拦截它们。但同样需要注意,这将使您的编程风格适应AOP的使用,因为目前AspectJ并没有针对lambda的显式支持。


在问题被更改后进行更新

您大约在1.5年前编辑了问题,大约在我的回答之后的1周内,提供了附加的示例代码片段(但遗憾的是,仍然没有提供MCVE)。然而,对问题或答案进行评论或回答的人不会收到有关问题或答案编辑的通知。如果您想引起他们的注意,您需要在附加评论中通知他们。今天我碰巧又打开了这个问题,是因为我在偶然搜索其他内容。因此,即使我希望您早在很久以前就解决了问题,我还是想出于记录的目的提供一些反馈,因为其他人也可能会偶然遇到这个问题。

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

您在@Before通知中混合使用了ProceedingJoinPoint,而这是仅在@Around通知中使用的JoinPoint子类,您试图将其向下转换并在其上进行proceed()。这注定会失败。这就好比试图将Ellipse实例转换为Circle(一个具有离心率为0的特定椭圆)或将Rectangle转换为Square(一个具有四条相等边的特定矩形)。为了能够使用SquareCircleProceedingJoinPoint,您试图转换的对象实际上需要是这样的一个实例,而这在这里并不是情况。在before-advice中的连接点对象不是proceeding joinpoint。

至于您的第二个示例,它是一个@Around通知,如果您希望更改原始返回值,那么这是可以的。但在这里...

> java &gt; return pjp.proceed(methodArguments) &gt;

您试图使用您之前从未定义的参数进行proceed。就像第一个问题一样,这不仅仅是一个AOP问题,而只是在Java中使用了未定义的变量。当然,它无法编译。实际上,如果您不更改参数,就没有必要这样做。只需在不带参数的情况下调用proceed(),它将使用原始参数。

英文:

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.

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

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...

> java
&gt; return pjp.proceed(methodArguments)
&gt;

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.

huangapple
  • 本文由 发表于 2020年10月5日 04:41:45
  • 转载请务必保留本文链接:https://go.coder-hub.com/64199679.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定