2

I am interested in how to use Feign client in AOP. For example:

API:

public interface LoanClient {
    @RequestLine("GET /loans/{loanId}")
    @MeteredRemoteCall("loans")
    Loan getLoan(@Param("loanId") Long loanId);
}

Config:

@Aspect
@Component // Spring Component annotation
public class MetricAspect {

    @Around(value = "@annotation(annotation)", argNames = "joinPoint, annotation")
    public Object meterRemoteCall(ProceedingJoinPoint joinPoint, 
                        MeteredRemoteCall annotation) throws Throwable {
    // do something
  }
}

But I do not know how to "intercept" the api method call. Where did I go wrong?

UPDATE:

My Spring class annotation:

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

    String serviceName();
}
Stephen Rauch
  • 47,830
  • 31
  • 106
  • 135
Anton Arsentyev
  • 371
  • 1
  • 7
  • 14
  • Where does the `@Component` annotation come from? From Spring maybe? If so, do you use Spring AOP or AspectJ? How do you compile the code? And where does `@MeteredRemoteCall` come from? A special library or is it your own annotation? Can you please show the annotation source code? I think I know the answer to your question, but it depends on your answers to my questions. – kriegaex Jan 26 '17 at 10:25
  • @kriegaex yes `@Component` annotation its Spring class, Updated my question with new class – Anton Arsentyev Jan 26 '17 at 12:37

2 Answers2

9

Your situation is somewhat complex because you have several problems:

  • You use Spring AOP, an "AOP lite" framework based on dynamic proxies (JDK proxies for interfaces, CGLIB proxies for classes). It only works for Spring beans/components, but from what I see your LoanClient is not a Spring @Component.
  • Even if it was a Spring component, Feign creates its own JDK dynamic proxies via reflection. They are outside the control of Spring. Probably there is a way to wire them into Spring manually, either programmatically or via XML configuration. But there I cannot help you because I do not use Spring.
  • Spring AOP only supports a subset of AspectJ pointcuts. Specifically, it does not support call() but only execution(). I.e. it only weaves into the place where a method is executed, not the place where it is called.
  • But the execution takes place in a method implementing an interface and annotations on interface methods such as your @MeteredRemoteCall are never inherited by their implementing classes. In fact, method annotations are never inherited in Java, only class-level annotations from class (not interface!) to respective subclass. I.e. even if your annotation class had an @Inherited meta annotation, it would not help for @Target({ElementType.METHOD}), only for @Target({ElementType.TYPE}). Update: Because I have answered this question several times before, I have just documented the problem and also a workaround in Emulate annotation inheritance for interfaces and methods with AspectJ.

So what can you do? The best option would be to use full AspectJ via LTW (load-time weaving) from within your Spring application. This enables you to use a call() pointcut instead of execution() which is implicitly used by Spring AOP. If you use @annotation() pointcuts on methods in AspectJ, it will match both calls and executions, as I will show you in a stand-alone example (no Spring, but the effect is the same as AspectJ with LTW in Spring):

Marker annotation:

package de.scrum_master.app;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

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

Feign client:

This sample client grabs full StackOverflow question pages (HTML source code) as strings.

package de.scrum_master.app;

import feign.Param;
import feign.RequestLine;

public interface StackOverflowClient {
    @RequestLine("GET /questions/{questionId}")
    @MeteredRemoteCall
    String getQuestionPage(@Param("questionId") Long questionId);
}

Driver application:

This application uses the Feign client interface in three different ways for demonstration purposes:

  1. Without Feign, manual instantiation via anonymous subclass
  2. Like #1, but this time with an extra marker annotation added to the implementing method
  3. Canonical usage via Feign
package de.scrum_master.app;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

import feign.Feign;
import feign.codec.StringDecoder;

