【Java开发】Spring Cloud 05 :远程服务调用Openfeign 替代 WebClient

news2024/11/16 18:29:35
在前边章节中,我们借助 Nacos 的服务发现能力,使用 WebClient 实现了服务间调用。从功能层面上来讲,我们已经完美地实现了微服务架构下的远程服务调用,但是从易用性的角度来看,这种实现方式似乎对开发人员并不怎么友好。为了发起一个服务请求,我把整个服务调用的所有信息都写在了代码中,从请求类型、请求路径、再到封装的参数和返回类型。编程体验相当麻烦不说,更关键的是这些代码没有很好地践行职责隔离的原则。

1 OpenFeign 介绍

在业务层中我们应该关注具体的业务实现,而 WebClient 的远程调用引入了很多与业务无关的概念,比如请求地址、请求类型等等。从职责分离的角度来说,我们应该尽量把这些业务无关的逻辑,从业务代码中剥离出去

那么,Spring Cloud 中有没有一个组件,在实现远程服务调用的同时,既能满足简单易用的接入要求,又能很好地将业务无关的代码与业务代码隔离开呢?因此接下来将介绍 Spring Cloud 中的一个叫做 OpenFeign 的组件,看看它是如何简化远程服务调用的。

OpenFeign 组件的前身是 Netflix Feign 项目,它最早是作为 Netflix OSS 项目的一部分,由 Netflix 公司开发。后来 Feign 项目被贡献给了开源组织,于是才有了我们今天使用的 Spring Cloud OpenFeign 组件。

OpenFeign 提供了一种声明式的远程调用接口,它可以大幅简化远程调用的编程体验。在了解 OpenFeign 的原理之前,我们先来体验一下 OpenFeign 的最终疗效。下边是一个 Hello World 的小案例,带你看一下由 OpenFeign 发起的远程服务调用的代码风格是什么样的。

String response = helloWorldService.hello("Yinyu Y.");

使用 OpenFeign 组件来实现远程调用非常简单,就像我们使用本地方法一样,只要一行代码就能实现 WebClient 组件好几行代码干的事情。而且这段代码不包含任何业务无关的信息,完美实现了调用逻辑和业务逻辑之间的职责分离。

2 OpenFeign 动态代理原理

OpenFeign 使用了一种“动态代理”技术来封装远程服务调用的过程,我们在上面的例子中看到的 helloWorldService 其实是一个特殊的接口,它是由 OpenFeign 组件中的 FeignClient 注解所声明的接口,接口中的代码如下所示。

@FeignClient(value = "hello-world-serv")
public interface HelloWorldService {

    @PostMapping("/sayHello")
    String hello(String guestName);
}

远程服务调用的信息被写在了 FeignClient 接口中。在上面的代码里,你可以看到,服务的名称、接口类型、访问路径已经通过注解做了声明。OpenFeign 通过解析这些注解标签生成一个“动态代理类”,这个代理类会将接口调用转化为一个远程服务调用的 Request,并发送给目标服务。

在项目初始化阶段,OpenFeign 会生成一个代理类,对所有通过该接口发起的远程调用进行动态代理:

上图中的步骤 1 到步骤 3 是在项目启动阶段加载完成的,只有第 4 步“调用远程服务”是发生在项目的运行阶段。

下面解释一下上图中的几个关键步骤:

首先,在项目启动阶段,OpenFeign 框架会发起一个主动的扫包流程,从指定的目录下扫描并加载所有被 @FeignClient 注解修饰的接口。

然后,OpenFeign 会针对每一个 FeignClient 接口生成一个动态代理对象,即图中的 FeignProxyService,这个代理对象在继承关系上属于 FeignClient 注解所修饰的接口的实例。

接下来,这个动态代理对象会被添加到 Spring 上下文中,并注入到对应的服务里,也就是图中的 LocalService 服务。

最后,LocalService 会发起底层方法调用。实际上这个方法调用会被 OpenFeign 生成的代理对象接管,由代理对象发起一个远程服务调用,并将调用的结果返回给 LocalService。

那么OpenFeign 是如何通过动态代理技术创建代理对象的:

  1. 项目加载:在项目的启动阶段,EnableFeignClients 注解扮演了“启动开关”的角色,它使用 Spring 框架的 Import 注解导入了 FeignClientsRegistrar 类,开始了 OpenFeign 组件的加载过程。

  1. 扫包:FeignClientsRegistrar 负责 FeignClient 接口的加载,它会在指定的包路径下扫描所有的 FeignClients 类,并构造 FeignClientFactoryBean 对象来解析 FeignClient 接口。

  1. 解析 FeignClient 注解:FeignClientFactoryBean 有两个重要的功能,一个是解析 FeignClient 接口中的请求路径和降级函数的配置信息;另一个是触发动态代理的构造过程。其中,动态代理构造是由更下一层的 ReflectiveFeign 完成的。

  1. 构建动态代理对象:ReflectiveFeign 包含了 OpenFeign 动态代理的核心逻辑,它主要负责创建出 FeignClient 接口的动态代理对象。ReflectiveFeign 在这个过程中有两个重要任务,一个是解析 FeignClient 接口上各个方法级别的注解,将其中的远程接口 URL、接口类型(GET、POST 等)、各个请求参数等封装成元数据,并为每一个方法生成一个对应的 MethodHandler 类作为方法级别的代理;另一个重要任务是将这些 MethodHandler 方法代理做进一步封装,通过 Java 标准的动态代理协议,构建一个实现了 InvocationHandler 接口的动态代理对象,并将这个动态代理对象绑定到 FeignClient 接口上。这样一来,所有发生在 FeignClient 接口上的调用,最终都会由它背后的动态代理对象来承接。

