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
的官方文档(https://projectreactor.io/docs/core/release/reference/#context )可见下面这段话:
Since version
3.1.0
, Reactor comes with an advanced feature that is somewhat comparable toThreadLocal
but can be applied to aFlux
or aMono
instead of aThread
. This feature is calledContext
.
并且官网也给出了为何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 总结
代码请查看:https://github.com/LarryDpk/pkslow-samples
参考资料: