使用feign调用第三方的http服务,对方返回response,之后这个Decoder接口会将对方的返回值,序列化成我们的返回值,例如下面的代码中,为什么我们能拿到User类型,而不是一个String类型,这就是Decoder来处理的
@FeignClient(name = "xxx", url = "xxx")
public interface UserAPI {
// 即使对方返回一个符合json格式的String类型,feign也能将这个String转换成User,这就是Decoder接口做的事情
@GetMapping(value = "/xxx/xxx")
User getUser(@RequestParam("userId") Integer userId);
}
下面的代码自定义了一个Decoder,由于Decoder是用来解析对方返回值,所以我这里使用Decoder来打印日志,这样省的每个接口自己打印对方返回值了
步骤1:自定义一个解码器
package 你的包名
import feign.Response;
import feign.codec.Decoder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.lang.reflect.Type;
import java.nio.charset.StandardCharsets;
/**
* <p>这个类本质上是个第三方Http Response的解码器,本类主要目的是在解码的同时打印日志
*
* @author shiwentian
* @since 1.7.2024
**/
public class OpenFeignPostInterceptor implements Decoder {
private static final Logger log = LoggerFactory.getLogger(OpenFeignPostInterceptor.class);
private final Decoder delegate;
public OpenFeignPostInterceptor(Decoder delegate) {
this.delegate = delegate;
}
@Override
public Object decode(Response response, Type type) throws IOException {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = response.body().asInputStream().read(buffer)) != -1) {
byteArrayOutputStream.write(buffer, 0, bytesRead);
}
byte[] body = byteArrayOutputStream.toByteArray();
String bodyStr = new String(body, StandardCharsets.UTF_8);
log.info("调用第三方接口返回:{}", bodyStr);
return delegate.decode(response.toBuilder().body(bodyStr, StandardCharsets.UTF_8).build(), type);
}
}
步骤2:将上面自定义的解码器通过lite的注入方式,注入到spring容器
package 你的包名;
import feign.codec.Decoder;
import feign.optionals.OptionalDecoder;
import org.springframework.beans.factory.ObjectFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.http.HttpMessageConverters;
import org.springframework.cloud.openfeign.support.ResponseEntityDecoder;
import org.springframework.cloud.openfeign.support.SpringDecoder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 本类用于注册自定义的解码器
*
* @author shiwentian
* @since 1.7.2024
**/
@Configuration
public class DecoderConfig {
// 这个地方需要注意,不能使用@Resource,涉及到泛型问题,如果没有泛型,那么使用@Resource是可以的
@Autowired
private ObjectFactory<HttpMessageConverters> messageConverters;
@Bean
public Decoder decode() {
return new OpenFeignPostInterceptor(new OptionalDecoder(new ResponseEntityDecoder(new SpringDecoder(this.messageConverters))));
}
}
好了,现在启动服务,通过feign调用第三方服务,会发现对方每次返回,都会打印对方的返回值
注意:我没有测试对方服务失败的情况,或者一些其他失败的情况,所以本文中的response.body().asInputStream()
这段代码可能会出现不健壮的情况,需要你自己对失败的情况进行处理
源代码参考:Spring Cloud Open Feign 2.2.9版本org.springframework.cloud.openfeign.FeignClientsConfiguration
类的public Decoder feignDecoder()
方法
本文到这里就结束了,下面是我自己写的一个通用类,用于自定义一个取对方返回值里面我们需要的数据:
一般调用feign接口,对方都会返回一个Result类,例如下面这样
public class SimpleResult {
private boolean success = true;
private String code = "200";
private String message = "OK";
// 其实我们只要data里的数据,这个data有一个需要注意的地方就是data可能是个分页查询的数据,所以我们要考虑到分页数据的问题
private Object data;
}
下面的代码将直接取data数据,包括分页查询,先定义一个注解,里面的字段很好理解
package 你的包名;
import com.alibaba.fastjson.JSONObject;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 这个注解代表feign的返回值的一些信息
*
* @author shiwentian
* @since 11.7.2024
**/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ResultData {
/**
* 表示对方接口的返回值中,哪个字段是我们需要的业务字段,支持"."和"[]"的形式,假设对方的返回值如下
* <pre>
* {
* "status":"success",
* "evn":{
* "name":"dev",
* "address":"武汉"
* }
* "data":[
* {"name":"人员名字1","orders":[
* {"orderId":"111","orderCode":"订单1号"},
* {"orderId":"222","orderCode":"订单2号"}
* ]},
* {"name":"人员名字2","orders":[
* {"orderId":"333","orderCode":"订单3号"}
* ]}
* ]
* }
* </pre>
* 如果ResultData(value="evn"),则Feign我们可以接到1条数据,如下
* <pre>
* {"name":"dev","address":"武汉"}
* </pre>
* 如果ResultData(value="data[].orders[]"),则Feign我们可以接到3条订单数据,如下
* <pre>
* [
* {"orderId":"111","orderCode":"订单1号"},
* {"orderId":"222","orderCode":"订单2号"},
* {"orderId":"333","orderCode":"订单3号"}
* ]
* </pre>
* 如果ResultData(value="data[]"),则Feign我们可以接到2条人员数据,如下
* <pre>
* [
* {"name":"人员名字1","orders":[
* {"orderId":"111","orderCode":"订单1号"},
* {"orderId":"222","orderCode":"订单2号"}
* ]},
* {"name":"人员名字2","orders":[
* {"orderId":"333","orderCode":"订单3号"}
* ]}
* ]
* </pre>
* 如果ResultData(value="data[].orders[].orderCode"),则Feign我们可以接到3条订单号,如下
* <pre>
* ["订单1号","订单2号","订单3号"]
* </pre>
**/
String value() default "data";
/**
* 表示对方返回值中,哪个字段用来代表成功或者失败
**/
String success() default "success";
/**
* 表示对方返回值中,哪个字段用来代表返回码
**/
String code() default "code";
/**
* 如果是分页查询,本字段则表示对方的返回值中,哪个字段代表[当前第几页]
*
* <p>如果要取的值不在第一层级,也可以使用"点"的写法,例如"data.pageNum",这样也可以
**/
String pageNum() default "pageNum";
/**
* 如果是分页查询,本字段则表示对方的返回值中,哪个字段代表[每页多少条]
*
* <p>如果要取的值不在第一层级,也可以使用"点"的写法,例如"data.pageSize",这样也可以
**/
String pageSize() default "pageSize";
/**
* 如果是分页查询,本字段则表示对方的返回值中,哪个字段代表[一共多少页]
*
* <p>如果要取的值不在第一层级,也可以使用"点"的写法,例如"data.totalPages",这样也可以
**/
String totalPages() default "totalPages";
/**
* 如果是分页查询,本字段则表示对方的返回值中,哪个字段代表[一共多少条]
*
* <p>如果要取的值不在第一层级,也可以使用"点"的写法,例如"data.totalCount",这样也可以
**/
String totalCount() default "totalCount";
/**
* 如果是分页查询,本字段则表示我们分页列表的Class对象,默认使用JSONObject
**/
Class<?> pageDataClass() default JSONObject.class;
}
然后feign接口如下
@FeignClient(name = "xxx", url = "xxx")
public interface UserAPI {
// 通常这里不返回User,而是一个专用的Result类,然后User在Result的data里
@GetMapping(value = "/xxx/xxx")
@ResultData// 这个注解直接获取data里的值
List<User> getUserList(@RequestParam("userId") Integer userId);
}
然后定义后置拦截器(Decoder接口)
package 你的包名;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.enn.config.exception.BusinessException;
import com.enn.config.result.SimplePageResult;
import com.enn.utils.ByteStreamUtils;
import com.github.pagehelper.PageInfo;
import feign.MethodMetadata;
import feign.RequestTemplate;
import feign.Response;
import feign.codec.Decoder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.annotation.AnnotationUtils;
import top.rdfa.framework.biz.ro.PagedRdfaResult;
import java.io.IOException;
import java.io.InputStream;
import java.io.Serializable;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
/**
* 参与者中心租户相关API后置拦截器,用于打印对方返回值
*
* <p>这个类本质上是个第三方Http Response的解码器,本类主要目的是在解码的同时打印日志
*
* @author shiwentian
* @since 1.7.2024
**/
public class OpenFeignPostInterceptor implements Decoder {
private static final Logger log = LoggerFactory.getLogger(OpenFeignPostInterceptor.class);
private final Decoder delegate;
public OpenFeignPostInterceptor(Decoder delegate) {
this.delegate = delegate;
}
/**
* 从一个json对象中,取指定的key的值,并且将这个值转换成int类型
*
* @param apiResult 一个json对象
* @param pageNumKey key值,允许多层次,例如"user.age",则直接拿到age
* @return 指定key的值
**/
public static int getIntValue(JSONObject apiResult, String pageNumKey) {
if (pageNumKey.contains(".")) {
JSONObject temp = apiResult;
String[] keys = pageNumKey.split("\\.");
for (int i = 0; i < keys.length; i++) {
// 进入if说明是最后一个key,例如(data.total中的total)
if (i == keys.length - 1) {
return temp.getIntValue(keys[i]);
} else {
temp = temp.getJSONObject(keys[i]);
}
}
} else {
return apiResult.getIntValue(pageNumKey);
}
throw new BusinessException("第三方接口调用错误,没有找到返回值中的:" + pageNumKey);
}
@Override
public Object decode(Response response, Type type) throws IOException {
InputStream inputStream = response.body().asInputStream();
byte[] body = ByteStreamUtils.inputStreamToBytes(inputStream);
String bodyStr = new String(body, StandardCharsets.UTF_8);
log.info("调用第三方接口返回:{}", bodyStr);
JSONObject apiResult = JSON.parseObject(bodyStr);
RequestTemplate template = response.request().requestTemplate();
MethodMetadata methodMetadata = template.methodMetadata();
Method method = methodMetadata.method();
ResultData resultData = AnnotationUtils.findAnnotation(method, ResultData.class);
String natureReturnValue = bodyStr;
if (resultData != null) {
if (apiResult.containsKey(resultData.success()) || apiResult.containsKey(resultData.code())) {
if (!apiResult.getBooleanValue(resultData.success()) && !apiResult.get(resultData.code()).equals("0") && !apiResult.get(resultData.code()).equals(200)) {
throw new BusinessException("第三方接口提示:" + apiResult.getString("message"));
}
String dataKey = resultData.value();
// 进入if说明我们取对方的返回值的时候,只取第一层级里的某个字段,例如对方返回{"data":{"name":"史文天"}},我们取data里的值
if (!dataKey.contains(".")) {
natureReturnValue = apiResult.getString(dataKey.replace("[]", ""));
// 进入if说明我们的feign接口是使用分页来接对方返回值的
String typeName = type.getTypeName();
if (typeName.contains("SimplePageResult") || typeName.contains("PageInfo") || typeName.contains("PagedRdfaResult")) {
int pageNum = getIntValue(apiResult, resultData.pageNum());
int pageSize = getIntValue(apiResult, resultData.pageSize());
int totalPages = getIntValue(apiResult, resultData.totalPages());
int totalCount = getIntValue(apiResult, resultData.totalCount());
// 进入if说明我们使用的是SimplePageResult类接对方的返回值
if (typeName.contains("SimplePageResult")) {
SimplePageResult pageResult = new SimplePageResult();
pageResult.setPageNum(pageNum);
pageResult.setPageSize(pageSize);
pageResult.setTotalPages(totalPages);
pageResult.setTotalCount(totalCount);
pageResult.setData(null);
List<?> list = JSON.parseArray(natureReturnValue, resultData.pageDataClass());
pageResult.setData(list);
return pageResult;
}
// 进入if说明我们使用的是mybatis-pagehelper的PageInfo接对方的返回值
else if (typeName.contains("PageInfo")) {
PageInfo<Object> pageResult = new PageInfo<>();
pageResult.setPageNum(pageNum);
pageResult.setPageSize(pageSize);
pageResult.setPages(totalPages);
pageResult.setTotal(totalCount);
pageResult.setList(new ArrayList<>());
List<?> list = JSON.parseArray(natureReturnValue, resultData.pageDataClass());
List<Object> pageList = pageResult.getList();
pageList.addAll(list);
return pageResult;
}
// 进入if说明我们使用的RDFA分页返回值接的
else {
PagedRdfaResult<Serializable> pageResult = new PagedRdfaResult<>();
pageResult.setPageNum(pageNum);
pageResult.setPageSize(pageSize);
pageResult.setTotalCount(totalCount);
pageResult.setSuccess(true);
pageResult.setCode("200");
pageResult.setMessage("");
List<?> list = JSON.parseArray(natureReturnValue, resultData.pageDataClass());
List<Serializable> rdfaList = new ArrayList<>();
for (Object o : list) {
rdfaList.add((Serializable) o);
}
pageResult.setData(rdfaList);
return pageResult;
}
}
}
// 进入else说明我们取对方的返回值的时候,取的不是第一层级里的内容,例如对方返回{"data":{"name":"史文天"}},我们取name里的值,而不是data里的值
else {
String[] floor = resultData.value().split("\\.");
JSONObject preSingle = apiResult;
boolean preIsSingle = true;
JSONArray preArray = new JSONArray();
for (String key : floor) {
// 如果当前key是数组,上一个key是单个json对象
if (key.contains("[]") && preIsSingle) {
preArray = preSingle.getJSONArray(key.replace("[]", ""));
preIsSingle = false;
}
// 如果当前key是数组,上一个key也是数组
else if (key.contains("[]") && !preIsSingle) {
JSONArray tempArray = new JSONArray();
for (Object o : preArray) {
JSONArray temp = ((JSONObject) o).getJSONArray(key.replace("[]", ""));
if (temp != null) {
tempArray.addAll(temp);
}
}
preArray = tempArray;
}
// 如果当前key是单个json对象,上一个key也是单个json对象
else if (!key.contains("[]") && preIsSingle) {
preSingle = preSingle.getJSONObject(key);
}
// 如果当前key是单个json对象,上一个key是数组
else {
JSONArray tempArray = new JSONArray();
for (Object o : preArray) {
JSONObject temp = ((JSONObject) o).getJSONObject(key);
tempArray.add(temp);
}
preArray = tempArray;
preIsSingle = true;
}
}
if (preIsSingle) {
natureReturnValue = preSingle.toJSONString();
} else {
natureReturnValue = preArray.toJSONString();
}
}
}
// 进入else说明我们从对方接口中并不知道本次请求是成功还是失败
else {
throw new BusinessException("第三方接口调用错误,没有找到返回值中的:" + resultData.success() + "[" + bodyStr + "]");
}
}
return delegate.decode(response.toBuilder().body(natureReturnValue, StandardCharsets.UTF_8).build(), type);
}
}
假设对方接口返回一个分页查询,而我们想使用下面这个SimplePageResult类接对方返回值,那代码可以如下:
public class SimplePageResult {
/**
* 当前第几页
**/
private int pageNum;
/**
* 每页多少条
**/
private int pageSize;
/**
* 一共多少页
**/
private long totalPages;
/**
* 一共多少条
**/
private long totalCount;
/**
* 分页数据
**/
private List<?> data;
@SuppressWarnings("unchecked")
public <T> List<T> getData() {
return (List<T>) data;
}
}
/**
* 分页查询人员,每页20条
*
* @param pageNum 当前第几页
**/
// 由于该注解存在,所以我会把对方的返回值封装成SimplePageResult这个类
// 详见上文的OpenFeignPostInterceptor
@ResultData
@PostMapping(value = "/getUserPage")
SimplePageResult getUserPage(@RequestParam("pageNum") int pageNum);