f10@t's blog

Spring基础学习 - AOP机制

字数统计: 6.1k阅读时长: 26 min
2020/05/09

AOP的概念

AOP(Aspect Oriented Programming,面向切面的编程)是面向对象编程(OOP)的补充,具有特定的一组概念和术语,通过预编译方式和运行期间动态代理实现程序功能的统一维护的一种技术。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高开发效率

核心概念

  • 连接点(Joinpoint):连接点是应用程序执行期间明确的一个点,典型例子包括方法调用、方法调用本身、类初始化和对象实例化。
  • 通知(Advice):在特定连接点执行的代码叫做通知,由类中的方法进行定义,如前置通知和后置通知分别在连接点之前和之后执行。
  • 切入点(Pointcut):定义何时执行通知的连接点集合,通过创建切入点可以更细致地控制如何将通知应用于应用程序中。
  • 切面(Advisor):指封装在类中的通知切入点的组合,其定义了应该包含在应用程序中的逻辑及其应该执行的位置。
  • 织入(weaving):指在适当位置插入切面到应用程序代码中的过程。通常在生成时完成。
  • 目标对象(Target):被AOP进程修改的对象称为目标对象,也被称为通知对象。
  • 引入(Introduction):指通过引入其他方法或字段来修改对象结构的过程。可以通过AOP来使任何对象实现特定的接口,而无须让对象的类显式地实现该接口。

这些概念描述起来比较难理解,下面会通过例子进行说明。

AOP有两种类型:

  • 静态AOP:通过修改应用程序的实际字节码并根据需要来更改和扩展应用程序代码的织入过程。
    • 优点:无需确定何时执行通知;
    • 缺点:任何修改都要求重新编译应用程序;
  • 动态AOP:Spring AOP就是动态的例子,织入的过程在运行时动态执行。
    • 优点:可以轻松修改应用程序的整个切面集而无须重新编译主代码;
    • 缺点:性能一般不如静态AOP;

一个简单实例

现在假设要输出James Bond!这个字符串,我们先写一个类,他只能输出Bond

1
2
3
4
5
6
7
package start;

public class Agent {
public void speak() {
System.out.print("Bond");
}
}

显然我们还差剩下的两部分,我们再写一个类,这个类实现了MethodInterceptor接口,意为方法拦截器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package start;

import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;

/**
* MethodInterceptor是一个标准的AOP Alliance接口,用于实现方法调用连接点的环绕通知
*/
public class AgentDecorator implements MethodInterceptor {
public Object invoke(MethodInvocation methodInvocation) throws Throwable {
System.out.print("James ");

Object retVal = methodInvocation.proceed();

System.out.println("!");
return retVal;
}
}

先输出James再调用proceed方法再输出!,该方法会执行下一个返回拦截器,文档描述如下:

1
2
3
4
5
6
7
/**
* Proceed to the next interceptor in the chain.
* <p>The implementation and the semantics of this method depends
* on the actual joinpoint type (see the children interfaces).
* @return see the children interfaces' proceed definition
* @throws Throwable if the joinpoint throws an exception
*/

最后写主函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package start;

import org.springframework.aop.framework.ProxyFactory;

public class AgentAOPDemo {
public static void main(String[] args) {
Agent target = new Agent();

ProxyFactory pf = new ProxyFactory();
// 代理工厂织入AgentDecorator
pf.addAdvice(new AgentDecorator());
// 指定织入target
pf.setTarget(target);

// 生成代理
Agent proxy = (Agent) pf.getProxy();

target.speak(); // 普通调用
System.out.println();
proxy.speak();
}
}

