Reactor响应式编程系列(八)- Reactor的上下文Context

Reactor响应式编程系列导航

一. Context

Context主要用来上下文中数据的传递,因为在响应式编程中经常会出现这两种场景:

  • 一个线程可能被用于处理多个异步订阅关系。
  • 一个订阅关系在元素下发的过程中往往也可能从一个线程切换到另一个线程。

因此需要引入一个Context用来解决上述情况中的数据传递问题。

Demo如下:

public static Scheduler custom_Scheduler() {
    Executor executor = new ThreadPoolExecutor(
            10,  
            10,  
            0L, TimeUnit.MILLISECONDS, 
            new LinkedBlockingQueue<>(1000),  
            Executors.defaultThreadFactory()
    );
    return Schedulers.fromExecutor(executor);
}

private static void sleep(long millis) {
    try {
        Thread.sleep(millis);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

@Test
public void flux_generate4() {
    final Random random = new Random();
    Flux.generate(ArrayList::new, (list, sink) -> {
        int value = random.nextInt(100);
        list.add(value);
        System.out.println("所发射元素产生的线程: "+Thread.currentThread().getName());
        sink.next(value);
        sleep(1000);
        if (list.size() == 20) {
            sink.complete();
        }
        return list;
    }).publishOn(custom_Scheduler(),1)
            .map(x -> String.format("[%s] %s", Thread.currentThread().getName(), x))
            .subscribe(System.out::println);
    sleep(20000);
}

结果如下:一次订阅,但是元素的消费线程却始终在切换。
在这里插入图片描述
这种情况会带来一个问题,若存在这么一种情况:我们需要在线程的上下文中存储一些对象数据,方便我们在整个执行的过程中使用他们,在生命周期结束的时候将存储的数据进行销毁。 所以我们该如何存储呢?

方案1:使用ThreadLocal,但是这会有个缺点,在我们自定义线程池的时候,在调度的时候几个订阅关系可能会共用一个线程池,故不同订阅关系之间可能会由于ThreadLocal而产生数据泄露。

方案2:使用Reactor提供的ContextAPI来解决,其用于服务Flux或者Mono单个订阅关系的上下文数据存储。

1.1 Context的简单用法

Demo1:从官网案例出发(版本3.4+):运行结果不会报错即可。

@Test
public void contextSimple1() {
    String key = "message";
    Mono<String> r = Mono.just("Hello")
            .flatMap(s -> Mono.deferContextual(ctx ->
                    Mono.just(s + " " + ctx.get(key))))
            .contextWrite(ctx -> ctx.put(key, "World"));

    StepVerifier.create(r).expectNext("Hello World").verifyComplete();
}

若不报错,也就是说最终的结果变成了Hello World

为什么会有这样的结果呢?

  1. 我们使用contextWrite()Context中写入一个键值对,key:messagevalue:World
  2. faltMap()中,通过Mono.deferContextual()获得一个Context对象,可以取出对应key的value值,并进行拼接。

同时我们还能从该例子中发现,设置上下文的操作contextWrite()在调用链的最底端,说明了什么?订阅是从下游流向上游的。 若将上述案例改成:


Demo2:改变contextWrite()的调用位置:

@org.junit.Test
public void test() {
    String key = "message";
    Mono<String> r = Mono.just("Hello")
            .contextWrite(ctx -> ctx.put(key, "World"))
            .flatMap(s -> Mono.deferContextual(ctx ->
                    Mono.just(s + " " + ctx.get(key))));
    
    StepVerifier.create(r)
            .expectNext("Hello World")
            .verifyComplete();
}

结果如下:
在这里插入图片描述
解释如下:

  1. 因为contextWrite()是从下往上传递的,因此并不会经过flatMap()
  2. 所以在flatMap()时,抛出了异常,因为Context为空的。

Demo3:存在多个contextWrite()的情况,且对应key一样

@org.junit.Test
public void test3() {
    String key = "message";
    Mono<String> r = Mono.just("Hello")
            .flatMap(s -> Mono.deferContextual(ctx ->
                    Mono.just(s + " " + ctx.get(key))))
            .contextWrite(ctx -> ctx.put(key, "Reactor"))
            .contextWrite(ctx -> ctx.put(key, "World"));

    StepVerifier.create(r)
            .expectNext("Hello Reactor")
            .verifyComplete();
}

最后期望的结果是:Hello Reactor,原因可以这么理解:
在这里插入图片描述

总结1:

  • Mono.deferContextual(c-> c.get(key)):获得Context中指定Key对应的Value值。
  • contextWrite(c -> c.put(key, value):往Context中塞入一个键值对。
  • 简单用法的格式:Mono.deferContextual(c ->Mono.just(c.get(key)))

1.2 Context相关方法解析

1.从Mono.deferContextual出发:

public static <T> Mono<T> deferContextual(Function<ContextView, ? extends Mono<? extends T>> contextualMonoFactory) {
	return onAssembly(new MonoDeferContextual<>(contextualMonoFactory));
}
↓↓↓看下MonoDeferContextual这个类↓↓
final class MonoDeferContextual<T> extends Mono<T> implements SourceProducer<T> {

	final Function<ContextView, ? extends Mono<? extends T>> contextualMonoFactory;

	MonoDeferContextual(Function<ContextView, ? extends Mono<? extends T>> contextualMonoFactory) {
		this.contextualMonoFactory = Objects.requireNonNull(contextualMonoFactory, "contextualMonoFactory");
	}

	@Override
	public void subscribe(CoreSubscriber<? super T> actual) {
		Mono<? extends T> p;
		// 该方法决定了Context和订阅者之间的绑定关系
		Context ctx = actual.currentContext();
		try {
			p = Objects.requireNonNull(contextualMonoFactory.apply(ctx),
					"The Mono returned by the contextualMonoFactory is null");
		}
		catch (Throwable e) {
			Operators.error(actual, Operators.onOperatorError(e, ctx));
			return;
		}

		p.subscribe(actual);
	}
}
``
2.我们先来分析下`actual.currentContext();`这个代码:

```java
public interface CoreSubscriber<T> extends Subscriber<T> {
	default Context currentContext(){
		return Context.empty();
	}
}
↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
public interface Context extends ContextView {
	static Context empty() {
		return Context0.INSTANCE;
	}
}

3.可见这里是返回了一个Context0的实例,而Context0Context接口的一个子类,来看下它的结构:

final class Context0 implements CoreContext {
	static final Context0 INSTANCE = new Context0();

	@Override
	public Context put(Object key, Object value) {
		Objects.requireNonNull(key, "key");
		Objects.requireNonNull(value, "value");
		return new Context1(key, value);
	}
}

大家有没有发现接口Context的子类的名称后面都带着数字
在这里插入图片描述
意思就是:

  1. 加入Context对象中已经有一个键值对了,那么纠结和之前存在的键值对重新创建一个ContextX对象
  2. 而X为键值对的个数,当X超过5的时候,就直接命名为ContextN

因此如果我们创建两个一样的键值对,可以看下Context2类相关的put方法:

@Override
public Context put(Object key, Object value) {
	Objects.requireNonNull(key, "key");
	Objects.requireNonNull(value, "value");

	if(this.key1.equals(key)){
		return new Context2(key, value, key2, value2);
	}

	if (this.key2.equals(key)) {
		return new Context2(key1, value1, key, value);
	}

	return new Context3(this.key1, this.value1, this.key2, this.value2, key, value);
}

很明显,当 key 相同的时候,其执行的并不是 update 操作,而是重新new了一个对象。


紧接着讲一个Demo,在Demo3的基础上,增加一个订阅关系:

@org.junit.Test
public void test4() {
    String key = "message";
    Mono<String> r = Mono
            .deferContextual(ctx -> Mono.just("Hello " + ctx.get(key)))//3
            .contextWrite(ctx -> ctx.put(key, "Reactor"))//2
            .flatMap( s -> Mono.deferContextual(ctx ->
                    Mono.just(s + " " + ctx.get(key))))//4
            .contextWrite(ctx -> ctx.put(key, "World"));//1

    StepVerifier.create(r)
            .expectNext("Hello Reactor World")//5
            .verifyComplete();
}

可见最终的结果是Hello Reactor World

  1. 根据从下往上的原则,第一步中第一次写入了一个Contextkeymessage
  2. 第二步中,可见key还是message,同样的写入了一个Context,但是根据上文的说法,这里并不会对已有的Context进行更新操作,而是重新创建了一个对象。因此第二次和第一次写入的Context并不是同一个。
  3. 第三步中,读取的上下文Context是最近的一次,也就是第二步中生成的Context对象。
  4. 通过deferContextual()源码我们发现,最后会调用一个subscribe()方法,也就是产生订阅关系,那么这里调用了两次deferContextual(),也就是有两个订阅关系,那么自然而然的就会调用两次flatMap方法
  5. 那么第一次调用,此时flatMap拼接的是最近Context中的内容,也就是Reactor。第二次写入则拼接World。也就成了最终的结果Hello Reactor World

一句话就是:Context 是与 Subscriber 关联的,而每一个操作符访问的 Context 来自其下游的 Subscriber


Demo:在flatMap方法内部去写一个Context,看看会有什么样的结果:

@org.junit.Test
public void test5() {
    String key = "message";
    Mono<String> r = Mono.just("Hello")
            .flatMap(s -> Mono
                    .deferContextual(ctxView -> Mono.just(s + " " + ctxView.get(key)))
            )
            .flatMap(s -> Mono
                    .deferContextual(ctxView -> Mono.just(s + " " + ctxView.get(key)))
                    .contextWrite(ctx -> ctx.put(key, "Reactor"))
            )
            .contextWrite(ctx -> ctx.put(key, "World"));

    StepVerifier.create(r)
            .expectNext("Hello World Reactor")
            .verifyComplete();
}

按照以往的说法,从下往上看,离源最近的Context的值是Reactor,应该优先输出,那么最终的结果是Hello World Reactor

  1. 因为subscriberContext写"ReactorflatMap方法中内部序列的一部分。
  2. 因此,它在主序列中不可见或传播,所以第一个序列flatMap则看不到它。

总结2

  • Context具有不变性,也就是若存在key相同的情况下,会再创建一个新的Context
  • 在一些方法内部(如faltMap)中写入的Context,相当于是一个局部变量,对于全局的主序列而言不可见。
  • 调用一次deferContextual(),就会产生一个订阅关系,即多一个Subscriber,因为源码中最后调用了xxx.subscribe()
  • Context 是与 Subscriber 一对一关联的,而每一个操作符访问的 Context 来自其下游的 Subscriber

二. Context实战Demo

static final String HTTP_CORRELATION_ID = "reactive.http.library.correlationId";

Mono<Tuple2<Integer, String>> doPut(String url, Mono<String> data) {
    Mono<Tuple2<String, Optional<Object>>> dataAndContext =
            // 	在延迟内,提取相关性ID密钥的值,并合并
            data.zipWith(Mono.deferContextual(c ->
                    Mono.just(c.getOrEmpty(HTTP_CORRELATION_ID)))
            );

    return dataAndContext.<String>handle((dac, sink) -> {
        // isPresent():即当前序列中是否有这个元素,有的话就返回true
        // 也就是如果该秘钥存在于上下文当做,那么就将相关性的ID作为标题
        if (dac.getT2().isPresent()) {
            sink.next("PUT <" + dac.getT1() + "> sent to " + url +
                    " with header X-Correlation-ID = " + dac.getT2().get());
        } else {
            sink.next("PUT <" + dac.getT1() + "> sent to " + url);
        }
        sink.complete();
    })
            .map(msg -> Tuples.of(200, msg));
}

@org.junit.Test
public void contextForLibraryReactivePut() {
    Mono<String> put = doPut("书库", Mono.just("Java编程"))
    		// 这里带上了对应的Id
            .contextWrite(Context.of(HTTP_CORRELATION_ID, "123"))
            .filter(t -> t.getT1() < 300)// getT1 获取第一个元素
            .map(Tuple2::getT2);
	// 最终的输出结果
    StepVerifier.create(put)
            .expectNext("PUT <Java编程> sent to 书库" +
                    " with header X-Correlation-ID = 123")
            .verifyComplete();
}

若将代码contextWrite(Context.of(HTTP_CORRELATION_ID, "123"))进行修改,如:

.contextWrite(Context.of("xxx", "123"))

也就是让所带的秘钥和Context中包含的秘钥对不上,最后输出:
在这里插入图片描述

  • 4
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Zong_0915

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值