8.借助AOP提供非侵入的使用姿势

一灰灰blog技术组件trace-watch-dog约 2776 字大约 9 分钟

再前面完成封装的TraceWatch,进一步简化了使用体验,但是依然存在一个明显的缺陷,对业务代码的侵入性较强,需要再业务代码中,进行主动的埋点

对应常年和Spring打交道的java开发者来说,一个很容易想到的优化方案就是借助AOP来简化业务代码的侵入,接下来我们就看一下,如何借助Spring的AOP能力,对我们之前提供的TraceWatch做一个能力增强

1. 方案设计

对于某个链路的耗时统计,首先确定有一个方法作为耗时记录的入口,表示开始记录耗时,然后就是再执行的过程中,发现有需要统计耗时的方法,则通过Around环绕切面来计算耗时,最后再入口方法执行完毕之后,输出耗时情况即可

1.1 整体实现流程

上面是一个简单的AOP集成说明:

  1. 首先自定义一个注解,用来表示哪些方法需要进行耗时记录
  2. 执行链路中的,首个被自定义注解标注的方法,作为耗时记录的入口
    • 即调用 TraceWatch.start 来创建 TraceRecoder
  3. 在入口方法内部执行的调用链路中,执行到需要记录耗时的方法时,通过traceRecoder.sync/async来加入耗时统计
  4. 在入口方法执行完毕时,输出耗时分布

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的使用方式,我们需要重点注意:

  1. 对于希望使用异步的方法,首先注解的async设置为true,其次如果存在返回结果,则必须是CompletableFuture类型
  2. 注意AOP切面不生效的场景,同样会导致无法记录耗时(如服务内部调用,注解装饰private方法等)

本文中的测试用例,可以到这里查看 trace-watch-dog-springopen in new window

本文中的实现对应的是 trace-watch-dog-springopen in new window 核心实现

Loading...