Spring AOP 中,切點有多少種定義方式?
在 Spring AOP 中,我們最常用的切點定義方式主要是兩種:
- 使用 execution 進行無侵入攔截。
- 使用注解進行攔截。
這應該是是小伙伴們?nèi)粘9ぷ髦惺褂米疃嗟膬煞N切點定義方式了。但是除了這兩種還有沒有其他的呢?今天松哥就來和大家聊一聊這個話題。
1. Pointcut 分類
來看下 Pointcut 的定義:
public interface Pointcut {
ClassFilter getClassFilter();
MethodMatcher getMethodMatcher();
Pointcut TRUE = TruePointcut.INSTANCE;
}
從方法名上就能看出來,getClassFilter 進行類的過濾,getMethodMatcher 進行方法過濾。通過過濾 Class 和過濾 Method,我們就能夠鎖定一個攔截對象了。
再來看下 Pointcut 類的繼承關(guān)系圖:
圖片
可以看到,其實實現(xiàn)類不算多,大部分看名字其實都能猜出來是干嘛的,這些實現(xiàn)類我們大致上又可以分為六大類:
- 靜態(tài)方法切點:StaticMethodMatcherPointcut 表示靜態(tài)方法切點的抽象基類,默認情況下匹配所有的類,然后通過不同的規(guī)則去匹配不同的方法。
- 動態(tài)方法切點:DynamicMethodMatcherPointcut 表示動態(tài)方法切點的抽象基類,默認情況下它匹配所有的類,然后通過不同的規(guī)則匹配不同的方法,這個有點類似于 StaticMethodMatcherPointcut,不同的是,StaticMethodMatcherPointcut 僅對方法簽名進行匹配并且僅匹配一次,而 DynamicMethodMatcherPointcut 會在運行期間檢查方法入?yún)⒌闹担捎诿看蝹魅氲膮?shù)可能都不一樣,所以沒調(diào)用都要去判斷,因此就導致 DynamicMethodMatcherPointcut 的性能差一些。
- 注解切點:AnnotationMatchingPointcut。
- 表達式切點:ExpressionPointcut。
- 流程切點:ControlFlowPointcut。
- 復合切點:ComposablePointcut。
除了上面這六個之外,另外還有一個落單的 TruePointcut,這個從名字上就能看出來是攔截一切。
所以滿打滿算,有七種類型的切點,接下來我們就來逐個分析一下。
2. TruePointcut
這個實現(xiàn)類從名字上看就是攔截所有,攔截一切,我們來看下這個類怎么做的:
final class TruePointcut implements Pointcut, Serializable {
//...
@Override
public ClassFilter getClassFilter() {
return ClassFilter.TRUE;
}
@Override
public MethodMatcher getMethodMatcher() {
return MethodMatcher.TRUE;
}
//...
}
首先小伙伴們注意,這個類不是 public 的,所以意味著我們自己開發(fā)中沒法直接使用這個切點。然后大家看到,在 getClassFilter 和 getMethodMatcher 方法中,這里都返回了對應的 TRUE,而這兩個 TRUE 實現(xiàn)非常簡單,就是在需要做比對的地方,不做任何比對,直接返回 true 即可,這就導致了最終將所有東西都攔截下來。
3. StaticMethodMatcherPointcut
StaticMethodMatcherPointcut 僅對方法名簽名(包括方法名和入?yún)㈩愋图绊樞颍┻M行匹配,靜態(tài)匹配僅判斷一次。
public abstract class StaticMethodMatcherPointcut extends StaticMethodMatcher implements Pointcut {
private ClassFilter classFilter = ClassFilter.TRUE;
/**
* Set the {@link ClassFilter} to use for this pointcut.
* Default is {@link ClassFilter#TRUE}.
*/
public void setClassFilter(ClassFilter classFilter) {
this.classFilter = classFilter;
}
@Override
public ClassFilter getClassFilter() {
return this.classFilter;
}
@Override
public final MethodMatcher getMethodMatcher() {
return this;
}
}
可以看到,這里類的匹配默認就是返回 true,方法的匹配則返回當前對象,也就是看具體的實現(xiàn)。
StaticMethodMatcherPointcut 有幾個寫好的實現(xiàn)類,我們來看下。
3.1 SetterPointcut
看名字就知道,這個可以用來攔截所有的 set 方法:
private static class SetterPointcut extends StaticMethodMatcherPointcut implements Serializable {
public static final SetterPointcut INSTANCE = new SetterPointcut();
@Override
public boolean matches(Method method, Class<?> targetClass) {
return (method.getName().startsWith("set") &&
method.getParameterCount() == 1 &&
method.getReturnType() == Void.TYPE);
}
private Object readResolve() {
return INSTANCE;
}
@Override
public String toString() {
return "Pointcuts.SETTERS";
}
}
可以看到,方法的匹配就是斷定當前方法是否為 set 方法,要求方法名以 set 開始,方法只有一個參數(shù)并且方法返回值為 null,精準定位到一個 set 方法。
舉一個使用的例子,如下:
ProxyFactory proxyFactory = new ProxyFactory();
proxyFactory.setTarget(new CalculatorImpl());
proxyFactory.addInterface(ICalculator.class);
proxyFactory.addAdvisor(new PointcutAdvisor() {
@Override
public Pointcut getPointcut() {
return Pointcuts.SETTERS;
}
@Override
public Advice getAdvice() {
return new MethodInterceptor() {
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
Method method = invocation.getMethod();
String name = method.getName();
System.out.println(name + " 方法開始執(zhí)行了。。。");
Object proceed = invocation.proceed();
System.out.println(name + " 方法執(zhí)行結(jié)束了。。。");
return proceed;
}
};
}
@Override
public boolean isPerInstance() {
return true;
}
});
ICalculator calculator = (ICalculator) proxyFactory.getProxy();
calculator.setA(5);
由于 SetterPointcut 是私有的,無法直接 new,可以通過工具類 Pointcuts 獲取到實例。
3.2 GetterPointcut
GetterPointcut 和 SetterPointcut 類似,如下:
private static class GetterPointcut extends StaticMethodMatcherPointcut implements Serializable {
public static final GetterPointcut INSTANCE = new GetterPointcut();
@Override
public boolean matches(Method method, Class<?> targetClass) {
return (method.getName().startsWith("get") &&
method.getParameterCount() == 0);
}
private Object readResolve() {
return INSTANCE;
}
@Override
public String toString() {
return "Pointcuts.GETTERS";
}
}
我覺得這個應該就不用過多解釋了吧,跟前面的 SetterPointcut 類似,對照理解就行了。
3.3 NameMatchMethodPointcut
這個是根據(jù)方法名來做匹配。
public class NameMatchMethodPointcut extends StaticMethodMatcherPointcut implements Serializable {
private List<String> mappedNames = new ArrayList<>();
public void setMappedName(String mappedName) {
setMappedNames(mappedName);
}
public void setMappedNames(String... mappedNames) {
this.mappedNames = new ArrayList<>(Arrays.asList(mappedNames));
}
public NameMatchMethodPointcut addMethodName(String name) {
this.mappedNames.add(name);
return this;
}
@Override
public boolean matches(Method method, Class<?> targetClass) {
for (String mappedName : this.mappedNames) {
if (mappedName.equals(method.getName()) || isMatch(method.getName(), mappedName)) {
return true;
}
}
return false;
}
protected boolean isMatch(String methodName, String mappedName) {
return PatternMatchUtils.simpleMatch(mappedName, methodName);
}
}
可以看到,這個就是從外部傳一個方法名稱列表進來,然后在 matches 方法中進行匹配即可,匹配的時候直接調(diào)用 equals 方法進行匹配,如果 equals 方法沒有匹配上,則調(diào)用 isMatch 方法進行匹配,這個最終調(diào)用到 PatternMatchUtils.simpleMatch 方法,這是 Spring 中提供的一個工具類,支持通配符的匹配。
舉個簡單例子:
ProxyFactory proxyFactory = new ProxyFactory();
proxyFactory.setTarget(new CalculatorImpl());
proxyFactory.addInterface(ICalculator.class);
proxyFactory.addAdvisor(new PointcutAdvisor() {
@Override
public Pointcut getPointcut() {
NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut();
pointcut.setMappedNames("add","set*");
return pointcut;
}
@Override
public Advice getAdvice() {
return new MethodInterceptor() {
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
Method method = invocation.getMethod();
String name = method.getName();
System.out.println(name + " 方法開始執(zhí)行了。。。");
Object proceed = invocation.proceed();
System.out.println(name + " 方法執(zhí)行結(jié)束了。。。");
return proceed;
}
};
}
@Override
public boolean isPerInstance() {
return true;
}
});
ICalculator calculator = (ICalculator) proxyFactory.getProxy();
calculator.add(3,4);
calculator.minus(3, 4);
calculator.setA(5);
這里我設(shè)置的是攔截方法名為 add 或者方法名以 set 開頭的方法。
3.4 JdkRegexpMethodPointcut
這個是支持通過正則去匹配方法名,匹配上的方法就會被攔截下來。
public class JdkRegexpMethodPointcut extends AbstractRegexpMethodPointcut {
private Pattern[] compiledPatterns = new Pattern[0];
private Pattern[] compiledExclusionPatterns = new Pattern[0];
@Override
protected void initPatternRepresentation(String[] patterns) throws PatternSyntaxException {
this.compiledPatterns = compilePatterns(patterns);
}
@Override
protected void initExcludedPatternRepresentation(String[] excludedPatterns) throws PatternSyntaxException {
this.compiledExclusionPatterns = compilePatterns(excludedPatterns);
}
@Override
protected boolean matches(String pattern, int patternIndex) {
Matcher matcher = this.compiledPatterns[patternIndex].matcher(pattern);
return matcher.matches();
}
@Override
protected boolean matchesExclusion(String candidate, int patternIndex) {
Matcher matcher = this.compiledExclusionPatterns[patternIndex].matcher(candidate);
return matcher.matches();
}
private Pattern[] compilePatterns(String[] source) throws PatternSyntaxException {
Pattern[] destination = new Pattern[source.length];
for (int i = 0; i < source.length; i++) {
destination[i] = Pattern.compile(source[i]);
}
return destination;
}
}
可以看到,這里實際上就是傳入正則表達式,然后通過正則表達式去匹配方法名是否滿足條件。正則表達式可以傳入多個,系統(tǒng)會在 JdkRegexpMethodPointcut 的父類中進行遍歷逐個進行匹配,我舉一個例子:
ProxyFactory proxyFactory = new ProxyFactory();
proxyFactory.setTarget(new CalculatorImpl());
proxyFactory.addInterface(ICalculator.class);
proxyFactory.addAdvisor(new PointcutAdvisor() {
@Override
public Pointcut getPointcut() {
JdkRegexpMethodPointcut pc = new JdkRegexpMethodPointcut();
pc.setPatterns("org.javaboy.bean.aop3.ICalculator.set.*");
pc.setExcludedPattern("org.javaboy.bean.aop3.ICalculator.setA");
return pc;
}
@Override
public Advice getAdvice() {
return new MethodInterceptor() {
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
Method method = invocation.getMethod();
String name = method.getName();
System.out.println(name + " 方法開始執(zhí)行了。。。");
Object proceed = invocation.proceed();
System.out.println(name + " 方法執(zhí)行結(jié)束了。。。");
return proceed;
}
};
}
@Override
public boolean isPerInstance() {
return true;
}
});
ICalculator calculator = (ICalculator) proxyFactory.getProxy();
calculator.add(3,4);
calculator.minus(3, 4);
calculator.setA(5);
上面這個例子也是攔截 setXXX 方法,不過需要注意的是,方法名匹配的時候使用的是方法的全路徑。
另外還需要注意,在配置匹配規(guī)則的時候,還可以設(shè)置 ExcludedPattern,實際上在匹配的時候,首先進行正向匹配,就是先看下方法名是否滿足規(guī)則,如果滿足,則方法名再和 ExcludedPattern 進行比對,如果不滿足,則這個方法才會被確定下來要攔截。
StaticMethodMatcherPointcut 中主要就給我們提供了這些規(guī)則。
4. DynamicMethodMatcherPointcut
這個是動態(tài)方法匹配的切點,默認也是匹配所有類,但是在方法匹配這個問題,每次都會匹配,我們來看下:
public abstract class DynamicMethodMatcherPointcut extends DynamicMethodMatcher implements Pointcut {
@Override
public ClassFilter getClassFilter() {
return ClassFilter.TRUE;
}
@Override
public final MethodMatcher getMethodMatcher() {
return this;
}
}
可以看到,getClassFilter 直接返回 TRUE,也就是類就直接匹配了,getMethodMatcher 返回的則是當前對象,那是因為當前類實現(xiàn)了 DynamicMethodMatcher 接口,這就是一個方法匹配器:
public abstract class DynamicMethodMatcher implements MethodMatcher {
@Override
public final boolean isRuntime() {
return true;
}
@Override
public boolean matches(Method method, Class<?> targetClass) {
return true;
}
}
小伙伴們看到,這里 isRuntime 方法返回 true,該方法為 true,意味著三個參數(shù)的 matches 方法將會被調(diào)用,所以這里兩個參數(shù)的 matches 方法可以直接返回 true,不做任何控制。
當然,也可以在兩個參數(shù)的 matches 方法中做一些前置的判斷。
我們來看一個簡單例子:
public class MyDynamicMethodMatcherPointcut extends DynamicMethodMatcherPointcut {
@Override
public boolean matches(Method method, Class<?> targetClass) {
return method.getName().startsWith("set");
}
@Override
public boolean matches(Method method, Class<?> targetClass, Object... args) {
return method.getName().startsWith("set") && args.length == 1 && Integer.class.isAssignableFrom(args[0].getClass());
}
}
在實際執(zhí)行過程中,兩個參數(shù)的 matches 方法返回 true,三個參數(shù)的 matches 方法才會執(zhí)行,如果兩個參數(shù)的 matches 方法返回 false,則三個參數(shù)的 matches 就不會執(zhí)行了。所以也可以兩個參數(shù)的 matches 方法直接固定返回 true,只在三個參數(shù)的 matches 方法中做匹配操作即可。
然后使用這個切點:
ProxyFactory proxyFactory = new ProxyFactory();
proxyFactory.setTarget(new CalculatorImpl());
proxyFactory.addInterface(ICalculator.class);
proxyFactory.addAdvisor(new PointcutAdvisor() {
@Override
public Pointcut getPointcut() {
return new MyDynamicMethodMatcherPointcut();
}
@Override
public Advice getAdvice() {
return new MethodInterceptor() {
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
Method method = invocation.getMethod();
String name = method.getName();
System.out.println(name + " 方法開始執(zhí)行了。。。");
Object proceed = invocation.proceed();
System.out.println(name + " 方法執(zhí)行結(jié)束了。。。");
return proceed;
}
};
}
@Override
public boolean isPerInstance() {
return true;
}
});
ICalculator calculator = (ICalculator) proxyFactory.getProxy();
calculator.add(3,4);
calculator.minus(3, 4);
calculator.setA(5);
5. AnnotationMatchingPointcut
這個就是判斷類或者方法上是否存在某個注解,如果存在,則將之攔截下來,否則不攔截。
先來看下這個類的定義:
public class AnnotationMatchingPointcut implements Pointcut {
private final ClassFilter classFilter;
private final MethodMatcher methodMatcher;
public AnnotationMatchingPointcut(Class<? extends Annotation> classAnnotationType) {
this(classAnnotationType, false);
}
public AnnotationMatchingPointcut(Class<? extends Annotation> classAnnotationType, boolean checkInherited) {
this.classFilter = new AnnotationClassFilter(classAnnotationType, checkInherited);
this.methodMatcher = MethodMatcher.TRUE;
}
public AnnotationMatchingPointcut(@Nullable Class<? extends Annotation> classAnnotationType,
@Nullable Class<? extends Annotation> methodAnnotationType) {
this(classAnnotationType, methodAnnotationType, false);
}
public AnnotationMatchingPointcut(@Nullable Class<? extends Annotation> classAnnotationType,
@Nullable Class<? extends Annotation> methodAnnotationType, boolean checkInherited) {
if (classAnnotationType != null) {
this.classFilter = new AnnotationClassFilter(classAnnotationType, checkInherited);
}
else {
this.classFilter = new AnnotationCandidateClassFilter(methodAnnotationType);
}
if (methodAnnotationType != null) {
this.methodMatcher = new AnnotationMethodMatcher(methodAnnotationType, checkInherited);
}
else {
this.methodMatcher = MethodMatcher.TRUE;
}
}
@Override
public ClassFilter getClassFilter() {
return this.classFilter;
}
@Override
public MethodMatcher getMethodMatcher() {
return this.methodMatcher;
}
public static AnnotationMatchingPointcut forClassAnnotation(Class<? extends Annotation> annotationType) {
Assert.notNull(annotationType, "Annotation type must not be null");
return new AnnotationMatchingPointcut(annotationType);
}
public static AnnotationMatchingPointcut forMethodAnnotation(Class<? extends Annotation> annotationType) {
Assert.notNull(annotationType, "Annotation type must not be null");
return new AnnotationMatchingPointcut(null, annotationType);
}
}
首先小伙伴們注意到,這個類一共有四個構(gòu)造方法,從上往下分別是:
- 傳入類上的注解名稱,根據(jù)類上的注解去判斷是否需要攔截。
- 在 1 的基礎(chǔ)之上,再增加一個 checkInherited,這個表示是否需要檢查父類上是否存在相關(guān)的注解。
- 傳入類上和方法上的注解類型,根據(jù)這個注解類型去判斷是否需要攔截。
- 在 3 的基礎(chǔ)之上,再增加一個 checkInherited,這個表示是否需要檢查父類上或者方法上是否存在相關(guān)的注解。
其中,第四個構(gòu)造方法中處理的情況類型比較多,如果用戶傳入了 classAnnotationType,則構(gòu)建 AnnotationClassFilter 類型的 ClassFilter,否則構(gòu)建 AnnotationCandidateClassFilter 類型的 ClassFilter;如果用戶傳入了 methodAnnotationType,則構(gòu)建 AnnotationMethodMatcher 類型的 MethodMatcher,否則方法匹配器就直接返回匹配所有方法。
那么接下來我們就來看下這幾種不同的匹配器。
5.1 AnnotationClassFilter
public class AnnotationClassFilter implements ClassFilter {
//...
@Override
public boolean matches(Class<?> clazz) {
return (this.checkInherited ? AnnotatedElementUtils.hasAnnotation(clazz, this.annotationType) :
clazz.isAnnotationPresent(this.annotationType));
}
//...
}
這里省略了一些代碼,關(guān)鍵地方就是這個匹配方法了,如果需要檢查父類是否包含該注解,則調(diào)用 AnnotatedElementUtils.hasAnnotation 方法進行查找,否則直接調(diào)用 clazz.isAnnotationPresent 方法判斷當前類上是否包含指定注解即可。
5.2 AnnotationCandidateClassFilter
private static class AnnotationCandidateClassFilter implements ClassFilter {
private final Class<? extends Annotation> annotationType;
AnnotationCandidateClassFilter(Class<? extends Annotation> annotationType) {
this.annotationType = annotationType;
}
@Override
public boolean matches(Class<?> clazz) {
return AnnotationUtils.isCandidateClass(clazz, this.annotationType);
}
}
這里就是調(diào)用了 AnnotationUtils.isCandidateClass 方法進行判斷,這個方法用來判斷指定類是不是可以承載指定注解的候選類,返回 true 的規(guī)則是:
- 以 java. 開頭的注解,所有的類都能承載,這種情況會返回 true。
- 目標類不能以 java. 開頭,也就是說 JDK 中的類都不行,不是以 java. 開頭的類就可以返回 true。
- 給定類也不能是 Ordered 類。
滿足如上條件,這個類就是符合規(guī)定的。
AnnotationCandidateClassFilter 主要是針對用戶沒有傳入類上注解的情況,這種情況一般都是根據(jù)方法上的注解進行匹配的,所以這里主要是排除一些系統(tǒng)類。
5.3 AnnotationMethodMatcher
public class AnnotationMethodMatcher extends StaticMethodMatcher {
@Override
public boolean matches(Method method, Class<?> targetClass) {
if (matchesMethod(method)) {
return true;
}
// Proxy classes never have annotations on their redeclared methods.
if (Proxy.isProxyClass(targetClass)) {
return false;
}
// The method may be on an interface, so let's check on the target class as well.
Method specificMethod = AopUtils.getMostSpecificMethod(method, targetClass);
return (specificMethod != method && matchesMethod(specificMethod));
}
private boolean matchesMethod(Method method) {
return (this.checkInherited ? AnnotatedElementUtils.hasAnnotation(method, this.annotationType) :
method.isAnnotationPresent(this.annotationType));
}
}
方法匹配就是首先檢查方法上是否有注解,如果開啟了 checkInherited,則去檢查一下父類對應的方法上是否有相關(guān)的注解,如果有,則表示方法匹配上了,返回 true。
否則先去檢查一下當前類是否是一個代理對象,代理對象中對應的方法肯定是沒有注解的,直接返回 false。
如果前面兩步還沒返回,最后考慮到目前這個方法可能是在一個接口上,要檢查一下它的實現(xiàn)類是否包含該注解。
這就是 AnnotationMatchingPointcut。松哥也舉一個簡單例子吧。
5.4 實踐
首先我自定義一個注解,如下:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MyAction {
}
然后將之添加到某一個方法上:
public class CalculatorImpl implements ICalculator {
@Override
public void add(int a, int b) {
System.out.println(a + "+" + b + "=" + (a + b));
}
@MyAction
@Override
public int minus(int a, int b) {
return a - b;
}
@Override
public void setA(int a) {
System.out.println("a = " + a);
}
}
最后來實踐一下:
ProxyFactory proxyFactory = new ProxyFactory();
proxyFactory.setTarget(new CalculatorImpl());
proxyFactory.addInterface(ICalculator.class);
proxyFactory.addAdvisor(new PointcutAdvisor() {
@Override
public Pointcut getPointcut() {
return new AnnotationMatchingPointcut(null, MyAction.class);
}
@Override
public Advice getAdvice() {
return new MethodInterceptor() {
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
Method method = invocation.getMethod();
String name = method.getName();
System.out.println(name + " 方法開始執(zhí)行了。。。");
Object proceed = invocation.proceed();
System.out.println(name + " 方法執(zhí)行結(jié)束了。。。");
return proceed;
}
};
}
@Override
public boolean isPerInstance() {
return true;
}
});
ICalculator calculator = (ICalculator) proxyFactory.getProxy();
calculator.add(3,4);
calculator.minus(3, 4);
calculator.setA(5);
只有 minus 方法被攔截下來了。
6. ExpressionPointcut
這個其實就是我們?nèi)粘i_發(fā)中使用最多的一種切點定義方式,可能項目中 99% 的切點定義都是使用的 ExpressionPointcut。這個具體用法我這里就不說了,因為比較豐富,都能單獨整一篇文章了,如果小伙伴對 ExpressionPointcut 的基礎(chǔ)用法還不熟悉的話,可以在公眾號【江南一點雨】后臺回復 ssm,有松哥之前錄制的入門視頻教程可以參考。
我這里就簡單把它的實現(xiàn)思路來和小伙伴們梳理一下,ExpressionPointcut 的實現(xiàn)都在 AspectJExpressionPointcut 類中,該類支持使用切點語言來對要攔截的方法做一個非常精確的描述,精確到要攔截方法的返回值,AspectJExpressionPointcut 類的實現(xiàn)比較長也比較復雜,我這里貼其中一些關(guān)鍵的代碼來看下:
public class AspectJExpressionPointcut extends AbstractExpressionPointcut
implements ClassFilter, IntroductionAwareMethodMatcher, BeanFactoryAware {
private static final Set<PointcutPrimitive> SUPPORTED_PRIMITIVES = Set.of(
PointcutPrimitive.EXECUTION,
PointcutPrimitive.ARGS,
PointcutPrimitive.REFERENCE,
PointcutPrimitive.THIS,
PointcutPrimitive.TARGET,
PointcutPrimitive.WITHIN,
PointcutPrimitive.AT_ANNOTATION,
PointcutPrimitive.AT_WITHIN,
PointcutPrimitive.AT_ARGS,
PointcutPrimitive.AT_TARGET);
@Override
public ClassFilter getClassFilter() {
obtainPointcutExpression();
return this;
}
@Override
public MethodMatcher getMethodMatcher() {
obtainPointcutExpression();
return this;
}
/**
* Check whether this pointcut is ready to match,
* lazily building the underlying AspectJ pointcut expression.
*/
private PointcutExpression obtainPointcutExpression() {
if (getExpression() == null) {
throw new IllegalStateException("Must set property 'expression' before attempting to match");
}
if (this.pointcutExpression == null) {
this.pointcutClassLoader = determinePointcutClassLoader();
this.pointcutExpression = buildPointcutExpression(this.pointcutClassLoader);
}
return this.pointcutExpression;
}
}
其實關(guān)鍵還是要獲取到 ClassFilter 和 MethodMatcher,然后調(diào)用其 matches 方法,當前類剛好就是實現(xiàn)了這兩個,所以直接返回 this 就可以了。在 getClassFilter 或者 getMethodMatcher 方法執(zhí)行之前,都會先調(diào)用 obtainPointcutExpression 方法,去解析我們傳入的 expression 字符串,將之解析為一個 PointcutExpression 對象,接下來的 matches 方法就可以此為依據(jù),進行匹配了。
7. ControlFlowPointcut
ControlFlowPointcut 主要是指目標方法從某一個指定類的指定方法中執(zhí)行,切點才生效,否則不生效。
舉個簡單例子,如下:
public class AopDemo04 {
public static void main(String[] args) {
ProxyFactory proxyFactory = new ProxyFactory();
proxyFactory.setTarget(new CalculatorImpl());
proxyFactory.addInterface(ICalculator.class);
proxyFactory.addAdvisor(new PointcutAdvisor() {
@Override
public Pointcut getPointcut() {
return new ControlFlowPointcut(AopDemo04.class, "evl");
}
@Override
public Advice getAdvice() {
return new MethodInterceptor() {
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
Method method = invocation.getMethod();
String name = method.getName();
System.out.println(name + " 方法開始執(zhí)行了。。。");
Object proceed = invocation.proceed();
System.out.println(name + " 方法執(zhí)行結(jié)束了。。。");
return proceed;
}
};
}
@Override
public boolean isPerInstance() {
return true;
}
});
ICalculator calculator = (ICalculator) proxyFactory.getProxy();
calculator.add(3,4);
System.out.println("http://///////////////");
evl(calculator);
}
public static void evl(ICalculator iCalculator) {
iCalculator.add(3, 4);
}
}
這里切點的意思就是說,必須要從 AopDemo04 這個類的 evl 方法中調(diào)用 add 方法,這個切點才會生效,如果是拿到了 ICalculator 對象后直接調(diào)用 add 方法,那么切點是不會生效的。
ControlFlowPointcut 的原理也很簡單,就是比較一下類名和方法名,如下:
public class ControlFlowPointcut implements Pointcut, ClassFilter, MethodMatcher, Serializable {
@Override
public boolean matches(Class<?> clazz) {
return true;
}
@Override
public boolean matches(Method method, Class<?> targetClass) {
return true;
}
@Override
public boolean isRuntime() {
return true;
}
@Override
public boolean matches(Method method, Class<?> targetClass, Object... args) {
this.evaluations.incrementAndGet();
for (StackTraceElement element : new Throwable().getStackTrace()) {
if (element.getClassName().equals(this.clazz.getName()) &&
(this.methodName == null || element.getMethodName().equals(this.methodName))) {
return true;
}
}
return false;
}
@Override
public ClassFilter getClassFilter() {
return this;
}
@Override
public MethodMatcher getMethodMatcher() {
return this;
}
}
大家可以看到,isRuntime 方法返回 true,表示這是一個動態(tài)的方法匹配器。關(guān)鍵的 matches 方法,就是根據(jù)調(diào)用棧中的信息,去比較給定的類名和方法名是否滿足。
8. ComposablePointcut
看名字就知道,這個可以將多個切點組合到一起,組合關(guān)系有求交集和求并集兩種,分別對應 ComposablePointcut 中的 intersection 方法和 union 方法。
如下案例:
ProxyFactory proxyFactory = new ProxyFactory();
proxyFactory.setTarget(new CalculatorImpl());
proxyFactory.addInterface(ICalculator.class);
proxyFactory.addAdvisor(new PointcutAdvisor() {
@Override
public Pointcut getPointcut() {
NameMatchMethodPointcut nameMatchMethodPointcut = new NameMatchMethodPointcut();
nameMatchMethodPointcut.setMappedNames("add");
ComposablePointcut pc = new ComposablePointcut((Pointcut) nameMatchMethodPointcut);
pc.union(new AnnotationMatchingPointcut(null, MyAction.class));
return pc;
}
@Override
public Advice getAdvice() {
return new MethodInterceptor() {
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
Method method = invocation.getMethod();
String name = method.getName();
System.out.println(name + " 方法開始執(zhí)行了。。。");
Object proceed = invocation.proceed();
System.out.println(name + " 方法執(zhí)行結(jié)束了。。。");
return proceed;
}
};
}
@Override
public boolean isPerInstance() {
return true;
}
});
ICalculator calculator = (ICalculator) proxyFactory.getProxy();
calculator.add(3,4);
calculator.minus(3, 4);
calculator.setA(5);
在上面的案例中,就是把 NameMatchMethodPointcut 和 AnnotationMatchingPointcut 兩個切點聯(lián)合起來,既攔截方法名為 add 的方法,也攔截含有 @MyAction 注解的方法。
如果將 union 方法改為 intersection,就表示攔截方法名為 add 且被 @MyAction 注解標記的方法。如下:
@Override
public Pointcut getPointcut() {
NameMatchMethodPointcut nameMatchMethodPointcut = new NameMatchMethodPointcut();
nameMatchMethodPointcut.setMappedNames("add");
ComposablePointcut pc = new ComposablePointcut((Pointcut) nameMatchMethodPointcut);
pc.intersection(new AnnotationMatchingPointcut(null, MyAction.class));
return pc;
}
其實這種組合切點的原理很簡單,先把我們提供的 ClassFilter 和 MethodMatcher 收集到一個集合中,如果是 union,就直接遍歷集合,只要有一個滿足,就返回 true;如果是 intersection,也是直接遍歷,如果有一個返回 false 就直接返回 false 即可。
以 ClassFilter 為例,我們來簡單看下源碼:
public ComposablePointcut union(ClassFilter other) {
this.classFilter = ClassFilters.union(this.classFilter, other);
return this;
}
public abstract class ClassFilters {
public static ClassFilter union(ClassFilter cf1, ClassFilter cf2) {
return new UnionClassFilter(new ClassFilter[] {cf1, cf2});
}
private static class UnionClassFilter implements ClassFilter, Serializable {
private final ClassFilter[] filters;
UnionClassFilter(ClassFilter[] filters) {
this.filters = filters;
}
@Override
public boolean matches(Class<?> clazz) {
for (ClassFilter filter : this.filters) {
if (filter.matches(clazz)) {
return true;
}
}
return false;
}
}
}
可以看到,傳入的多個 ClassFilter 被組裝到一起,在 matches 方法中逐個遍歷,只要其中一個返回 true,就是 true。
9. 小結(jié)
好啦,這就是松哥今天和小伙伴們介紹的 7 中 Pointcut 了,希望借此小伙伴們對 Spring AOP 中切點的類型有一個完整的了解。再來回顧一下這其中切點:
- 靜態(tài)方法切點:StaticMethodMatcherPointcut 表示靜態(tài)方法切點的抽象基類,默認情況下匹配所有的類,然后通過不同的規(guī)則去匹配不同的方法。
- 動態(tài)方法切點:DynamicMethodMatcherPointcut 表示動態(tài)方法切點的抽象基類,默認情況下它匹配所有的類,然后通過不同的規(guī)則匹配不同的方法,這個有點類似于 StaticMethodMatcherPointcut,不同的是,StaticMethodMatcherPointcut 僅對方法簽名進行匹配并且僅匹配一次,而 DynamicMethodMatcherPointcut 會在運行期間檢查方法入?yún)⒌闹担捎诿看蝹魅氲膮?shù)可能都不一樣,所以沒調(diào)用都要去判斷,因此就導致 DynamicMethodMatcherPointcut 的性能差一些。
- 注解切點:AnnotationMatchingPointcut 根據(jù)制定注解攔截目標方法或者類。
- 表達式切點:ExpressionPointcut 這個是我們?nèi)粘i_發(fā)中使用最多的一種切點定義方式。
- 流程切點:ControlFlowPointcut 這個是要求必須從某一個位置調(diào)用目標方法,切點才會生效。
- 復合切點:ComposablePointcut 這個是把多個攔截器組裝在一起使用,有交集和并集兩種組裝方式。
- TruePointcut 這是框架內(nèi)部使用的一個切點,表示攔截一切。