WebClient抛UnsupportedMediaTypeException异常解决

 更新时间:2022年02月24日 14:49:34   作者:kl  
这篇文章主要为大家介绍了WebClient抛UnsupportedMediaTypeException异常的解决方案,文中给大家介绍了六中方案,有需要的朋友可以借鉴参考下,希望能够有所帮助

前言

前面分享了Spring5中的WebClient使用方法详解 后,就有朋友在segmentfault上给博主提了一个付费的问题,这个是博主在segmentfault平台上面收到的首个付费问答,虽然酬劳不多,只有十元,用群友的话说性价比太低了。但在解决问题过程中对WebClient有了更深入的了解却是另一种收获。解决这个问题博主做了非常详细的排查和解决,现将过程记录在此,供有需要的朋友参考。

问题背景

使用WebClient请求一个接口,使用bodyToMono方法用一个Entity接收响应的内容,伪代码如下:

IdExocrResp resp = WebClient.create()
                .post()
                .uri("https://id.exocr.com:8080/bankcard")
                .body(BodyInserters.fromFormData(formData))
                .retrieve()
                .bodyToMono(IdExocrResp.class)
                .block();

上面的代码在运行时会抛一个异常,异常如下:

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行,根据异常堆栈信息找到对应的代码分析:

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的,代码如:

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校验规则

自定义解码器

/**
 * @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)));
    }
}

设置解码器

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即可解决问题,代码如:

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()了,博主以为这种方式最简洁优雅,代码如:

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抛UnsupportedMediaTypeException异常解决的详细内容,更多关于WebClient抛UnsupportedMediaTypeException的资料请关注脚本之家其它相关文章!

相关文章

  • 基于springMvc+hibernate的web application的构建

    基于springMvc+hibernate的web application的构建

    下面小编就为大家带来一篇基于springMvc+hibernate的web application的构建。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2017-10-10
  • Maven仓库加载顺序的实例解析

    Maven仓库加载顺序的实例解析

    Maven仓库一般分为本地仓库和远程仓库。那么在实际开发中,在配置了多个仓库的情况下,他们之间的加载访问顺序是怎么样的呢,本文就详细的来介绍一下
    2021-12-12
  • Spring Boot入门(web+freemarker)

    Spring Boot入门(web+freemarker)

    这篇文章主要介绍了Spring Boot入门(web+freemarker)的相关资料,需要的朋友可以参考下
    2017-06-06
  • java isInterrupted()判断线程的实例讲解

    java isInterrupted()判断线程的实例讲解

    在本篇内容里小编给大家分享的是一篇关于java isInterrupted()判断线程的实例讲解内容,有兴趣的朋友们可以学习下。
    2021-05-05
  • Spring Data JPA分页复合查询原理解析

    Spring Data JPA分页复合查询原理解析

    这篇文章主要介绍了Spring Data JPA分页复合查询原理解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2019-11-11
  • 使用@Autowired注解有错误提示的解决

    使用@Autowired注解有错误提示的解决

    这篇文章主要介绍了使用@Autowired注解有错误提示的解决,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2021-11-11
  • Netty与Spring Boot的整合实现

    Netty与Spring Boot的整合实现

    这篇文章主要介绍了Netty与Spring Boot的整合的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2019-08-08
  • SpringBoot如何优雅地使用Swagger2

    SpringBoot如何优雅地使用Swagger2

    这篇文章主要介绍了SpringBoot如何优雅地使用Swagger2,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2020-07-07
  • java枚举enum和Enum类的使用

    java枚举enum和Enum类的使用

    本文主要介绍了java枚举enum和Enum类的使用,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2023-03-03
  • 解决idea导入ssm项目启动tomcat报错404的问题

    解决idea导入ssm项目启动tomcat报错404的问题

    今天小编就为大家分享一篇解决idea导入ssm项目启动tomcat报错404的问题,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2018-08-08

最新评论