那么上面说到的“元数据的解析”是如何完成的呢?它依赖于 OpenFeign 组件中的 Contract 协议解析功能。Contract 是 OpenFeign 组件中定义的顶层抽象接口,它有一系列的具体实现,其中和我们实战项目有关的是 SpringMvcContract 这个类,从这个类的名字中我们就能看出来,它是专门用来解析 Spring MVC 标签的。

SpringMvcContract 的继承结构是 SpringMvcContract->BaseContract->Contract。这里拿一段 SpringMvcContract 的代码,帮助你深入理解它是如何将注解解析为元数据的。这段代码的主要功能是解析 FeignClient 方法级别上定义的 Spring MVC 注解。


// 解析FeignClient接口方法级别上的RequestMapping注解
protected void processAnnotationOnMethod(MethodMetadata data, Annotation methodAnnotation, Method method) {
   // 省略部分代码...
   
   // 如果方法上没有使用RequestMapping注解,则不进行解析
   // 其实GetMapping、PostMapping等注解都属于RequestMapping注解
   if (!RequestMapping.class.isInstance(methodAnnotation)
         && !methodAnnotation.annotationType().isAnnotationPresent(RequestMapping.class)) {
      return;
   }

   // 获取RequestMapping注解实例
   RequestMapping methodMapping = findMergedAnnotation(method, RequestMapping.class);
   // 解析Http Method定义,即注解中的GET、POST、PUT、DELETE方法类型
   RequestMethod[] methods = methodMapping.method();
   // 如果没有定义methods属性则默认当前方法是个GET方法
   if (methods.length == 0) {
      methods = new RequestMethod[] { RequestMethod.GET };
   }
   checkOne(method, methods, "method");
   data.template().method(Request.HttpMethod.valueOf(methods[0].name()));

   // 解析Path属性,即方法上写明的请求路径
   checkAtMostOne(method, methodMapping.value(), "value");
   if (methodMapping.value().length > 0) {
      String pathValue = emptyToNull(methodMapping.value()[0]);
      if (pathValue != null) {
         pathValue = resolve(pathValue);
         // 如果path没有以斜杠开头,则补上/
         if (!pathValue.startsWith("/") && !data.template().path().endsWith("/")) {
            pathValue = "/" + pathValue;
         }
         data.template().uri(pathValue, true);
         if (data.template().decodeSlash() != decodeSlash) {
            data.template().decodeSlash(decodeSlash);
         }
      }
   }

   // 解析RequestMapping中定义的produces属性
   parseProduces(data, method, methodMapping);

   // 解析RequestMapping中定义的consumer属性
   parseConsumes(data, method, methodMapping);

   // 解析RequestMapping中定义的headers属性
   parseHeaders(data, method, methodMapping);
   data.indexToExpander(new LinkedHashMap<>());
}

通过上面的方法,我们可以看到,OpenFeign 对 RequestMappings 注解的各个属性都做了解析。

如果你在项目中使用的是 GetMapping、PostMapping 之类的注解,没有使用 RequestMapping,那么 OpenFeign 还能解析吗?当然可以。以 GetMapping 为例,它对 RequestMapping 注解做了一层封装。如果你查看下面关于 GetMapping 注解的代码,你会发现这个注解头上也挂了一个 RequestMapping 注解。因此 OpenFeign 可以正确识别 GetMapping 并完成加载。

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@RequestMapping(method = RequestMethod.GET)
public @interface GetMapping {
// ...省略部分代码
}

到这里,相信你已经了解了 OpenFeign 的工作流程,下边将 coupon-customer-serv 中的 WebClient 调用替换为基于 OpenFeign 的远程服务调用。

3 OpenFeign 实现服务间调用

OpenFeign 能实现对实战项目中的 WebClient 请求做大幅度的简化,让跨服务请求就像调用本地方法一样简单。

本章节改造的项目是 coupon-customer-serv 服务,因为它内部需要调用 template 和 calculation 两个服务完成自己的业务逻辑,非常适合用 Feign 来做跨服务调用的改造。

在集成 OpenFeign 组件之前,我们需要把它的依赖项 spring-cloud-starter-OpenFeign 添加到 coupon-customer-impl 子模块内的 pom.xml 文件中。

<!-- OpenFeign组件 -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

在上面的代码中,你并不需要指定组件的版本号,因为我们在顶层项目中定义的 spring-cloud-dependencies 依赖项中已经定义了各个 Spring Cloud 的版本号,它们会随着 Maven 项目的继承关系传递到子模块中。

