问题描述
在使用自定义 open feign 进行远程调用的时候,请求数据时候出现很神奇的问题
先上报错
Invalid mime type "charset=utf-8": does not contain '/'
从报错信息我们可以知道,出错的原因是出现了无效的mime类型,导致数据decoder的时候出现错误。
使用 postman 测试接口
这里问题就一面了然了,第三方对接的接口返回的是一个错误的 mime type “charset=utf-8”。
确实在 content-type 中携带 charset=utf-8 是一种常见的做法,尤其是当 Content-Type 是 application/json 或 application/x-www-form-urlencoded 时,能明确告诉接收方使用的字符编码格式
正确的使用情况
- application/json:使用 charset=utf-8。
Content-Type: application/json; charset=utf-8
JSON数据大多数情况使用的默认编码就是UTF-8
- application/x-www-form-urlencoded:使用 charset=utf-8
Content-Type: application/x-www-form-urlencoded; charset=utf-8
所以到这里我们就能很清楚,单独携带charset=utf-8是错误的,因为在对数据decoder使用需要根据 mime type 进行相应的解析处理。
自定义 Feign 配置
@Configuration
@Component
public class FeignClientConfig {
private final ObjectFactory<HttpMessageConverters> messageConverters;
public FeignClientConfig(ObjectFactory<HttpMessageConverters> messageConverters) {
this.messageConverters = messageConverters;
}
/**
* 根据自定义的Feign请求接口,自定义请求的 URL 来创建请求客户端。
* 适用于根据上下文自定义请求URL的场景
* @param type 根据该类型,去解析这个类型的注解,根据注解来生成请求对象
* @param url 自定义的请求 URL
*/
public <T> T createClient(Class<T> type, String url) {
Encoder encoder = new SpringEncoder(this.messageConverters);
Decoder decoder = new SpringDecoder(this.messageConverters);
return Feign.builder()
.encoder(encoder)
.decoder(decoder)
.target(type, url);
}
}
在使用 Feign 进行调用时,响应数据会通过解码器(Decoder)进行解析。在这个过程中,我们使用的是 SpringDecoder 对象。
SpringDecoder 的构造函数接收一个 Object<<HttpMessageConverters>> 作为参数,对于响应的 Content-Type 格式校验,通常是由 Spring 的 HttpMessageConverter 来完成的
通过查看 SpringDecoder 对象
public class SpringDecoder implements Decoder {
private ObjectFactory<HttpMessageConverters> messageConverters;
public SpringDecoder(ObjectFactory<HttpMessageConverters> messageConverters) {
this.messageConverters = messageConverters;
}
public Object decode(final Response response, Type type) throws IOException, FeignException {
if (!(type instanceof Class) && !(type instanceof ParameterizedType) && !(type instanceof WildcardType)) {
throw new DecodeException(response.status(), "type is not an instance of Class or ParameterizedType: " + type, response.request());
} else {
HttpMessageConverterExtractor<?> extractor = new HttpMessageConverterExtractor(type, ((HttpMessageConverters)this.messageConverters.getObject()).getConverters());
return extractor.extractData(new FeignResponseAdapter(response));
}
}
}
这里我们可以看到执行在进行decoder时候用到了传入HttpMessageConverters,用户对各种不同的转换
HttpMessageConverterExtractor<?> extractor = new HttpMessageConverterExtractor(type, ((HttpMessageConverters) this.messageConverters.getObject()).getConverters());
这里我们可以去看HttpMessageConverterExtractor的getContentType的实现
protected MediaType getContentType(ClientHttpResponse response) {
MediaType contentType = response.getHeaders().getContentType();
if (contentType == null) {
if (this.logger.isTraceEnabled()) {
this.logger.trace("No content-type, using 'application/octet-stream'");
}
contentType = MediaType.APPLICATION_OCTET_STREAM;
}
return contentType;
}
这里又调用response.getHeaders().getContentType();点击方法查看
@Nullable
public MediaType getContentType() {
// 是获取当中 "Content-Type" 头字段的第一个值
String value = this.getFirst("Content-Type");
return StringUtils.hasLength(value) ? MediaType.parseMediaType(value) : null;
}
这里我们可以看到判断是否有值,如果有值就调用 MediaType.parseMediaType方法
我们只需要知道当前调用了MimeTypeUtils,对value进行比对是否符合mine type格式,如果不符合抛出 InvalidMediaTypeException
到这里我们就知道具体的检查流程
解决方法
实现 Feign Decoder
@Component
public class FeignResponseDecoder implements Decoder {
private final Decoder springDecoder;
private static final String CONTENT_TYPE = "Content-Type";
private static final String JSON_CONTENT_TYPE = "application/json";
private static final String CHARSET_UTF_8 = "charset=utf-8";
public FeignResponseDecoder(@Lazy Decoder springDecoder) {
this.springDecoder = springDecoder;
}
@Override
public Object decode(Response response, Type type) throws IOException, DecodeException, FeignException {
if (isContentTypeModificationNeeded(response)) {
Map<String, Collection<String>> newHeaders = new HashMap<>();
// 注意这里不能直接 new HasMap<>(response.header()),
// 如果这样的话put("content-type: application/json"), 变成了增加多一个content-type,而不是替换
newHeaders.put(CONTENT_TYPE, Collections.singletonList(JSON_CONTENT_TYPE));
response.headers().forEach((key, value) -> {
if (!key.equalsIgnoreCase(CONTENT_TYPE)) {
newHeaders.put(key, value);
}
});
response = Response.builder()
.status(response.status())
.reason(response.reason())
.headers(newHeaders) // 使用新的头部映射
.request(response.request())
.body(response.body().asInputStream(), response.body().length())
.build();
}
return springDecoder.decode(response, type);
}
/**
* @param response 响应体
* @return 判断是否需要进行修改
*/
private boolean isContentTypeModificationNeeded(Response response) {
// 获取第一个 Content-Type 头部的值
String contentType = response.headers().getOrDefault(CONTENT_TYPE, List.of())
.stream()
.findFirst()
.orElse("")
.trim();
// 判断是否需要修改 Content-Type
return contentType.isEmpty() || !contentType.contains("/") || contentType.equalsIgnoreCase(CHARSET_UTF_8);
}
}
由于我们知道当前只要第三方返回 content-type: charset:utf-8 都是JSON格式,所以我们只需要对这种请求进行处理,处理完之后返回一个SpringEncoder就能进解决这个问题
对 FeignConfig 进行修改
public <T> T createClient(Class<T> type, String url) {
// 使用 SpringFormEncoder 包装 SpringEncoder 以支持表单编码
Encoder encoder = new SpringFormEncoder(new SpringEncoder(this.messageConverters));
Decoder decoder = new FeignResponseDecoder(new SpringDecoder(this.messageConverters));
return Feign.builder()
.encoder(encoder)
.decoder(decoder)
.target(type, url);
}
修改部分
Decoder decoder = new FeignResponseDecoder(new SpringDecoder(this.messageConverters));
进行单元测试,查看是否会报错,再此执行发现就不报错了,到这里问题就解决了
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。