Spring切面可以应用五种类型的通知:
- 前置通知(Before):在目标方法被调用之前调用通知方法
- 后置通知(After):在目标方法完成之后调用通知方法,此时不会关心目标方法是否执行成功或者抛出异常
- 返回通知(After-returning):在目标方法成功执行之后调用通知方法
- 异常通知(After-throwing):在目标方法抛出异常后调用通知方法
- 环绕通知(Around):通知包裹了被通知的方法,在被通知的方法调用之前和调用之后分别执行自定义的行为
(2)连接点(Join point):
连接点是在应用执行过程中能够插入切面的一个点,这个点可以是调用方法时,抛出异常时甚至修改一个字段时,切面代码可以利用这些点插入到应用的正常流程之中,并添加新的行为
(3)切点(Pointcut):
如果说通知定义了切面的“什么”和“何时”的话,那么切点就定义了“何处”,切点的定义会匹配通知所要织入的一个或多个连接点。我们通常使用明确的类和方法名称,或是利用正则表达式定义所匹配的类和方法名称来指定这些切点
(4)切面(Aspect):
切面是通知和切点的结合,通知和切点共同定义了切面的全部内容:它是什么,在何时和在何处完成其功能
(5)织入(Weaving):
织入是把切面应用到目标对象并创建新的代理对象的过程。切面在指定的连接点被织入到目标对象中,在目标对象的生命周期里有多个点可以进行织入:
- 编译期:切面在目标类编译时被织入,这种方式需要特殊的编译器。AspectJ的织入编译器就是就是以这种方式织入切面的
- 类加载期:切面在目标类加载到JVM时被织入
- 运行期:切面在应用运行的某个时刻被织入。一般情况下,在织入切面时,AOP容器会为目标对象动态地创建一个代理对象。Spring AOP就是以这种方式织入切面的
注:如果更习惯于使用XML文件来定义AOP的话,可以参考我的这篇文章:https://www.zifangsky.cn/805.html
二 基本用法示例
在正式开始介绍Spring AOP的基本用法之前,首先定义一个测试接口和它的实现类:
i)Performence.java:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
package cn.zifangsky.pointcut;
/**
* 模拟音乐家演奏
* @author zifangsky
*
*/
public interface Performence {
public void play();
/**
* 带音乐家名字的演奏方法
* @param pianist 音乐家名字
*/
public void play(String pianist);
}
|
ii)PianoPerformence.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
|
package cn.zifangsky.pointcut;
import org.springframework.stereotype.Component;
@Component("piano")
public class PianoPerformence implements Performence {
@Override
public void play() {
System.out.println("开始演奏‘The Rain‘");
try {
Thread.sleep(2000); //模拟演奏
// throw new RuntimeException("测试");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@Override
public void play(String pianist) {
if(pianist != null){
System.out.println(pianist + " -->开始演奏");
}else{
System.out.println("开始演奏");
}
}
}
|
(1)第一个实例:
i)定义一个切面:
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
|
package cn.zifangsky.pointcut;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class Audience0 {
@Before("execution(* cn.zifangsky.pointcut.Performence.play(..))")
public void seat(){
System.out.println("坐下");
}
@Before("execution(* cn.zifangsky.pointcut.Performence.play(..))")
public void silence(){
System.out.println("保持安静");
}
@AfterReturning("execution(* cn.zifangsky.pointcut.Performence.play(..))")
public void applause(){
System.out.println("鼓掌");
}
@AfterThrowing("execution(* cn.zifangsky.pointcut.Performence.play(..))")
public void fail(){
System.out.println("表演失败");
}
}
|
在使用注解定义一个切面时,需要添加的注解是:@Aspect。同时下面方法上的几个注解的含义分别是:
- @Before:定义一个前置通知
- @AfterReturning:定义一个返回通知
- @AfterThrowing:定义一个异常通知
当然,根据我前面的介绍内容,这里没有介绍到的通知还有:
- @After:定义一个后置通知
- @Around:定义一个环绕通知
ii)使用@EnableAspectJAutoProxy注解启用自动代理:
如果使用JavaConfig的话,可以这样配置:
1
2
3
4
5
6
7
8
9
10
11
12
|
package cn.zifangsky.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
@Configuration
@EnableAspectJAutoProxy
@ComponentScan(basePackages="cn.zifangsky.pointcut")
public class ConcertConfig {
}
|
可以看出,这个类首先使用了@Configuration注解,表明这个类属于一个配置类。然后使用@EnableAspectJAutoProxy注解启用了AspectJ自动代理。最后是使用了@ComponentScan注解指定需要扫描哪些包中的注解,这里配置的就是上面定义的Audience0类所在的包
如果不想使用JavaConfig的话,可以在Spring的配置文件中这样配置:
1
2
|
<context:component-scan base-package="cn.zifangsky.pointcut" />
<aop:aspectj-autoproxy />
|
iii)基于JavaConfig的测试:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
package cn.zifangsky.test.base;
import javax.annotation.Resource;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import cn.zifangsky.config.ConcertConfig;
import cn.zifangsky.pointcut.Performence;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes={ConcertConfig.class})
public class TestAspect {
@Resource(name="piano")
Performence performence;
@Test
public void testPlay(){
performence.play();
}
}
|
输出如下:
1
2
3
4
|
坐下
保持安静
开始演奏‘The Rain‘
鼓掌
|
注:如果想要测试异常通知的效果的话,可以将我上面注释掉的 “throw new RuntimeException(“测试”);” 的注释去掉,再次测试即可
iv)基于xml配置的测试:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
package cn.zifangsky.test.base;
import javax.annotation.Resource;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import cn.zifangsky.pointcut.Performence;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:/context/context.xml")
public class TestAspect2 {
@Resource(name = "piano")
Performence performence;
@Test
public void testPlay() {
performence.play();
}
}
|
输出:略
(2)简化上面的切面写法:
在上面的切面定义的类中,可以看到每个具体的方法上面都有一个很长的“通知”表达式。其实,在这里是可以有简化写法的:
i)注释掉Audience0类的切面注解,即:
1
2
|
//@Aspect
//@Component
|
ii)定义一个新的切面:
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
|
package cn.zifangsky.pointcut;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
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
public class Audience1 {
/**
* 定义了一个切点,其值是一个切点表达式,含义是:
* 在这个类的这个方法的执行前和执行后触发下面定义的“通知”
* 前面的*表示任意返回值,后面的点则表示任意多个参数,也就是所有名为play的方法的任意重载的方法都会触发此通知
*/
@Pointcut("execution(* cn.zifangsky.pointcut.Performence.play(..))")
public void performance(){}
@Before("performance()")
public void seat(){
System.out.println("坐下");
}
@Before("performance()")
public void silence(){
System.out.println("保持安静");
}
@AfterReturning("performance()")
public void applause(){
System.out.println("鼓掌");
}
@AfterThrowing("performance()")
public void fail(){
System.out.println("表演失败");
}
}
|
从上面的代码可以看出,这里使用@Pointcut注解给被@Aspect注解标注的切面定义了一个可重用的切点。这样在使用其他通知时就可以直接在该切点商织入通知了,也就是使用被@Pointcut注解标注的 performance() 方法
注:这里的 performance() 方法的实际内容并不重要,在这里它实际上应该是空的。这个方法本身只是一个标志,供@Pointcut这个注解依附
iii)再次测试:
也就是再次运行TestAspect类的testPlay()方法。当然,输出结果跟上面一样,略
(3)环绕通知:
i)同样注释 Audience1 类的切面注解
ii)定义一个环绕通知:
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
|
package cn.zifangsky.pointcut;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class Audience2 {
@Pointcut("execution(* cn.zifangsky.pointcut.Performence.play(..))")
public void performance(){}
@Around("performance()")
public void countTimeMillis(ProceedingJoinPoint pJoinPoint){
try {
long date1 = System.currentTimeMillis();
pJoinPoint.proceed(); //将控制权移交给被通知方法
long date2 = System.currentTimeMillis();
System.out.println("执行方法耗时: " + (date2 - date1));
} catch (Throwable e) {
System.out.println("Around--表演失败");
}
}
}
|
从上面的代码可以看出,环绕通知跟前置通知和后置通知不同的是,它会在一个方法的执行之前和执行之后都会执行一些操作。关于这个通知方法,可以看到它接收了一个ProceedingJoinPoint对象作为参数。这个对象是必须要有的,因为我们要在通知中通过它来调用被通知的方法。通知方法中可以做任何的事情,当要将控制权交给被通知的方法时,它需要调用ProceedingJoinPoint 的 proceed()方法
iii)测试:
再次运行TestAspect类的testPlay()方法。最后输出如下:
1
2
|
开始演奏‘The Rain‘
执行方法耗时: 2000
|
(4)向通知方法中传递被通知方法的参数:
i)同样注释 Audience2 类的切面注解
ii)定义一个新的切面:
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
|
package cn.zifangsky.pointcut;
import java.util.HashMap;
import java.util.Map;
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
public class Audience3 {
private Map<String, Integer> playedTimes = new HashMap<>();
/**
* 参数为String的方法
* @param pianist 被通知方法的中的名为"pianist"的参数
*/
@Pointcut("execution(* cn.zifangsky.pointcut.Performence.play(String))"
+ "&& args(pianist)")
public void performance(String pianist){}
/**
* 前置通知
* @param pianist 被通知方法的中的名为"pianist"的参数
*/
@Before("performance(pianist)")
public void updateTimes(String pianist){
System.out.println(pianist + ":");
int currentCount = getPlayCount(pianist);
playedTimes.put(pianist, currentCount + 1);
}
/**
* 演奏次数统计
* @param pianist
* @return
*/
public int getPlayCount(String pianist){
return playedTimes.containsKey(pianist) ? playedTimes.get(pianist) : 0;
}
}
|
在定义切点时,指定了将有一个 String 类型的 play() 方法作为切点,同时其参数名是“pianist”
在下面定义具体的通知时,将获取到的被通知的方法中的“pianist”进行了计数处理。统计其演奏次数
iii)测试:
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
|
package cn.zifangsky.test.base;
import javax.annotation.Resource;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import cn.zifangsky.config.ConcertConfig;
import cn.zifangsky.pointcut.Audience3;
import cn.zifangsky.pointcut.Performence;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes={ConcertConfig.class})
public class TestAspect3 {
@Resource(name="piano")
Performence performence;
@Autowired
Audience3 audience3;
@Test
public void testPlayWithPianist(){
performence.play("肖邦");
performence.play("班得瑞");
performence.play("宗次郎");
performence.play("班得瑞");
performence.play("班得瑞");
performence.play("久石让");
performence.play("雅尼");
System.out.println("‘班得瑞‘演奏次数: " + audience3.getPlayCount("班得瑞"));
}
}
|
输出如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
肖邦:
肖邦 -->开始演奏
班得瑞:
班得瑞 -->开始演奏
宗次郎:
宗次郎 -->开始演奏
班得瑞:
班得瑞 -->开始演奏
班得瑞:
班得瑞 -->开始演奏
久石让:
久石让 -->开始演奏
雅尼:
雅尼 -->开始演奏
‘班得瑞‘演奏次数: 3
|