前言
前面分享了《Spring5的WebClient使用详解》后,就有朋友在segmentfault上给博主提了一个付费的问题,这个是博主在segmentfault平台上面收到的首个付费问答,虽然酬劳不多,只有十元,用群友的话说性价比太低了。但在解决问题过程中对WebClient有了更深入的了解却是另一种收获。解决这个问题博主做了非常详细的排查和解决,现将过程记录在此,供有需要的朋友参考。
segmentfault问答地址:https://segmentfault.com/q/1590000021168484
问题背景
使用WebClient请求一个接口,使用bodyToMono方法用一个Entity接收响应的内容,伪代码如下:
代码语言:javascript复制 IdExocrResp resp = WebClient.create()
.post()
.uri("https://id.exocr.com:8080/bankcard")
.body(BodyInserters.fromFormData(formData))
.retrieve()
.bodyToMono(IdExocrResp.class)
.block();
上面的代码在运行时会抛一个异常,异常如下:
代码语言:javascript复制Exception in thread "main" org.springframework.web.reactive.function.UnsupportedMediaTypeException: Content type 'application/octet-stream' not supported for bodyType=IdExocrResp
at org.springframework.web.reactive.function.BodyExtractors.lambda$readWithMessageReaders$12(BodyExtractors.java:201)
Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException:
Error has been observed at the following site(s):
直译过来大概的意思就是,不支持application/octet-stream类型的Content Type。
问题分析
如上异常,抛异常的代码在BodyExtractors的201行,根据异常堆栈信息找到对应的代码分析:
代码语言:javascript复制 private static S readWithMessageReaders(
ReactiveHttpInputMessage message, BodyExtractor.Context context, ResolvableType elementType,
Function readerFunction,
Function errorFunction,
Supplier emptySupplier) {
if (VOID_TYPE.equals(elementType)) {
return emptySupplier.get();
}
MediaType contentType = Optional.ofNullable(message.getHeaders().getContentType())
.orElse(MediaType.APPLICATION_OCTET_STREAM);
return context.messageReaders().stream()
.filter(reader -> reader.canRead(elementType, contentType))
.findFirst()
.map(BodyExtractors::cast)
.map(readerFunction)
.orElseGet(() -> {
ListmediaTypes = context.messageReaders().stream()
.flatMap(reader -> reader.getReadableMediaTypes().stream())
.collect(Collectors.toList());
return errorFunction.apply(
new UnsupportedMediaTypeException(contentType, mediaTypes, elementType));
});
}
可以看到,在这个body提取器类中,有一个默认的contentType 策略,如果server端没有返回contentType ,默认就使用APPLICATION_OCTET_STREAM来接收数据。问题正是这里导致的。因为在这个接口的响应header里,contentType 为null,其实正确的应该是application/json,只是服务器没指定,然后被默认策略设置为application/octet-stream后,在默认的JSON解码器里是不支持,导致抛出了不支持的MediaType异常。定位到真实原因后,博主给出了如下方案
解决方案
方案一
如果服务端是自己的服务,可以修改服务端的程序指定ContentType为application/json类型返回即可。如果是第三方的服务,没法改动server端请参考下面的方案
方案二
使用String接收后,然后在flatMap里在过滤自己解码一遍,String类型可以接收application/octet-stream类型的Content Type的,代码如:
代码语言:javascript复制 IdExocrResp resp = WebClient.create()
.post()
.uri("xxx")
.body(BodyInserters.fromFormData(formData))
.retrieve()
.bodyToMono(String.class)
.flatMap(str -> Mono.just(JSON.parseObject(str, IdExocrResp.class)))
.block();
方案三
因为响应的值确实是json,只是在响应的header里没有指定Content Type为application/json。而最终异常也是因为json解码器不支持导致的,所以我们可以定制json解码器,重写支持的MediaType校验规则
自定义解码器
代码语言:javascript复制/**
* @author: kl @kailing.pub
* @date: 2019/12/3
*/
public class CustomJacksonDecoder extends AbstractJackson2Decoder {
public CustomJacksonDecoder() {
super(Jackson2ObjectMapperBuilder.json().build());
}
/**
* 添加 MediaType.APPLICATION_OCTET_STREAM 类型的支持
* @param mimeType
* @return
*/
@Override
protected boolean supportsMimeType(MimeType mimeType) {
return (mimeType == null
|| mimeType.equals(MediaType.APPLICATION_OCTET_STREAM)
|| super.getDecodableMimeTypes().stream().anyMatch(m -> m.isCompatibleWith(mimeType)));
}
}
设置解码器
代码语言:javascript复制 ExchangeStrategies strategies = ExchangeStrategies.builder()
.codecs(configurer -> configurer.customCodecs().decoder(new CustomJacksonDecoder()))
.build();
MultiValueMap formData = new LinkedMultiValueMap<>();
IdExocrResp resp = WebClient.builder()
.exchangeStrategies(strategies)
.build()
.post()
.uri("https://id.exocr.com:8080/bankcard")
.body(BodyInserters.fromFormData(formData))
.retrieve()
.bodyToMono(IdExocrResp.class)
.block();
方案四
因为响应的DefaultClientResponse里没有Content-Type,所以可以使用exchange()拿到clientResponse后重新build一个ClientResponse,然后设置Content-Type为application/json即可解决问题,代码如:
代码语言:javascript复制 MultiValueMap formData = new LinkedMultiValueMap<>();
IdExocrResp resp = WebClient.create()
.post()
.uri("https://id.exocr.com:8080/bankcard")
.body(BodyInserters.fromFormData(formData))
.exchange()
.flatMap(res -> ClientResponse.from(res)
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.body(res.body(BodyExtractors.toDataBuffers()))
.build()
.bodyToMono(IdExocrResp.class))
.block();
方案五
同方案四的思路,重新构造一个带Content-Type为application/json的clientResponse,但是处理逻辑是在filter里,就不需要使用exchange()了,博主以为这种方式最简洁优雅,代码如:
代码语言:javascript复制 MultiValueMap formData = new LinkedMultiValueMap<>();
IdExocrResp resp = WebClient.builder()
.filter((request, next) ->
next.exchange(request).map(response -> {
Fluxbody = response.body(BodyExtractors.toDataBuffers());
return ClientResponse.from(response)
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.body(body)
.build();
}))
.build()
.post()
.uri("https://id.exocr.com:8080/bankcard")
.body(BodyInserters.fromFormData(formData))
.retrieve()
.bodyToMono(IdExocrResp.class)
.block();
方案六
前面原因分析的时候已经说了,MediaType为空时spring默认设置为application/octet-stream了。这里的设计其实可以更灵活点的,比如除了默认的策略外,还可以让用户自由的设置默认的Content Type类型。这个就涉及到改动Spring的框架代码了,博主已经把这个改动提交到Spring的官方仓库了,如果合并了的话,就可以在下个版本使用这个方案解决问题了
pr地址:https://github.com/spring-projects/spring-framework/pull/24120
结语
最近对WebClient和reactor-netty的研究正热,已经帮两位朋友解决过疑难问题了。在这里打个广告,欢迎对java领域有任何问题的朋友通过https://segmentfault.com/u/klbozhu_5d66460788cde的窗口向我提问,钱多钱少的不是很重要,最重要的是看我有没有兴趣和缘分哈