基于@Aspect的Spring AOP实现竟然没有运行:Spring“最直觉”的调试思路

摘要
本文通过一个问题“基于@Aspect的Spring AOP实现竟然没有运行”的调试全过程,给出了Spring最基础最“直觉”最“特别”的调试思路:考虑对象是否被框架加载/实例化了?


比较了解Spring AOP的同学可以直接看第二部分“2. 调试思路总结”。阅读本文大概需要10min。

1. 基于@Aspect的Spring AOP实现为什么没有运行

1.1 基于@Aspect的Spring AOP实现代码

这是一个用IntelliJ IDEA开发的使用Maven搭建Spring Boot应用。
基于@Aspect的Spring AOP实现竟然没有运行:Spring“最直觉”的调试思路

AnnotationApplication.java的源码:

package com.aop.annotation;

import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;

import java.util.Arrays;

@SpringBootApplication
public class AnnotationApplication {
    public static void main(String[] args) {
        SpringApplication.run(AnnotationApplication.class, args);
    }
}

Operation.java是AOP的目标对象,即符合PointCut所指定的条件,被AOP织入横切逻辑(Advice)的对象。

package com.aop.annotation;

import org.springframework.stereotype.Component;

@Component
public class Operation {
    public int add(int operand1, int operand2) throws Exception {
        System.out.println("add operation.");
        int result = operand1 + operand2;
        if (result < 10) {
            throw new Exception("not valid result.");
        } else {
            System.out.println("The result is: " + result);
            return result;
        }
    }

    public void show() {
        System.out.println("function show().");
    }
}

BeforeAdvice.java 是定义了Before Advice的Aspect对象。

package com.aop.annotation;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;

@Aspect
public class BeforeAdvice {
    @Pointcut("execution(* Operation.*(..))")  // Pointcut指定了符合条件的一组JoinPoint: Operation类中的所有函数。
    public void before(){}

    @Before("before()")
    public void beforeAdviceOp(JoinPoint joinPoint) {
        System.out.println("*****before operation*****");
        System.out.println("JointPoint: \n");
        System.out.println("signature: " + joinPoint.getSignature().toString());
        System.out.println("source location: " + joinPoint.getSourceLocation().toString());
        System.out.println("target: " + joinPoint.getTarget().toString());
    }
}

假如AOP代码书写正确的话,可以在IntelliJ IDE可以在Aspect类中找到对应的JoinPoint。
基于@Aspect的Spring AOP实现竟然没有运行:Spring“最直觉”的调试思路

AnnotationApplicationTests.java是‘非规范”的单元测试。

package com.aop.annotation;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.stereotype.Component;
import org.springframework.test.context.junit4.SpringRunner;

@ComponentScan("com.aop.annotation")
@RunWith(SpringRunner.class)
@SpringBootTest
public class AnnotationApplicationTests {
    @Autowired
    Operation operation;

    @Test
    public void testBeforeAdvice() {
        System.out.println("calling add...");
        try {
            operation.add(1, 2); 
        } catch (Exception e) {
            System.out.println(e.getStackTrace());
        }

        operation.show();
    }

}

运行测试用例,发现BeforeAdvice并没有执行。
基于@Aspect的Spring AOP实现竟然没有运行:Spring“最直觉”的调试思路

1.2 Spring AOP的原理理解

Aspect对象是对织入Join Point的Advice进行封装的概念实体,所以,Aspect对象与其他普通对象一样,需要在使用之前将其实例化。

AspectJ通过@Aspect注解以及相对应的Advice注解就可以自动完成将Advice切入目标对象对应的Join Point中。但是,为了调用Aspect对象中封装的Advice,首先需要将其实例化。在Spring中,目标对象往往都是由IoC管理的Bean对象,那么,为了使用Spring的依赖注入将AspectJ的切面(Aspect对象中封装的横切逻辑Advice)注入到目标对象中,也需要将Aspect对象声明为Bean。

1.3 问题解决

