SpringBoot使用AOP的简单示例

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指定参数列表,可以使用(..)来匹配任意参数

更多关于切入点表达式的内容:

&&连接符后面的内容是什么意思?

这里需要提及的是, 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


  1. xml配置并未使用TrackCounter中的全部通知 ↩︎