Spring Boot AOP 实践

面向切面的程序设计。嗯..

https://img2022.cnblogs.com/blog/1106277/202203/1106277-20220304134035668-2139042219.jpg

其实,面向切面编程(Aspect-oriented programming,AOP,又译作面向方面的程序设计剖面导向程序设计)和 OOP 一样都是计算机科学中的一种程序设计思想。例如:日志收集功能。传统的 OOP 虽然也能实现,但 AOP 思想为我们打开了另一扇窗。AOP 将项目的日志收集功能拆分出来成为一个关注点(Concern)叫切面也可以 (Aspect),使用的时候通过代理模式织入(Weaving)即可。织入后就会在关注点上搞一些动作,这个过程就是通知 (Advice) 我们通过 AOP 来实现一个日志收集系统。

我们可以通过 Springboot + AOP 实现一个简单日志采集功能

  • 引入 Pom 坐标

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
        <exclusions>
            <exclusion>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-logging</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
    
    <!-- 日志 -->
    <!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-log4j2 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-log4j2</artifactId>
    </dependency>
    
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-aop</artifactId>
    </dependency>
    
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
        <exclusions>
            <exclusion>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-logging</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
    
  • 先配置一下日志的格式,在 resource 中添加一个 log4j2-spring.xml 文件写入以下内容,如果是IDEA 就在 vm option 中添加 Dlog4j.skipJansi=false 来展示色彩

    <?xml version="1.0" encoding="UTF-8"?>
    <Configuration status="info" name="log4j2-name" monitorInterval="5">
        <Properties>
            <Property name="console.pattern">------------- %n 执行时间:%style{%d{yyyy-MM-dd HH:mm:ss.SSS}}{Magenta}%n 消息类型:%highlight{%p}%n 当前线程:%style{%t}{Cyan}%n 消息所在类: %style{%c}{Yellow}%n 返回的消息:%m%n</Property>
        </Properties>
    
        <Appenders>
            <Console name="console-appender">
                <PatternLayout pattern="${console.pattern}"/>
            </Console>
    
        </Appenders>
        <Loggers>
            <Logger name="org.springframework.boot" level="error"/>
            <Logger name="org.apache" level="error"/>
            <Root level="info">
                <AppenderRef ref="console-appender"/>
            </Root>
        </Loggers>
    </Configuration>
    
  • 配置 Aspect

    /**
     * 使用 Aspect注解 开启
     */
    @Aspect
    @Component
    public class LogAspect {
        private Logger logger = LogManager.getLogger(LogAspect.class);
    
        /**
         * 配置切入点
         */
        @Pointcut("execution(public * com.example.demo..*.*(..))*)")
        public void pointcut() {
    
        }
    
        /**
         * 前置通知
         *
         * @param joinPoint
         */
        @Before("pointcut()")
        public void doBeforeAccessCheck(JoinPoint joinPoint) {
            logger.info("Before方法进入");
        }
    
        /**
         * 后置通知 (不管执行了与否)
         *
         * @param joinPoint
         */
        @After("pointcut()")
        public void doAfterAccessCheck(JoinPoint joinPoint) {
            logger.info("After方法进入");
        }
    
        /**
         * 代码必须正常返回之后才会通知
         *
         * @param joinPoint
         * @param jsonReturn
         */
        @AfterReturning(value = "pointcut()", returning = "jsonReturn")
        public void doAfterReturn(JoinPoint joinPoint, Object jsonReturn) {
            logger.info("AfterReturning方法进入");
        }
    
        /**
         * 代码抛出异常之后才会通知
         *
         * @param joinPoint
         * @param throwing
         */
        @AfterThrowing(value = "pointcut()", throwing = "throwing")
        private void doAfterThrowing(JoinPoint joinPoint, Exception throwing) {
            logger.info("AfterThrowing方法进入");
        }
    
        /**
         * 环绕通知 控制目标代码是否执行,可以在执行前后、抛异常后执行任意拦截代码
         *
         * @param pjp
         * @return
         * @throws Throwable
         */
        @Around(value = "pointcut()", argNames = "pjp")
        private Object doAround(ProceedingJoinPoint pjp) throws Throwable {
            Long startTime = System.currentTimeMillis();
            Object proceed = pjp.proceed();
            Long endTime = System.currentTimeMillis();
            logger.info("Around方法进入,用时{}", endTime - startTime);
            return proceed;
        }
    
    }
    
  • 定义一个 controller

    @RestController
    public class IndexController {
    
        @GetMapping("")
        public String index() {
            return "访问了首页";
        }
    
        @GetMapping("error")
        public User errorIndex() throws Exception {
            throw new Exception("访问了错误的首页");
        }
    }
    
  • 启动服务访问首页 *GET* [http://localhost:8080/](http://localhost:8080/) 即可看到日志信息

    ------------- 
     执行时间:2022-03-04 13:24:59.858
     消息类型:INFO
     当前线程:http-nio-8080-exec-1
     消息所在类: com.example.demo.aspects.LogAspect
     返回的消息:Before方法进入
    ------------- 
     执行时间:2022-03-04 13:25:00.875
     消息类型:INFO
     当前线程:http-nio-8080-exec-1
     消息所在类: com.example.demo.aspects.LogAspect
     返回的消息:AfterReturning方法进入
    ------------- 
     执行时间:2022-03-04 13:25:00.876
     消息类型:INFO
     当前线程:http-nio-8080-exec-1
     消息所在类: com.example.demo.aspects.LogAspect
     返回的消息:After方法进入
    ------------- 
     执行时间:2022-03-04 13:25:00.878
     消息类型:INFO
     当前线程:http-nio-8080-exec-1
     消息所在类: com.example.demo.aspects.LogAspect
     返回的消息:Around方法进入,用时1019
    

这样就实现了一个简单的日志系统。我们还可以使用反射来更加灵活自由的实现我们的日志系统。

  • 创建一个 Log 的注解。
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Log {

    String title() default "标题";

}
  • 修改 LogAspect 的代码为下面的内容
/**
 * 使用 Aspect 开启
 */
@Aspect
@Component
public class LogAspect {
    private Logger logger = LogManager.getLogger(LogAspect.class);

    @AfterReturning(pointcut = "@annotation(logAnnotation)", returning = "jsonResult")
    void afterLogReturn(JoinPoint joinPoint, Log logAnnotation, Object jsonResult) {
        logger.info("一个Log注解的通知:{}", logAnnotation.msg());
    }
}
  • IndexController 中添加 Log 注解
@RestController
public class IndexController {

    @GetMapping("")
    @Log(msg = "首页的日志记录")
    public String index() throws InterruptedException {
        Thread.sleep(1000);
        return "访问了首页";
    }

    @GetMapping("error")
    @Log(msg = "错误页的日志记录")
    public User errorIndex() throws Exception {
        throw new Exception("访问了错误的首页");
    }
}
  • 启动服务访问首页 *GET* [http://localhost:8080/](http://localhost:8080/) 即可看到日志信息
执行时间:2022-03-04 13:37:37.431
消息类型:INFO
当前线程:http-nio-8080-exec-2
消息所在类: com.example.demo.aspects.LogAspect
返回的消息:一个Log注解的通知:首页的日志记录

看也是可以拿到的,这样就有两种解决手段了,到此 AOP 的简单实践就完毕了。

参考文献:

[1] https://baike.baidu.com/item/织入/4602338

[2] https://zh.wikipedia.org/wiki/面向切面的程序设计

[3] https://openhome.cc/Gossip/Spring/Pointcut.html [Pointcut 表示式]

posted @ 2022-03-04 13:41  奔跑的砖头  阅读(54)  评论(0编辑  收藏  举报