- 最近在优化一些项目接口,发现有一些执行时间超过 1S,就想着搞一个注解监控每个方法的执行时间。
前言
- 以下内容摘自 https://www.tianmaying.com/tutorial/spring-aop
软件工程有一个基本原则叫做“关注点分离”(Concern Separation),通俗的理解就是不同的问题交给不同的部分去解决,每部分专注于解决自己的问题。这年头互联网也天天强调要专注嘛!
这其实也是一种“分治”或者“分类”的思想,人解决复杂问题的能力是有限的,所以为了控制复杂性,我们解决问题时通常都要对问题进行拆解,拆解的同时建立各部分之间的关系,各个击破之后整个问题也迎刃而解了。
面向切面编程(Aspect Oriented Programming,AOP)其实就是一种关注点分离的技术。我们软件开发时经常提一个词叫做“业务逻辑”或者“业务功能”,我们的代码主要就是实现某种特定的业务逻辑。但是我们往往不能专注于业务逻辑,比如我们写业务逻辑代码的同时,还要写事务管理、缓存、日志等等通用化的功能,而且每个业务功能都要和这些业务功能混在一起,痛苦!所以,为了将业务功能的关注点和通用化功能的关注点分离开来,就出现了 AOP 技术。这些通用化功能的代码实现,对应的就是我们说的切面(Aspect)。
业务功能代码和切面代码分开之后,责任明确,开发者就能各自专注解决问题了,代码可以优雅的组织了,设计更加高内聚低耦合了(终极目标啊!)。但是请注意,代码分开的同时,我们如何保证功能的完整性呢?你的业务功能依然需要有事务和日志等特性,即切面最终需要合并(专业术语叫做织入, Weave)到业务功能中。怎么做到呢?这里就涉及 AOP 的底层技术啦,有三种方式:
- 编译时织入:在代码编译时,把切面代码融合进来,生成完整功能的 Java 字节码,这就需要特殊的 Java 编译器了,AspectJ 属于这一类
- 类加载时织入:在 Java 字节码加载时,把切面的字节码融合进来,这就需要特殊的类加载器,AspectJ 和 AspectWerkz 实现了类加载时织入
- 运行时织入:在运行时,通过动态代理的方式,调用切面代码增强业务功能,Spring 采用的正是这种方式。动态代理会有性能上的开销,但是好处就是不需要神马特殊的编译器和类加载器啦,按照写普通 Java 程序的方式来就行了!
更多的细节,不在这里展开了,网上资料一大堆。下面我们就直接上干货。
环境和依赖
环境
- SpringBoot 2.3.2
- IDEA
- MAVEN
- jdk1.8+
依赖
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-aopartifactId>
dependency>
其中,starter-aop 中依赖了以下两个包:
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-aopartifactId>
<version>5.2.2.RELEASEversion>
<scope>compilescope>
dependency>
<dependency>
<groupId>org.aspectjgroupId>
<artifactId>aspectjweaverartifactId>
<version>1.9.5version>
<scope>compilescope>
dependency>
aspectjweaver 中又包含了 aspectjrt.jar 的相关功能,实现编译时织入。
实现
自定义注解 WatchTimeAnnotation
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 可以根据自己的情况,进行增减属性
*
* @author hager
* @since 2020-8-25 22:59:18
*/
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public@interface WatchTimeAnnotation {
/**
* 方法名
*/
String name() default "";
/**
* 描述内容
*/
String desc() default "";
/**
* 日志等级,数字越大,打印日志级别越高
* 最好,这里换成枚举
*/
int level() default 0;
}
切入点监控日志打印
import com.example.mybatisplusdemo.annoations.WatchTimeAnnotation;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.util.StopWatch;
/**
* 这里可以根据自己的诉求,进行改造,比如什么时候打印什么样的日志等
*
* @author hager
* @since 2020-8-25 23:00:30
*/
@Aspect
@Component
publicclass WatchTimeAspect {
finalstatic Logger LOGGER = LoggerFactory.getLogger(WatchTimeAspect.class);
@Around("com.example.mybatisplusdemo.aspects.CommonJoinPointConfig.watchTimeAspectPointCutWithAnnos()")
public Object around(ProceedingJoinPoint point) throws Throwable {
MethodSignature methodSignature = (MethodSignature) point.getSignature();
// Get intercepted method details
String className = methodSignature.getDeclaringType().getSimpleName();
String methodName = methodSignature.getName();
String value = "";
String desc = "";
int level = 0;
WatchTimeAnnotation timeAnnotation = methodSignature.getMethod().getAnnotation(WatchTimeAnnotation.class);
if (null != timeAnnotation) {
value = timeAnnotation.name();
desc = timeAnnotation.desc();
level = timeAnnotation.level();
}
final StopWatch stopWatch = new StopWatch();
//Measure method execution time
stopWatch.start();
Object result = point.proceed();
stopWatch.stop();
//Log method execution time
if (level >= 5) {
LOGGER.warn("【执行时间-warning】 {}.{}(value={},desc={})::{} ms",
className, methodName,
value, desc,
stopWatch.getTotalTimeMillis());
} elseif (level > 0 && level < 5) {
LOGGER.info("【执行时间】 {}.{}(value={},desc={})::{} ms",
className, methodName,
value, desc,
stopWatch.getTotalTimeMillis());
} else {
LOGGER.info("不打印日志");
}
return result;
}
}
统一切入点管理配置
import org.aspectj.lang.annotation.Pointcut;
/**
* 统一管理注解切入点
*
* @author hager
* @since 2020-8-25 23:02:46
*/
publicclass CommonJoinPointConfig {
/**
* 切入点,注解形式
*/
@Pointcut("@annotation(com.example.mybatisplusdemo.annoations.WatchTimeAnnotation)")
public void watchTimeAspectPointCutWithAnnos() {
}
/**
* 切入点,执行类方法
*/
@Pointcut("execution(* com.example.mybatisplusdemo.service.*.*(..))")
public void watchTimeAspectPointCutWithExec() {
}
}
使用
在 service 实现方法上增加对应注解即可
@Override
@WatchTimeAnnotation(name = "selectListBySql", desc = "描述内容", level = 5)
public ListselectListBySql() {
return baseMapper.selectListBySql();
}
输出
2020-08-25 23:08:19.895 WARN 18388 --- [nio-8082-exec-2]
c.e.m.aspects.WatchTimeAspect
: 【执行时间-warning】 DemoUserServiceImpl.selectListBySql(value=selectListBySql,
desc=描述内容)::48 ms
总结
该篇笔记,通过使用 SpringBoot AOP 与注解的结合实现了执行方法的时间监控。通过灵活控制注解以及属性,进而控制日志的输出。了解相关技术点后,可以举一反三的应用于其他功能上。比如日志、参数拼接、鉴权等等场景。
扩展
关于 AOP 这块还是有很多重要的内容的,在这里作为扩展内容梳理一下。
连接点(Joint Point):广义上来讲,方法、异常处理块、字段这些程序调用过程中可以抽像成一个执行步骤(或者说执行点)的单元。从 Spring AOP 来讲,就是指 java 的方法和异常处理代码块。
切点(Pointcut):是连接点的描述定义,Spring AOP 通过切点来定位到哪些连接点。切点表达式语言就是切点用来定义连接点的语法。有以下 9 种切点指示符:execution、within、this、target、args、@target、@args、@within、@annotation。注意:private 修饰的无法拦截。
@Aspect,作用是把当前类标识为一个切面供容器读取
@Before 前置通知,在调用目标方法之前执行通知定义的任务。相当于 BeforeAdvice 的功能。
@After 后置通知,在目标方法执行结束后,无论执行结果如何都执行通知定义的任务,final 增强,不管是抛出异常或者正常退出都会执行。
@Around 环绕通知,和前两个写法不同,实现的效果包含了前置和后置通知。相当于 MethodInterceptor
- 当使用环绕通知时,proceed 方法必须调用,否则拦截到的方法就不会再执行了
- 环绕通知=前置+目标方法执行+后置通知,proceed 方法就是用于启动目标方法执行的
- 常用于 环绕通知可以项目做全局异常处理、日志记录、用来做数据全局缓存、全局的事物处理 等
@AfterReturning,切入点返回结果之后执行,也就是都前置后置环绕都执行完了,这个就执行了。相当于 AfterReturningAdvice,方法退出时执行。
- 应用场景可以用来在订单支付完成之后就行二次的结果验证,重要参数的二次校验,防止在方法执行中的时候参数被修改等等
@AfterThrowing,这个是在切入执行报错的时候执行的。相当于 ThrowsAdvice
资料
- https://howtodoinjava.com/spring-boot2/aop-aspectj/
- https://docs.spring.io/spring/docs/2.0.x/reference/aop.html
- https://www.baeldung.com/spring-aop-pointcut-tutorial
- 扩展资料 https://www.jianshu.com/p/d32b17d079ef
文中部分图片来自网上,如有侵权,请联系删除。