Feign实战技巧篇

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: Feign实战技巧篇

介绍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
  1. 编写GoodsApiFallback
@Slf4j
@Component
public 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) {
            }
        };
    }
}
  1. 在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抛出异常。

上代码:

  • 编写自定义解码器
@Slf4j
public 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中
@Configuration
public 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("调用商品服务发生异常");
  }
}
  • 订单服务打印curl日志
@GetMapping("/get-goods")
public Goods getGoods(){
  return goodsApi.getGoods();
}

这个章节和前面的没有关系,只是效仿前端请求时可以复制一个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
@Bean
public 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有没有什么感触,大家以前可能一直觉得对一些框架进行扩展是一件有多难,有多厉害的事情,其实也没有那么难,很多时候我们只需要基于框架中的逻辑进行一些小小的扩展即可,总结来说就是,发现它,继承它,修改它。

那么,我们下期再见~

相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
目录
相关文章
|
4月前
|
Java UED 开发者
Spring Boot 降级功能的神秘面纱:Hystrix 与 Resilience4j 究竟藏着怎样的秘密?
【8月更文挑战第29天】在分布式系统中,服务稳定性至关重要。为应对故障,Spring Boot 提供了 Hystrix 和 Resilience4j 两种降级工具。Hystrix 作为 Netflix 的容错框架,通过隔离依赖、控制并发及降级机制增强系统稳定性;Resilience4j 则是一个轻量级库,提供丰富的降级策略。两者均可有效提升系统可靠性,具体选择取决于需求与场景。在面对服务故障时,合理运用这些工具能确保系统基本功能正常运作,优化用户体验。以上简介包括了两个工具的简单示例代码,帮助开发者更好地理解和应用。
113 0
|
5月前
Feign使用原理
Feign使用原理
134 0
|
7月前
|
JSON Java 关系型数据库
【Feign】 基于 Feign 远程调用、 自定义配置、性能优化、实现 Feign 最佳实践
【Feign】 基于 Feign 远程调用、 自定义配置、性能优化、实现 Feign 最佳实践
292 0
|
7月前
|
XML Java 数据格式
进阶注解探秘:深入Spring高级注解的精髓与实际运用
进阶注解探秘:深入Spring高级注解的精髓与实际运用
79 2
|
负载均衡 Dubbo Java
简单理解Feign的原理与使用
简单理解Feign的原理与使用
248 0
|
XML 存储 缓存
【Spring专题】「技术原理」从源码角度去深入分析关于Spring的异常处理ExceptionHandler的实现原理
ExceptionHandler是Spring框架提供的一个注解,用于处理应用程序中的异常。当应用程序中发生异常时,ExceptionHandler将优先地拦截异常并处理它,然后将处理结果返回到前端。该注解可用于类级别和方法级别,以捕获不同级别的异常。
21071 2
【Spring专题】「技术原理」从源码角度去深入分析关于Spring的异常处理ExceptionHandler的实现原理
|
缓存 负载均衡 算法
十五.SpringCloud源码剖析-Ribbon工作流程
Ribbon是由Netflix公司开源的一个客户端负载均衡器,主要功能是实现服务之间的负载均衡调用,内置丰富的负载均衡算法,本章意在探讨Ribbon的核心工作流程,Ribbon基本使用请看《[SpringCloud极简入门-客户端负载均衡Ribbon](https://blog.csdn.net/u014494148/article/details/105002095)》
|
运维 微服务
SpringCloud学习(十四):Hystrix的服务熔断实现
熔断机制是应对雪崩效应的一种微服务链路保护机制。当扇出链路的某个微服务出错不可用或者响应时间太长时,会进行服务的降级,进而熔断该节点微服务的调用,快速返回错误的响应信息。
178 0
SpringCloud学习(十四):Hystrix的服务熔断实现
SpringCloud学习(十三):Hystrix的服务降级实现
SpringCloud学习(十三):Hystrix的服务降级实现
135 0
SpringCloud学习(十三):Hystrix的服务降级实现

热门文章

最新文章