这里就涉及到了上面说到的几个概念:目标对象时Agent实例、将我们写好的类(一个通知)织入ProxyFactory`代理工厂,再指定织入目标,最后生成代理并调用方法:

我们写的类实际上用于方法调用连接点环绕通知

Spring AOP核心原理

Spring AOP是AOP实现的一种。其核心架构基于代理,上面的例子我们也看到,想要创建一个类的被通知(也有认为advice翻译为加强,我的理解是修饰)实例时,必须使用ProxyFactory创建这个类的代理实例,并且应当向该工厂提供想要织入到代理中的所有。

上面只是为了演示效果和原理,这种使用ProxyFactory创建AOP代理的单纯方法一般不会使用,我们只需要使用Spring提供的ProxyFactoryBean类、AOP命名空间和@AspectJ注解就可以执行了。

运行的时候,Spring会分析上下文中定义的bean的横切点,再动态的生成代理bean,最终由代理执行。

注意:spring中,若被通知对象实现了一个接口,则代理对象实现为JDK动态代理实现;否则代理对象实现为CGLIB。

我们重点关注下这几个概念:JoinPoint,Advisor,Advice。

  • Spring连接点:Spring AOP中只支持一种连接点类型:方法调用。即指类里可以被增强的方法。虽然少于其他AOP实现,但是方法调用是最常用的。

  • Spring切面:Spring AOP中实现了Advisor的类就是切面,它有两个子接口:PointcutAdvisorIntroductionAdvisor

  • 通知:拦截到连接点后加入的代码就是通知,Spring支持六种通知类型,如下表:

环绕通知我们在上面的例子中就见到了。下面我们通过代码学习前置通知、后置返回通知、环绕通知和异常通知。

不同Advice类型

前置通知(MethodBeforeAdvice)

这是Spring中最有用的通知类型之一,先说说它的作用:可以在一个方法执行之前进行一定的自定义步骤,那好比说访问资源前的检查操作。通过实现MethodBeforeAdvice接口中的before方法即可。下面我们写一个简单接口和类用于测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 接口
package advice;

public interface Singer {
void sing();
}

// 实现类
package advice.MethodBefore;

import advice.Singer;

public class Guitarist implements Singer {
private String lyric = "You're gonna live forever in me.";

@Override
public void sing() {
System.out.println(lyric);
}
}

然后我们写主函数类,并实现上述接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package advice.MethodBefore;

import org.springframework.aop.MethodBeforeAdvice;
import org.springframework.aop.framework.ProxyFactory;

import java.lang.reflect.Method;

public class SimpleBeforeAdvice implements MethodBeforeAdvice {
@Override
public void before(Method method, Object[] args, Object target) throws Throwable {
System.out.println("Before '" + method.getName() + "', tune guitar.");
}

public static void main(String[] args) {
Guitarist lzw = new Guitarist();

ProxyFactory pf = new ProxyFactory();
pf.addAdvice(new SimpleBeforeAdvice());
pf.setTarget(lzw);

Guitarist proxy = (Guitarist) pf.getProxy();

proxy.sing();
}
}

先设置了一个目标类,创建代理工厂类、将通知织入、设置目标对象、获取代理、执行。

那覆写的before方法我们输出提示信息就好了,结果:

所谓前置,顾名思义,很好理解。

前置通知在安全性方面的作用很不错,你可以通过实现MethodBefore接口来实现客户端访问某一个方法前的检查工作,将安全方面的处理工作交由before方法完成,这实现了不同业务的分离。

后置返回通知(AfterReturningAdvice)

后置返回也是顾名思义,在被指定的连接点方法执行后再执行,但是因为他不能对连接点方法的返回值做修改,所以一般用于方法返回值异常时,做一些错误检查工作。下面先写一个简单例子看下它的执行流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package advice.AfterReturn;

import advice.Guitarist;
import org.springframework.aop.AfterReturningAdvice;
import org.springframework.aop.framework.ProxyFactory;

import java.lang.reflect.Method;

public class SimpleAfterReturningAdvice implements AfterReturningAdvice {
public static void main(String[] args) {
Guitarist guitarist = new Guitarist();

ProxyFactory pf = new ProxyFactory();
pf.setTarget(guitarist);
pf.addAdvice(new SimpleAfterReturningAdvice());

Guitarist proxy = (Guitarist) pf.getProxy();
proxy.sing();
}

@Override
public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable {
System.out.println("After '" + method.getName() + "' put down guitar");
}
}

一样的步骤,只不过实现接口为AfterReturningAdvice,输出:

可以看到是再连接点方法执行后输出的。

环绕通知(MethodInterceptor)

环绕通知的功能近似于将前置通知和后置返回通知组合起来,他是可以对返回的值进行修改的,此外它还可以阻止方法执行。Spring很多功能都是使用方法拦截器创建的,如远程代理支持和事务管理功能。

我们还是通过代码来说明:

1
2
3
4
5
6
7
8
9
10
11
12
13
package advice.Around;

public class WorkerBean {
public void doSomeWork(int noOfTimes) {
for (int i = 0; i < noOfTimes; i++) {
work();
}
}

private void work() {
System.out.println("");
}
}

这是一个简单类,他的作用是执行传入的参数noOfTimes次的println操作。我们现在想实现程序性能的考量,写了下面这个类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
package advice.Around;

import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.springframework.util.StopWatch;

import java.lang.reflect.Method;

public class ProfilingInterceptor implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
StopWatch sw = new StopWatch();
// 开始计时
sw.start();

// 调用方法
Object returnValue = invocation.proceed();
// 暂停计时
sw.stop();
dumpInfo(invocation, sw.getTotalTimeMillis());
return returnValue;
}

private void dumpInfo(MethodInvocation invocation, long ms) {
//获取方法调用的各类信息
Method m = invocation.getMethod();
//获取目标对象
Object target = invocation.getThis();
Object[] arguments = invocation.getArguments();

System.out.println("Executed method " + m.getName());
System.out.println("On object of type: " + target.getClass().getName());

System.out.println("With arguments: ");
for (Object argument : arguments) {
System.out.print(" > " + argument);
}
System.out.println("\n");
System.out.println("Took: " + ms + " ms");
}
}

ProfilingInterceptor类实现了MethodInterceptor接口,并复写了其中的invoke方法,可以做到方法的拦截,首先使用StopWatch类计时,再调用方法,最后停止计时并输出此次方法执行的数据。

最后是这个Demo的主函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package advice.Around;

import org.springframework.aop.framework.ProxyFactory;

public class ProfilingDemo {
public static void main(String[] args) {
WorkerBean bean = getWorkerBean();
bean.doSomeWork(100);
}

private static WorkerBean getWorkerBean() {
WorkerBean bean = new WorkerBean();
ProxyFactory pf = new ProxyFactory();
pf.setTarget(bean);
pf.addAdvice(new ProfilingInterceptor());

return (WorkerBean)pf.getProxy();
}
}

我们调用代理类的doSomeWork方法,程序输出如下:

从中我们可以看出:函数名为doSomeWork,其所属对象为WorkerBean类型,他的参数为100,总共执行花费了15ms时间。最终我们实现了拦截方法、并再其执行前后运行我们的指定操作,这就是环绕通知。

后置异常通知(ThrowsAdvice)

异常同时类似于后置返回通知,原因是它也在连接点之后执行,但多了一个条件:只有方法抛出异常它才会被触发,此外,他也不能控制程序的执行,但是可以更改抛出的异常类型。可以用于提供集中哦式错误日志记录从而减少再应用程序中散步的错误日志代码数量。

依旧用代码进行说明:

1
2
3
4
5
6
7
8
9
10
11
package advice.Throws;

public class ErrorBean {
public void errorProneMethod() throws Exception {
throw new Exception("Generic Exception");
}

public void otherErrorProneMethod() throws Exception {
throw new IllegalArgumentException("IllegalArgument Exception");
}
}

这是一个简单的java类,其中包含了两种方法,它们分别会抛出不同类型的异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
package advice.Throws;

import org.springframework.aop.ThrowsAdvice;
import org.springframework.aop.framework.ProxyFactory;

import java.lang.reflect.Method;

public class ShowThrowsAdvice implements ThrowsAdvice {
public static void main(String[] args) {
ErrorBean bean = new ErrorBean();
ProxyFactory pf = new ProxyFactory();
pf.setTarget(bean);
pf.addAdvice(new ShowThrowsAdvice());
ErrorBean proxy = (ErrorBean) pf.getProxy();

try {
proxy.errorProneMethod();
} catch (Exception ignored) {

}

try {
proxy.otherErrorProneMethod();
} catch (Exception ignored) {

}
}

public void afterThrowing(Exception e) throws Throwable {
System.out.println("* * *");
System.out.println("Generic Exception Capture");
System.out.println("Caught: " + e.getClass().getName());
System.out.println("* * *\n");
}

public void afterThrowing(Method method, Object args, Object target,
IllegalArgumentException e) throws Throwable {
System.out.println("* * *");
System.out.println("IllegalArgumentException Capture");
System.out.println("Caught: " + e.getClass().getName());
System.out.println("Method: " + method.getName());
System.out.println("* * *\n");
}
}

这是主类,先来看一下ThrowsAdvice接口:

1
2
3
public interface ThrowsAdvice extends AfterAdvice {
//继承了AfterAdvice接口,所以属于后置通知类型
}

这个接口中什么方法都没有,看看它的注释:

1
2
3
4
There are not any methods on this interface, as methods are invoked by
reflection. Implementing classes must implement methods of the form:

void afterThrowing([Method, args, target], ThrowableSubclass);

即,这个接口的确没有方法,因为方法会通过反射机制被自动调用,且实现了这个接口的方法必须要实现void afterThrowing([Method, args, target], ThrowableSubclass);格式的函数,其中,前三个类型的参数是可选的(The first three arguments are optional, and only useful if we want further information about the joinpoint),最后那个参数是必须的。

因为在我们的ErrorBean类中它抛出了两个异常,所以我们需要定义两个afterThrowing方法来分别捕获并处理。上面两个方法本质没区别,第二个对Method对象信息稍加输出作为演示:

可以看到在两个方法抛出各自的异常后,触发了两个处理异常的函数。

根据上述的特点,后置异常通知可以实现对整个Exception层次结构的重新划分、处理,并收集异常日志记录。

顾问Advisor和切入点Pointcut

上面说的四个核心原理都是Advice,但其功能过于简单,且只能将通知织入到目标类的所有目标方法中,无法完成将通知织入到指定目标方法中。

Spring提供的(Advisor)可以完成更为复杂的切面织入功能。我们通过一个例子来说明。

为什么翻译成这个鬼名字。。。。

前面也说到了,Advice织入到一个类的所有方法中,如果现在我只想将通知织入到其中的一个方法而不是所有方法中呢?下面看一个例子,我们定一个简单类以及公共接口:

1
2
3
4
5
package pointCut.Static;

public interface Singer {
void sing();
}
1
2
3
4
5
6
7
8
9
10
11
12
package pointCut.Static;

public class GoodGuitarist implements Singer {
@Override
public void sing() {
System.out.println("Who says I can't be free");
}

public void good() {
System.out.println("I am a good guitarist.");
}
}

我们先使用环绕通知的方式来感受下Advice类型的切面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package pointCut.Static;

import org.springframework.aop.framework.ProxyFactory;

public class OldAdvice {
public static void main(String[] args) {
ProxyFactory pf = new ProxyFactory();
GoodGuitarist goodGuitarist = new GoodGuitarist();

pf.addAdvice(new SimpleAdvice());
pf.setTarget(goodGuitarist);
GoodGuitarist proxyOne = (GoodGuitarist) pf.getProxy();

proxyOne.sing();
proxyOne.good();
}
}

通知类如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package pointCut.Static;

import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;

public class SimpleAdvice implements MethodInterceptor {
// 使用环绕通知来体现效果
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
System.out.println(">> Invoking " + invocation.getMethod().getName());
Object retVal = invocation.proceed();
System.out.println(">> Done\n");
return retVal;
}
}

可以看到两个方法都被织入了通知,那如果我只想让sing方法被织入通知呢?这个时候我们就需要使用到切入点并与通知一起组合成顾问Advisor,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package pointCut.Static;

import org.springframework.aop.ClassFilter;
import org.springframework.aop.support.StaticMethodMatcherPointcut;

import java.lang.reflect.Method;

public class SimpleStaticPointcut extends StaticMethodMatcherPointcut {

@Override
public boolean matches(Method method, Class<?> targetClass) {
// 方法名为sing
return ("sing".equals(method.getName()));
}
}

主函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package pointCut.Static;

import org.aopalliance.aop.Advice;
import org.springframework.aop.Advisor;
import org.springframework.aop.Pointcut;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.aop.support.DefaultPointcutAdvisor;

public class StaticPointcutDemo {
public static void main(String[] args) {
GoodGuitarist johnMayer = new GoodGuitarist();

Pointcut pc = new SimpleStaticPointcut();
Advice advice = new SimpleAdvice();
Advisor advisor = new DefaultPointcutAdvisor(pc, advice);

ProxyFactory pf = new ProxyFactory();
pf.addAdvisor(advisor);
pf.setTarget(johnMayer);
GoodGuitarist proxyOne = (GoodGuitarist) pf.getProxy();

proxyOne.sing();
proxyOne.good();
}
}

再次运行:

image-20200530233959956

可以看到这次good函数就没有被通知环绕,这就是使用了Advisor的区别。上述方法为创建静态切入点,通过使用切入点pointcut可以配置通知适用的方法,而无须将相关代码放入通知中,提高了通知的可重用性。

可以看到Advice + Pointcut = Advisor,使用DefaultPointcutAdvisor(advice, pointcut)函数就可以完成这个组装工作。

下面来看三个重要的接口:Pointcut、ClassFilter、MethodMatcher

  • pointcut

A pointcut is composed of a {@link ClassFilter} and a {@link MethodMatcher}.

该接口定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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();


/**
* Canonical Pointcut instance that always matches.
*/
Pointcut TRUE = TruePointcut.INSTANCE;

}

其中有两个get方法分别用来获取ClassFilter和Method,下面会对这两个进行说明,这里只要知道一个切入点包含了这两个东西就可以了。

  • ClassFilter

Filter that restricts matching of a pointcut or introduction to a given set of target classes.

该接口的定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@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);


/**
* Canonical instance of a ClassFilter that matches all classes.
*/
ClassFilter TRUE = TrueClassFilter.INSTANCE;

}

其中定义了一个方法matches,参数为一个Class实例,用来判断一个切入点时候是否和这个Class实例匹配,返回值是boolean类型。

  • MethodMatcher

Part of a {@link Pointcut}: Checks whether the target method is eligible(合法) for advice.

该接口定义如下,我直接把说明也写进去了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
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 (may be {@code null}, in which case
* the candidate class must be taken to be the method's declaring class)
* @return whether or not this method matches statically
*/
//使用静态判断的方法来判断传入的Method方法实例是否匹配目标targetClass实例
boolean matches(Method method, @Nullable 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
*/
//判断一个MethodMatcher实例是动态的(true)还是静态的(false)
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 (may be {@code null}, in which case
* the candidate class must be taken to be the method's declaring class)
* @param args arguments to the method
* @return whether there's a runtime match
* @see MethodMatcher#matches(Method, Class)
*/
//使用动态判断的方法来判断传入的Method方法实例是否匹配目标targetClass实例,可以传参数
boolean matches(Method method, @Nullable Class<?> targetClass, Object... args);


/**
* Canonical instance that matches all methods.
*/
MethodMatcher TRUE = TrueMethodMatcher.INSTANCE;

}

这里需要额外说明一下MethodMatcher接口所谓的静态和动态执行,简单来说的话代码注释已经很清楚了:

1
2
A MethodMatcher may be evaluated <b>statically</b> or at <b>runtime</b> (dynamically).
Static matching involves method and (possibly) method attributes. Dynamic matching also makes arguments for a particular call available, and any effects of running previous advice applying to the joinpoint.

在这个接口中我们可以看到matches被覆写了一次,两个分别为静态和动态执行的函数,而isRuntime()函数就是用来判断MethodMatcher是静态的还是动态的(false为静态,true为动态)。

典型使用动态执行的场景是你需要对函数执行的参数进行检查的是否才用,一般建议使用静态切入点(因为开销小,没有这些额外检查),但是如果Advice增加了大量的开销,则可以使用动态切入点来避免不必要的通知调用。

可以通过下面的UML图大致了解结构(图丑不要吐槽哈哈哈)

上面这些接口你可以不用实现,在Spring中已经有了一些场景啊的接入点实现,共有八个,其中2个分别用于创建动态和静态的便捷类的抽象类,剩下6个用来完成如下操作:

  • 构成多个切入点
  • 处理控制流切入点
  • 执行简单的基于名称的匹配
  • 使用正则表达式定义切入点
  • 使用AspectJ表达式定义切入点
  • 定义在类或方法级别查找特定注解的切入点

总结如下(图来自《Spring5高级编程(第五版)》):

在上面的Demo中我们就是用了StaticMethodMatcherPointcut来演示了静态切入点,并使用了matches()方法对指定类对象的指定方法进行了过滤。

上面这八个这里不做过多演示,只对StaticMethodMatcherPointcut和DynamicMethodMatcherPointcut进行说明。

使用静态切入点(StaticMethodMatcherPointcut)

这里不再过多叙述,直接看上面的Demo就可以了,补充一点是Demo中只实现了MethodMatcher接口中的matches方法,没有覆写StaticMethodMatcherPointcut中的getClassFilter方法,该方法可以用来判断一个类的类型是否符合要求,比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Override
public ClassFilter getClassFilter() {
//使用lambda
return clazz -> (clazz == GoodGuitarist);
}

//不适用lambda
@Override
public ClassFilter getClassFilter() {
return new ClassFilter() {
@Override
public boolean matches(Class<?> clazz) {
return (clazz == GoodGuitarist.class);
}
};
}

这样只有GoodGuitarist的实例才会通过匹配。

使用动态切入点(DynamicMethodMatcherPointcut)

直接上代码进行说明,我们先定义一个简单的类:

1
2
3
4
5
6
7
8
9
10
11
12
13
package pointCut.Dynamic;

public class SimpleBean {

public void foo(int x) {
System.out.println("Invoked foo() with: " + x);
}

public void bar() {
System.out.println("Invoked bar()");
}

}

我们定义了两个方法,我们现在希望只通知foo函数当且仅当其参数x等于100时,我们定义一个动态的切入点,继承DynamicMethodMatcherPointcut就可以了(汗...虽说名字好理解,但也太长了吧)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package pointCut.Dynamic;

import org.springframework.aop.ClassFilter;
import org.springframework.aop.support.DynamicMethodMatcherPointcut;

import java.lang.reflect.Method;

public class SimpleDynamicPointcut extends DynamicMethodMatcherPointcut {

@Override
public ClassFilter getClassFilter() {
return clazz -> (clazz == SimpleBean.class);
}

@Override
public boolean matches(Method method, Class<?> targetClass) {
System.out.println("Static check for " + method.getName());
return ("foo".equals(method.getName()));
}

/*
判断调用方法foo的参数是否等于100,等于则返回false
*/
@Override
public boolean matches(Method method, Class<?> targetClass, Object... args) {
System.out.println("Dynamic chech for " + method.getName());
int x = (Integer) args[0];
return (x != 100);
}
}

我们覆写DynamicMethodMatcher中的matches方法,这部分属于静态检查,判断是否方法的名字是foo函数,确保只有foo函数被调用才会被通知。下来我们实现了MethodMatcher接口中的针对动态检查的matches方法,用于判断参数是否等于100。另外我们覆写了DynamicMethodMatcherPointcut中的getClassFilter方法确保传入的对象是SimpleBean的实例。主函数如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package pointCut.Dynamic;

import org.springframework.aop.Advisor;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.aop.support.DefaultPointcutAdvisor;
import pointCut.Static.SimpleAdvice;

public class DynamicPointcutDemo {
public static void main(String[] args) {
ProxyFactory pf = new ProxyFactory();
SimpleBean simpleBean = new SimpleBean();
SimpleDynamicPointcut pointcut = new SimpleDynamicPointcut();
SimpleAdvice advice = new SimpleAdvice();
Advisor advisor = new DefaultPointcutAdvisor(pointcut, advice);

pf.addAdvisor(advisor);
pf.setTarget(simpleBean);

SimpleBean proxyBean = (SimpleBean) pf.getProxy();
proxyBean.bar();
proxyBean.foo(10);
proxyBean.foo(50);
proxyBean.foo(100);
}
}

步骤也很简单,获取通知实例、切入点实例、目标实例、代理工厂实例,将通知和切入点结合为切面,再将切面织入代理工厂、目标对象织入代理工厂,最终获取代理,并执行方法,结果如下:

从结果中我们看出对两个方法都进行了静态的检查,注意:可以看到foo方法被进行了两次静态检查,其中红框给里这些都是初始化的检查,其余都是被调用的时候进行的检查,因为我们先调用的是bar函数,所以在黄框这里对bar函数进行了静态检查,但是发现它不是foo函数,则不会通知,调用就完事了。下来的三个蓝框为foo函数自然是通过了静态检查,其中10和50因为不等于100所以在动态检查处matches方法返回true,所以通过了检查->被通知。

此外可以看出静态检查对于调用阶段只进行一次,我们调用了三次foo函数,可以看到只执行了一次静态检查。

动态检查提供了很好的检查机制,但是也可以看到,对于同一个实例你调用一次他就检查一次,不像静态检查一次就完事了,所以带来了开销,所以建议一般使用静态检查,在必要的时候再用动态检查。

参考

  • https://www.jianshu.com/p/5b9a0d77f95f
  • https://www.cnblogs.com/zhangzongle/p/5944906.html
  • 《Spring5高级编程(第五版)》

CATALOG
  1. 1. AOP的概念
    1. 1.1. 核心概念
    2. 1.2. 一个简单实例
  2. 2. Spring AOP核心原理
    1. 2.1. 不同Advice类型
      1. 2.1.1. 前置通知(MethodBeforeAdvice)
      2. 2.1.2. 后置返回通知(AfterReturningAdvice)
      3. 2.1.3. 环绕通知(MethodInterceptor)
      4. 2.1.4. 后置异常通知(ThrowsAdvice)
    2. 2.2. 顾问Advisor和切入点Pointcut
      1. 2.2.1. 使用静态切入点(StaticMethodMatcherPointcut)
      2. 2.2.2. 使用动态切入点(DynamicMethodMatcherPointcut)
  3. 3. 参考