Annotation AOP intercepts on interfaces are not scenario compatible

In Java development, interface oriented programming is probably the norm, and aspect is a basic skill that most people use more or less when using Spring. When these two things come together, something interesting happens: adding annotations to the interface method, the annotation-oriented section interception, doesn’t work

It’s kind of weird, you know, when you first get this question, it’s hard to believe; Transaction annotations are also mostly written on interfaces, and don’t seem to have encountered this problem.

So let’s take a closer look. What’s going on here

I. Scene reenactment

This scenario is relatively simple, one interface, one implementation class; One note, one cut

1. Project environment

Use SpringBoot 2.2.1.RELEASE + IDEA + Maven for development

Adding AOP dependencies

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>
Copy the code

2. The emersion of case

Declare a note

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AnoDot {
}
Copy the code

AOP implements a logging plug-in (Application)

@Aspect
@Component
public class LogAspect {
    private static final String SPLIT_SYMBOL = "|";


    @Pointcut("execution(public * com.git.hui.boot.aop.demo.*.*(..) ) || @annotation(AnoDot)")
    public void pointcut(a) {}@Around(value = "pointcut()")
    public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        Object res = null;
        String req = null;
        long start = System.currentTimeMillis();
        try {
            req = buildReqLog(proceedingJoinPoint);
            res = proceedingJoinPoint.proceed();
            return res;
        } catch (Throwable e) {
            res = "Un-Expect-Error";
            throw e;
        } finally {
            long end = System.currentTimeMillis();
            System.out.println(req + ""+ JSON.toJSONString(res) + SPLIT_SYMBOL + (end - start)); }}private String buildReqLog(ProceedingJoinPoint joinPoint) {
        // Target object
        Object target = joinPoint.getTarget();
        // The method of execution
        Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
        // Request parameters
        Object[] args = joinPoint.getArgs();

        StringBuilder builder = new StringBuilder(target.getClass().getName());
        builder.append(SPLIT_SYMBOL).append(method.getName()).append(SPLIT_SYMBOL);
        for (Object arg : args) {
            builder.append(JSON.toJSONString(arg)).append(",");
        }
        return builder.substring(0, builder.length() - 1) + SPLIT_SYMBOL; }}Copy the code

Then define an interface and implementation class. Note the following two methods, one annotation on the interface and one annotation on the implementation class

public interface BaseApi {
    @AnoDot
    String print(String obj);

    String print2(String obj);
}

@Component
public class BaseApiImpl implements BaseApi {
    @Override
    public String print(String obj) {
        System.out.println("ano in interface:" + obj);
        return "return:" + obj;
    }

    @AnoDot
    @Override
    public String print2(String obj) {
        System.out.println("ano in impl:" + obj);
        return "return:"+ obj; }}Copy the code

The test case

@SpringBootApplication
public class Application {

    public Application(BaseApi baseApi) {
        System.out.println(baseApi.print("hello world"));
        System.out.println("-- -- -- -- -- -- -- -- -- -- -");
        System.out.println(baseApi.print2("hello world"));
    }

    public static void main(String[] args) { SpringApplication.run(Application.class); }}Copy the code

After execution, the output result is as follows (there is a picture and the truth, don’t say I lie to you 🙃)

3. Transaction annotation test

If this doesn’t work, will the transaction annotations we usually write on interfaces work?

Add a dependency for mysql operations

<dependencies>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-jdbc</artifactId>
    </dependency>
</dependencies>
Copy the code

Database configuration application.properties

## DataSource
spring.datasource.url=JDBC: mysql: / / 127.0.0.1:3306 / story? useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=GMT%2b8
spring.datasource.username=root
spring.datasource.password=
Copy the code

Next comes our interface definition and implementation

public interface TransApi {
    @Transactional(rollbackFor = Exception.class)
    boolean update(int id);
}

@Service
public class TransApiImpl implements TransApi {
    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Override
    public boolean update(int id) {
        String sql = "replace into money (id, name, money) values (" + id + ", 'Transaction Test ', 200)";
        jdbcTemplate.execute(sql);

        Object ans = jdbcTemplate.queryForMap("select * from money where id = 111");
        System.out.println(ans);

        throw new RuntimeException("Transaction Rollback"); }}Copy the code

Note that in the update method above, the transaction annotation is on the interface, and we need to confirm whether the call will be rolled back

@SpringBootApplication
public class Application {
    public Application(TransApiImpl transApi, JdbcTemplate jdbcTemplate) {
        try {
            transApi.update(111);
        } catch (Exception e) {
            System.out.println(e.getMessage());
        }

        System.out.println(jdbcTemplate.queryForList("select * from money where id=111"));
    }

    public static void main(String[] args) { SpringApplication.run(Application.class); }}Copy the code

Rolled back, have you!!

Sure enough, there was no problem. It scared me into a cold sweat. If there was a problem, then… (Dare not think dare not think)

So the question is, why doesn’t the first method work??

II. Interface annotation section interception implementation

