[享学Feign] 十、使用feign-jackson模块让Feign天然支持POJO的编码和解码

Redis作者说到:“灵活性被过分高估–>约束才是解放”。

–> 返回专栏总目录 <–
代码下载地址:https://github.com/f641385712/feign-learning

前言

上文介绍了Feign的Client相关模块,体验到Feign核心内容的高扩展性同时,亦能明显感觉到其子模块其实为对Feign核心功能的延伸,让其更能适应复杂的生产环境要求。

本文将介绍它的另一个实用模块:feign-jackson。它能解决我们平时工作中非常大的一个痛点:Feign只能编码/解码字符串类型的数据。有了它便能使得我们编码更加的面向对象,对Feign的内部处理细节更加无感~

说明:若不熟悉Jackson,请务必参阅我的专栏[享学Jackson](单击这里电梯直达),该专栏有可能是全网最好、最全的完整教程。

正文

Feign作为一个HC,它最大的特点就是简化Client端的开发,能完全面向接口编程。然而在实际编码中,我们最常用的编码方式是面向对象编程、传递数据,形如下面这这样:

/**
 * 查询列表
 */
@RequestLine("GET /person/list")
List<Person> getList();

/**
 * 新增一条记录
 */
@RequestLine("POST /person")
Long create(Person person);

但是这对于源生Feign的core部分都是不能支持的,因为POJO不能被正常的编码/解码。
接下来就介绍feign-jackson模块,它让这一切成为了可能~


feign-jackson

它的GAV:

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

我们知道,默认情况下Feign它使用的编码器是feign.codec.Encoder.Default,此编码器功能相对捡漏:只能编码字符串类型(字节数组类型不讨论)。

比如如下例子:

扫描二维码关注公众号,回复: 9170799 查看本文章
@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
public void fun2() {
    JacksonDemoClient client = Feign.builder()
            .logger(new Logger.ErrorLogger()).logLevel(Logger.Level.FULL).retryer(Retryer.NEVER_RETRY) // 输出日志
            .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();}
}

运行程序,控制台输出日志:

// 第一个请求完全正常,因为是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)
...
 -------------------------- 
 
// 第二个抛错:Person不能被编码
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)
	...

请求1完全正常,因为它是String类型,可以正常被编码进Body里。
请求2的抛错也完全在情理之中,原因为:不能编码Person类型。

在实际生产中,case2的写法远比case1多,那怎么破呢???


解决方案

因为使用JSON串作为数据交换格式是当前主流方式,所以编码要求亟待解决。针对以上问题,我此处提出两种解决方案,供以参考:


方案一:手动编码(序列化)

正所谓几乎一切信息均可用字符串来表示,相信这也是为何feign-core只提供最底层的字符串/字节数组编码支持的原因。

按此指导思想,若我们自己手动把POJO编码/序列化为字符串,那岂不就OK了?所以你可这么做:

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

    // 手动完成编码操作,编码为字符串
    ObjectMapper mapper = new ObjectMapper();
    String bodyStr =  mapper.writeValueAsString(new Person());

    // 然后调用方法一完成请求发送
    try { client.jacksonDemo1(bodyStr); }catch (Exception e) { e.printStackTrace();}
}

控制台打印:

[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)
...

可清晰看到Body体是个JSON字符串,达到了解决问题的目的。
总结一下这种方式,它有如下优缺点:

  • 优点:不需要额外导包,仅用Feign的核心功能即可完成工作
  • 缺点:非常多。
    • 硬编码,还得自己处理null问题
    • 不够面向对象
    • 全部参数使用字符串接收,失去了静态语言的优势
    • 容错性极差

方案二:使用feign-jackson自动化处理

既然方案一有这么多缺点,并且解决此问题的方式又是可以通用处理的,所以feign把它抽取出来行程了一个子模块feign-jackson,它就很好的帮我们解决了此问题。

使用情况如下:

// 编码器显示指定使用`JacksonEncoder`
@Test
public void fun3() {
    JacksonDemoClient client = Feign.builder()
    		.logger(new Logger.ErrorLogger()).logLevel(Logger.Level.FULL).retryer(Retryer.NEVER_RETRY) // 输出日志
            .encoder(new JacksonEncoder())
            .target(JacksonDemoClient.class, "http://localhost:8080");

    client.jacksonDemo2(new Person());
}

运行程序,控制台打印:

[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)
...

body体内容是个JSON串,一切正常,而这一切仅仅使用了feign-jackson提供的编码器JacksonEncoder而已,非常的方便。

那么,如果传值为null,情况如何呢?

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

运行测试程序,抛出异常:java.lang.IllegalArgumentException: Body parameter 0 was null。对于这个结果也很容易接受:POST/PUT请求的Body是不允许为null的(但是空串是被允许的哦~)。


原理解析

feign-jackson模块仅仅提供了三个类:一个编码器实现JacksonEncoder,两个解码器实现JacksonDecoderJacksonIteratorDecoder


