• Spring入门(十一):Spring AOP使用进阶


    在上篇博客中,我们了解了什么是AOP以及在Spring中如何使用AOP,本篇博客继续深入讲解下AOP的高级用法。

    1. 声明带参数的切点

    假设我们有一个接口CompactDisc和它的实现类BlankDisc:

    package chapter04.soundsystem;
    
    /**
     * 光盘
     */
    public interface CompactDisc {
        void play();
    
        void play(int songNumber);
    }
    
    package chapter04.soundsystem;
    
    import java.util.List;
    
    /**
     * 空白光盘
     */
    public class BlankDisc implements CompactDisc {
        /**
         * 唱片名称
         */
        private String title;
    
        /**
         * 艺术家
         */
        private String artist;
    
        /**
         * 唱片包含的歌曲集合
         */
        private List<String> songs;
    
        public BlankDisc(String title, String artist, List<String> songs) {
            this.title = title;
            this.artist = artist;
            this.songs = songs;
        }
    
        @Override
        public void play() {
            System.out.println("Playing " + title + " by " + artist);
            for (String song : songs) {
                System.out.println("-Song:" + song);
            }
        }
    
        /**
         * 播放某首歌曲
         *
         * @param songNumber
         */
        @Override
        public void play(int songNumber) {
            System.out.println("Play Song:" + songs.get(songNumber - 1));
        }
    }
    

    现在我们的需求是记录每首歌曲的播放次数,按照以往的做法,我们可能会修改BlankDisc类的逻辑,在播放每首歌曲的代码处增加记录播放次数的逻辑,但现在我们使用切面,在不修改BlankDisc类的基础上,实现相同的功能。

    首先,新建切面SongCounter如下所示:

    package chapter04.soundsystem;
    
    import org.aspectj.lang.annotation.Aspect;
    import org.aspectj.lang.annotation.Before;
    import org.aspectj.lang.annotation.Pointcut;
    
    import java.util.HashMap;
    import java.util.Map;
    
    @Aspect
    public class SongCounter {
        private Map<Integer, Integer> songCounts = new HashMap<>();
    
        /**
         * 可重用的切点
         *
         * @param songNumber
         */
        @Pointcut("execution(* chapter04.soundsystem.CompactDisc.play(int)) && args(songNumber)")
        public void songPlayed(int songNumber) {
        }
    
        @Before("songPlayed(songNumber)")
        public void countSong(int songNumber) {
            System.out.println("播放歌曲计数:" + songNumber);
            int currentCount = getPlayCount(songNumber);
            songCounts.put(songNumber, currentCount + 1);
        }
    
        /**
         * 获取歌曲播放次数
         *
         * @param songNumber
         * @return
         */
        public int getPlayCount(int songNumber) {
            return songCounts.getOrDefault(songNumber, 0);
        }
    }
    

    重点关注下切点表达式execution(* chapter04.soundsystem.CompactDisc.play(int)) && args(songNumber),其中int代表参数类型,songNumber代表参数名称。

    新建配置类SongCounterConfig:

    package chapter04.soundsystem;
    
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.context.annotation.EnableAspectJAutoProxy;
    
    import java.util.ArrayList;
    import java.util.List;
    
    @Configuration
    @EnableAspectJAutoProxy
    public class SongCounterConfig {
        @Bean
        public CompactDisc yehuimei() {
            List<String> songs = new ArrayList<>();
            songs.add("东风破");
            songs.add("以父之名");
            songs.add("晴天");
            songs.add("三年二班");
            songs.add("你听得到");
    
            BlankDisc blankDisc = new BlankDisc("叶惠美", "周杰伦", songs);
            return blankDisc;
        }
    
        @Bean
        public SongCounter songCounter() {
            return new SongCounter();
        }
    }
    

    注意事项:

    1)配置类要添加@EnableAspectJAutoProxy注解启用AspectJ自动代理。

    2)切面SongCounter要被声明bean,否则切面不会生效。

    最后,新建测试类SongCounterTest如下所示:

    package chapter04.soundsystem;
    
    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 static org.junit.Assert.assertEquals;
    
    @RunWith(SpringJUnit4ClassRunner.class)
    @ContextConfiguration(classes = SongCounterConfig.class)
    public class SongCounterTest {
        @Autowired
        private CompactDisc compactDisc;
    
        @Autowired
        private SongCounter songCounter;
    
        @Test
        public void testSongCounter() {
            compactDisc.play(1);
    
            compactDisc.play(2);
    
            compactDisc.play(3);
            compactDisc.play(3);
            compactDisc.play(3);
            compactDisc.play(3);
    
            compactDisc.play(5);
            compactDisc.play(5);
    
            assertEquals(1, songCounter.getPlayCount(1));
            assertEquals(1, songCounter.getPlayCount(2));
    
            assertEquals(4, songCounter.getPlayCount(3));
    
            assertEquals(0, songCounter.getPlayCount(4));
    
            assertEquals(2, songCounter.getPlayCount(5));
        }
    }
    

    运行测试方法testSongCounter(),测试通过,输出结果如下所示:

    播放歌曲计数:1

    Play Song:东风破

    播放歌曲计数:2

    Play Song:以父之名

    播放歌曲计数:3

    Play Song:晴天

    播放歌曲计数:3

    Play Song:晴天

    播放歌曲计数:3

    Play Song:晴天

    播放歌曲计数:3

    Play Song:晴天

    播放歌曲计数:5

    Play Song:你听得到

    播放歌曲计数:5

    Play Song:你听得到

    2. 限定匹配带有指定注解的连接点

    在之前我们声明的切点中,切点表达式都是使用全限定类名和方法名匹配到某个具体的方法,但有时候我们需要匹配到使用某个注解的所有方法,此时就可以在切点表达式使用@annotation来实现,注意和之前在切点表达式中使用execution的区别。

    为了更好的理解,我们还是通过一个具体的例子来讲解。

    首先,定义一个注解Action:

    package chapter04;
    
    import java.lang.annotation.*;
    
    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface Action {
        String name();
    }
    

    然后定义2个使用@Action注解的方法:

    package chapter04;
    
    import org.springframework.stereotype.Service;
    
    @Service
    public class DemoAnnotationService {
        @Action(name = "注解式拦截的add操作")
        public void add() {
            System.out.println("DemoAnnotationService.add()");
        }
    
        @Action(name = "注解式拦截的plus操作")
        public void plus() {
            System.out.println("DemoAnnotationService.plus()");
        }
    }
    

    接着定义切面LogAspect:

    package chapter04;
    
    import org.aspectj.lang.JoinPoint;
    import org.aspectj.lang.annotation.After;
    import org.aspectj.lang.annotation.Aspect;
    import org.aspectj.lang.annotation.Pointcut;
    import org.aspectj.lang.reflect.MethodSignature;
    import org.springframework.stereotype.Component;
    
    import java.lang.reflect.Method;
    
    @Aspect
    @Component
    public class LogAspect {
        @Pointcut("@annotation(chapter04.Action)")
        public void annotationPointCut() {
        }
    
        @After("annotationPointCut()")
        public void after(JoinPoint joinPoint) {
            MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
            Method method = methodSignature.getMethod();
            Action action = method.getAnnotation(Action.class);
            System.out.println("注解式拦截 " + action.name());
        }
    }
    

    注意事项:

    1)切面使用了@Component注解,以便Spring能自动扫描到并创建为bean,如果这里不添加该注解,也可以通过Java配置或者xml配置的方式将该切面声明为一个bean,否则切面不会生效。

    2)@Pointcut("@annotation(chapter04.Action)"),这里我们在定义切点时使用了@annotation来指定某个注解,而不是之前使用execution来指定某些或某个方法。

    我们之前使用的切面表达式是execution(* chapter04.concert.Performance.perform(..))是匹配到某个具体的方法,如果想匹配到某些方法,可以修改为如下格式:

    execution(* chapter04.concert.Performance.*(..))
    

    然后,定义配置类AopConfig:

    package chapter04;
    
    import org.springframework.context.annotation.ComponentScan;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.context.annotation.EnableAspectJAutoProxy;
    
    @Configuration
    @ComponentScan
    @EnableAspectJAutoProxy
    public class AopConfig {
    }
    

    注意事项:配置类需要添加@EnableAspectJAutoProxy注解启用AspectJ自动代理,否则切面不会生效。

    最后新建Main类,在其main()方法中添加如下测试代码:

    package chapter04;
    
    import org.springframework.context.annotation.AnnotationConfigApplicationContext;
    
    public class Main {
        public static void main(String[] args) {
            AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AopConfig.class);
    
            DemoAnnotationService demoAnnotationService = context.getBean(DemoAnnotationService.class);
    
            demoAnnotationService.add();
            demoAnnotationService.plus();
    
            context.close();
        }
    }
    

    输出结果如下所示:

    DemoAnnotationService.add()

    注解式拦截 注解式拦截的add操作

    DemoAnnotationService.plus()

    注解式拦截 注解式拦截的plus操作

    可以看到使用@Action注解的add()和plus()方法在执行完之后,都执行了切面中定义的after()方法。

    如果再增加一个使用@Action注解的subtract()方法,执行完之后,也会执行切面中定义的after()方法。

    3. 项目中的实际使用

    在实际的使用中,切面很适合用来记录日志,既满足了记录日志的需求又让日志代码和实际的业务逻辑隔离开了,

    下面看下具体的实现方法。

    首先,声明一个访问日志的注解AccessLog:

    package chapter04.log;
    
    import java.lang.annotation.ElementType;
    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    import java.lang.annotation.Target;
    
    /**
     * 访问日志 注解
     */
    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface AccessLog {
        boolean recordLog() default true;
    }
    

    然后定义访问日志的切面AccessLogAspectJ:

    package chapter04.log;
    
    import com.alibaba.fastjson.JSON;
    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.aspectj.lang.reflect.MethodSignature;
    import org.springframework.stereotype.Component;
    
    @Aspect
    @Component
    public class AccessLogAspectJ {
        @Pointcut("@annotation(AccessLog)")
        public void accessLog() {
    
        }
    
        @Around("accessLog()")
        public void recordLog(ProceedingJoinPoint proceedingJoinPoint) {
            try {
                Object object = proceedingJoinPoint.proceed();
    
                AccessLog accessLog = ((MethodSignature) proceedingJoinPoint.getSignature()).getMethod().getAnnotation(AccessLog.class);
    
                if (accessLog != null && accessLog.recordLog() && object != null) {
                    // 这里只是打印出来,一般实际使用时都是记录到公司的日志中心
                    System.out.println("方法名称:" + proceedingJoinPoint.getSignature().getName());
                    System.out.println("入参:" + JSON.toJSONString(proceedingJoinPoint.getArgs()));
                    System.out.println("出参:" + JSON.toJSONString(object));
                }
            } catch (Throwable throwable) {
                // 这里可以记录异常日志到公司的日志中心
                throwable.printStackTrace();
            }
        }
    }
    

    上面的代码需要在pom.xml中添加如下依赖:

    <!-- https://mvnrepository.com/artifact/com.alibaba/fastjson -->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>fastjson</artifactId>
        <version>1.2.59</version>
    </dependency>
    

    然后定义配置类LogConfig:

    package chapter04.log;
    
    import org.springframework.context.annotation.ComponentScan;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.context.annotation.EnableAspectJAutoProxy;
    
    @Configuration
    @ComponentScan
    @EnableAspectJAutoProxy
    public class LogConfig {
    }
    

    注意事项:不要忘记添加@EnableAspectJAutoProxy注解,否则切面不会生效。

    然后,假设你的对外接口是下面这样的:

    package chapter04.log;
    
    import org.springframework.stereotype.Service;
    
    @Service
    public class MockService {
        @AccessLog
        public String mockMethodOne(int index) {
            return index + "MockService.mockMethodOne";
        }
    
        @AccessLog
        public String mockMethodTwo(int index) {
            return index + "MockService.mockMethodTwo";
        }
    }
    

    因为要记录日志,所以每个方法都添加了@AccessLog注解。

    最后新建Main类,在其main()方法中添加如下测试代码:

    package chapter04.log;
    
    import org.springframework.context.annotation.AnnotationConfigApplicationContext;
    
    public class Main {
        public static void main(String[] args) {
            AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(LogConfig.class);
    
            MockService mockService = context.getBean(MockService.class);
    
            mockService.mockMethodOne(1);
            mockService.mockMethodTwo(2);
    
            context.close();
        }
    }
    

    输出日志如下所示:

    方法名称:mockMethodOne

    入参:[1]

    出参:"1MockService.mockMethodOne"

    方法名称:mockMethodTwo

    入参:[2]

    出参:"2MockService.mockMethodTwo"

    如果某个方法不需要记录日志,可以不添加@AccessLog注解:

    public String mockMethodTwo(int index) {
        return index + "MockService.mockMethodTwo";
    }
    

    也可以指定recordLog为false:

    @AccessLog(recordLog = false)
    public String mockMethodTwo(int index) {
        return index + "MockService.mockMethodTwo";
    }
    

    这里只是举了个简单的记录日志的例子,大家也可以把切面应用到记录接口耗时等更多的场景。

    4. 源码及参考

    源码地址:https://github.com/zwwhnly/spring-action.git,欢迎下载。

    Craig Walls 《Spring实战(第4版)》

    汪云飞《Java EE开发的颠覆者:Spring Boot实战》

    AOP(面向切面编程)_百度百科

    原创不易,如果觉得文章能学到东西的话,欢迎点个赞、评个论、关个注,这是我坚持写作的最大动力。

    如果有兴趣,欢迎添加我的微信:zwwhnly,等你来聊技术、职场、工作等话题(PS:我是一名奋斗在上海的程序员)。

  • 相关阅读:
    准备找工作了,求助于在成都的师兄、师姐们提供一些信息!大家都看看吧!
    大学课程名的中文翻译
    火箭今天赢了!!!
    如何成为一个受欢迎的人!
    理解DataSet的数据缓存机制
    餐桌上的不文雅行
    今天看到居然有100多个Gmail的邀请函,还没有Gmail的朋友留下邮箱地址!
    Update:代码行计算器(LinesCounter)
    找工作之“加班费”
    动画片里的Ajax
  • 原文地址:https://www.cnblogs.com/zwwhnly/p/11422874.html
Copyright © 2020-2023  润新知