介绍Feign在我的项目中的正确打开方式

看了上一期Feign近程调用的小伙伴可能会问:阿鉴,你不是说上一期讲的是Feign的99%罕用形式吗?怎么明天还有正确打开方式一说呀?

阿鉴:是99%的罕用形式,阿鉴相对没有诓大家,只是这一期的1%作为画龙点睛之笔而已,嘿嘿

先来一套案例

  • 商品服务接口

    @RestController@RequestMapping("/goods")public class GoodsController {      @GetMapping("/get-goods")    public Goods getGoods() throws InterruptedException {        TimeUnit.SECONDS.sleep(10);        System.out.println("xxxxxxx");        return new Goods().setName("苹果")                .setPrice(1.1)                .setNumber(2);    }        @PostMapping("save")    public void save(@RequestBody Goods goods){        System.out.println(goods);    }}
  • 商品服务feign接口

    @FeignClient(name = "my-goods", path = "/goods", contextId = "goods")public interface GoodsApi {    @GetMapping("/get-goods")    Goods getGoods();    @PostMapping(value = "/save")    void save(Goods goods);}
  • 订单服务接口

    @RestController@RequestMapping("/order")public class OrderController {    @Resource    private GoodsApi goodsApi;    @GetMapping("/get-goods")    public Goods getGoods(){        return goodsApi.getGoods();    }    @PostMapping("/save-goods")    public String saveGoods(){        goodsApi.save(new Goods().setName("banana").setNumber(1).setPrice(1.1));        return "ok";    }}
没错,这就是上一期的查问和保留接口,由订单服务调用商品服务

失常状况下,这个案例运行是没有任何问题的,然而,我的项目理论运行中会遇到各种各样的问题。咱们一个一个来。

超时

在上一次,咱们理解到,当服务提供者响应超时时(网络出了问题,或者服务的确响应不过去),服务调用者是能够配置超时工夫及时掐断申请来防止线程阻塞。如以下配置

feign:  client:    config:      default:        # 连贯超时工夫 单位毫秒 默认10秒        connectTimeout: 1000        # 申请超时工夫 单位毫秒 默认60秒        readTimeout: 5000

当初,咱们在商品服务的接口中进行睡眠10s来模仿超时状况

而后,发动调用,你会发现本来接口返回的数据是个json格局,当初因为超时Feign抛出异样,页面成了这个样子:

竟然返回了个页面!

这样子必定是不行的,咱们须要当呈现超时状况时,返回一个预期的谬误,比方服务调用失败的异样

Feign的作者也想到了这一点,给咱们提供一个Fallback的机制,用法如下:

  1. 开启hystrix

    feign:  hystrix:    enabled: true
  2. 编写GoodsApiFallback

    @Slf4j@Componentpublic class GoodsApiFallback implements FallbackFactory<GoodsApi> {    @Override    public GoodsApi create(Throwable throwable) {        log.error(throwable.getMessage(), throwable);        return new GoodsApi() {            @Override            public Goods getGoods() {                return new Goods();            }                        @Override            public void save(Goods goods) {            }        };    }}
  3. 在FeignClient中增加属性fallbackFactory

    @FeignClient(name = "my-goods", path = "/goods", contextId = "goods", fallbackFactory = GoodsApiFallback.class)public interface GoodsApi {}

当再次申请超时时,就会启用fallback中的响应逻辑,而咱们编写的逻辑是返回一个new Goods(),所以超时时申请逻辑中会失去一个空的Goods对象,就像这样:

看起来,因为超时返回的信息不敌对的问题的确解决了,然而,咱们在fallback中返回了一个空对象,这时候就会造成逻辑凌乱:是商品服务里没有这个商品还是服务超时呢?不知道了...

应用带有异样信息的返回对象

为了解决这个逻辑的凌乱,于是咱们想到应用一个能够带有异样信息的返回对象,他的构造如下:

{  "code": 0,  "message": "",  "data": {}}
咱们定义,code为0时示意正确返回

