Spring Cloud Open Feign系列【11】Feign 编码解码器Encoder和Decoder源码分析

2年前 (2022) 程序员胖胖胖虎阿
387 0 0

文章目录

  • 概述
  • 接口及相关实现类
    • 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也提供了很多其他编码器实现类。
Spring Cloud Open Feign系列【11】Feign 编码解码器Encoder和Decoder源码分析
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也提供了很多解码器实现类:
Spring Cloud Open Feign系列【11】Feign 编码解码器Encoder和Decoder源码分析
StringDecoder 会判断是否字符串类型,然后解析为String,如果不是则会抛出解析异常。

DefaultGzipDecoder 会判断响应消息头是否有"Content-Encoding:gzip,有的话则会进行gzip解压缩,然后再调用其他解码器。

ResponseEntityDecoder 会判断是否是 Spring MVC中的HttpEntity类型,然后返回ResponseEntity对象。

SpringDecoderSpringEncoder ,是默认的解码器,使用HttpMessageConverter进行解码。

执行流程源码分析

接下里我们分析下默认的SpringDecoderSpringEncoder是如何工作的。

1. 项目结构改造

创建一个新的order-api模块,用来存放订单服务的实体类,然后别的服务需要调用订单服务的话,直接引入这个API 模块,就能公用POJO类了。然后订单和账户服务都引入这个API 包。
Spring Cloud Open Feign系列【11】Feign 编码解码器Encoder和Decoder源码分析
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,具体创建的流程后续会分析。
Spring Cloud Open Feign系列【11】Feign 编码解码器Encoder和Decoder源码分析
然后进入到ReflectiveFeignresolve方法解析请求模板,在该解析过程中,会调用编码器,对请求体进行编码。

		// 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,可以看到编码参数传入了请求体对象、类型、请求模板。
Spring Cloud Open Feign系列【11】Feign 编码解码器Encoder和Decoder源码分析
在编码器的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 客户端框架发送实际请求时,会被设置到请求对象中,整个请求编码处理的流程就分析结束了。
Spring Cloud Open Feign系列【11】Feign 编码解码器Encoder和Decoder源码分析

3. 解码器流程

对象经过编码为二进制数据放入到请求体中发送后,获取到远程服务返回的响应体后,需要解码为对象。

可以看到,OkHttp 请求获取到响应后,会将okhttp3.Response转为Feign 中的Response响应对象,又转为负载均衡的RibbonResponse
Spring Cloud Open Feign系列【11】Feign 编码解码器Encoder和Decoder源码分析
然后回到方法执行器SynchronousMethodHandlerexecuteAndDecode方法进行解码操作,首先会判断当前执行器是否有解码器,没有则会调用AsyncResponseHandler异步响应处理器进行处理。
Spring Cloud Open Feign系列【11】Feign 编码解码器Encoder和Decoder源码分析
在处理过程中,会根据状态码进行解码,状态码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,可以看到解码方法很简单,就是使用消息转换器,将响应体中的数据解码为对象并返回。
Spring Cloud Open Feign系列【11】Feign 编码解码器Encoder和Decoder源码分析
Spring Cloud Open Feign系列【11】Feign 编码解码器Encoder和Decoder源码分析
如果发生了异常,还会调用ErrorDecoder, 可以使用该接口,自定义异常处理逻辑。
Spring Cloud Open Feign系列【11】Feign 编码解码器Encoder和Decoder源码分析

4. 异常解码处理流程

Feign 让我们远程调用时像调用本地方法一样方便,响应200时,会对远程返回的Response进行解码,转为我们方法执行返回的对象。

在发生异常时,也会对异常信息进行处理,首先我们模拟一个404异常。

Response中,状态码为404,错误信息都在响应体中。
Spring Cloud Open Feign系列【11】Feign 编码解码器Encoder和Decoder源码分析
在调用发可以看到程序会抛出FeignException$NotFound 404
Spring Cloud Open Feign系列【11】Feign 编码解码器Encoder和Decoder源码分析
在结果处理器AsyncResponseHandler中,会使用错误解码器ErrorDecoder对异常进行解码,然后调用JDK(juc包)中的CompletableFuture类(异步任务)进行最终的异常处理。
Spring Cloud Open Feign系列【11】Feign 编码解码器Encoder和Decoder源码分析
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进行异常处理,最终远程的异常信息就被解码成了本地异常,返回给了浏览器。
Spring Cloud Open Feign系列【11】Feign 编码解码器Encoder和Decoder源码分析

5. 404异常特殊处理

在处理响应的时候,可以看到一个404会特殊处理的代码,如果设置了解码404、并且不是Void返回类型,则会进行特殊处理。
Spring Cloud Open Feign系列【11】Feign 编码解码器Encoder和Decoder源码分析
首先在@FeignClient上配置decode404 = true

@FeignClient(name = "order-service",decode404 = true)

然后发现成功返回,只是对象属性都是null,原来配置了以后,会使用解码器SpringEncoder,解析为返回值对象,而不是抛出异常,看来这个配置实际是不应该的,我们应该抛出异常。
Spring Cloud Open Feign系列【11】Feign 编码解码器Encoder和Decoder源码分析

自定义编码解码器

在了解了以上知识后,自定义就比较都容易了,我们只需要实现Decoder、Encoder,重写其方法就可以了,然后注入到IOC中,或者在@FeignClient上引入配置类就可以了…

相关文章

暂无评论

暂无评论...