Spring WebFlux获取Body和访问IP

当通过subscribe获取body体的时候,总是报null。

Spring Boot版本:2.1.1.RELEASE

官方样例

Spring官方有一个样例:spring-boot-sample-webflux

包含一个启动类、2个Controller和一个Handler。

Controller是两个基本的基于@RestController注解的Controller类,与以往@Controller注解的区别就是包含了@RequestBody注解。这次主要就是看看这个Handler

1
2
3
4
5
6
7
8
@Component
public class EchoHandler {

public Mono<ServerResponse> echo(ServerRequest request) {
return ServerResponse.ok().body(request.bodyToMono(String.class), String.class);
}

}
1
2
3
4
@Bean
public RouterFunction<ServerResponse> monoRouterFunction(EchoHandler echoHandler) {
return route(POST("/echo"), echoHandler::echo);
}

通过Bean注册路由信息,这和以往的@Controller有所不同,但是和Vert.x很相似。在Handler中通过ServerRequest接口接收请求信息。

1
2
3
4
5
6
// Vert.x 路由部署方式
...
router.get("/").handler(this::indexhandler);
router.route().handler(BodyHandler.create().setMergeFormAttributes(true));
router.route("/static/*").handler(StaticHandler.create("static"));
...

运行样例:

使用Httpie携带Body 信息请求/echo,返回请求的Body信息

1
2
3
4
5
6
7
8
E:\spring-boot-sample-webflux>http post :8080/echo foo=bar
HTTP/1.1 200 OK
Content-Length: 14
Content-Type: text/plain;charset=UTF-8

{
"foo": "bar"
}

获取请求

上面的实例可以获取Body信息,但是当我们想subscribe获取这个body内的信息时,会得到一个null的对象。

错误代码:

1
2
3
4
request.bodyToMono(String.class)
.subscribe(body -> {
System.out.println("body data->" + body);
});

Spring Boot#15320找到了答案

1
2
3
4
5
This code is calling subscribe on the request body and decouples it from the response rendering. 
Because you're decoupling the request handling from the bit that reads the request body,
you're running into a race condition:
by the time the response is handled, Spring WebFlux is cleaning the HTTP resources (request and response resources),
which means that your other subscription might not have time to read the body.

主要意思就是:

1
2
3
调用subscribe()方法获取请求体会割裂响应,此时正处于竞争态。
在处理响应时,Spring WebFlux正在清理HTTP资源(请求和响应资源)。
意味着你其他的订阅无法得到请求体。

所以想对Body做操作时,需要链式调用,最好不要使用订阅等方法,而且这里还处于竞争态,更不能使用。所以单独获取Body或者某一请求值的话,直接使用flatMap()方法

1
2
3
4
public Mono<ServerResponse> test2(ServerRequest request) {
return request.bodyToMono(String.class)
.flatMap(s -> ServerResponse.ok().body(Mono.just(s), String.class));
}

这样就可以在flatMap()操作body了,同时在后面加上了log()。看下调用情况

1
2
3
4
5
6
7
8
2019-01-11 15:19:25.971  INFO 19612 --- [ctor-http-nio-2] reactor.Mono.FlatMap.2                   : | onSubscribe([Fuseable] MonoFlatMap.FlatMapMain)
2019-01-11 15:19:25.971 INFO 19612 --- [ctor-http-nio-2] reactor.Mono.FlatMap.2 : | request(unbounded)
2019-01-11 15:19:25.975 INFO 19612 --- [ctor-http-nio-2] reactor.Mono.OnErrorResume.1 : onSubscribe(FluxOnErrorResume.ResumeSubscriber)
2019-01-11 15:19:25.975 INFO 19612 --- [ctor-http-nio-2] reactor.Mono.OnErrorResume.1 : request(unbounded)
2019-01-11 15:19:25.984 INFO 19612 --- [ctor-http-nio-2] reactor.Mono.OnErrorResume.1 : onNext({"foo": "bar"})
2019-01-11 15:19:25.990 INFO 19612 --- [ctor-http-nio-2] reactor.Mono.FlatMap.2 : | onNext(org.springframework.web.reactive.function.server.DefaultEntityResponseBuilder$DefaultEntityResponse@b65494c)
2019-01-11 15:19:26.017 INFO 19612 --- [ctor-http-nio-2] reactor.Mono.FlatMap.2 : | onComplete()
2019-01-11 15:19:26.017 INFO 19612 --- [ctor-http-nio-2] reactor.Mono.OnErrorResume.1 : onComplete()

可以在第五行看到onNext({"foo": "bar"}),显示出了请求体。同时可以看到,reactor.Mono.OnErrorResume.1这里是调用了OnErrorResume方法,我理解就是存在竞争,它是从错误中恢复出来的。

当获取多个参数时,需要使用zip()方法将几个参数zip在一起,例如获取请求IP和请求体

1
2
3
4
5
6
7
8
9
10
11
12
13
public Mono<ServerResponse> test(ServerRequest request) {
return Mono.zip(request.bodyToMono(String.class),
Mono.just(request.remoteAddress()
.map(InetSocketAddress::getHostString)
.orElseThrow(RuntimeException::new)))
.flatMap(tuple -> {
String bodyData = tuple.getT1();
String remoteIp = tuple.getT2();
log.info("BodyData =>" + bodyData);
log.info("RemoteIp =>" + remoteIp);
return ServerResponse.ok().body(Mono.just(bodyData + "\n" + remoteIp), String.class);
});
}

请求/test端点

1
2
3
4
5
6
7
C:\spring-boot-sample-webflux>http post :8080/test foo=bar
HTTP/1.1 200 OK
Content-Length: 30
Content-Type: text/plain;charset=UTF-8

{"foo": "bar"}
0:0:0:0:0:0:0:1

总结

​ 想要在Spring 5,Spring Boot 2上得到更好的体验。可以尝试转到流式编程上,包括使用lambda表达式等。可以增加代码的可读性和整洁性。

​ 在这个例子里面,主要是获取多个请求参数,我自己也绕了好久,从以往的注解中跳出来。更多的可以看看Pivotal开源的Reactor或者RxJava等等。

结束!🔚


Buy Me A Coffee.