基于此,咱们便能够批改以上逻辑:

  • 商品服务失常返回时,code:0
  • 呈现超时状态时,code: -1

被调整代码如下:

  • 商品服务

    @GetMapping("/get-goods")public BaseResult<Goods> getGoods() throws InterruptedException {  System.out.println("xxxxxxx");  return BaseResult.success(new Goods().setName("苹果")                            .setPrice(1.1)                            .setNumber(2));}
  • 商品服务feign接口

    @GetMapping("/get-goods")BaseResult<Goods> getGoods();
  • 商品服务feign接口Fallback

    return new GoodsApi() {  @Override  public BaseResult<Goods> getGoods() {    BaseResult<Goods> result = new BaseResult<>();    result.setCode(-1);    result.setMessage("商品服务响应超时");    return result;  }}
  • 订单服务

    @GetMapping("/get-goods")public Goods getGoods(){  BaseResult<Goods> result = goodsApi.getGoods();  if(result.getCode() != 0){    throw new RuntimeException("调用商品服务产生谬误:" + result.getMessage());  }  return result.getData();}

当初,既解决了服务响应超时返回信息不敌对的问题,也解决了逻辑凌乱问题,功败垂成?

对立异样校验并解包

以上的解决方案的确能够了,个别我的项目的手法也就到这里了,只是用起来。。。

你会发现一个很恶心的问题,原本咱们的应用形式是这样的:

Goods goods = goodsApi.getGoods();

当初成了这样:

BaseResult<Goods> result = goodsApi.getGoods();if(result.getCode() != 0){  throw new RuntimeException("调用商品服务产生谬误:" + result.getMessage());}Goods goods = result.getData();

而且这段代码到处都是,因为很多Feign接口嘛,每个Feign接口的校验逻辑都是截然不同:

BaseResult<xxx> result = xxxApi.getXxx();if(result.getCode() != 0){  throw new RuntimeException("调用xxx服务产生谬误:" + result.getMessage());}Xxx xxx = result.getData();

———————分割线———————

我,阿鉴,作为一个有代码洁癖的人,会容许这种事件产生吗?不可能!

什么好用的形式与平安的形式不可兼得,作为一个成年人:我都要!

当初咱们就来把它变成既是原来的应用形式,又能失去敌对的返回信息。

在上一期,咱们提到了Feign有个编解码的流程,而解码这个动作,就会波及到将服务端返回的信息,解析成客户端须要的内容。

所以思路就是:自定义一个解码器,将服务器返回的信息进行解码,判断BaseResult的code值,code为0间接把data返回,code不为0抛出异样。

上代码:

  • 编写自定义解码器

    @Slf4jpublic class BaseResultDecode extends ResponseEntityDecoder {    public BaseResultDecode(Decoder decoder) {        super(decoder);    }    @Override    public Object decode(Response response, Type type) throws IOException, FeignException {        if (type instanceof ParameterizedType) {            if (((ParameterizedType) type).getRawType() != BaseResult.class) {                type = new ParameterizedTypeImpl(new Type[]{type}, null, BaseResult.class);                Object object = super.decode(response, type);                if (object instanceof BaseResult) {                    BaseResult<?> result = (BaseResult<?>) object;                    if (result.isFailure()) {                        log.error("调用Feign接口出现异常,接口:{}, 异样: {}", response.request().url(), result.getMessage());                        throw new BusinessException(result.getCode(), result.getMessage());                    }                    return result.getData();                }            }        }        return super.decode(response, type);    }}
Feign中默认的解码器是ResponseEntityDecoder,所以咱们只须要继承它,在原有的根底上作一些批改就能够了。
  • 将解码器注入到Spring中

    @Configurationpublic class DecodeConfiguration {    @Bean    public Decoder feignDecoder(ObjectFactory<HttpMessageConverters> messageConverters) {        return new OptionalDecoder(                new BaseResultDecode(new SpringDecoder(messageConverters)));    }}

这段代码是间接抄的源码,源码中是这样:

