Multiple annotation aspects interacting with reactor core chain cause IllegalStateException in method invocation

Viewed 662

I am working on a Spring Web reactive project and I am currently implementing the collection of different metrics for requests. It turns out that having more than one @Around annotations interacting with the Mono return object of my rest-controller method cause Spring to throw an IllegalStateException.

I am using Java 11 with spring boot 2.1.5-RELEASE

I stepped through the spring code and tried to find out what exactly is going wrong, and it seems to me that when calling proceed() on the ProceedingJoinPoint the spring aop logic confuses arguments of different annotations. Specifically the getUserAttribute call in AbstractApsectJAdvice line 684 returns null and seems to be provided with a wrong argument. I also tried changing the order of the aspect methods without success.

The following method is the rest handler in question, the @TimedMono and @HostLanguageRequestCounted annotations both add into the reactor chain

    @Counted("statistics_incoming_requests")
    @TimedMono("statistics_incoming_requestExecutionTime")
    @PostMapping("/translate/{id}/{field}")
    @HostLanguageRequestCounted("statistics_incoming_hostLanguageRequestCount")
    public Mono<TranslationResponseBody> getTranslation(@PathVariable String id,
            @PathVariable String field,
            @RequestBody TranslationRequestBody requestBody) {
        return translateService.translate(id, requestBody.getTargetLanguage(), requestBody.getText(), field);
    }

This is the method handling the TimedMono annotation:

      @Around(value = "@annotation(timed)")
    @Order(2)
    @SuppressWarnings("checkstyle:illegalThrows")
    public Mono<?> timeExecution(ProceedingJoinPoint pjp, TimedMono timed) throws Throwable {
        Long tStart = System.nanoTime();
        Object m = pjp.proceed();
        long tEnd = System.nanoTime();
        if (!(m instanceof Mono)) {
            throw (new Exception("Method must return a mono object"));
        }

        Mono<?> mono = (Mono) m;
        Consumer<Object> stopTimer = obj -> {
            meterRegistry.timer(timed.value())
                    .record(tEnd - tStart, TimeUnit.NANOSECONDS);
        };
        return mono.doOnError(stopTimer).doOnNext(stopTimer);
    }

And the method handling the HostLanguageRequestCounted annotation:

    @Around(value = "@annotation(hostLanguageRequestCounted)")
    @Order(1)
    public Object gatherTenantMetrics(ProceedingJoinPoint pjp,
            HostLanguageRequestCounted hostLanguageRequestCounted) throws Throwable {
        Optional<TranslationRequestBody> body = HostLanguageRequestCountedMetricAspect
                .getArgumentOfType(TranslationRequestBody.class, pjp);
        return Mono.subscriberContext()
                .flatMap(ctx -> {
                    log.info(ctx.toString());
                    return Mono.just(ctx.get("host"));
                }).flatMap(host -> {
                    if (host != null && body.isPresent() && body.get().getTargetLanguage() != null) {
                        String metricKey = hostLanguageRequestCounted.value();
                        metricKey += "_" + host.toString().toLowerCase();
                        metricKey += "_" + body.get().getTargetLanguage().toLowerCase();
                        meterRegistry.counter(metricKey).increment();
                    }
                    try {
                        return (Mono<?>) pjp.proceed();
                    } catch (Throwable t) {
                        return Mono.error(t);
                    }
                });
    }

    private static <T> Optional<T> getArgumentOfType(Class<T> clazz, ProceedingJoinPoint pjp) {
        return Arrays.stream(pjp.getArgs())
                .filter((arg) -> clazz.isAssignableFrom(arg.getClass()))
                .map((obj) -> (T) obj).findFirst();
    }

in which the exception is thrown (on return (Mono) pjp.proceed();)

The exception thrown is the following:

java.lang.IllegalStateException: Required to bind 2 arguments, but only bound 1 (JoinPointMatch was NOT bound in invocation) at org.springframework.aop.aspectj.AbstractAspectJAdvice.argBinding(AbstractAspectJAdvice.java:605) at org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethod(AbstractAspectJAdvice.java:633) at org.springframework.aop.aspectj.AspectJAroundAdvice.invoke(AspectJAroundAdvice.java:70) at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:175) at org.springframework.aop.aspectj.MethodInvocationProceedingJoinPoint.proceed(MethodInvocationProceedingJoinPoint.java:88)

The next entry in the stack trace is the line in my code.

Is this an actual bug in spring or am I doing something wrong ? When I remove one of the annotations, the other one will execute without problems.

0 Answers
Related