[Enjoy Feign]10. Enable Feign to naturally support POJO encoding and decoding using feign-jackson module

"Flexibility is overestimated - constraints are liberation," says the Redis author.

->Return to Column Directory <-
Code download address: https://github.com/f641385712/feign-learning

Catalog

Preface

The Client-related modules of Feign are described above. While experiencing the high extensibility of Feign core content, it is also apparent that its sub-modules are actually an extension of Feign core functionality to better suit the complex production environment requirements.

This article describes another useful module of it: feign-jackson.It can solve a very big pain point in our daily work: Feign can only encode/decode string-type data.It allows us to code more object-oriented and less sensitive to Feign's internal processing details~

Note: If you are unfamiliar with Jackson, be sure to refer to my column [Enjoy Jackson] (Click here for elevator access) This column is probably the best and most complete tutorial on the Web.

text

As a HC, Feign is characterized by its simplicity of Client-side development and full Interface-oriented programming.However, in actual encoding, the most common encoding method we use is object-oriented programming, data transfer, as follows:

/**
 * Query List
 */
@RequestLine("GET /person/list")
List<Person> getList();

/**
 * Add a new record
 */
@RequestLine("POST /person")
Long create(Person person);

However, this is not supported for the core part of the source Feign because POJO cannot be properly encoded/decoded.
Next, we introduce the feign-jackson module, which makes this possible~

feign-jackson

Its GAV:

<dependency>
    <groupId>io.github.openfeign</groupId>
    <artifactId>feign-jackson</artifactId>
    <version>${feign.version}</version>
</dependency>

We know that by default, Feign uses an encoder named feign.codec.Encoder.Default, which is relatively leaky: it can only encode string types (byte array types are not discussed).

Examples include the following:

@Getter
@Setter
public class Person {
    private String name = "YourBatman";
    private Integer age = 18;
}

public interface JacksonDemoClient {

    @RequestLine("POST /feign/jacksondemo")
    String jacksonDemo1(String body);

    @RequestLine("POST /feign/jacksondemo")
    String jacksonDemo2(Person person);
}

Test program:

@Test
public void fun2() {
    JacksonDemoClient client = Feign.builder()
            .logger(new Logger.ErrorLogger()).logLevel(Logger.Level.FULL).retryer(Retryer.NEVER_RETRY) // Output Log
            .target(JacksonDemoClient.class, "http://localhost:8080");

    try { client.jacksonDemo1("this is http body"); }catch (Exception e) { e.printStackTrace();}

    System.err.println(" -------------------------- ");

    try { client.jacksonDemo2(new Person()); }catch (Exception e) { e.printStackTrace();}
}

Run the program, console output log:

