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
模块,它让这一切成为了可能~
它的GAV:
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-jackson</artifactId>
<version>${feign.version}</version>
</dependency>
我们知道,默认情况下Feign它使用的编码器是feign.codec.Encoder.Default
,此编码器功能相对捡漏:只能编码字符串类型(字节数组类型不讨论)。
比如如下例子:
@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-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
,两个解码器实现JacksonDecoder
和JacksonIteratorDecoder
。
顾名思义,它借助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容器里面的),这也方便你保持整个工程序列化/反序列化处理的一致性。
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
,结合下面这个解码器进行支持。
再次强调:请注意
java.lang.Iterable
和java.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库,而不是其它三方库,这都是有内在原因的。
所以通过长期积累,让自己的知识面、技术面成为一个体系,这不就是作为一个架构师最应该有的基本功麽?
原创不易,码字不易,多谢你的点赞、收藏、关注。把本文分享到你的朋友圈是被允许的,但拒绝抄袭
。