文章目录
- 概述
- 接口及相关实现类
-
- Encoder 接口
- Decoder接口
- 执行流程源码分析
-
- 1. 项目结构改造
- 2. 编码器流程
- 3. 解码器流程
- 4. 异常解码处理流程
- 5. 404异常特殊处理
- 自定义编码解码器
概述
在现实世界,编解码的概念就存在了。编码是信息从一种形式或格式转换为另一种形式的过程,解码,是编码的逆过程。在电子计算机、电视、遥控和通讯等方面广泛使用。
在程序中,也是广泛使用了这个概念,比如Base64 编码解码。
在Feign
中,也存在编解码的概念:
- Encoder :编码器,作用于请求阶段,将对象编码到请求体中。
- Decoder:解码器,作用域响应阶段,解析HTTP 响应消息。
接口及相关实现类
Encoder 接口
Encoder
接口声明了一个编码方法,并提供了一个默认的实现类Default
。
Default
编码器,它仅能处理String类型、[byte]类型,显然是很难使用的,因为我们经常会在请求参数中放入对象。
public interface Encoder {
Type MAP_STRING_WILDCARD = Util.MAP_STRING_WILDCARD;
// 编码
// var1=》 要编码的对象
// var2 =》 对象类型
// var3 =》 请求模板对象
void encode(Object var1, Type var2, RequestTemplate var3) throws EncodeException;
public static class Default implements Encoder {
public Default() {
}
public void encode(Object object, Type bodyType, RequestTemplate template) {
// String类型,直接调用toString(),并设置到请求模板的body 中
if (bodyType == String.class) {
template.body(object.toString());
// 二进制,塞入二进制
} else if (bodyType == byte[].class) {
template.body((byte[])((byte[])object), (Charset)null);
} else if (object != null) {
// 不是NULL ,爆出编码异常
throw new EncodeException(String.format("%s is not a type supported by this encoder.", object.getClass()));
}
}
}
}
Default
编码器肯定是用不了的,所以Feign
也提供了很多其他编码器实现类。
SpringFormEncoder
支持对application/x-www-form-urlencoded`` multipart/form-data
格式的表单数据编码,也就是直接表单及文件上传请求。
SpringEncoder
是Spring Cloud默认使用的编码器,会调用Spring MVC 中的消息转换器(HttpMessageConverter
)进行编码,而消息转换器适配了很多种数据格式,String、Byte、Json、XML都是支持的。
Decoder接口
Encoder
接口声明了一个解码方法,并提供了一个默认的实现类Default
。
Default
解码器,继承了StringDecoder
,也只能解码二进制和字符串,所有肯定可是用不上的。。。
public interface Decoder {
// 解码,参数为响应对象,及其类型
Object decode(Response var1, Type var2) throws IOException, DecodeException, FeignException;
public static class Default extends StringDecoder {
public Default() {
}
// 解码
public Object decode(Response response, Type type) throws IOException {
// 不是404 和204
if (response.status() != 404 && response.status() != 204) {
if (response.body() == null) {
// 响应体为null 直接返回null
return null;
} else {
// 判断返回类型是不是二进制,是的话直接返回流,不是解析为String
return byte[].class.equals(type) ? Util.toByteArray(response.body().asInputStream()) : super.decode(response, type);
}
} else {
// 404 和204 返回空
return Util.emptyValueOf(type);
}
}
}
}
Feign
也提供了很多解码器实现类:
StringDecoder
会判断是否字符串类型,然后解析为String,如果不是则会抛出解析异常。
DefaultGzipDecoder
会判断响应消息头是否有"Content-Encoding:gzip
,有的话则会进行gzip解压缩,然后再调用其他解码器。
ResponseEntityDecoder
会判断是否是 Spring MVC中的HttpEntity
类型,然后返回ResponseEntity
对象。
SpringDecoder
和SpringEncoder
,是默认的解码器,使用HttpMessageConverter
进行解码。
执行流程源码分析
接下里我们分析下默认的SpringDecoder
和SpringEncoder
是如何工作的。
1. 项目结构改造
创建一个新的order-api
模块,用来存放订单服务的实体类,然后别的服务需要调用订单服务的话,直接引入这个API 模块,就能公用POJO类了。然后订单和账户服务都引入这个API 包。
在order-service
中,添加一个Post 请求接口,接受一个订单对象,使用@RequestBody
注解接受,因为Feign Post 请求时,请求参数是放在请求体中的。
@PostMapping("/post")
public Order insertOrder(@RequestBody Order order) throws InterruptedException {
return order;
}
在账户服务中,添加一个Feign 远程调用订单服务接口。
@FeignClient(name = "order-service")
public interface OrderFeign {
@GetMapping("order/insert")
List<Order> insertOrder(@RequestParam("accountId") Long accountId, @RequestParam("commodityCode") String commodityCode, @RequestParam("count") Long count, @RequestParam("money") Long money);
@PostMapping("/order/post")
public Order post(Order order);
}
最后再添加账户访问接口,并启动项目测试下,是否能通。
@GetMapping("/insertOrder")
public Object insertOrder() {
Order order = new Order();
order.setAccountId(11L);
order.setPort("123");
Order post = orderFeign.post(order);
// 模拟给当前账户下单
return post;
}
2. 编码器流程
之前分析过,方法执行器SynchronousMethodHandler
在代理执行时,会创建一个请求模板对象RequestTemplate
,具体创建的流程后续会分析。
然后进入到ReflectiveFeign
的resolve
方法解析请求模板,在该解析过程中,会调用编码器,对请求体进行编码。
// argv=》 请求参数
protected RequestTemplate resolve(Object[] argv, RequestTemplate mutable, Map<String, Object> variables) {
// 请求参数=》 order 对象
Object body = argv[this.metadata.bodyIndex()];
Util.checkArgument(body != null, "Body parameter %s was null", new Object[]{this.metadata.bodyIndex()});
try {
// 编码器编码
this.encoder.encode(body, this.metadata.bodyType(), mutable);
} catch (EncodeException var6) {
throw var6;
} catch (RuntimeException var7) {
throw new EncodeException(var7.getMessage(), var7);
}
return super.resolve(argv, mutable, variables);
}
接着进入到默认编码器SpringEncoder
,可以看到编码参数传入了请求体对象、类型、请求模板。
在编码器的encode
方法中,会循环Spring MVC中的消息转换器,对于普通JAVA 对象,会调用JSON 相关的转换器,将对象转为 byte数组,并最终设置到RequestTemplate
中。
public void encode(Object requestBody, Type bodyType, RequestTemplate request) throws EncodeException {
// requestBody 请求体不为null 时,才进行编码
if (requestBody != null) {
// 获取请求模板中的 "Content-Type",一般不会设置消息头,所以这里为null
Collection<String> contentTypes = (Collection)request.headers().get("Content-Type");
MediaType requestContentType = null;
String message;
// 请求模板中的 "Content-Type"存在,则转为 MediaType
if (contentTypes != null && !contentTypes.isEmpty()) {
message = (String)contentTypes.iterator().next();
requestContentType = MediaType.valueOf(message);
}
// 如果是 multipart/form-data 类型,则使用SpringFormEncoder 转为MultipartFile
if (Objects.equals(requestContentType, MediaType.MULTIPART_FORM_DATA)) {
this.springFormEncoder.encode(requestBody, bodyType, request);
} else {
// 如果参数为 MultipartFile类型,则会告诉我们参数为MultipartFile,应将@RequestMapping的“consumes”参数指定为MediaType.MULTIPART_FORM_DATA_VALUE
if (bodyType == MultipartFile.class) {
log.warn("For MultipartFile to be handled correctly, the 'consumes' parameter of @RequestMapping should be specified as MediaType.MULTIPART_FORM_DATA_VALUE");
}
// 获取Spring MVC 中的消息转换器 HttpMessageConverters集合
Iterator var11 = ((HttpMessageConverters)this.messageConverters.getObject()).getConverters().iterator();
// 循环消息转换器
while(var11.hasNext()) {
// 当前消息转换器=》HttpMessageConverter eg:ByteArrayHttpMessageConverter
HttpMessageConverter messageConverter = (HttpMessageConverter)var11.next();
// 声明 HttpOutputMessage对象,封装了请求输出流和消息头
SpringEncoder.FeignOutputMessage outputMessage;
try {
// 如果是GenericHttpMessageConverter接口的实例(JSON 转换器),直接检查并写入数据到FeignOutputMessage 对象中,调用 converter.write写入。
// 这里使用的是MappingJackson2HttpMessageConverter
if (messageConverter instanceof GenericHttpMessageConverter) {
outputMessage = this.checkAndWrite(requestBody, bodyType, requestContentType, (GenericHttpMessageConverter)messageConverter, request);
} else {
// 不是,则会检查能否写入,能写入则直接写入
outputMessage = this.checkAndWrite(requestBody, requestContentType, messageConverter, request);
}
} catch (HttpMessageConversionException | IOException var10) {
throw new EncodeException("Error converting request body", var10);
}
// 使用转转器,获取到了请求体数据,循环没获取到,则会爆出编码异常
if (outputMessage != null) {
// 将数据设置到请求模板的body 中。
request.headers((Map)null);
request.headers(FeignUtils.getHeaders(outputMessage.getHeaders()));
Charset charset;
if (messageConverter instanceof ByteArrayHttpMessageConverter) {
charset = null;
} else if (messageConverter instanceof ProtobufHttpMessageConverter && ProtobufHttpMessageConverter.PROTOBUF.isCompatibleWith(outputMessage.getHeaders().getContentType())) {
charset = null;
} else {
charset = StandardCharsets.UTF_8;
}
request.body(Body.encoded(outputMessage.getOutputStream().toByteArray(), charset));
return;
}
}
message = "Could not write request: no suitable HttpMessageConverter found for request type [" + requestBody.getClass().getName() + "]";
if (requestContentType != null) {
message = message + " and content type [" + requestContentType + "]";
}
throw new EncodeException(message);
}
}
}
最后,请求参数对象,经过编码,在调用HTTP 客户端框架发送实际请求时,会被设置到请求对象中,整个请求编码处理的流程就分析结束了。
3. 解码器流程
对象经过编码为二进制数据放入到请求体中发送后,获取到远程服务返回的响应体后,需要解码为对象。
可以看到,OkHttp 请求获取到响应后,会将okhttp3.Response
转为Feign 中的Response
响应对象,又转为负载均衡的RibbonResponse
,
然后回到方法执行器SynchronousMethodHandler
的executeAndDecode
方法进行解码操作,首先会判断当前执行器是否有解码器,没有则会调用AsyncResponseHandler
异步响应处理器进行处理。
在处理过程中,会根据状态码进行解码,状态码200并且有返回值进行解码,状态码404,判断是否调用解码器解码,其他错误状态码,会调用ErrorDecoder
进行解码。
Object result;
// 200 <=状态码 < 300
if (response.status() >= 200 && response.status() < 300) {
// feign 执行的方法是否是void
if (this.isVoidType(returnType)) {
resultFuture.complete((Object)null);
} else {
// 方法是有返回值的,调用解码器
result = this.decode(response, returnType);
shouldClose = this.closeAfterDecode;
resultFuture.complete(result);
}
// 如果是404 并且配置了解码404 并且返回值不是void
} else if (this.decode404 && response.status() == 404 && !this.isVoidType(returnType)) {
// 解码响应体
result = this.decode(response, returnType);
shouldClose = this.closeAfterDecode;
resultFuture.complete(result);
} else {
// 其他状态码 调用 错误解码器进行解码。
resultFuture.completeExceptionally(this.errorDecoder.decode(configKey, response));
}
}
最终进入到SpringDecoder
,可以看到解码方法很简单,就是使用消息转换器,将响应体中的数据解码为对象并返回。
如果发生了异常,还会调用ErrorDecoder
, 可以使用该接口,自定义异常处理逻辑。
4. 异常解码处理流程
Feign
让我们远程调用时像调用本地方法一样方便,响应200时,会对远程返回的Response
进行解码,转为我们方法执行返回的对象。
在发生异常时,也会对异常信息进行处理,首先我们模拟一个404异常。
在Response
中,状态码为404,错误信息都在响应体中。
在调用发可以看到程序会抛出FeignException$NotFound 404
。
在结果处理器AsyncResponseHandler
中,会使用错误解码器ErrorDecoder
对异常进行解码,然后调用JDK(juc包)中的CompletableFuture
类(异步任务)进行最终的异常处理。
ErrorDecoder
使用的是默认的实现类Default
,服务端没有告诉客户端需要重试,会将异常解析为FeignException
直接返回。
public Exception decode(String methodKey, Response response) {
// 将异常转为 FeignException
FeignException exception = FeignException.errorStatus(methodKey, response);
// 查询是否有`Retry-After` 响应消息头
Date retryAfter = this.retryAfterDecoder.apply((String)this.firstOrNull(response.headers(), "Retry-After"));
// 如果是有 retryAfter ,表示这是需要重试的,所以会抛出RetryableException,方法执行器会捕获到这个,进行重试,
// 这和超时时间重试一样,说明要是发生其他异常,又有"Retry-After"时,也会进行重试
// 没有重试,直接返回FeignException
return (Exception)(retryAfter != null ? new RetryableException(response.status(), exception.getMessage(), response.request().httpMethod(), exception, retryAfter, response.request()) : exception);
}
最后经过异步处理,throw出了该异常,又由Spring MVC进行异常处理,最终远程的异常信息就被解码成了本地异常,返回给了浏览器。
5. 404异常特殊处理
在处理响应的时候,可以看到一个404会特殊处理的代码,如果设置了解码404、并且不是Void返回类型,则会进行特殊处理。
首先在@FeignClient
上配置decode404 = true
@FeignClient(name = "order-service",decode404 = true)
然后发现成功返回,只是对象属性都是null,原来配置了以后,会使用解码器SpringEncoder
,解析为返回值对象,而不是抛出异常,看来这个配置实际是不应该的,我们应该抛出异常。
自定义编码解码器
在了解了以上知识后,自定义就比较都容易了,我们只需要实现Decoder、Encoder
,重写其方法就可以了,然后注入到IOC中,或者在@FeignClient
上引入配置类就可以了…
转载请注明:Spring Cloud Open Feign系列【11】Feign 编码解码器Encoder和Decoder源码分析 | 胖虎的工具箱-编程导航