一 简介
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 |