`new OptionalDecoder(

  new ResponseEntityDecoder(new SpringDecoder(this.messageConverters)))`

我只是把ResponseEntityDecoder替换成了本人的BaseResultDecode

当初咱们把代码换回原来的形式吧

  • 商品服务

    @GetMapping("/get-goods")public BaseResult<Goods> getGoods() throws InterruptedException {  System.out.println("xxxxxxx");  return BaseResult.success(new Goods().setName("苹果")                            .setPrice(1.1)                            .setNumber(2));}
这里还是须要放回BaseResult
  • 商品服务feign接口

    @GetMapping("/get-goods")Goods getGoods();
  • 商品服务feign接口Fallback

    return new GoodsApi() {  @Override  public Goods getGoods() {    throw new RuntimeException("调用商品服务产生异样");  }}
  • 订单服务

    @GetMapping("/get-goods")public Goods getGoods(){  return goodsApi.getGoods();}

打印curl日志

这个章节和后面的没有关系,只是效仿前端申请时能够复制一个curl进去,调试时非常不便。

同样的逻辑:自定义一个日志打印器

代码如下:

  • 自定义logger

    public class CurlLogger extends Slf4jLogger {    private final Logger logger;    public CurlLogger(Class<?> clazz) {        super(clazz);        this.logger = LoggerFactory.getLogger(clazz);    }    @Override    protected void logRequest(String configKey, Level logLevel, Request request) {        if (logger.isDebugEnabled()) {            logger.debug(toCurl(request.requestTemplate()));        }        super.logRequest(configKey, logLevel, request);    }    public String toCurl(feign.RequestTemplate template) {        String headers = Arrays.stream(template.headers().entrySet().toArray())                .map(header -> header.toString().replace('=', ':')                        .replace('[', ' ')                        .replace(']', ' '))                .map(h -> String.format(" --header '%s' %n", h))                .collect(Collectors.joining());        String httpMethod = template.method().toUpperCase(Locale.ROOT);        String url = template.url();        if(template.body() != null){            String body = new String(template.body(), StandardCharsets.UTF_8);            return String.format("curl --location --request %s '%s' %n%s %n--data-raw '%s'", httpMethod, url, headers, body);        }        return String.format("curl --location --request %s '%s' %n%s", httpMethod, url, headers);    }}
同样,间接继承默认的Slf4jLogger
  • 自定义日志工厂

    public class CurlFeignLoggerFactory extends DefaultFeignLoggerFactory {    public CurlFeignLoggerFactory(Logger logger) {        super(logger);    }    @Override    public Logger create(Class<?> type) {        return new CurlLogger(type);    }}
  • 注入到Spring

    @Beanpublic FeignLoggerFactory curlFeignLoggerFactory(){  return new CurlFeignLoggerFactory(null);}

成果如下:

curl --location --request POST 'http://my-goods/goods/save'  --header 'Content-Encoding: gzip, deflate '  --header 'Content-Length: 40 '  --header 'Content-Type: application/json '  --header 'token: 123456 ' 

小结

在本章节,我给大家介绍了在理论我的项目中Feign的应用形式:应用带有异样信息的返回对象

以及这样应用的起因:须要让服务调用方可能失去明确的响应信息

这样应用的弊病:总是须要判断服务返回的信息是否正确

解决形式:自定义一个解码器

最初还给大家提供了一个打印curl日志的形式。

最初的最初,阿鉴想对大家说几句,不晓得大家看了阿鉴的自定义解码器和自定义logger有没有什么感触,大家以前可能始终感觉对一些框架进行扩大是一件有多难,有多厉害的事件,其实也没有那么难,很多时候咱们只须要基于框架中的逻辑进行一些小小的扩大即可,总结来说就是,发现它,继承它,批改它。

那么,咱们下期再见~

想要理解更多精彩内容,欢送关注公众号:程序员阿鉴,阿鉴在公众号欢送你的到来~

集体博客空间:https://zijiancode.cn/archive...