添加好依赖项之后,我们就可以进行大刀阔斧的 OpenFeign 改造了。在 coupon-customer-impl 子模块下的 CouponCustomerServiceImpl 类中,我们通过 WebClient 分别调用了 template 和 calculation 的服务。本章节将带你对 template 的远程调用过程进行改造,将其替换为 OpenFeign 风格的调用。

3.1 改造 Template 远程调用

OpenFeign 组件通过接口代理的方式发起远程调用,那么我们改造过程的第一步就是要定义一个 OpenFeign 接口。

这需要在 coupon-customer-impl 项目下创建了一个 package,它的路径是 com.yinyu.coupon.customer.feign。在这个路径下我定义了一个叫做 TemplateService 的 Interface,用来实现对 coupon-template-serv 的远程调用代理。我们来看一下这个接口的源代码。

@FeignClient(value = "coupon-template-serv", path = "/template")
public interface TemplateService {
    // 读取优惠券
    @GetMapping("/getTemplate")
    CouponTemplateInfo getTemplate(@RequestParam("id") Long id);
    
    // 批量获取
    @GetMapping("/getBatch")
    Map<Long, CouponTemplateInfo> getTemplateInBatch(@RequestParam("ids") Collection<Long> ids);
}

在上面的代码中,我们在接口上声明了一个 FeignClient 注解,它专门用来标记被 OpenFeign 托管的接口。在 FeignClient 注解中声明的 value 属性是目标服务的名称,在代码中我指定了 coupon-template-serv,你需要确保这里的服务名称和 Nacos 服务器上显示的服务注册名称是一样的

此外,FeignClient 注解中的 path 属性是一个可选项,如果你要调用的目标服务有一个统一的前置访问路径,比如 coupon-template-serv 所有接口的访问路径都以 /template 开头,那么你可以通过 path 属性来声明这个前置路径,这样一来,你就不用在每一个方法名上的注解中带上前置 Path 了。

在项目的启动阶段,OpenFeign 会查找所有被 FeignClient 注解修饰的接口,并代理该接口的所有方法调用。当我们调用接口方法的时候,OpenFeign 就会根据方法上定义的注解自动拼装 HTTP 请求路径和参数,并向目标服务发起真实调用。

因此,我们还需要在方法上定义 spring-web 注解(如 GetMapping、PostMapping),让 OpenFeign 拼装出正确的 Request URL 和请求参数。这时你要注意,OpenFeign 接口中定义的路径和参数必须与你要调用的目标服务中的保持一致

接下来你就可以替换 CouponCustomerServiceImpl 中的业务逻辑调用了。首先,我们在 CouponCustomerServiceImpl 接口中注入刚才定义的 TemplateService 接口。

@Autowired
private TemplateService templateService;

然后,我们就可以对具体的业务逻辑进行替换了。以 CouponCustomerServiceImpl 类中的 placeOrder 下单接口为例,其中有一步是调用 coupon-template-serv 获取优惠券模板数据,这个服务请求是使用 WebClient 发起的,我们来看一下改造之前的方法实现。

webClientBuilder.build().get()
    .uri("http://coupon-template-serv/template/getTemplate?id=" + templateId)
    .retrieve()
    .bodyToMono(CouponTemplateInfo.class)
    .block();        

从上面的代码中你可以看出,我们写了一大长串的代码,只为了发起一次服务请求。如果使用 OpenFeign 接口来替换,那画风就不一样了,我们看一下改造后的服务调用过程。

templateService.getTemplate(couponInfo.getTemplateId())

你可以看到,使用 OpenFeign 接口发起远程调用就像使用本地服务一样简单。和 WebClient 的调用方式相比,OpenFeign 组件不光可以提高代码可读性和可维护性,还降低了远程调用的 Coding 成本。

在 CouponCustomerServiceImpl 类中的 findCoupon 方法里,我们调用了 coupon-template-serv 的批量查询接口获取模板信息,这个过程也可以使用 OpenFeign 接口实现,下面是具体的实现代码。

// 获取这些优惠券的模板ID
List<Long> templateIds = coupons.stream()
        .map(Coupon::getTemplateId)
        .distinct()
        .collect(Collectors.toList());

// 发起请求批量查询券模板
Map<Long, CouponTemplateInfo> templateMap = templateService
        .getTemplateInBatch(templateIds);

其他的地方也是类似的,到这里,我们已经把 template 服务的远程调用改成了 OpenFeign 接口调用的方式。

3.2 改造 Calculation 远程调用

首先,我们在 TemplateService 同样的目录下创建一个新的接口,名字是 CalculationService,后面你会使用它作为 coupon-calculation-serv 的代理接口。我们来看一下这个接口的源码。

@FeignClient(value = "coupon-calculation-serv", path = "/calculator")
public interface CalculationService {

    // 订单结算
    @PostMapping("/checkout")
    ShoppingCart checkout(ShoppingCart settlement);

    // 优惠券试算
    @PostMapping("/simulate")
    SimulationResponse simulate(SimulationOrder simulator);
}