public class Application {
    public static void main(String[] args) {
        StackOverflowClient soClient;
        long questionId = 41856687L;

        soClient = new StackOverflowClient() {
            @Override
            public String getQuestionPage(Long loanId) {
                return "StackOverflowClient without Feign";
            }
        };
        System.out.println("  " + soClient.getQuestionPage(questionId));

        soClient = new StackOverflowClient() {
            @Override
            @MeteredRemoteCall
            public String getQuestionPage(Long loanId) {
                return "StackOverflowClient without Feign + extra annotation";
            }
        };
        System.out.println("  " + soClient.getQuestionPage(questionId));

        // Create StackOverflowClient via Feign
        String baseUrl = "http://stackoverflow.com";
        soClient = Feign
            .builder()
            .decoder(new StringDecoder())
            .target(StackOverflowClient.class, baseUrl);
        Matcher titleMatcher = Pattern
            .compile("<title>([^<]+)</title>", Pattern.CASE_INSENSITIVE)
            .matcher(soClient.getQuestionPage(questionId));
        titleMatcher.find();
        System.out.println("  " + titleMatcher.group(1));
    }
}

Console log without aspect:

  StackOverflowClient without Feign
  StackOverflowClient without Feign + extra annotation
  java - How to use AOP with Feign calls - Stack Overflow

As you can see, in case #3 it just prints the question title of this very StackOverflow question. ;-) I am using the regex matcher in order to extract it from the HTML code because I did not want to print the full web page.

Aspect:

This is basically your aspect with additional joinpoint logging.

package de.scrum_master.aspect;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;

import de.scrum_master.app.MeteredRemoteCall;

@Aspect
public class MetricAspect {
    @Around(value = "@annotation(annotation)", argNames = "joinPoint, annotation")
    public Object meterRemoteCall(ProceedingJoinPoint joinPoint, MeteredRemoteCall annotation)
        throws Throwable
    {
        System.out.println(joinPoint);
        return joinPoint.proceed();
    }
}

Console log with aspect:

call(String de.scrum_master.app.StackOverflowClient.getQuestionPage(Long))
  StackOverflowClient without Feign
call(String de.scrum_master.app.StackOverflowClient.getQuestionPage(Long))
execution(String de.scrum_master.app.Application.2.getQuestionPage(Long))
  StackOverflowClient without Feign + extra annotation
call(String de.scrum_master.app.StackOverflowClient.getQuestionPage(Long))
  java - How to use AOP with Feign calls - Stack Overflow

As you can see, the following joinpoints get intercepted for each of the three cases:

  1. Only call() because even with manual instantiation the implementing class does not have the interface method's annotation. So execution() cannot be matched.
  2. Both call() and execution() because we manually added the marker annotation to the implementing class.
  3. Only call() because the dynamic proxy created by Feign does not have the interface method's annotation. So execution() cannot be matched.

I hope this helps you understand what happened and why.

Bottom line: Use full AspectJ in order to let your pointcut match against call() joinpoints. Then your problem is solved.

Community
  • 1
  • 1
kriegaex
  • 63,017
  • 15
  • 111
  • 202
  • Thanks for detailed answer! Can you provide dependencies that you use ? aspectjr and aspectweaver ? – Anton Arsentyev Jan 26 '17 at 14:16
  • May be I cant catch calls of Feign method because I use Spring Boot ? – Anton Arsentyev Jan 26 '17 at 14:28
  • For full AspectJ you need to compile your aspect code with the AspectJ compiler (Ajc), either from command-line or via e.g. AspectJ Maven plugin which in turn uses _aspectjtools.jar_ for compilation. For LTW you need _aspectjweaver.jar_ as a Java agent. How to exactly use it from Spring is explained in the link I provided in my answer, I guess. _aspectjrt.jar_ is smaller than _aspectjweaver.jar_ and not needed for LTW because the runtime is already contained as a subset in the weaver jar. The runtime jar is needed for compile-time weaving which is my preferred method to use AspectJ. – kriegaex Jan 26 '17 at 14:34
0

Maybe it is too late but it can be worked in an easier way. Your code was correct just a tiny bug was there. You should use @within instead of @annotation. I mean the correct code is something like this:

@Aspect
@Component // Spring Component annotation
public class MetricAspect {

@Around(value = "@within(package.path.MeteredRemoteCall)", argNames = "joinPoint")
public Object meterRemoteCall(ProceedingJoinPoint joinPoint) throws Throwable {
  // do something
  }
}
Omid
  • 314
  • 1
  • 13