首先将目前Spring加载的对象打印出来。在AnnotationApplication.java中加入appContext.getBeanDefinitionNames()

package com.aop.annotation;


import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;

import java.util.Arrays;

@SpringBootApplication
public class AnnotationApplication {
    public static void main(String[] args) {
        SpringApplication.run(AnnotationApplication.class, args);
    }
    @Bean
    public CommandLineRunner run(ApplicationContext appContext) {
        return args -> {

            String[] beans = appContext.getBeanDefinitionNames();
            Arrays.stream(beans).sorted().forEach(System.out::println);

        };
    }
}

点击运行,在Console窗口中只看到了目标对象的实例operation以及刚刚添加的的CommanlineRunner实例run。(以下两图为Spring加载的实例的部分截图),并没有找到BeforeAdvice的实例。
基于@Aspect的Spring AOP实现竟然没有运行:Spring“最直觉”的调试思路基于@Aspect的Spring AOP实现竟然没有运行:Spring“最直觉”的调试思路


修改BeforeAdvice.java,在BeforeAdivice类加上@Component注解,将其声明为Bean。

package com.aop.annotation;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

@Aspect
@Component  // 将切面声明的同时也要声明为Bean,才能使用Spring的依赖注入为AspectJ切面注入协作者
public class BeforeAdvice {
    @Pointcut("execution(* Operation.*(..))")
    public void before(){}

    @Before("before()")
    public void beforeAdviceOp(JoinPoint joinPoint) {
        System.out.println("*****before operation*****");
        System.out.println("JointPoint: \n");
        System.out.println("signature: " + joinPoint.getSignature().toString());
        System.out.println("source location: " + joinPoint.getSourceLocation().toString());
        System.out.println("target: " + joinPoint.getTarget().toString());
    }
}

再次运行单元测试AnnotationApplicationTests.java。在Console中打印出来的已经加载的类中可以找到BeforeAdvice的实例beforeAdvice,也可以看到beforeAdvice执行结果。
基于@Aspect的Spring AOP实现竟然没有运行:Spring“最直觉”的调试思路
基于@Aspect的Spring AOP实现竟然没有运行:Spring“最直觉”的调试思路
程序运行正确,问题解决了。

2. 调试思路总结

2.1 问题描述

当我们发现一个Spring工程能够启动,即Console窗口出现了成功启动的信息(如下图所示)
基于@Aspect的Spring AOP实现竟然没有运行:Spring“最直觉”的调试思路
但是发现本应该运行的method/代码段并没有运行。

2.2 Debug思路

在确认代码没有出错的情况下,首先应该想到的判断是:没有运行的method/代码段所在的对象(为便于阐述,暂设为ClassA)是否被实例化了?在Spring中,对象实例化存在以下两种方式:

  • 手动new
  • 声明为Bean(注解方式或者xml配置方式)

这种考虑对象是否被框架加载/实例化了的情况是Spring所有特有的。


可以通过appContext.getBeanDefinitionNames()Spring加载/实例化的类打印出来进一步确定对象是否被实例化:

// XXXApplication.java
package com.aop.annotation;

import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import java.util.Arrays;

@SpringBootApplication
public class AnnotationApplication {
    public static void main(String[] args) {
        SpringApplication.run(AnnotationApplication.class, args);
    }
    
    @Beand
    public CommandLineRunner run(ApplicationContext appContext) {
        return args -> {

            String[] beans = appContext.getBeanDefinitionNames();
            Arrays.stream(beans).sorted().forEach(System.out::println);

        };
    }
}

在Console窗口查找`ClassA`的实例,倘若没有找到,则说明该类没有被实例化,那么`ClassA`的所有method/代码段就无法执行了。 倘若找到了`ClassA`的实例,那么导致问题的因素是复杂各样的了,此时我们需要具体问题具体分析了,暂时不在本文的讨论范围。

倘若ClassA 没有被指明name的话@Component("name")@Service("name")@Repository("name"), 则Spring在加载该类时默认实例的名称为首字母小写的类的名字classA