我在接口类之上声明了一个 FeignClient 注解,指向了 coupon-calculation-serv 服务,并且在 path 属性中注明了服务访问的前置路径是 /calculator 👇

@FeignClient(value = "coupon-calculation-serv", path = "/calculator")
public interface CalculationService {

    // 优惠券结算
    @PostMapping("/checkout")
    ShoppingCart checkout(ShoppingCart settlement);

    // 优惠券列表挨个试算
    // 给客户提示每个可用券的优惠额度,帮助挑选
    @PostMapping("/simulate")
    SimulationResponse simulate(SimulationOrder simulator);
}

在接口中我还定义了两个方法,分别指向 checkout 用户下单接口和 simulate 优惠券试算接口,这两个接口的访问路径和 coupon-calculation-serv 中定义的路径是一模一样的。

有了前面 template 服务的改造经验,相信你应该很轻松就能搞定 calculation 服务调用的改造。首先,我们需要把刚才定义的 CalculationService 注入到 CouponCustomerServiceImpl 中。

@Autowired
private CalculationService calculationService;

然后,你只用在调用 coupon-calculation-serv 服务的地方,将 WebClient 调用替换成下面这种 OpenFeign 调用的方式就可以了:

// order清算
ShoppingCart checkoutInfo = calculationService.checkout(order);

// order试算
calculationService.simulate(order)

到这里,我们就完成了 template 和 calculation 服务调用过程的改造。在我们启动项目来验证改造成果之前,还有最为关键的一步需要完成,那就是配置 OpenFeign 的加载路径。

3.3 配置 OpenFeign 的加载路径

此时,如果你是使用的是 IDEA 等编译器,那么注入 template 和 calculation 服务将会报错 👇

因此我们打开 coupon-customer-serv 项目的启动类,你可以通过在类名之上添加一个 EnableFeignClients 注解的方式定义 OpenFeign 接口的加载路径,你可以参考以下代码。

// 省略其他无关注解
@EnableFeignClients(basePackages = {"com.geekbang"})
public class Application {

}

在这段代码中,我们在 EnableFeignClients 注解的 basePackages 属性中定义了一个 com.yinyu 的包名,这个注解就会告诉 OpenFeign 在启动项目的时候做一件事儿:找到所有位于 com.yinyu 包路径(包括子 package)之下使用 FeignClient 修饰的接口,然后生成相关的代理类并添加到 Spring 的上下文中。这样一来,我们才能够在项目中用 Autowired 注解注入 OpenFeign 接口。

上面就是使用包路径扫描的方式来加载 FeignClient 接口。除此之外,你还可以通过直接加载指定 FeignClient 接口类的方式,或者从指定类所在的目录进行扫包的方式来加载 FeignClient 接口:

// 通过指定Client类来加载
@EnableFeignClients(clients = {TemplateService.class, CalculationService.class})

// 扫描特定类所在的包路径下的FeignClient
@EnableFeignClients(basePackageClasses = {TemplateService.class})

在这三种加载方式中,我比较推荐你在项目中使用一劳永逸的“包路径”加载的方式。因为不管以后你添加了多少新的 FeignClient 接口,只要这些接口位于 com.geekbang 包路径之下,你就不用操心加载路径的配置。

到这里,我们就完成了 OpenFeign 的实战项目改造,你可以在本地启动项目来验证改造后的程序是否可以正常工作。

问题:如果你的服务需要暴露给很多业务方使用,每个业务方都要维护一套独立的 OpenFeign 接口似乎也不太方便,你能想到什么更好的接口管理办法吗?
解决:每个服务提供方的api模块单独添加一个openfeign的模块,服务调用方添加对应的openfeign模块即可

4 OpenFeign 组件进阶技巧

本章节将进一步深入 OpenFeign 的功能特性,学习几个 OpenFeign 的进阶使用技巧:异常信息排查、超时判定和服务降级。

异常信息排查是我们开发人员每天都要面对的事情。如果你正在开发一个大型微服务应用,你经常需要集成一些由其他团队开发的 API,这就免不了要参与各种联调和问题排查。如果你是一个经验丰富的老码农,那你一定经常说这样一句话:“你的 Request 参数是什么?”这句台词在我们平时的 API 联调和线上异常排查中出镜率很高,因为服务请求的入参和出参是分析和排查问题的重要线索

为了获得服务请求的参数和返回值,我们经常使用的一个做法就是打印日志。你可以在程序中使用 log.info 或者 log.debug 方法将服务请求的入参和返回值一一打印出来。但是,对一些复杂的业务场景来说就没有那么轻松了。

假如你在开发的是一个下单服务,执行一次下单流程前前后后要调用十多个微服务。你需要在请求发送的前后分别打印 Request 和 Response,不仅麻烦不说,我们还未必能把包括 Header 在内的完整请求信息打印出来。我们如何才能引入一个既简单又不需要硬编码的日志打印功能,让它自动打印所有远程方法的 Request 和 Response,方便我们做异常信息排查呢?接下来介绍一个 OpenFeign 的小功能,轻松实现远程调用参数的日志打印。

