Spring-cloud-openfeign解码器Decoder接口(后置拦截器)

使用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);
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值