Spring Cloud Open Feign series [11] Feign codec Encoder and Decoder source code analysis

summary

In the real world, the concept of codec exists. Encoding is the process of transforming information from one form or format to another. Decoding is the inverse process of encoding. It is widely used in computer, television, remote control and communication.

In the program, this concept is also widely used, such as Base64 encoding and decoding.

In Feign, there is also the concept of encoding and decoding:

  • Encoder: encoder, which is used in the request phase to encode the object into the request body.
  • Decoder: decoder, scope response phase, parsing HTTP response message.

Interface and related implementation classes

Encoder interface

The Encoder interface declares an encoding method and provides a Default implementation class Default.

The Default encoder can only handle String type and [byte] type, which is obviously difficult to use because we often put objects in the request parameters.

public interface Encoder {

    Type MAP_STRING_WILDCARD = Util.MAP_STRING_WILDCARD;
	
	// code
	// var1 = "object to encode"
	// var2 = object type
	// var3 = "request template object"
    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 type, directly call toString() and set it in the body of the request template
            if (bodyType == String.class) {
                template.body(object.toString());
             // Binary, insert binary
            } else if (bodyType == byte[].class) {
                template.body((byte[])((byte[])object), (Charset)null);
            } else if (object != null) {
            	// It is not NULL, and the encoding exception is thrown
                throw new EncodeException(String.format("%s is not a type supported by this encoder.", object.getClass()));
            }

        }
    }
}

The Default encoder is definitely useless, so Feign also provides many other encoder implementation classes.

SpringFormEncoder supports the encoding of form data in application / x-www-form-urlencoded ` ` multipart / form data format, that is, direct form and file upload requests.

Spring encoder is the default encoder used by Spring Cloud. It will call the message converter (HttpMessageConverter) in Spring MVC for encoding. The message converter adapts to many data formats, including String, Byte, Json and XML.

Decoder interface

The Encoder interface declares a decoding method and provides a Default implementation class Default.

The Default decoder inherits the StringDecoder and can only decode binary and string. All of them are definitely useless...

public interface Decoder {
	// Decoding, the parameters are the response object and its type
    Object decode(Response var1, Type var2) throws IOException, DecodeException, FeignException;

    public static class Default extends StringDecoder {
        public Default() {
        }
		// decode
        public Object decode(Response response, Type type) throws IOException {
        	// Not 404 and 204 
            if (response.status() != 404 && response.status() != 204) {
                if (response.body() == null) {
                	// The response body is null and returns null directly
                    return null;
                } else {
                	// Judge whether the return type is binary. If yes, it will directly return the stream instead of resolving to String 
                    return byte[].class.equals(type) ? Util.toByteArray(response.body().asInputStream()) : super.decode(response, type);
                }
            } else {
            	// 404 and 204 return null
                return Util.emptyValueOf(type);
            }
        }
    }
}

Feign also provides many decoder implementation classes:

The StringDecoder will judge whether it is a String type, and then resolve it to String. If not, it will throw a resolution exception.

DefaultGzipDecoder will judge whether the response header contains "content encoding: gzip". If it does, gzip decompression will be performed, and then other decoders will be called.

The ResponseEntityDecoder will determine whether it is the HttpEntity type in Spring MVC, and then return the ResponseEntity object.

Spring decoder and spring encoder are the default decoders, which are decoded using HttpMessageConverter.

Execution process source code analysis

Next, we will analyze how the default spring decoder and spring encoder work.

1. Project structure transformation

Create a new order API module to store the entity class of the order service. If other services need to call the order service, directly introduce this API module to share the POJO class. Then, both order and account services are introduced into the API package.

In order service, add a Post request interface to accept an order object and use the @ RequestBody annotation to accept it, because the request parameters are placed in the request body when Feign Post requests.

    @PostMapping("/post")
    public Order insertOrder(@RequestBody Order order) throws InterruptedException {
        return order;
    }

In the account service, add a Feign remote call order service interface.

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