4.1 日志信息打印

为了让 OpenFeign 可以主动将请求参数打印到日志中,我们需要做两个代码层面的改动。

首先,你需要在配置文件中指定 FeignClient 接口的日志级别为 Debug。这样做是因为 OpenFeign 组件默认将日志信息以 debug 模式输出,而默认情况下 Spring Boot 的日志级别是 Info,因此我们必须将应用日志的打印级别改为 debug 后才能看到 OpenFeign 的日志。

我们打开 coupon-customer-impl 模块的 application.yml 配置文件,在其中加上以下几行 logging 配置项。

logging:
  level:
    com.geekbang.coupon.customer.feign.TemplateService: debug
    com.geekbang.coupon.customer.feign.CalculationService: debug

在上面的配置项中,我指定了 TemplateService 和 CalculationService 的日志级别为 debug,而其它类的日志级别不变,仍然是默认的 Info 级别。

接下来,你还需要在应用的上下文中使用代码的方式声明 Feign 组件的日志级别。这里的日志级别并不是我们传统意义上的 Log Level,它是 OpenFeign 组件自定义的一种日志级别,用来控制 OpenFeign 组件向日志中写入什么内容。你可以打开 coupon-customer-impl 模块的 Configuration 配置类,在其中添加这样一段代码。

@Bean
Logger.Level feignLogger() {
    return Logger.Level.FULL;
}

在上面这段代码中,我指定了 OpenFeign 的日志级别为 Full,在这个级别下所输出的日志文件将会包含最详细的服务调用信息。OpenFeign 总共有四种不同的日志级别,我来带你了解一下这四种级别下 OpenFeign 向日志中写入的内容。

  • NONE:不记录任何信息,这是 OpenFeign 默认的日志级别

  • BASIC:只记录服务请求的 URL、HTTP Method、响应状态码(如 200、404 等)和服务调用的执行时间

  • HEADERS:在 BASIC 的基础上,还记录了请求和响应中的 HTTP Headers

  • FULL:在 HEADERS 级别的基础上,还记录了服务请求和服务响应中的 Body 和 metadata,FULL 级别记录了最完整的调用信息

我们将 Feign 的日志级别指定为 Full,并启动项目发起一个远程调用,你就可以在日志中看到整个调用请求的信息,包括请求路径、Header 参数、Request Payload 和 Response Body。以下是调用日志的示例,你可以参考一下。

---> POST http://coupon-calculation-serv/calculator/simulate HTTP/1.1
Content-Length: 458
Content-Type: application/json

