Spring WebFlux (Reactor3)上下文Context处理

相关阅读

Spring WebFlux(Reactor3)重试

Spring WebFlux(Reactor3)响应式编程处理异常

前言

在Spring MVC中我们可以使用ThreadLocal保存一些信息,作为请求的上下文。一些公用的变量就不用写在方法中,可以通过上下文来获取(比如:用户的登录信息)。在《重构》中马丁福勒也推荐在能通过其他方式获取到的变量时,不要使用方法参数传递进来。在Spring WebFlux中会存在多次线程切换,ThreadLocal在Spring WebFlux中就无法发挥作用了,除非手动的做ThreadLocal线程间的复制,这样处理明显是不明智的行为。那在Spring WebFlux中如何处理上下文信息,多线程间变量的传递问题?

Reactor上下文处理

Spring WebFlux底层使用Reactor,在Reactor中有一个Context特性,用来存放调用链的上下文信息。在学习Spring WebFlux处理上下文信息的方式前,我们先了解一下Reactor是如何处理的。

Reactor中的Context也是一个Map结构,以key-value的形式存储。下面通过几个来自Reactor Guide的例子实际看看,Reactor是怎么处理上下文的。由于Reactor的Context在3.4.3做了改版,Spring Boot 2.3.3是基于Reactor 3.3+的,所以两个版本的例子我都贴上来了。

例子1:

3.4.3版本

String key = "message";
Mono 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();

3.3+版本

String key = "message";
        Mono r = Mono.just("Hello")
                .flatMap( s -> Mono.subscriberContext()
                        .map( ctx -> s + " " + ctx.get(key)))
                .subscriberContext(ctx -> ctx.put(key, "World"));
 
        StepVerifier.create(r)
                .expectNext("Hello World")
                .verifyComplete();

我们可以看到contextWrite设置上下文的操作在调用链的最尾端。在其上游的操作中能成功获取到元素。订阅是从下游流上上游的,看过Reactor或者RxJava源码的同学可能会理解深一点,要做到这点只用在调用next方法前将上下文设置进去就可以了。

例子2:

3.4.3版本

String key = "message";
Mono r = Mono.just("Hello")
    .contextWrite(ctx -> ctx.put(key, "World")) 
    .flatMap( s -> Mono.deferContextual(ctx ->
        Mono.just(s + " " + ctx.getOrDefault(key, "Stranger")))); 

StepVerifier.create(r)
            .expectNext("Hello Stranger") 
            .verifyComplete();

3.3+版本 

String key = "message";
        Mono r = Mono.just("Hello")
                .subscriberContext(ctx -> ctx.put(key, "World"))
                .flatMap( s -> Mono.subscriberContext()
                        .map( ctx -> s + " " + ctx.getOrDefault(key, "Stranger")));
 
        StepVerifier.create(r)
                .expectNext("Hello Stranger")
                .verifyComplete();

 

第一个例子中contextWrite在调用链的最尾端,我们也知道了contextWrite会对上游的操作产生影响。那如果放在前面呢?第二个例子将contextWrite放到了调用链的中间,可以看到中间的contextWrite没有对后面的flatMap产生影响。也就是contextWrite只对它上游的操作生效,对下游的操作不生效。

例子3:

3.4.3版本

String key = "message";
Mono r = Mono
    .deferContextual(ctx -> Mono.just("Hello " + ctx.get(key)))
    .contextWrite(ctx -> ctx.put(key, "Reactor")) 
    .contextWrite(ctx -> ctx.put(key, "World")); 

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

3.3+版本 

String key = "message";
        Mono r = Mono.just("Hello")
                .flatMap( s -> Mono.subscriberContext()
                        .map( ctx -> s + " " + ctx.get(key)))
                .subscriberContext(ctx -> ctx.put(key, "Reactor"))
                .subscriberContext(ctx -> ctx.put(key, "World"));
 
        StepVerifier.create(r)
                .expectNext("Hello Reactor")
                .verifyComplete();

在前面的两个例子中,我们看到Reactor的contextWrite只会对其上游的操作生效,那如果再下游有多个contextWrite呢?Reactor会从邻近的contextWrite中读取上下文的信息。

例子4:

3.4.3版本

