基于@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应用。
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。
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并没有执行。
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的实例。
修改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执行结果。
程序运行正确,问题解决了。
2. 调试思路总结
2.1 问题描述
当我们发现一个Spring工程能够启动,即Console窗口出现了成功启动的信息(如下图所示)
但是发现本应该运行的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
。