{"products":[{"productId":null,"price":3000, xxxx省略请求参数
---> END HTTP (458-byte body)
<--- HTTP/1.1 200 (29ms)
connection: keep-alive
content-type: application/json
date: Sat, 27 Nov 2021 15:11:26 GMT
keep-alive: timeout=60
transfer-encoding: chunked

{"bestCouponId":26,"couponToOrderPrice":{"26":15000}}
<--- END HTTP (53-byte body)

有了这些详细的日志信息,你在开发联调阶段排查异常问题就易如反掌了。到这里,我们就详细了解了 OpenFeign 的日志级别设置。接下来将介绍如何在 OpenFeign 中配置超时判定条件。

4.2 OpenFeign 超时判定

超时判定是一种保障可用性的手段。如果你要调用的目标服务的 RT(Response Time)值非常高,那么你的调用请求也会处于一个长时间挂起的状态,这是造成服务雪崩的一个重要因素。为了隔离下游接口调用超时所带来的的影响,我们可以在程序中设置一个超时判定的阈值,一旦下游接口的响应时间超过了这个阈值,那么程序会自动取消此次调用并返回一个异常。

我们以 coupon-customer-serv 为例,customer 服务依赖 template 服务来读取优惠券模板的信息,如果你想要对 template 的远程服务调用添加超时判定配置,那么我们可以在 coupon-customer-impl 模块下的 application.yml 文件中添加下面的配置项。

feign:
  client:
    config:
      # 全局超时配置
      default:
        # 网络连接阶段1秒超时
        connectTimeout: 1000
        # 服务请求响应阶段5秒超时
        readTimeout: 5000
      # 针对某个特定服务的超时配置
      coupon-template-serv:
        connectTimeout: 1000
        readTimeout: 2000

从上面这段代码中可以看出,所有超时配置都放在 feign.client.config 路径之下,我在这个路径下面声明了两个节点:default 和 coupon-template-serv。

default 节点配置了全局层面的超时判定规则,它的生效范围是所有 OpenFeign 发起的远程调用。coupon-template-serv 下面配置的超时规则只针对向 template 服务发起的远程调用。如果你想要对某个特定服务配置单独的超时判定规则,那么可以用同样的方法,在 feign.client.config 下添加目标服务名称和超时判定规则。

这里需要你注意的一点是,如果你同时配置了全局超时规则和针对某个特定服务的超时规则,那么后者的配置会覆盖全局配置,并且优先生效。

在超时判定的规则中我定义了两个属性:connectTimeout 和 readTimeout。其中,connectTimeout 的超时判定作用于“建立网络连接”的阶段;而 readTimeout 的超时判定则作用于“服务请求响应”的阶段(在网络连接建立之后)。我们常说的 RT(即服务响应时间)受后者影响比较大。另外,这两个属性对应的超时时间单位都是毫秒。

配置好超时规则之后,我们可以验证一下。你可以在 template 服务中使用 Thread.sleep 方法强行让线程挂起几秒钟,制造一个超时场景。这时如果你通过 customer 服务调用了 template 服务,那么在日志中可以看到下面的报错信息,提示你服务请求超时。

[TemplateService#getTemplate] <--- ERROR SocketTimeoutException: Read timed out (2077ms)
[TemplateService#getTemplate] java.net.SocketTimeoutException: Read timed out

到这里,相信你已经清楚如何通过 OpenFeign 的配置项来设置超时判定规则了。接下来了解一下 OpenFeign 是如何通过降级来处理服务异常的。

4.3 OpenFeign 降级

降级逻辑是在远程服务调用发生超时或者异常(比如 400、500 Error Code)的时候,自动执行的一段业务逻辑。你可以根据具体的业务需要编写降级逻辑,比如执行一段兜底逻辑将服务请求从失败状态中恢复,或者发送一个失败通知到相关团队提醒它们来线上排查问题。

降级逻辑是在远程服务调用发生超时或者异常(比如 400、500 Error Code)的时候,自动执行的一段业务逻辑。你可以根据具体的业务需要编写降级逻辑,比如执行一段兜底逻辑将服务请求从失败状态中恢复,或者发送一个失败通知到相关团队提醒它们来线上排查问题。

后边会使用 Spring Cloud Alibaba 的组件 Sentinel 讲解如何搭建中心化的服务容错控制逻辑,这是一种重量级的服务容错手段。

借助 OpenFeign 实现 Client 端的服务降级,尽管它的功能远不如 Sentinel 强大,但它相比于 Sentinel 而言更加轻量级且容易实现,足以满足一些简单的服务降级业务需求。

OpenFeign 对服务降级的支持是借助 Hystrix 组件实现的,由于 Hystrix 已经从 Spring Cloud 组件库中被移除,所以我们需要在 coupon-customer-impl 子模块的 pom 文件中手动添加 hystrix 项目的依赖。


<!-- hystrix组件,专门用来演示OpenFeign降级 -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
    <version>2.2.10.RELEASE</version>
    <exclusions>
        <!-- 移除Ribbon负载均衡器,避免冲突 -->
        <exclusion>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-netflix-ribbon</artifactId>
        </exclusion>
    </exclusions>
</dependency>

添加好依赖项之后,我们就可以编写 OpenFeign 的降级类了。OpenFeign 支持两种不同的方式来指定降级逻辑,一种是定义 fallback 类,另一种是定义 fallback 工厂。

通过 fallback 类实现降级是最为简单的一种途径,如果你想要为 TemplateService 这个 FeignClient 接口指定一段降级流程,那么我们可以定义一个降级类并实现 TemplateService 接口,可以参考一下这个 TemplateServiceFallback 类。

@Slf4j
@Component
public class TemplateServiceFallback implements TemplateService {

    @Override
    public CouponTemplateInfo getTemplate(Long id) {
        log.info("fallback getTemplate");
        return null;
    }

    @Override
    public Map<Long, CouponTemplateInfo> getTemplateInBatch(Collection<Long> ids) {
        log.info("fallback getTemplateInBatch");
        return null;
    }
}

在上面的代码中,我们可以看出 TemplateServiceFallback 实现了 TemplateService 中的所有方法。我们以其中的 getTemplate 方法为例,如果在实际的方法调用过程中,OpenFeign 接口的 getTemplate 远程调用发生了异常或者超时的情况,那么 OpenFeign 会主动执行对应的降级方法,也就是 TemplateServiceFallback 类中的 getTemplate 方法。

你可以根据具体的业务场景,编写合适的降级逻辑。

降级类定义好之后,你还需要在 TemplateService 接口中将 TemplateServiceFallback 类指定为降级类,这里你可以借助 FeignClient 接口的 fallback 属性来配置,你可以参考下面的代码。

@FeignClient(value = "coupon-template-serv", path = "/template",
       // 通过fallback指定降级逻辑
       fallback = TemplateServiceFallback.class)
public interface TemplateService {
      // ... 省略方法定义
}

如果你想要在降级方法中获取到异常的具体原因,那么你就要借助 fallback 工厂的方式来指定降级逻辑了。按照 OpenFeign 的规范,自定义的 fallback 工厂需要实现 FallbackFactory 接口,可以参考一下这个 TemplateServiceFallbackFactory 类。

@Slf4j
@Component
public class TemplateServiceFallbackFactory implements FallbackFactory<TemplateService> {

    @Override
    public TemplateService create(Throwable cause) {
        // 使用这种方法你可以捕捉到具体的异常cause
        return new TemplateService() {

            @Override
            public CouponTemplateInfo getTemplate(Long id) {
                log.info("fallback factory method test");
                return null;
            }

            @Override
            public Map<Long, CouponTemplateInfo> getTemplateInBatch(Collection<Long> ids) {
                log.info("fallback factory method test");
                return Maps.newHashMap();
            }
        };
    }
}

从上面的代码中,你可以看出,抽象工厂 create 方法的入参是一个 Throwable 对象。这样一来,我们在降级方法中就可以获取到原始请求的具体报错异常信息了。

当然了,你还需要将这个工厂类添加到 TemplateService 注解中,这个过程和指定 fallback 类的过程有一点不一样,你需要借助 FeignClient 注解的 fallbackFactory 属性来完成。你可以参考下面的代码。

@FeignClient(value = "coupon-template-serv", path = "/template",
        // 通过抽象工厂来定义降级逻辑
        fallbackFactory = TemplateServiceFallbackFactory.class)
public interface TemplateService {
        // ... 省略方法定义
}

到这里,我们就完成了 OpenFeign 进阶功能的学习。

以下是实践层面的建议:

在日志打印方面,OpenFeign 的日志信息是测试开发联调过程中的好帮手,但是在生产环境中你是用不上的,因为几乎所有公司的生产环境都不会使用 Debug 级别的日志,最多是 Info 级别。

在超时判定方面,有时候我们在线上会使用多维度的超时判定,比如 OpenFeign + 网关层超时判定 + Sentinel 等等判定。它们可以互相作为兜底方案,一旦某个环节突然发生故障,另一个可以顶上去。但这就形成了一个木桶理论,也就是几种判定规则中最严格的那个规则会优先生效。

关于服务降级的方案选型,很多开发人员过于追求功能强大的新技术,但我们做技术选型的时候也要考虑开发成本和维护成本

比如像 Sentinel 这类中心化的服务容错控制台,它的功能固然强大,各种花式玩法它都考虑到了。但相对应地,如果你要在项目中引入 Sentinel,在运维层面你要多维护一个 Sentinel 服务集群,并且在代码中接入 Sentinel 也是一个成本项。如果你只需要一些简单的降级功能,那 OpenFeign+Hystrix 的 Client 端降级方案就完全可以满足你的要求,我认为没必要拿大炮打苍蝇,过于追求一步到位的高大上方案。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/174994.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

软件测试复习10:测试文档

专栏&#xff1a;《软件测试》 个性签&#xff1a;顺境不惰&#xff0c;逆境不馁&#xff0c;以心制境&#xff0c;万事可成。——曾国藩 测试大纲&#xff1a;招标用&#xff0c;总体策略&#xff0c;对软件的了解&#xff0c;测试人员&#xff0c;资质等。 测试计划&#…

将Bean创建到Spring容器,从Spring容器拿出Bean

目录一、XML文件中&#xff0c;将Bean创建到Spring容器1. 基本类型注册2. 类装配3. 有参构造方法装配4. 扩展注入5. Bean的作用域6. Bean的其他配置二、配置类中&#xff0c;将Bean创建到Spring容器1. 在mapper、service、controller中创建&#xff0c;等着被componentScan扫描…

C++ | 关于STL中的空间配置器 | 源码剖析

文章目录为什么需要空间配置器一级空间配置器二级空间配置器内存池解析refill 填充内存池chunk_alloc 申请堆空间deallocate 资源的归还空间配置器的再次封装空间配置器与容器的结合我们知道在C和C中都有关于内存管理的问题&#xff0c;C语言用malloc和free这两个函数体现内存管…

ClassLoader-在spring中的应用

背景标题起的挺大&#xff0c;忽悠人的。其实是我跟着视频学习手写模拟spring底层原理中遇到的问题&#xff0c;关于classLoader的几行代码&#xff0c;不知道是什么意思&#xff0c;所以特地来记下笔记。关于ClassLoader我好像在遥远的几年前看深入理解虚拟机时看到过&#xf…

Datawhale 202301 设计模式 | 第二章 人工智能 现代方法 智能体

智能体和环境 理性智能体 (rational agent) 需要为取得最佳结果或在存在不确定性时取得最佳期望结果而采取行动。 任何通过传感器(sensor) 感知 环境(environment) 并通过 执行器(actuator) 作用于该环境 的事物都可以被视为 智能体(agent) 。 行为 理性智能体 (rational ag…

Linux常用命令——systemctl命令

在线Linux命令查询工具(http://www.lzltool.com/LinuxCommand) systemctl 系统服务管理器指令 补充说明 systemctl命令是系统服务管理器指令&#xff0c;它实际上将 service 和 chkconfig 这两个命令组合到一起。 任务旧指令新指令使某服务自动启动chkconfig --level 3 ht…

属性值的计算过程 css样式显示的计算过程 页面的渲染流程

目录属性值的计算过程属性值计算过程简介通过例子来理解&#xff1a;详细解释&#xff1a;方法例子属性值的计算过程 一个元素一个元素依次渲染&#xff0c;顺序按照页面文档的树形目录结构进行 渲染每个元素的前提条件&#xff1a;该元素的所有CSS属性必须有值 一个元素&am…

数学魔法结局:muldiv

介绍了一些棘手的数学魔法&#xff0c;但我一直没有抽出时间说出妙语。目标是计算 同时正确处理溢出。我们的秘密武器是 EVM 的mulmod指令。这条指令完全符合我们的要求&#xff0c;只是它返回的是余数而不是商。那么我们的策略是什么&#xff1f; 计算 512 位乘积一种⋅b使用…

【数据结构】6.5 图的遍历

文章目录遍历定义深度优先搜索(DFS)算法步骤邻接矩阵上的遍历邻接矩阵深度优先算法DFS算法效率分析广度优先搜索(BFS)邻接表的广度优先算法BFS算法效率分析DFS与BFS算法效率比较遍历定义 和树的遍历类似&#xff0c;图的遍历也是从图中的某一个顶点出发&#xff0c;按照某种方法…

UPS BP650CH实现nas自动关机

家里有个自己拼凑的nas需要防止断电不正常关机&#xff0c;因此购买了施耐德后背式BP650CH&#xff0c;之所以选这款是因为带了串口&#xff0c;串口终究还是很方便的东西。不管linux还是window还是其他系统都能够使用&#xff0c;通过串口直接获得ups的信息&#xff0c;就不需…

JDBC Maven MyBatis

文章目录JDBC&#xff08;Java Database Connectivity&#xff09;入门API详解DriverManger&#xff08;驱动管理类&#xff09;Connection(数据库连接对象)作用StatementResultSet&#xff08;结果集对象&#xff09;PreparedStatement连接池MavenMaven模型Maven 常用命令依赖…

简单二叉树的介绍

1.树的结构&#xff08;了解&#xff09;1.1概念树是一种非线性的数据结构&#xff0c;它是由n&#xff08;n>0)个有限节点总成一个具有层次关系的集合。把它叫做树是因为它看起来像一颗倒挂的树&#xff0c;也就是说它的根是朝上&#xff0c;而叶子是朝下的&#xff08;本人…

工作玩手机识别监测系统 YOLOv5

工作玩手机识别监测系统通过YOLOV5网络深度学习算法模型对画面中人员玩手机行为进行实时监测&#xff0c;当识别到有人在玩手机行为时&#xff0c;无需人为干预立即抓拍存档触发告警。YOLO算法- YOLO算法是一种基于回归的算法&#xff0c;它不是选择图像中有趣的部分&#xff0…

WT588D语音芯片介绍

WT588D语音芯片简介WT588D 语音芯片是一款功能强大的可重复擦除烧写的语音单片机芯片。WT588D 让语音芯片不再为控制方式而寻找合适的外围单片机电路&#xff0c;高度集成的单片机技术足于取代复杂的外围控制电路。配套WT588DVoiceChip 上位机操作软件可随意更换WT588D 语音单片…

基于 docker 搭建 mysql5.7 主从复制

安装 docker 的教程可以看我的另一篇文章&#xff0c;拉取 mysql 镜像的步骤也在里面&#xff0c;在这不再重复&#xff1a;https://blog.csdn.net/wanzijy/article/details/128695674 1. 主机搭建 因为本人虚拟机中已经存在了 mysql &#xff0c;所以在使用镜像创建容器的时…

【论文翻译】End-to-End Human Pose and Mesh Reconstruction with Transformers

【cvpr论文】End-to-End Human Pose and Mesh Reconstruction with Transformers (thecvf.com) 【github】microsoft/MeshTransformer: Research code for CVPR 2021 paper "End-to-End Human Pose and Mesh Reconstruction with Transformers" (github.com) 摘要 我…

学习笔记:Java 并发编程③

若文章内容或图片失效&#xff0c;请留言反馈。 部分素材来自网络&#xff0c;若不小心影响到您的利益&#xff0c;请联系博主删除。 视频链接&#xff1a;https://www.bilibili.com/video/av81461839配套资料&#xff1a;https://pan.baidu.com/s/1lSDty6-hzCWTXFYuqThRPw&am…

在甲骨文云容器实例(Container Instances)上部署Ubuntu Desktop

甲骨文云推出了容器实例&#xff0c;这是一项无服务器计算服务&#xff0c;可以即时运行容器&#xff0c;而无需管理任何服务器。 今天我们尝试一下通过容器实例部署Ubuntu Bionic Desktop。 创建容器实例 在甲骨文容器实例页面&#xff0c;单击"创建容器实例"&…

Java 笔试题

Java 笔试题目录概述需求&#xff1a;设计思路实现思路分析1.java 面试题参考资料和推荐阅读Survive by day and develop by night. talk for import biz , show your perfect code,full busy&#xff0c;skip hardness,make a better result,wait for change,challenge Surviv…

分享151个PHP源码,总有一款适合您

PHP源码 分享151个PHP源码&#xff0c;总有一款适合您 下面是文件的名字&#xff0c;我放了一些图片&#xff0c;文章里不是所有的图主要是放不下...&#xff0c; 151个PHP源码下载链接&#xff1a;https://pan.baidu.com/s/1T_Hs4j0t39b-Y8UWHmAKyw?pwd7ao0 提取码&#…