Finally, add the account access interface and start the project to test whether it can be connected.

    @GetMapping("/insertOrder")
    public Object insertOrder() {
        Order order = new Order();
        order.setAccountId(11L);
        order.setPort("123");
        Order post = orderFeign.post(order);
        // Simulate placing an order for the current account
        return post;
    }

2. Encoder process

As previously analyzed, the method executor SynchronousMethodHandler will create a request template object RequestTemplate during agent execution, and the specific creation process will be analyzed later.

Then enter the resolve method of ReflectiveFeign to parse the request template. In the parsing process, the encoder will be called to encode the request body.

		// argv = request parameters
        protected RequestTemplate resolve(Object[] argv, RequestTemplate mutable, Map<String, Object> variables) {
        	// Request parameter = order object
            Object body = argv[this.metadata.bodyIndex()];
            Util.checkArgument(body != null, "Body parameter %s was null", new Object[]{this.metadata.bodyIndex()});

            try {
            	// Encoder coding
                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);
        }

Then go to the default encoder spring encoder. You can see that the encoding parameters have passed in the request body object, type and request template.

In the encode method of the encoder, the message converter in Spring MVC will be looped. For ordinary JAVA objects, JSON related converters will be called to convert the objects into byte arrays and finally set them into RequestTemplate.

    public void encode(Object requestBody, Type bodyType, RequestTemplate request) throws EncodeException {
    	// The requestBody is encoded only when the request body is not null
        if (requestBody != null) {
        	// Get the "content type" in the request template. Generally, the message header will not be set, so it is null here
            Collection<String> contentTypes = (Collection)request.headers().get("Content-Type");
            MediaType requestContentType = null;
            String message;
            // If the "content type" in the request template exists, it will be changed to MediaType 
            if (contentTypes != null && !contentTypes.isEmpty()) {
                message = (String)contentTypes.iterator().next();
                requestContentType = MediaType.valueOf(message);
            }
			// If it is multipart / form data type, use SpringFormEncoder to convert it to MultipartFile 
            if (Objects.equals(requestContentType, MediaType.MULTIPART_FORM_DATA)) {
                this.springFormEncoder.encode(requestBody, bodyType, request);
            } else {
            	// If the parameter is MultipartFile type, it will tell us that the parameter is MultipartFile, and the "consumers" parameter of @ RequestMapping should be specified as 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");
                }
				// Gets the HttpMessageConverters collection of message converters in Spring MVC
                Iterator var11 = ((HttpMessageConverters)this.messageConverters.getObject()).getConverters().iterator();
				// Circular message converter 
                while(var11.hasNext()) {
                	// Current message converter = HttpMessageConverter eg:ByteArrayHttpMessageConverter
                    HttpMessageConverter messageConverter = (HttpMessageConverter)var11.next();
					// Declare the HttpOutputMessage object, which encapsulates the request output flow and message header
                    SpringEncoder.FeignOutputMessage outputMessage;
                    try {
                    	// If it is an instance of the GenericHttpMessageConverter interface (JSON converter), it directly checks and writes data to the FeignOutputMessage object and calls converter.write to write.
                    	// The MappingJackson2HttpMessageConverter is used here
                        if (messageConverter instanceof GenericHttpMessageConverter) {
                            outputMessage = this.checkAndWrite(requestBody, bodyType, requestContentType, (GenericHttpMessageConverter)messageConverter, request);
                        } else {
                        	// If not, it will check whether it can be written. If it can be written, it will be written directly
                            outputMessage = this.checkAndWrite(requestBody, requestContentType, messageConverter, request);
                        }
                    } catch (HttpMessageConversionException | IOException var10) {
                        throw new EncodeException("Error converting request body", var10);
                    }
					// Using the transponder, the request body data is obtained. If the loop fails to obtain it, the coding exception will be burst
                    if (outputMessage != null) {
                    	// Set the data into the body of the request template.
                        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);
            }
        }
    }

Finally, the request parameter object is encoded. When calling the HTTP client framework to send the actual request, it will be set into the request object, and the analysis of the whole request encoding process is completed.

3. Decoder process

After the object is encoded into binary data and sent into the request body, it needs to be decoded into an object after obtaining the response body returned by the remote service.

