响应式编程的首要问题 - 不好调试
我们在分析传统代码的时候,在哪里打了断点,就能看到直观的调用堆栈,来搞清楚,谁调用了这个代码,之前对参数做了什么修改,等等。但是在响应式编程中,这个问题就很麻烦。来看下面的例子。
public class FluxUtil1 { public static Flux<Integer> test(Flux<Integer> integerFlux) { return FluxUtil2.test2(integerFlux.map(Object::toString)); } } public class FluxUtil2 { public static Flux<Integer> test2(Flux<String> stringFlux) { return stringFlux.map(Integer::new); } } public class FluxTest { public static void main(String[] args) { Flux<Integer> integerFlux = Flux.fromIterable(List.of(1, 2, 3)); FluxUtil1.test(integerFlux.log()).subscribe(integer -> { System.out.println(integer); }); } }
我们调试到 subscribe 订阅消费(这个后面会讲),我们一般会想知道我们订阅的这个东西,之前经过了怎样的处理,但是在System.out.println(integer)
打断点,看到的却是:
根本看不出来是FluxUtil1
,FluxUtil2
处理过这个Flux。简单的代码还好,复杂起来调试简直要人命。官方也意识到了这一点,所以提供了一种在操作时捕捉堆栈缓存起来的机制。
这里我们先给出这些机制如何使用,后面我们会分析其中的实现原理。
1. 通过打开全局 Operator 堆栈追踪
设置reactor.trace.operatorStacktrace
这个环境变量为 true,即启动参数中加入 -Dreactor.trace.operatorStacktrace=true
,这样启动全局 Operator 堆栈追踪。
这个也可以通过代码动态打开或者关闭:
//打开 Hooks.onOperatorDebug(); //关闭 Hooks.resetOnOperatorDebug();
打开这个追踪之后,在每多一个 Operator,就会多出来一个 FluxOnAssembly(这个后面原理会详细说明)。通过这个 FluxOnAssembly,里面就有堆栈信息。怎么获取呢?可以通过Scannable.from(某个Flux).parents().collect(Collectors.toList())
获取里面所有层的 Flux,其中包含了 FluxOnAssembly, FluxOnAssembly 就包含了堆栈信息。
我们这里,在System.out.println(integer)
打断点,加入查看Scannable.from(FluxUtil1.test(integerFlux.log())).parents().collect(Collectors.toList())
,就能看到:
可以看出,每次map
操作究竟发生在哪一行代码,都能看到。
如果使用的是专业版的 IDEA,还可以配置:
然后可以在打断点 Debug 就能看到具体堆栈:
2. 通过加入 ReactorDebugAgent 实现
添加依赖:
<dependency> <groupId>io.projectreactor</groupId> <artifactId>reactor-tools</artifactId> <version>略</version> </dependency>
之后,可以通过这两个代码,开启
//启用 ReactorDebugAgent.init(); //如果有类没有生效,例如初始化没加载,后来动态加载的类,可以调用这个重新处理启用 ReactorDebugAgent.processExistingClasses();
这样,可以动态修改线上应用开启Debug
模式,例如通过 Arthas 这个工具的 ognl 调用静态方法的功能(https://alibaba.github.io/arthas/ognl.html)。
如果使用的是专业版的 IDEA,还可以配置:
然后可以在打断点 Debug 就能看到具体堆栈: