8.借助AOP提供非侵入的使用姿势
再前面完成封装的TraceWatch
,进一步简化了使用体验,但是依然存在一个明显的缺陷,对业务代码的侵入性较强,需要再业务代码中,进行主动的埋点
对应常年和Spring打交道的java开发者来说,一个很容易想到的优化方案就是借助AOP来简化业务代码的侵入,接下来我们就看一下,如何借助Spring的AOP能力,对我们之前提供的TraceWatch
做一个能力增强
1. 方案设计
对于某个链路的耗时统计,首先确定有一个方法作为耗时记录的入口,表示开始记录耗时,然后就是再执行的过程中,发现有需要统计耗时的方法,则通过Around
环绕切面来计算耗时,最后再入口方法执行完毕之后,输出耗时情况即可
1.1 整体实现流程
上面是一个简单的AOP集成说明:
- 首先自定义一个注解,用来表示哪些方法需要进行耗时记录
- 执行链路中的,首个被自定义注解标注的方法,作为耗时记录的入口
- 即调用
TraceWatch.start
来创建TraceRecoder
- 即调用
- 在入口方法内部执行的调用链路中,执行到需要记录耗时的方法时,通过
traceRecoder.sync/async
来加入耗时统计 - 在入口方法执行完毕时,输出耗时分布
1.2 方案细节确认
如何确定入口
上面的流程中,说的是第一个被切面拦截的方法,作为入口,那么这种方式是否合适呢?
如有一个通用的请求校验方法,在支付的链路中,需要记录耗时分布;但是这个方法又会被其他的如提交订单、查看订单等场景使用,又不希望记录耗时,显然这种场景下,使用上面的姿势就不太合适
因此我们自定义注解中,新增一个传播属性 Progation
,设置下面三种类型
- REQUIRED: 支持当前trace记录,如果当前上下文中不存在DefaultTraceRecoder存在,则新创建一个TraceRecoder作为入口开始记录
- SUPPORTS: 支持当前trace记录,如果当前上下文中不存在DefaultTraceRecoder存在,则以同步的SyncTraceRecoder方式执行,不参与耗时统计
- NEVER: 不支持记录,不管当前存不存在,都以同步的方式执行,且不参与记录
耗时记录任务名规则
当不指定具体的任务名时,使用类名#方法名
来作为这个耗时的任务名
同步异步选择
默认方法都是同步调用,那么需要异步并行调用时,我们可以通过一个参数来控制
异步方法返回值如何获取
如果某一个方法是异步去执行,那这个方法的返回值怎么获取呢?
如果是直接返回结果,那对于调用者而言,这个异步执行就是个伪并行了(因为需要等待它执行完毕获取结果),因此对于异步调用的方法,返回结果应该由CompletableFuture
来包裹
2. AOP实现
2.0 前置依赖
我们接下来借助Spring的AOP来实现,首先需要集成相关依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<version>2.3.2.RELEASE</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.2.8.RELEASE</version>
<scope>provided</scope>
</dependency>
为了避免对使用者项目带来影响,因此我们的实现对引入的依赖是scope是provided
2.1 注解定义
首先我们来定义一下关键注解
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface TraceDog {
/**
* traceName,为空时,默认使用serviceName#methodName
*
* @return
*/
String value() default "";
/**
* 传播属性,默认是当前开了traceWatch,则记录;没有开启,则同步的方式执行
* 因此,在记录链路的开始,请将这个属性设置为 REQUIRED
*
* @return
*/
Propagation propagation() default Propagation.SUPPORTS;
/**
* 同步还是异步, 默认都是同步执行这段方法
*
* @return
*/
boolean async() default false;
}
2.2 传播属性定义
内置传播属性的枚举Propagation
,入口处使用REQUIRED
来标识,过程中则使用SUPPORTS
来标识
public enum Propagation {
/**
* 支持当前trace记录,如果当前上下文中不存在DefaultTraceRecoder存在,则新创建一个TraceRecoder作为入口开始记录
*/
REQUIRED(0),
/**
* 支持当前trace记录,如果当前上下文中不存在DefaultTraceRecoder存在,则以同步的SyncTraceRecoder方式执行,不参与耗时统计
*/
SUPPORTS(1),
/**
* 不支持记录,不管当前存不存在,都以同步的方式执行,且不参与记录
*/
NEVER(2),
;
private final int value;
Propagation(int value) {
this.value = value;
}
public int getValue() {
return value;
}
}
2.3 切面实现
切面的实现逻辑中,直接根据注解来切连接点,然后基于传播属性,判断是否为入口;对于过程执行中,则需要重点确认下同步还是异步调用
@Aspect
public class TraceAspect {
@Around("@annotation(traceDog)")
public Object handle(ProceedingJoinPoint joinPoint, TraceDog traceDog) throws Throwable {
if (traceDog.propagation() == Propagation.NEVER) {
return executed(joinPoint);
}
MethodSignature methodSignature = ((MethodSignature) joinPoint.getSignature());
if (traceDog.propagation() == Propagation.REQUIRED && TraceWatch.getRecoder() == null) {
// 入口点: 开启trace耗时记录
try (ITraceRecoder traceRecoder = TraceWatch.startTrace(genTraceName(methodSignature, traceDog))) {
return executed(joinPoint);
}
} else {
// 过程耗时记录
// trace链路的阶段过程
ITraceRecoder recoder = TraceWatch.getRecoderOrElseSync();
if (traceDog.async()) {
// 异步执行
} else {
// 同步执行
}
}
}
}
在上面的实现中,我们通过 genTraceName
获取任务名,其规则就是优先从注解中取,拿不到时用类名#方法名
作为任务名
private String genTraceName(MethodSignature methodSignature, TraceDog traceDog) {
if (!StringUtils.isEmpty(traceDog.value())) {
return traceDog.value();
}
return methodSignature.getDeclaringTypeName() + "#" + methodSignature.getMethod().getName();
}
接下来我们在来包装一下,目标方法的同步/异步执行,对于异步调用的方法,因为方法本身返回的是CompletableFuture
类型,TraceRecoder.async()
返回的也是CompletableFuture
,因此我们需要执行下 join()
来获取真实的返回
private Object executed(ProceedingJoinPoint joinPoint) {
try {
return joinPoint.proceed();
} catch (Throwable e) {
throw new RuntimeException(e);
}
}
private Object executeWithFuture(ProceedingJoinPoint joinPoint) {
try {
return ((CompletableFuture) joinPoint.proceed()).join();
} catch (Throwable e) {
throw new RuntimeException(e);
}
}
同步异步执行的实现逻辑补全,其中复杂一些的在于异步的实现,需要区分有没有返回结果
- 如果有返回结果,且是
CompletableFuture
封装,则需要调用上面的executeWithFuture
走异步执行 - 如果有返回结果,但是直接返回了对象,这种场景对于调用者而言等同于同步调用直接拿到返回结果,因此我们依然走同步执行
- 如果没有返回结果,则使用
async
异步执行
// trace链路的阶段过程
ITraceRecoder recoder = TraceWatch.getRecoderOrElseSync();
if (traceDog.async()) {
if (CompletableFuture.class.isAssignableFrom(methodSignature.getReturnType())) {
// 有返回结果的场景,因为watchDog本身就包装了返回结果;因此我们需要将实际业务执行的返回结果拿出来使用,否则对于调用方而言,就出现了两层Future
return recoder.async(() -> executeWithFuture(joinPoint), genTraceName(methodSignature, traceDog));
} else if (methodSignature.getReturnType() == Void.class || methodSignature.getReturnType() == void.class) {
// 无返回结果的场景
return recoder.async(() -> executed(joinPoint), genTraceName(methodSignature, traceDog));
} else {
// 都不满足,则采用同步执行
// --> 这种通常是方法上声明了异步,但是返回结果没有做适配,直接返回了对象。这种场景即便放在线程池中执行,因为也是直接获取方法的返回,相比较同步还多了线程切换的开销
// --> 使用异步的方法,返回结果需要时 CompletureFuture 进行封装,在最后需要的地方进行获取结果
return recoder.sync(() -> executed(joinPoint), genTraceName(methodSignature, traceDog));
}
} else {
return recoder.sync(() -> executed(joinPoint), genTraceName(methodSignature, traceDog));
}
2.4 自动注册
上面就完成了AOP的核心功能实现,接下来就是针对SpringBoot/Spring场景下,做些自动装配的工作
bean声明配置类
@Configuration
public class SpringTraceDogConfiguration {
@Bean
public TraceAspect traceAspect() {
return new TraceAspect();
}
}
SpringBoot的自动装配,在resource/META-INF
目录下,新增spring.factories
文件
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.github.liuyueyi.hui.components.trace.SpringTraceDogConfiguration
对于不是SpringBoot的场景,则可以通过@EnableTraceWatchDog
来开启
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(SpringTraceDogConfiguration.class)
@Documented
public @interface EnableTraceWatchDog {
}
3. 使用示例与小结
3.1 使用示例
接下来我们演示一下基于AOP的使用姿势
下两个demoBean
@Component
public class DemoService {
private Random random = new Random();
/**
* 随机睡眠一段时间
*
* @param max
*/
private void randSleep(String task, int max) {
randSleepAndRes(task, max);
}
private int randSleepAndRes(String task, int max) {
int sleepMillSecond = random.nextInt(max);
try {
System.out.println(task + "==> 随机休眠 " + sleepMillSecond + "ms");
Thread.sleep(sleepMillSecond);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
return sleepMillSecond;
}
@TraceDog
public void sync() {
randSleep("A-同步执行sync", 20);
}
// 方法的耗时不要,但是记录方法内部的代码块执行耗时
public int ignoreCost() {
// 代码块的耗时统计
TraceWatch.getRecoderOrElseSync().sync(() -> randSleep("B-代码块", 30), "B-代码块");
return randSleepAndRes("B-ignoreCost", 50);
}
@TraceDog(async = true, value = "C-标记异步,实际同步执行")
public int c() {
return randSleepAndRes("C-标记异步,实际同步执行", 50);
}
@TraceDog(async = true, value = "D-异步返回")
public CompletableFuture<Integer> d() {
return CompletableFuture.completedFuture(randSleepAndRes("异步返回d", 50));
}
@TraceDog(value = "E-异步代码块", async = true)
public void e() {
randSleep("异步代码块e", 50);
}
}
@Component
public class Index {
@Autowired
private DemoService demoService;
@TraceDog(propagation = Propagation.REQUIRED)
public Map buildIndexVo() {
Map<String, Object> ans = new HashMap<>();
demoService.sync();
ans.put("ignore", demoService.ignoreCost());
ans.put("c", demoService.c());
CompletableFuture<Integer> f = demoService.d();
demoService.e();
ans.put("d", f.join());
return ans;
}
}
已Index#buildIndexVo()
作为统计入口,DemoService
中提供了五个方法,但是只有四个上有@TraceDog
注解,因此方法耗时统计也只会有这四个; 由于ignoreCost
内部添加了一个代码块的执行耗时,因此最终的耗时分布输出会额外加上这个代码块的耗时,共5个
接下来实际访问测试一下,添加测试依赖
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.1</version>
<scope>test</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>2.7.18</version>
<scope>test</scope>
</dependency>
测试入口如下
@ComponentScan("com.github.liuyueyi.hhui.trace.test")
@RunWith(SpringJUnit4ClassRunner.class)
@EnableAspectJAutoProxy()
@EnableTraceWatchDog
public class BasicDemo {
@Autowired
private Index index;
@Test
public void testIndex() {
Map map = index.buildIndexVo();
System.out.println(map);
}
}
执行之后的输出如
3.2 小结
本文我们主要借助AOP对耗时分布统计的工具类做了使用侧的能力增强,从上面的使用示例也可以看出,不需要再业务代码中进行埋点,再需要的方法上,添加上注解就行了,当然若我们对某一段代码块的耗时需要进行统计时,也可以再具体的方法内,通过raceWatch.getRecoderOrElseSync().sync(() -> {}, "任务名");
方式来实现
对于AOP的使用方式,我们需要重点注意:
- 对于希望使用异步的方法,首先注解的
async
设置为true,其次如果存在返回结果,则必须是CompletableFuture
类型 - 注意AOP切面不生效的场景,同样会导致无法记录耗时(如服务内部调用,注解装饰private方法等)
本文中的测试用例,可以到这里查看 trace-watch-dog-spring
本文中的实现对应的是 trace-watch-dog-spring 核心实现