// The first request is perfectly normal because it is of type String
[JacksonDemoClient#jacksonDemo1] ---> POST http://localhost:8080/feign/jacksondemo HTTP/1.1
[JacksonDemoClient#jacksonDemo1] Content-Length: 17
[JacksonDemoClient#jacksonDemo1] 
[JacksonDemoClient#jacksonDemo1] this is http body
[JacksonDemoClient#jacksonDemo1] ---> END HTTP (17-byte body)
...
 -------------------------- 
 
// Second error: Person cannot be encoded
feign.codec.EncodeException: class com.yourbatman.modules.beans.Person is not a type supported by this encoder.
	at feign.codec.Encoder$Default.encode(Encoder.java:94)
	...

Request 1 is perfectly normal because it is a String type and can be normally encoded into a Body.
The mistake in Request 2 is entirely reasonable because the Person type cannot be coded.

In practice, case2 is written much more than case1. How can it be broken?

Solution

Because using JSON string as data exchange format is the current mainstream, encoding requirements need to be resolved.To address the above issues, here are two solutions for your reference:

Option 1: Manual encoding (serialization)

This is why feign-core only provides the lowest level of string/byte array encoding support.

With this guideline, would it be OK if we manually encoded/serialized POJO as a string ourselves?So you can do this:

@Test
public void fun3() throws JsonProcessingException {
    JacksonDemoClient client = Feign.builder()
            .logger(new Logger.ErrorLogger()).logLevel(Logger.Level.FULL).retryer(Retryer.NEVER_RETRY) // Output Log
            .target(JacksonDemoClient.class, "http://localhost:8080");

    // Complete encoding manually, encoding as a string
    ObjectMapper mapper = new ObjectMapper();
    String bodyStr =  mapper.writeValueAsString(new Person());

    // Then call method one to complete the request sending
    try { client.jacksonDemo1(bodyStr); }catch (Exception e) { e.printStackTrace();}
}

Console Printing:

[JacksonDemoClient#jacksonDemo1] ---> POST http://localhost:8080/feign/jacksondemo HTTP/1.1
[JacksonDemoClient#jacksonDemo1] Content-Length: 30
[JacksonDemoClient#jacksonDemo1] 
[JacksonDemoClient#jacksonDemo1] {"name":"YourBatman","age":18}
[JacksonDemoClient#jacksonDemo1] ---> END HTTP (30-byte body)
...

You can clearly see that the Body is a JSON string to solve the problem.
To summarize this approach, it has the following advantages and disadvantages:

  • Benefits: No additional guides are required, just the core Feign features will do the job
  • Disadvantages: Very much.
    • Hard coding, null issues to deal with yourself
    • Not enough object-oriented
    • All parameters are received using strings, losing the advantage of static languages
    • Very poor fault tolerance
    • ...
Scenario 2: Using feign-jackson automation

Since the solution has so many drawbacks and the way to solve this problem is universal, feign takes it out and travels through a sub-module, feign-jackson, which helps us solve this problem very well.

The usage is as follows:

// Encoder Display Specifies Use `JacksonEncoder`
@Test
public void fun3() {
    JacksonDemoClient client = Feign.builder()
    		.logger(new Logger.ErrorLogger()).logLevel(Logger.Level.FULL).retryer(Retryer.NEVER_RETRY) // Output Log
            .encoder(new JacksonEncoder())
            .target(JacksonDemoClient.class, "http://localhost:8080");

    client.jacksonDemo2(new Person());
}

Run the program, console print:

[JacksonDemoClient#jacksonDemo2] ---> POST http://localhost:8080/feign/jacksondemo HTTP/1.1
[JacksonDemoClient#jacksonDemo2] Content-Length: 44
[JacksonDemoClient#jacksonDemo2] 
[JacksonDemoClient#jacksonDemo2] {
  "name" : "YourBatman",
  "age" : 18
}
[JacksonDemoClient#jacksonDemo2] ---> END HTTP (44-byte body)
...

The body content is a JSON string, and everything works fine, and it's just convenient to use the encoder Jackson Encoder provided by feign-jackson.

So what happens if the passed value is null?

...
client.jacksonDemo2(null);
...

Run the test program and throw an exception: java.lang.IllegalArgumentException: Body parameter 0 was null.It's also easy to accept this result: Body requested by POST/PUT is not allowed to be null (but empty strings are allowed oh~).

Principle Analysis

The feign-jackson module only provides three classes: one encoder for JacksonEncoder, two decoders for JacksonDecoder and JacksonIteratorDecoder.

JacksonEncoder

As the name implies, it uses com.fasterxml.jackson.databind.ObjectMapper to complete encoding/serialization.Because ObjectMapper can serialize any type (not just POJO), it can be used as a generic encoder.

public class JacksonEncoder implements Encoder {

	private final ObjectMapper mapper;

	// constructor
	public JacksonEncoder() {
	    this(Collections.emptyList());
	}
 // You can register any module for ObjectMapper
  public JacksonEncoder(Iterable<Module> modules) {
    this(new ObjectMapper()
    	// null-valued key s are not serialized to JSON strings
    	// The default behavior of ObjectMapper is serialization.
        .setSerializationInclusion(JsonInclude.Include.NON_NULL)
        // Default beautifies output
        // Actually, I don't think it's necessary to set the last production property to false
        .configure(SerializationFeature.INDENT_OUTPUT, true)
        // Register module Modules
        .registerModules(modules));
  }

	// If the default ObjectMapper is not what you want, you can use your own
	// For example, it is better to use ObjectMapper inside the SpringBoot container as a global ~~~
  public JacksonEncoder(ObjectMapper mapper) {
    this.mapper = mapper;
  }

	// Execute Coding
  @Override
  public void encode(Object object, Type bodyType, RequestTemplate template) {
    ...
    // Write string/POJO as Byte array in Body with UTF-8 encoding
    template.body(mapper.writerFor(javaType).writeValueAsBytes(object), Util.UTF_8);
    ...
  }
}

The above logic is simple and clear, and the main focus is on customizing the ObjectMapper instance. By default, it does not output null values and beautifies the output (I don't think it's necessary, beautifying the output wastes performance).

Therefore, if you use this encoder in a production environment, it is recommended that you use your own ObjectMapper instance (such as inside an SB container), which also allows you to maintain consistency in the serialization/deserialization of the entire project.

JacksonDecoder
public class JacksonDecoder implements Decoder {

	private final ObjectMapper mapper;

	... // Constructor.Will help you turn off this feature `DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES`
	...

  @Override
  public Object decode(Response response, Type type) throws IOException {
	// If the wood has a body, return to null
    if (response.body() == null)
      return null;
	... 
	return mapper.readValue(reader, mapper.constructType(type));
  }

}

The implementation uses ObjectMapper#readValue() for decoding/deserialization, which by default helps you turn off the DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES feature.
Similarly, the deserialization of ObjectMapper supports all types, so the decoder can be generic.

Note: You must know from reading my [Enjoy Jackson] column that Spring also turns off DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES by default.MapperFeature#DEFAULT_VIEW_INCLUSION is also turned off.

Use examples
public interface DecoderClient {

    @RequestLine("GET /feign/demo1/list")
    List<String> getDemo1List();
}

Test cases:

@Test
public void fun4() {
    DecoderClient client = Feign.builder()
    		.decoder(new JacksonDecoder()) // Decode using Jackson
            .target(DecoderClient.class, "http://localhost:8080");

    List<String> list = client.getDemo1List();
    System.out.println(list);
}

Run to output normally: [A, B, C].List s can all be deserialized properly, so POJO will be fine. I won't show it here.

Description: The Server returns a List <String> and the code is omitted.

However, if you use java.util.stream.Stream as the method return value:

@RequestLine("GET /feign/demo1/list")
Stream<String> getDemo1List();

Run the test program with errors:

feign.FeignException: Cannot construct instance of `java.util.stream.Stream` (no Creators, like default construct, exist): abstract types either need to be mapped to concrete types, have custom deserializer, or contain additional type information
 at [Source: (BufferedReader); line: 1, column: 1] reading GET http://localhost:8080/feign/demo1/list
 ...

That is, if your return value is Stream, then this decoder is not solved and needs to use StreamDecoder, combined with the following decoder to support it.

JacksonIteratorDecoder

Again, note the differences between java.lang.Iterable and java.util.Iterator.Collection interface is inherited from Iterable, not Iterator

As the name implies, it can decode a method whose return value type is Iterator.The following:

@RequestLine("GET /feign/demo1/list")
Iterator<String> getDemo1List2();


@Test
public void fun5() {
    DecoderClient client = Feign.builder()
            .decoder(JacksonIteratorDecoder.create())
            .target(DecoderClient.class, "http://localhost:8080");

    Iterator<String> it = client.getDemo1List2();
    while(it.hasNext()){
        System.out.println(it.next());
    }
}

Run the program and the console prints the results correctly:

A
B
C

It can also be used in conjunction with StreamDecoder to support return values of type java.util.stream.Stream:

Feign.builder().decoder(StreamDecoder.create(JacksonIteratorDecoder.create()))

Specific examples are no longer needed.

However, it is important to note that this decoder is customized for Iterator type return values and is not universal, so it should be used cautiously in production environments and usually only in special situations.

summary

That's all about the feign-jackson module. You should be able to feel that it's very useful even though the source code is simple.
Another feeling is that technology was interwoven many times before, for example, the most popular JSON library, Jackson, was used here for encoding/decoding, instead of the other three-party libraries, which is intrinsic.

So, through long-term accumulation, make your knowledge and technology into a system, which is not the basic work that an architect should have most?

statement

The original is not easy, the code is not easy. Thank you for your compliment, collection and attention.Sharing this article with your circle of friends is allowed, but you refuse to copy it.You can also join my family of Java engineers and architects in learning and communicating with Scavenger on the left.

294 original articles published, 454 approved, 380,000 visits+
His message board follow

Tags: encoding Java JSON codec

Posted on Fri, 14 Feb 2020 20:47:50 -0800 by KaFF