Spring Boot AOP 实践
面向切面的程序设计。嗯..
其实,面向切面编程(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 表示式]