You can see that after the OkHttp request obtains the Response, OkHttp3 The Response is converted to the Response response object in Feign and to the load balanced RibbonResponse,

Then return to the executeanddecade method of the SynchronousMethodHandler of the method executor to decode. First, it will judge whether the current executor has a decoder. If not, it will call the AsyncResponseHandler asynchronous response processor for processing.

In the process of processing, it will be decoded according to the status code, the status code 200 and the return value will be decoded, the status code 404 will determine whether to call the decoder for decoding, and other error status codes will call the ErrorDecoder for decoding.

                Object result;
                // 200 < = status code < 300 
                if (response.status() >= 200 && response.status() < 300) {
                	// Is the method executed by feign void 
                    if (this.isVoidType(returnType)) {  
                        resultFuture.complete((Object)null);
                    } else {
                    		// Method has a return value and calls the decoder
                        result = this.decode(response, returnType);
                        shouldClose = this.closeAfterDecode;
                        resultFuture.complete(result);
                    }
                // If it is 404 and decoding 404 is configured and the return value is not void
                } else if (this.decode404 && response.status() == 404 && !this.isVoidType(returnType)) {
                	// Decoding response body
                    result = this.decode(response, returnType);
                    shouldClose = this.closeAfterDecode;
                    resultFuture.complete(result);
                } else {
                	// Other status codes call the error decoder for decoding.
                    resultFuture.completeExceptionally(this.errorDecoder.decode(configKey, response));
                }
            }

Finally, when you enter the spring decoder, you can see that the decoding method is very simple, that is, use the message converter to decode the data in the response body into an object and return it.


If an exception occurs, the ErrorDecoder will also be called. You can use this interface to customize the exception handling logic.

4. Exception decoding processing flow

Feign makes remote calls as convenient as calling local methods. When responding to 200, it will decode the Response returned remotely and turn it into the object returned by our method execution.

When an exception occurs, the exception information will also be processed. First, we simulate a 404 exception.

In the Response, the status code is 404, and the error information is in the Response body.

You can see that the program will throw FeignException$NotFound 404 when calling.

In the result processor AsyncResponseHandler, the error decoder ErrorDecoder is used to decode the exception, then the CompletableFuture class (asynchronous task) in the JDK (juc package) is called for the final exception handling.

The ErrorDecoder uses the Default implementation class Default. If the server does not tell the client that it needs to retry, it will resolve the exception as FeignException and return it directly.

        public Exception decode(String methodKey, Response response) {
        	// Turn exception to FeignException 
            FeignException exception = FeignException.errorStatus(methodKey, response);
            // Query whether there is a 'retry after' response header
            Date retryAfter = this.retryAfterDecoder.apply((String)this.firstOrNull(response.headers(), "Retry-After"));
            // If there is retryAfter, it means that it needs to be retried, so RetryableException will be thrown, and the method executor will catch this and retry,
            // This is the same as the timeout retry, which means that if other exceptions occur and there is "retry after", it will also be retried
            // Without retry, FeignException is returned directly 
            return (Exception)(retryAfter != null ? new RetryableException(response.status(), exception.getMessage(), response.request().httpMethod(), exception, retryAfter, response.request()) : exception);
        }

Finally, after asynchronous processing, throw the exception, and Spring MVC handles the exception. Finally, the remote exception information is decoded into a local exception and returned to the browser.

5. 404 special handling of exceptions

When processing the response, you can see a code that will be specially processed by 404. If decoding 404 is set and it is not a Void return type, special processing will be performed.

First, configure decode404 = true on @ FeignClient

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

Then, it is found that the object is returned successfully, but the object properties are null. After the original configuration, the decoder SpringEncoder will be used to parse it into a return value object instead of throwing an exception. It seems that this configuration should not actually be. We should throw an exception.

Custom codec

After understanding the above knowledge, customization is easier. We only need to implement Decoder and Encoder, rewrite their methods, and then inject them into IOC, or introduce configuration classes on @ FeignClient

Keywords: Java Spring Cloud Network Protocol http

Added by gavinbsocom on Mon, 03 Jan 2022 22:45:47 +0200