在本人前面的文章Spring Aop原理之切点表达式解析中讲解了Spring是如何解析切点表达式的,在分析源码的时候,出现了如下将要讲述的问题,我认为是不合理的,后来本人单纯使用aspectj进行试验,发现结果与Spring源码所表现出来的状态是一致的。
1. 现象
我们首先声明一个目标类Dog
,其方法执行将会被代理,声明如下:
public class Dog {
public void run() {
System.out.println("Tidy is running.");
}
}
然后是切面类声明如下:
@Aspect
public class DogAspect {
@Around("execution(public void Dog.*(..))")
public Object aspect(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("before run. ");
Object result = joinPoint.proceed();
System.out.println("after run.");
return result;
}
}
可以看到,这里DogAspect
中声明的切面将会环绕Dog.run()
方法的执行。下面是xml配置和驱动类声明:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
<bean id="dog" class="Dog"/>
<bean id="aspect" class="DogAspect"/>
<aop:aspectj-autoproxy/>
</beans>
public class DogApp {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
Dog dog = context.getBean(Dog.class);
dog.run();
}
}
执行结果如下:
before run.
Tidy is running.
after run.
2. 问题阐述
这里我们的切点表达式中修饰符使用的是public,而通过Spring的源码可以发现,其是支持多个修饰符的,比如如下的切点表达式:
@Around("execution(public protected void Dog.*(..))")
当使用该切点表达式的时候,上述程序也是可以正常运行的,但是比较奇怪的是,目标方法Dog.run()
是没有被代理的。从业务的角度来看,上述表达式理论上应该匹配使用public或者protected修饰的方法,而Dog.run()
方法是符合该条件的,但是这里却没有。
3. 原因分析
这里我们还是通过源码来分析上述问题,即当出现多个修饰符的时候Spring是如何对目标方法进行匹配的。如下是Spring Aop对修饰符解析的源码:
public ModifiersPattern parseModifiersPattern() {
// 存储修饰符的变量,使用二进制位进行标识
int requiredFlags = 0;
// 存储应该被过滤的修饰符,使用二进制位进行标识
int forbiddenFlags = 0;
int start;
while (true) {
start = tokenSource.getIndex();
boolean isForbidden = false;
// 如果当前修饰符前面使用!,则表示该修饰符是需要被过滤掉的修饰符
isForbidden = maybeEat("!");
// 获取当前的修饰符
IToken t = tokenSource.next();
// 通过修饰符的名称获取其对应的一个二进制位数据。这里的ModifiersPattern其实比较简单,
// 大家可以简单的将其理解为一个Map即可,即将每个修饰符映射到唯一一个二进制位
int flag = ModifiersPattern.getModifierFlag(t.getString());
// 如果flag为-1,说明当前字符串不是修饰符,此时退出循环,进行下一步的解析
if (flag == -1) {
break;
}
// 如果当前修饰符是应该被过滤的修饰符,则将其存储在forbiddenFlags中;
// 如果当前修饰符是被需要的修饰符,则将其存储在requiredFlags中
if (isForbidden) {
forbiddenFlags |= flag;
} else {
requiredFlags |= flag;
}
}
tokenSource.setIndex(start);
// 如果被需要的修饰符和被禁止的修饰符都不存在,说明当前切点表达式将匹配以任意类型修饰符修饰的方法
if (requiredFlags == 0 && forbiddenFlags == 0) {
return ModifiersPattern.ANY;
} else {
// 如果有任意一个值不为0,说明当前切点表达式对修饰符有要求,因而将其封装到ModifiersPattern中
return new ModifiersPattern(requiredFlags, forbiddenFlags);
}
}
可以看到,Spring是将被需要的修饰符和被禁止的修饰符分别存储在两个变量中的:requiredFlags和forbiddenFlags。对于我们上述声明的两个修饰符public和protected,其对应的flag值分别是1和4。也就是说,此时requiredFlags的值为5,而forbiddenFlags的值为0。这两个值都存储在一个ModifiersPattern类型的对象中。上文中我们讲过,Spring Aop对目标方法的匹配是通过递归实现的,因而这里对目标方法的匹配逻辑肯定是在ModifiersPattern中声明了,下面是其匹配相关的源码:
public class ModifiersPattern extends PatternNode {
private int requiredModifiers;
private int forbiddenModifiers;
public ModifiersPattern(int requiredModifiers, int forbiddenModifiers) {
this.requiredModifiers = requiredModifiers;
this.forbiddenModifiers = forbiddenModifiers;
}
public boolean matches(int modifiers) {
return ((modifiers & requiredModifiers) == requiredModifiers)
&& ((modifiers & forbiddenModifiers) == 0);
}
}
可以看到,ModifiersPattern.matches()
就是其匹配逻辑所在,参数modifiers就是目标方法的修饰符。在其实现逻辑中,与requiredModifiers相关的代码可以看出,如果在切点表达式中声明了两个修饰符,那么要求目标方法的修饰符也必须是至少包含这两个。对于这里的例子也就是说,目标方法必须使用至少public和protected进行修饰。这就是问题的所在,理论上Java是不允许方法拥有两个修饰符的,也就是说这里切点表达式是无论如何都无法匹配上任何方法的。
4. 个人观点
本人开始以为上述问题是Spring产生的bug,后来查阅了相关文档,暂时没发现对上述问题有描述的文档。后来本人单纯使用aspectj的jar包进行实验,发现结果是一致的,使用aspectj的jar包实验方法如下面这篇博文所示:AspectJ——简介以及在IntelliJ IDEA下的配置。这说明这就是切点表达式规定的表示方式,但是本人认为这种方式是不合理的,原因主要有两点:
- 从用户的角度来讲,当切点表达式中使用了两个修饰符时,一般的思考方向就是这种写法应该是或的关系,即将匹配使用其中任意一种修饰符的目标对象;
- 从Java语法的角度来讲,Java是不允许一个类或方法同时使用两种修饰符的,因而对于上述使用两种修饰符的切点表达式,其将匹配不到任何方法,既然匹配不到任何方法,那为什么还允许这么写呢?