SpringBoot使用AOP的简单示例
有一个cd接口,其实体类用于播放歌曲,同时我们想在播放歌曲的时候记录每个曲目的播放次数。看起来,记录次数这个事和播放曲目是不相干的事情,当然,我们可以在每首歌曲播放完成之后记录,但是更好的办法是使用一个切面,切入到播放方法中,来完成这件事,这样可以减少无关逻辑对代码的侵入。
此程序分别使用了基于@Aspect注解和基于XML配置文件2种方式进行了切面注入,2种方式效果是等同的。
此程序使用的是Spring AOP,并没有使用功能更加丰富的AspectJ,Spring AOP很大部分借鉴了AspectJ,如果只是简单的方法层面的织入,那么Spring AOP就能够满足需求。如果需要构造器或者属性拦截,或者需要为spring bean引入新方法,那么就需要使用AspectJ了。
1 开始 #
从 start.spring.io下载空项目,引入Spring AOP依赖:
1<dependency>
2 <groupId>org.springframework.boot</groupId>
3 <artifactId>spring-boot-starter-aop</artifactId>
4</dependency>
2 配置 #
2.1 基于JavaBean+注解的配置 #
2.1.1 注入Bean #
1@Configuration
2@Profile("jc")
3public class DiskConfig {
4
5 @Bean("jcd")
6 public CompactDisk saveRock() {
7 BlankDisk cd = new BlankDisk();
8 cd.setArtist("Fall Out Boy");
9 cd.setTitle("Save Rock And Roll");
10 List<String> tracks = new ArrayList<>();
11 tracks.add("The Phoenix");
12 tracks.add("My Songs Know What You Did In the Dark (Light Em Up)");
13 tracks.add("Alone Together");
14 tracks.add("Where Did the Party Go");
15 tracks.add("Just One Yesterday (feat. Foxes)");
16 tracks.add("The Mighty Fall (feat. Big Sean)");
17 tracks.add("Missing You");
18 tracks.add("Death Valley");
19 cd.setTracks(tracks);
20 return cd;
21 }
22
23 @Bean("jtc")
24 public TrackCounter trackCounter() {
25 return new TrackCounter();
26 }
27}
2.1.2 创建切面 #
使用注解@Aspect
可以将一个Bean声明为切面:
1@Aspect
2@Slf4j
3public class TrackCounter {
4
5 private Map<Integer, Integer> trackCounts = new HashMap<>();
6
7 public int getPlayCount(int trackNumber) {
8 return trackCounts.getOrDefault(trackNumber, 0);
9 }
10
11 @Pointcut("execution( * com.wangy.aop.disk.BlankDisk.playTrack(..)) && args(trackNumber)")
12 public void pc1(int trackNumber){
13 }
14
15 @Pointcut("execution(* com.wangy.aop.disk.BlankDisk.playTrack(int))")
16 public void pc2(){}
17
18 @Before(value = "pc2()")
19 public void init(){
20 // do something
21 log.info("start playing");
22 }
23
24 @AfterReturning(value = "pc1(trackNumber)")
25 public void countTrack(int trackNumber) {
26 log.info("Track {} played", trackNumber);
27 trackCounts.put(trackNumber, getPlayCount(trackNumber) + 1);
28 }
29
30 @AfterThrowing(value = "pc1(trackNumber)")
31 public void skipTrack(int trackNumber) {
32 log.info("track {} skipped", trackNumber);
33 }
34
35 @After(value = "pc2()")
36 public void after(){
37 // do something
38 }
39
40 @Around(value = "pc1(trackNumber)")
41 public void aroundTest(ProceedingJoinPoint jp, int trackNumber) throws Throwable {
42 int pl = 2;
43 // do some judgement
44 if (getPlayCount(trackNumber) > pl) {
45 log.info("track {} has been played more than twice, skip this track", trackNumber);
46 // change the behavior of pointcut method
47 CompactDisk target = (CompactDisk) jp.getTarget();
48 target.playTrack(-1);
49 }else{
50 jp.proceed();
51 }
52 }
53}
使用@Aspect
注解将TrackCounter
bean声明为一个切面,同时使用@Pointcut
注解声明切点,再使用对应的通知注解声明通知
- @Before
- @After
- @AfterReturning
- @AfterThrowing
- @Around
若使用xml配置切面,那么TrackCounter
类看起来和普通的java bean没有差别,稍后会在xml配置文件中将其配置为一个切面
注意上面的切面表达式:
1 execution( * com.wangy.aop.disk.BlankDisk.playTrack(int)) && args(trackNumber)
前半部分是常见的切面表达式,用于指定切入点;
- 第一个
*
指示任意返回类型 - 使用全限定名指定类和方法名,括号内的
int
指定参数列表,可以使用(..)
来匹配任意参数
更多关于切入点表达式的内容:
- https://www.cnblogs.com/liaojie970/p/7883687.html
- https://howtodoinjava.com/spring-aop/aspectj-pointcut-expressions/
- https://www.baeldung.com/spring-aop-pointcut-tutorial
&&
连接符后面的内容是什么意思?
这里需要提及的是, Spring AOP支持AspectJ切点指示器的子集,除了最常用的execution()
指示器之外,还有其他的指示器:
AspectJ指示器 | 描述 |
---|---|
args() | 限制连接点匹配参数为指定类型的执行方法 |
@args() | 限制连接点匹配参数由指定注解标注的执行方法 |
execution() | 用于匹配是连接点的执行方法 |
this() | 限制连接点匹配AOP代理的bean引用为指定类型的类 |
target | 限制连接点匹配目标对象为指定类型的类 |
@target() | 限制连接点匹配特定的执行对象,这些对象对应的类需要有指定类型的注解 |
within() | 限制连接点匹配指定的类型 |
@within() | 限制连接点匹配指定注解所标注的类型(当使用Spring AOP时,方法定义在由指定的注解所标注的类里) |
@annotation | 限制匹配带有指定注解的连接点 |
这里的arg(trackNumber)
限定符,表明传递给连接点(切入点)playTrack(int)
的int类型参数也会传递到通知中去。
关于
args()
条件的作用,sping官方文档有说明: https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#aop-ataspectj-advice-params需要注意到启动类中使用了
@EnableAspectJAutoProxy
注解, 这意味着开启AspectJ自动代理,使得Spring框架拥有AOP能力:
1@SpringBootApplication
2@EnableAspectJAutoProxy
3public class AopApplication {
4 public static void main(String[] args) {
5 SpringApplication.run(AopApplication.class, args);
6 }
7}
2.2 基于xml文件的配置 #
xml配置如下1:
1<beans profile="xc">
2 <aop:aspectj-autoproxy/>
3
4 <bean id="xtrackCounter" class="com.wangy.aop.TrackCounter" name="xtc"/>
5
6 <bean id="xcd" class="com.wangy.aop.disk.BlankDisk" name="xcd">
7 <property name="title" value="Save Rock And Roll"/>
8 <property name="artist" value="Fall Out Boy"/>
9 <property name="tracks">
10 <list>
11 <value>The Phoenix</value>
12 <value>My Songs Know What You Did In the Dark (Light Em Up)</value>
13 <value>Alone Together</value>
14 <value>Where Did the Party Go</value>
15 <value>Just One Yesterday (feat. Foxes)</value>
16 <value>The Mighty Fall (feat. Big Sean)</value>
17 <value>Missing You</value>
18 <value>Death Valley</value>
19 </list>
20 </property>
21 </bean>
22
23 <aop:config>
24 <aop:aspect ref="xtrackCounter">
25 <aop:pointcut id="tc"
26 expression="execution(* com.wangy.aop.disk.BlankDisk.playTrack(int)) and args(trackNumber))"/>
27 <aop:after-returning pointcut-ref="tc" method="countTrack"/>
28 <aop:around method="aroundTest" pointcut-ref="tc"/>
29 </aop:aspect>
30 </aop:config>
31</beans>
对应前文中的JavaBean配置中使用的profile,在xml中将所有的配置声明为一个叫'xc'的profile
。
3 测试 #
测试包中提供了2个测试类,分别用于测试基于JavaBean+注解
、基于xml文件的aop配置;
- [TrackCounterTest]用于测试基于javaBean和注解实现的aop,这是推荐的方式
- [TrackCountTestWithXml]用于测试基于xml配置的aop,在运行此测试时,需要注释掉
TrackCount
类上的@Aspect
注解,以免Application Context注入2个切面
以下是使用xml配置的测试样例:
1@SpringBootTest
2@SpringJUnitConfig(locations = {"classpath:spring-aop.xml"})
3@ActiveProfiles("xc")
4public class TrackCountTestWithXml {
5
6 @Autowired
7 private CompactDisk cd;
8
9 @Autowired
10 private TrackCounter tc;
11
12 @Test
13 public void testTc() {
14 cd.playTrack(1);
15 cd.playTrack(1);
16 cd.playTrack(1);
17
18 cd.playTrack(2);
19
20 cd.playTrack(4);
21 cd.playTrack(4);
22
23 cd.playTrack(6);
24 cd.playTrack(6);
25 cd.playTrack(6);
26 try {
27 cd.playTrack(6);
28 } catch (Exception e) {
29 //ignore
30 }
31 assertEquals(3, tc.getPlayCount(1));
32 assertEquals(1, tc.getPlayCount(2));
33 assertEquals(0, tc.getPlayCount(3));
34 assertEquals(2, tc.getPlayCount(4));
35 assertEquals(0, tc.getPlayCount(5));
36 assertEquals(3, tc.getPlayCount(6));
37 }
38}
4 参考 #
demo地址:https://github.com/wangy325/simple_springboot_aop_demo
切入点表达式使用总结:https://www.cnblogs.com/zhangxufeng/p/9160869.html
xml配置并未使用TrackCounter中的全部通知 ↩︎