文章目录
1. 前言
在上一篇博客中我们学习了基于
ProxyFactory
添加前置通知、后置通知、后置返回通知、异常通知以及更为强大的环绕通知的学习。ProxyFactory
通过addAdvice
方法用于配置代理的通知,其底层是将此方法委托给addAdvisor
方法如下所示,并且ProxyFactory
通过setTarget
方法对目标的所有方法进行代理。
上面默认创建都是DefaultPointAdvisor
实例,其通知默认将会适用于代理目标对象的所有方法。但有时候我们并不想让通知对一个类的所有方法都进行适配。这个时候我们就可以使用Pointcut
这个接口的实现去决定哪些类的那些方法将会适配通知。
2. Pointcut接口
Spring通过切点来实现对那些方法进行通知适配而切入点是通过Pointcut
接口实现来完成的。
public interface Pointcut {
/**
* Return the ClassFilter for this pointcut.
* @return the ClassFilter (never {@code null})
*/
ClassFilter getClassFilter();
/**
* Return the MethodMatcher for this pointcut.
* @return the MethodMatcher (never {@code null})
*/
MethodMatcher getMethodMatcher();
}
Pointcut接口定义了两个方法getClassFilter()于 getMethodMatcher(),通过ClassFilter
对象的match
方法来判断切点是否适用于某些类其方法如下所示:
@FunctionalInterface
public interface ClassFilter {
/**
* Should the pointcut apply to the given interface or target class?
* @param clazz the candidate target class
* @return whether the advice should apply to the given target class
*/
boolean matches(Class<?> clazz);
}
可以看到ClassFilter接口的 match方法传入一个Class实例用来检查该实例是否适用于通知,如果方法返回true则表示该类适用。MethodMatcher 接口稍微复杂一些其接口方法如下:
public interface MethodMatcher {
/**
* Perform static checking whether the given method matches.
* <p>If this returns {@code false} or if the {@link #isRuntime()}
* method returns {@code false}, no runtime check (i.e. no
* {@link #matches(java.lang.reflect.Method, Class, Object[])} call)
* will be made.
* @param method the candidate method
* @param targetClass the target class
* @return whether or not this method matches statically
*/
boolean matches(Method method, Class<?> targetClass);
/**
* Is this MethodMatcher dynamic, that is, must a final call be made on the
* {@link #matches(java.lang.reflect.Method, Class, Object[])} method at
* runtime even if the 2-arg matches method returns {@code true}?
* <p>Can be invoked when an AOP proxy is created, and need not be invoked
* again before each method invocation,
* @return whether or not a runtime match via the 3-arg
* {@link #matches(java.lang.reflect.Method, Class, Object[])} method
* is required if static matching passed
*/
boolean isRuntime();
/**
* Check whether there a runtime (dynamic) match for this method,
* which must have matched statically.
* <p>This method is invoked only if the 2-arg matches method returns
* {@code true} for the given method and target class, and if the
* {@link #isRuntime()} method returns {@code true}. Invoked
* immediately before potential running of the advice, after any
* advice earlier in the advice chain has run.
* @param method the candidate method
* @param targetClass the target class
* @param args arguments to the method
* @return whether there's a runtime match
* @see MethodMatcher#matches(Method, Class)
*/
boolean matches(Method method, Class<?> targetClass, Object... args);
}
Spring支持两种类型的MethodMatcher
,如上面isRuntime
确定MethodMatcher
是静态的还是动态,若方法返回值为true则表示动态的否则将会是静态的。对于静态的切入点Spring会对每一个目标类的方法调用一次MethodMatcher
的 matches
方法并将返回的返回值进行缓存。这样后面方法再次调用的时候便会取这个缓存。对于动态的切入点Spring会每一次调用matches
方法。上面我们可以看到matches
重载方法中 matches(Method method, Class<?> targetClass, Object… args) 这个可以对方法的参数进行检查以确定目标方法是否适用于通知,例如可以用这个方法实现:当参数是一个String类型且以execute字符串开头的目标方法才适用于通知。
Spring提供了如下Pointcut
接口的实现如下图所示:接下来将对其中几个重要的实现进行演示说明。
2.1 NameMatchMethodPointcut
在创建切入点的时候我们可以指定匹配方法名然后使通知在这些匹配的方法上执行,这个时候就可以使用NameMatchMethodPointcut
这个类下面是这个类的简单实现如下所示:
继续使用之前的SimpleBeforeAdvice
类作为通知其相关测试类方法代码如下:
@Test
public void testNameMatchMethodPointcut() {
NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut();
// 匹配add方法
pointcut.addMethodName("add");
// 匹配sub方法
pointcut.addMethodName("sub");
// 使用默认的advisor
DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor(pointcut, new SimpleBeforeAdvice());
ProxyFactory proxyFactory = new ProxyFactory();
proxyFactory.addAdvisor(advisor);
proxyFactory.setTarget(new CalculatorImpl());
Calculator proxy = (Calculator) proxyFactory.getProxy();
int add = proxy.add(1, 5);
log.info("加法计算值:{}", add);
double divide = proxy.divide(10, 10);
log.info("除法计算值:{}", divide);
}
测试方法输出结果如下所示:
before......advice....start
执行的方法是:add
执行的参数是:[1, 5]
执行的对象是:com.codegeek.aop.day1.CalculatorImpl@6cc558c6
before......advice...end
2020-06-01 23:43:17 [main] [INFO] [PointCutTest.java:34] 加法计算值:6
2020-06-01 23:43:17 [main] [INFO] [PointCutTest.java:36] 除法计算值:1.0
可以看到``NameMatchMethodPointcut` 这个类的addMethodName 方法添加了两个方法,然后才会有前置通知运行只会匹配上述两个方法。所以Calaulator这个类的divide方法并没有执行前置通知。
2.2 JdkRegexpMethodPointcut
上面介绍了NameMatchMethodPointcut
类可以对指定的方法进行匹配,但是一个个添加也确实麻烦一些。如果使用正则匹配是不是更方便一些呢?例如想匹配以指定前缀开头的所有方法呢?我们还是使用之前的前置通知类测试代码如下所示:
public void testJdkRegexPointCut() {
JdkRegexpMethodPointcut pointcut = new JdkRegexpMethodPointcut();
// 设置匹配所以以get开头的方法
pointcut.setPattern(".*get");
DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor(pointcut, new SimpleBeforeAdvice());
ProxyFactory proxyFactory = new ProxyFactory();
proxyFactory.addAdvisor(advisor);
proxyFactory.setTarget(new CalculatorImpl());
Calculator proxy = (Calculator) proxyFactory.getProxy();
int add = proxy.add(1, 5);
log.info("加法计算值:{}", add);
double divide = proxy.divide(10, 10);
log.info("除法计算值:{}", divide);
}
测试运行结果如下:
2020-06-02 00:16:05 [main] [INFO] [PointCutTest.java:52] 加法计算值:6
2020-06-02 00:16:05 [main] [INFO] [PointCutTest.java:54] 除法计算值:1.0
我们可以看到前置通知并没有执行,这是因为我们执行的方法并没有匹配到切点通知的正则。
2.3 DyanmicMethodMatcherPointcut
上面介绍两种切点方式都是静态切入点的实现,下面将演示如何使用动态方法切入点,我们设置当调用Calculator
的 add 方法 参数之和大于50才执行相应的通知。
新增DynamicMethodMatcherPointcut类实现,需要注意的是需要重写matches 方法。
public class SimpleDynamicMethodPointcut extends DynamicMethodMatcherPointcut {
/**
* 此抽象方法必须被重写
*
* @param method
* @param targetClass
* @param args
* @return
*/
@Override
public boolean matches(Method method, Class<?> targetClass, Object... args) {
// 匹配add方法
Integer sum = 0;
for (Object arg : args) {
Integer a = (Integer) arg;
sum += a;
}
if (method.getName().equals("add") && sum > 50) return true;
return false;
}
}
测试类如下所示:
@Test
public void testDynamicMethod() {
SimpleDynamicMethodPointcut pointcut = new SimpleDynamicMethodPointcut();
DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor(pointcut, new SimpleBeforeAdvice());
ProxyFactory proxyFactory = new ProxyFactory();
proxyFactory.addAdvisor(advisor);
proxyFactory.setTarget(new CalculatorImpl());
Calculator proxy = (Calculator) proxyFactory.getProxy();
System.out.println();
int add = proxy.add(55, 5);
log.info("加法计算值:{}", add);
}
可以看到当方法参数大于50即执行了前置通知如下所示:
before......advice....start
执行的方法是:add
执行的参数是:[55, 5]
执行的对象是:com.codegeek.aop.day1.CalculatorImpl@132e0cc
before......advice...end
2020-06-02 00:39:46 [main] [INFO] [PointCutTest.java:67] 加法计算值:60
当我们将方法的参数改成如下:
int add = proxy.add(5, 5);
测试运行结果如下:
2020-06-02 00:41:39 [main] [INFO] [PointCutTest.java:67] 加法计算值:10
可以很清晰看到当方法入参没有满足参数之和大于50就不会执行前置通知方法。
2.4 AspectJExpressionPointcut
Spring也内置了基于AspectJ切入点表达式支持的类,如果需要使用AspectJ切入点表达式需要在项目中添加如下依赖:
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>1.8.10</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.10</version>
</dependency>
测试类代码如下所示:
@Test
public void testAspectExpressionPointcut() {
AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
pointcut.setExpression("execution(* *..aop.*day1..*(..))");
DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor(pointcut, new SimpleBeforeAdvice());
ProxyFactory proxyFactory = new ProxyFactory();
proxyFactory.addAdvisor(advisor);
proxyFactory.setTarget(new CalculatorImpl());
Calculator proxy = (Calculator) proxyFactory.getProxy();
System.out.println();
int add = proxy.add(5, 5);
log.info("加法计算值:{}", add);
}
输出结果如下:
before......advice....start
执行的方法是:add
执行的参数是:[5, 5]
执行的对象是:com.codegeek.aop.day1.CalculatorImpl@5c44c582
before......advice...end
2020-06-02 09:18:42 [main] [INFO] [PointCutTest.java:82] 加法计算值:10
可以使用AspectJExpressionPointcut的setExpression方法设置匹配类方法规则, 上面测试类的表达式意味着通知只在包含了aop包下任意一个包含day1的子包的任何类的任何方法。
2.5 AnnotationMatchingPointcut
如果在某些类的某些方法添加了指定的注解,如果需要基于自己的注解来实现特定的通知需要使用到AnnotationMatchingPointcut 类的支持,接下来将演示这个类的使用:
首先定一个注解:
@Target({ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @ interface AdviceRequired {
}
然后我们将其添加Calculator实现类的sub方法上:
@Service
public class CalculatorImpl implements Calculator {
@Override
public int add(int a, int b) {
return a + b;
}
@Override
@AdviceRequired
public int sub(int a, int b) {
return a - b;
}
@Override
public double divide(int a, int b) {
return a / b;
}
}
测试类代码如下:
@Test
public void testAnnotationPointcut() {
AnnotationMatchingPointcut pointcut = AnnotationMatchingPointcut.forMethodAnnotation(AdviceRequired.class);
DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor(pointcut,new SimpleBeforeAdvice());
ProxyFactory proxyFactory = new ProxyFactory();
proxyFactory.setTarget(new CalculatorImpl());
proxyFactory.addAdvisor(advisor);
Calculator proxy = (Calculator) proxyFactory.getProxy();
int sub = proxy.sub(5, 5);
log.info("减法计算值:{}", sub);
int add = proxy.add(5, 5);
log.info("加法计算值:{}", add);
}
运行结果如下所示:
before......advice....start
执行的方法是:sub
执行的参数是:[5, 5]
执行的对象是:com.codegeek.aop.day1.CalculatorImpl@600b90df
before......advice...end
2020-06-02 09:58:26 [main] [INFO] [PointCutTest.java:95] 减法计算值:0
我们可以清晰看到加了@AdviceRequired注解的sub方法实现了通知执行,而add方法并不会执行该通知。
3.总结
在Spring AOP中,有3个常用的概念,Advices、Pointcut、Advisor,解释如下,
Advices:表示一个method执行前或执行后的动作。
Pointcut:表示根据method的名字或者正则表达式去拦截一个method。
Advisor:Advice和Pointcut组成的独立的单元,并且能够传给proxy factory 对象。
以上代码均可在 codegeekgao.git 下载查看。