Pressing the desire to find out for a moment, let’s see what we can do if we want to intercept annotations on interfaces.

Since the interception is not on, mostly because the subclass does not inherit the annotation of the parent class, so in the pointcut match, can not match; In that case, let’s have it look at the parent class when it matches and see if it has a corresponding annotation

1. Customize the Pointcut

Though is the custom, but also did not ask us to implement this interface directly, we choose StaticMethodMatcherPointcut to completion logic

import org.springframework.core.annotation.AnnotatedElementUtils;

public static class LogPointCut extends StaticMethodMatcherPointcut {

    @SneakyThrows
    @Override
    public boolean matches(Method method, Class
        aClass) {
        // Use the Spring toolkit directly to get annotations on method.
        returnAnnotatedElementUtils.hasAnnotation(method, AnoDot.class); }}Copy the code

Next we implement the aspect logic declaratively

2. Customize Advice

This advice is the section logic we need to execute, similar to the log output above, except that the parameters are different

Custom advice implements a MethodInterceptor interface, the top level of which is advice

public static class LogAdvice implements MethodInterceptor {
    private static final String SPLIT_SYMBOL = "|";

    @Override
    public Object invoke(MethodInvocation methodInvocation) throws Throwable {
        Object res = null;
        String req = null;
        long start = System.currentTimeMillis();
        try {
            req = buildReqLog(methodInvocation);
            res = methodInvocation.proceed();
            return res;
        } catch (Throwable e) {
            res = "Un-Expect-Error";
            throw e;
        } finally {
            long end = System.currentTimeMillis();
            System.out.println("ExtendLogAspect:" + req + ""+ JSON.toJSONString(res) + SPLIT_SYMBOL + (end - start)); }}private String buildReqLog(MethodInvocation joinPoint) {
        // Target object
        Object target = joinPoint.getThis();
        // The method of execution
        Method method = joinPoint.getMethod();
        // Request parameters
        Object[] args = joinPoint.getArguments();

        StringBuilder builder = new StringBuilder(target.getClass().getName());
        builder.append(SPLIT_SYMBOL).append(method.getName()).append(SPLIT_SYMBOL);
        for (Object arg : args) {
            builder.append(JSON.toJSONString(arg)).append(",");
        }
        return builder.substring(0, builder.length() - 1) + SPLIT_SYMBOL; }}Copy the code

3. Customize the Advisor

Integrate the custom pointcut above with the advice to implement our cut

public static class LogAdvisor extends AbstractBeanFactoryPointcutAdvisor {
    @Setter
    private Pointcut logPointCut;

    @Override
    public Pointcut getPointcut(a) {
        returnlogPointCut; }}Copy the code

4. Finally register the section

To register is to declare it as a bean and throw it into the Spring container

@Bean
public LogAdvisor init(a) {
    LogAdvisor logAdvisor = new LogAdvisor();
    // Customize the implementation posture
    logAdvisor.setLogPointCut(new LogPointCut());
    logAdvisor.setAdvice(new LogAdvice());
    return logAdvisor;
}
Copy the code

Then execute the above test case again, with the following output

The comments on the interface are also blocked, but the last output time is a bit exaggerated. The output time is a bit exaggerated

  • You can use StopWatch to see exactly where the overhead increases so much (about StopWatch usage, next introduction).
  • A single execution of the statistical deviation problem will be called above, after the execution of one hundred times, and then look at the time, tends to balance, as shown below

5. Summary

At this point, we have implemented the interception of annotations on the interface, which solves our needs, but still leaves unanswered questions

  • Why can’t annotations on interfaces be intercepted?
  • Why do transaction annotations work when placed on interfaces? What is the implementation mechanism for transaction annotations?
  • Can custom pointcuts be played with our annotations?
  • Why does the initial execution take much time? After multiple executions, the time tends to be normal.

It is no surprise that I have no definite answers to these questions. I will share them later after studying them

III. Can’t miss the source code and related knowledge points

0. Project

  • Project: github.com/liuyueyi/sp…
  • Interface section interception: github.com/liuyueyi/sp…
  • Transaction: github.com/liuyueyi/sp…

AOP blog series

  • SpringBoot base series AOP cannot intercept annotation scenario compatibility on the interface
  • The SpringBoot Foundation series implements a simple distributed timing task
  • SpringBoot foundation AOP interception priority details
  • AOP implementation of logging function in SpringBoot application
  • Advanced use skills of AOP in SpringBoot Basics
  • SpringBoot foundation AOP basic use posture summary

1. An ashy Blog

As far as the letter is not as good, the above content is purely one’s opinion, due to the limited personal ability, it is inevitable that there are omissions and mistakes, if you find bugs or have better suggestions, welcome criticism and correction, don’t hesitate to appreciate

Below a gray personal blog, record all the study and work of the blog, welcome everyone to go to stroll

  • A grey Blog Personal Blog blog.hhui.top
  • A Grey Blog-Spring feature Blog Spring.hhui.top
  • If you feel ok, don’t put down the attention has been lonely to long grass public number: “a gray blog”