JacksonEncoder

顾名思义,它借助com.fasterxml.jackson.databind.ObjectMapper来完成编码/序列化的操作。因为ObjectMapper可以序列化任意类型(不仅仅是POJO),所以它可以作为一个通用的编码器来使用

public class JacksonEncoder implements Encoder {

	private final ObjectMapper mapper;

	// 构造器
	public JacksonEncoder() {
	    this(Collections.emptyList());
	}
 // 你可为ObjectMapper注册任意的模块
  public JacksonEncoder(Iterable<Module> modules) {
    this(new ObjectMapper()
    	// 值为null的key是不会序列化到JSON串的
    	// 而ObjectMapper的默认行为是会序列化进去的哦
        .setSerializationInclusion(JsonInclude.Include.NON_NULL)
        // 默认会美化输出
        // 其实我觉得生产上次属性可置为false,没啥必要
        .configure(SerializationFeature.INDENT_OUTPUT, true)
        // 注册module模块们
        .registerModules(modules));
  }

	// 若默认的ObjectMapper不如你意,你可以用你自己的
	// 比如SpringBoot容器内的ObjectMapper作为全局使用的~~~~比较好
  public JacksonEncoder(ObjectMapper mapper) {
    this.mapper = mapper;
  }

	// 执行编码
  @Override
  public void encode(Object object, Type bodyType, RequestTemplate template) {
    ...
    // 采用UTF-8编码,把字符串/POJO写为Byte数组放进Body体里
    template.body(mapper.writerFor(javaType).writeValueAsBytes(object), Util.UTF_8);
    ...
  }
}

以上逻辑简单明了,关注点主要体现在对ObjectMapper实例的定制化上,它默认是不输出null值,且美化输出(其实我倒觉得没必要,美化输出浪费性能嘛~)。

因此生产环境下若你使用此编码器,建议使用你自己的ObjectMapper实例(比如SB容器里面的),这也方便你保持整个工程序列化/反序列化处理的一致性


JacksonDecoder
public class JacksonDecoder implements Decoder {

	private final ObjectMapper mapper;

	... // 构造器。会帮你关闭`DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES`这个特征
	...

  @Override
  public Object decode(Response response, Type type) throws IOException {
	// 如果木有body,就返回null呗
    if (response.body() == null)
      return null;
	... 
	return mapper.readValue(reader, mapper.constructType(type));
  }

}

实现使用的是ObjectMapper#readValue()进行解码/反序列化,它默认会帮你把DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES这个特征关闭。
同样的,ObjectMapper的反序列化支持所有类型,所以该解码器可以通用。

说明:读过我[享学Jackson]专栏的必定知道:Spring它默认也关闭了DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES这个特征值。并且还关闭了MapperFeature#DEFAULT_VIEW_INCLUSION这个特征值。


使用示例
public interface DecoderClient {

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

测试用例:

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

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

运行便能正常输出:[A, B, C]List都能被正常反序列化,所以POJO肯定就是可以的喽,这里就不加以演示了。

说明:Server端返回的是个List<String>,代码就省略了。

但是,但是,但是,若你用java.util.stream.Stream作为方法返回值:

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

运行测试程序,抛错:

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
 ...

也就是说,如果你的返回值是Stream,那么这个解码器是解决不了的,需要使用StreamDecoder,结合下面这个解码器进行支持。


JacksonIteratorDecoder

再次强调:请注意java.lang.Iterablejava.util.Iterator的区别。Collection接口是继承自Iterable,而非Iterator

顾名思义,它能解码返回值类型是Iterator的方法。如下:

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

运行程序,控制台正确打印结果:

A
B
C

也可结合StreamDecoder一起使用,让他支持java.util.stream.Stream类型的返回值:

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

具体示例就不用再给出了。

但是,但是,但是需要注意的是:此解码器是为Iterator类型返回值定制的,并不具有普适性,所以生产环境下慎用,一般只有特殊场景才让它们出马。


总结

关于feign-jackson这个模块的介绍就到这了,应该能感觉到此模块虽然源码简单,但还是非常实用的。
另外还有一种感觉就是技术之前很多时候都是相互交织的,比如本处的编码/解码均使用到了Jackson这个最流行的JSON库,而不是其它三方库,这都是有内在原因的。

所以通过长期积累,让自己的知识面、技术面成为一个体系,这不就是作为一个架构师最应该有的基本功麽?

分隔线

声明

原创不易,码字不易,多谢你的点赞、收藏、关注。把本文分享到你的朋友圈是被允许的,但拒绝抄袭。你也可左边扫码加入我的 Java高工、架构师 系列群大家庭学习和交流。
往期精选

发布了294 篇原创文章 · 获赞 454 · 访问量 39万+

猜你喜欢

转载自blog.csdn.net/f641385712/article/details/104320834