在Spring WebFlux的任何地方获取Request对象

1 不一样的世界

在常规的 Spring Web 项目中,我们要获取 Request 对象是非常方便的,不少库都提供了静态方法来获取。获取代码如下:

ServletRequestAttributes requestAttributes = (ServletRequestAttributes)RequestContextHolder.getRequestAttributes();
// get the request
HttpServletRequest request = requestAttributes.getRequest();

在类 RequestContextHolder 提供了静态方法,也就意味着你可以在任何地方调用。而它使用了 ThreadLocal 来保存 Request 对象,也就是不同线程是可以获取各自的Request对象。

但在响应式 WebFlux 的世界里,并没有提供类似的 Holder 类,而 WebFlux 是无法感知线程的,任何一个线程可以在任何时候处理任何请求,如果它觉得切换当前线程更有效率,它就会这么做。但在 Servlet Based 的应用里,它会为某个请求安排一个线程去处理完整个过程。

这个巨大的差别,意味着不能简单地通过 ThreadLocal 来保存和获取 Request 了。

2 先保存,再获取

为了在后面可以方便获得 Request 对象,我们就需要在开始的时候把它存在一个可以使用、并且是相同 scope 的容器里。这里需要解决两个关键问题:

(1) Request 对象从何而来;

(2)存在哪里?

针对问题(1), 我们可以回想什么时候会出现 Request 对象,最容易想得到的就是 WebFilter 了,它的方法签名如下:

public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain);

我们可以通过 ServerWebExchange 直接获取到 Request 对象:

ServerHttpRequest request = exchange.getRequest();

而因为 Filter 是可以先于应用逻辑执行的,所以满足要求,问题(1)解决。

针对问题(2),需要一个与 Reavtive 请求相同范围的容器, reactor.util.context.Context 可以满足需求。查看 reactor 的官方文档(Reactor 3 Reference Guide )可见下面这段话:

Since version 3.1.0 , Reactor comes with an advanced feature that is somewhat comparable to ThreadLocal but can be applied to a Flux or a Mono instead of a Thread . This feature is called Context .

并且官网也给出了为何 ThreadLocal 在某些场景不适用的解释,有兴趣可以看看。

3 代码实现

3.1 WebFilter获取并保存

首先,在 WebFilter 中获取 Request 对象并保存,代码如下:

@Configuration
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE)
public class ReactiveRequestContextFilter implements WebFilter {
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        return chain.filter(exchange)
                .subscriberContext(ctx -> ctx.put(ReactiveRequestContextHolder.CONTEXT_KEY, request));
    }
}

ServerWebExchange 中获取到 ServerHttpRequest 对象,再通过 put 方法把它放进 Context 里。

3.2 工具类Holder

实现一个工具类来提供静态方法,在 Filter 后的任何场景都可以使用:

public class ReactiveRequestContextHolder {
    public static final Class<ServerHttpRequest> CONTEXT_KEY = ServerHttpRequest.class;

    public static Mono<ServerHttpRequest> getRequest() {
        return Mono.subscriberContext()
                .map(ctx -> ctx.get(CONTEXT_KEY));
    }
}

3.3 在Controller中使用

我们尝试在 Controller 中使用 ReactiveRequestContextHolder 来获取 Request

@RestController
public class GetRequestController {

    @RequestMapping("/request")
    public Mono<String> getRequest() {
        return ReactiveRequestContextHolder.getRequest()
                .map(request -> request.getHeaders().getFirst("user"));
    }
}

上面方法获取了 Request 对象,然后再获取了 Request 中的 Header

启动应用,测试如下:

$ curl http://localhost:8088/request -H 'user: pkslow'
pkslow

$ curl http://localhost:8088/request -H 'user: larry'
larry

$ curl http://localhost:8088/request -H 'user: www.pkslow.com'
www.pkslow.com

可以成功获取请求头 user

4 总结

代码请查看:GitHub - LarryDpk/pkslow-samples: samples for www.pkslow.com


原文:在Spring WebFlux的任何地方获取Request对象 | 南瓜慢说官方网站
作者:南瓜慢说