String key = "message";
Mono r = Mono
    .deferContextual(ctx -> Mono.just("Hello " + ctx.get(key))) 
    .contextWrite(ctx -> ctx.put(key, "Reactor")) 
    .flatMap( s -> Mono.deferContextual(ctx ->
        Mono.just(s + " " + ctx.get(key)))) 
    .contextWrite(ctx -> ctx.put(key, "World")); 

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

3.3+版本 

String key = "message";
        Mono r = Mono.just("Hello")
                .flatMap( s -> Mono.subscriberContext()
                        .map( ctx -> s + " " + ctx.get(key)))
                .subscriberContext(ctx -> ctx.put(key, "Reactor"))
                .flatMap( s -> Mono.subscriberContext()
                        .map( ctx -> s + " " + ctx.get(key)))
                .subscriberContext(ctx -> ctx.put(key, "World"));
 
        StepVerifier.create(r)
                .expectNext("Hello Reactor World")
                .verifyComplete();


  这个例子中第一个获取上下文的信息从邻近下游读取到Reactor,第二个flatMap邻近下游读取到的World。

例子5:

3.4.3版本

String key = "message";
Mono 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();

3.3+版本 

String key = "message";
        Mono r =
                Mono.just("Hello")
                        .flatMap( s -> Mono.subscriberContext()
                                .map( ctx -> s + " " + ctx.get(key))
                        )
                        .flatMap( s -> Mono.subscriberContext()
                                .map( ctx -> s + " " + ctx.get(key))
                                .subscriberContext(ctx -> ctx.put(key, "Reactor"))
                        )
                        .subscriberContext(ctx -> ctx.put(key, "World"));
 
        StepVerifier.create(r)
                .expectNext("Hello World Reactor")
                .verifyComplete();

第一个contextWrite只影响内部的操作流。
第二个contextWrite只会影响外部主操作流。
通过上面的例子,可能你会觉得Reactor Context好难用,一不小心就会掉到陷阱中。所以在使用Context的时候,一定要理解清楚Reactor Context的行为。在Spring 5.x中事务的实现就由Reactor的Context替换了ThreadLocal,理解Context是很有必要的。


Spring WebFlux中处理上下文

首先,理一下具体的思路。通过上面的学习我们知道Reactor可以处理上下文的信息传递,根据Reactor Context只对上游生效的特性,我们拦截请求在后置处理处做一次contextWrite或subscriberContext设置的上下文信息。这样就可以达到
我们的目的了。通过上面的分析,我们只用在Spring WebFlux中找到和Spring MVC中HandlerInterceptorAdapter类似的东西,在后置处理器中做contextWrite或subscriberContext操作就可以了。遗憾的是在Spring WebFlux中没有HandlerInterceptorAdapter,但是可以使用过滤器WebFilter。

@Component
@Slf4j
public class AuthFilter implements WebFilter {
    @Override
    public Mono filter(ServerWebExchange exchange, WebFilterChain chain) {
        return chain.filter(exchange).subscriberContext(ctx -> {
            log.info("设置context内容。");
//注意这里一定要return ctx.put。put操作是产生一个新的context。如果是return ctx;会导致值没有设置进去
           return  ctx.put("token", "xx");
        });
    }

}

按照上面的处理方式,在我们业务开发中就能取到上下文的信息了。

@PostMapping(name = "测试", value = "/save")
    public Mono save(@RequestBody Mono testMono) {
      
        return testService.add(testMono).log("", Level.INFO, true).flatMap(test -> {
            return Mono.subscriberContext().map(ctx -> {
                log.info("context:" + ctx.get("token"));
                return test;
            });
        });
    }

如果想设置多个值可以像下面这样操作。

@Component
public class WebFluxFilter implements WebFilter {
    @Override
    public Mono filter(ServerWebExchange exchange, WebFilterChain chain) {
        return chain.filter(exchange)
                .subscriberContext(ctx -> {
                    return ctx.put("token", "123123123123");
                })
                .subscriberContext(ctx -> {
                    return ctx.put("liu", "hah");
                });
    }
}

 

总结

使用Spring WebFlux处理上下文,要先了解Reactor的Context处理逻辑。这样才能避免踩坑,毕竟上下文的信息错取引发的问题可不是小问题。

 

 

转载请注明出处

https://blog.csdn.net/LCBUSHIHAHA/article/details/114837031

参考资料

https://projectreactor.io/docs/core/release/reference/#_checking_the_execution_path_with_publisherprobe

你可能感兴趣的:(spring,java)