赞
踩
本次使用 OpenFeign 的业务场景为:用户下单前领取产品优惠券,使用优惠券再下单。
架构设计:用户服务(CustomerService)和优惠券管理服务(CouponTemplateService)分别是平台的独立子服务。
使用OpenFeign在用户服务里读取优惠券信息的步骤为:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
PS. 在上面的代码中,不需要指定组件的版本号,因为在顶层项目中定义的 spring-cloud-dependencies 依赖项中已经定义了各个 Spring Cloud 的版本号,它们会随着 Maven 项目的继承关系传递到子模块中。
OpenFeign 组件通过接口代理的方式发起远程调用,那么使用过程的第一步就是要定义一个 OpenFeign 接口。
在 用户服务(CustomerService)项目下创建了一个 package,它的路径是 com.domo.customer.feign。在这个路径下定义一个叫做 TemplateService 的 Interface,用来实现对 coupon-template-service的远程调用代理。
@FeignClient(value = "coupon-template-service", path = "/template")
public interface TemplateService {
// 读取优惠券
@GetMapping("/getTemplate")
CouponTemplateInfo getTemplate(@RequestParam("id") Long id);
}
在上面的代码中,在接口上声明了一个 FeignClient 注解,它专门用来标记被 OpenFeign 托管的接口。
在 FeignClient 注解中声明的 value 属性是目标服务的名称,需要确保这里的服务名称和 Nacos 上显示的服务注册名称是一样的。
此外,FeignClient 注解中的 path 属性是一个可选项,如果要调用的目标服务有一个统一的前置访问路径,那么可以通过 path 属性来声明这个前置路径,这样一来,就不用在每一个方法名上的注解中带上前置 Path 了。
在项目的启动阶段,OpenFeign 会查找所有被 FeignClient 注解修饰的接口,并代理该接口的所有方法调用。当业务方法中调用接口方法的时候,OpenFeign 就会根据方法上定义的注解自动拼装 HTTP 请求路径和参数,并向目标服务发起真实调用。
因此,还需要在方法上定义 spring-web 注解(如 GetMapping、PostMapping),让 OpenFeign 拼装出正确的 Request URL 和请求参数。这里要注意,OpenFeign 接口中定义的路径和参数必须与你要调用的目标服务中的保持一致。
@Service
public class CustomerServiceImpl implements CustomerService {
@Autowired
private TemplateService templateService;
@Override
public CouponTemplateInfo getTemplate(Long templateId){
CouponTemplateInfo templateInfo = templateService.getTemplate(templateId);
return templateInfo;
}
}
// 省略其他无关注解
@EnableFeignClients(basePackages = {"com.demo"})
public class CustomerServiceApplication {
public static void main(String[] args) {
SpringApplication.run(CustomerServiceApplication .class, args);
}
}
在这段代码中,在 EnableFeignClients 注解的 basePackages 属性中定义了一个 com.demo的包名,这个注解就会告诉 OpenFeign 在启动项目的时候,找到所有位于 com.demo包路径(包括子 package)之下使用 FeignClient 修饰的接口,然后生成相关的代理类并添加到 Spring 的上下文中。这样一来,才能够在项目中用 Autowired 注解注入 OpenFeign 接口。
扩展:
上面就是使用包路径扫描的方式来加载 FeignClient 接口。除此之外,还可以通过直接加载指定 FeignClient 接口类的方式,或者从指定类所在的目录进行扫包的方式来加载 FeignClient 接口。
// 通过指定Client类来加载
@EnableFeignClients(clients = {TemplateService.class})
// 扫描特定类所在的包路径下的FeignClient
@EnableFeignClients(basePackageClasses = {TemplateService.class})
这三种加载方式,项目中推荐使用的是包路径加载的方式。因为不管以后添加了多少新的 FeignClient 接口,只要这些接口位于指定包路径之下,就不用操心加载路径的配置。
服务请求的入参和出参是分析和排查问题的重要线索。为了获得服务请求的参数和返回值,经常使用的一个做法就是打印日志。
为了让 OpenFeign 可以主动将请求参数打印到日志中,需要做两个代码层面的改动。
首先,需要在配置文件中指定 FeignClient 接口的日志级别为 Debug。这样做是因为 OpenFeign 组件默认将日志信息以 debug 模式输出,而默认情况下 Spring Boot 的日志级别是 Info,因此我们必须将应用日志的打印级别改为 debug 后才能看到 OpenFeign 的日志。
打开 customer 模块的 application.yml 配置文件,在其中加上以下几行 logging 配置项
logging:
level:
com.demo.customer.feign.TemplateService: debug
在上面的配置项中,指定了 TemplateService 的日志级别为 debug,而其它类的日志级别不变,仍然是默认的 Info 级别。
接下来,还需要在应用的上下文中使用代码的方式声明 Feign 组件的日志级别。这里的日志级别并不是我们传统意义上的 Log Level,它是 OpenFeign 组件自定义的一种日志级别,用来控制 OpenFeign 组件向日志中写入什么内容。可以打开customer 模块的 Configuration 配置类,在其中添加这样一段代码。
@Bean
Logger.Level feignLogger() {
return Logger.Level.FULL;
}
在上面这段代码中,指定了 OpenFeign 的日志级别为 Full,在这个级别下所输出的日志文件将会包含最详细的服务调用信息。OpenFeign 总共有四种不同的日志级别,下面是这四种级别下 OpenFeign 向日志中写入的内容。
将 Feign 的日志级别指定为 Full,并启动项目发起一个远程调用,就可以在日志中看到整个调用请求的信息,包括请求路径、Header 参数、Request Payload 和 Response Body。调用日志信息如下:
---> POST http://coupon-template-serv/template/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)
有了这些详细的日志信息,在开发联调阶段排查异常问题就易如反掌了。
超时判定是一种保障可用性的手段。如果要调用的目标服务的 RT(Response Time)值非常高,那么调用请求也会处于一个长时间挂起的状态,这是造成服务雪崩的一个重要因素。为了隔离下游接口调用超时所带来的的影响,可以在程序中设置一个超时判定的阈值,一旦下游接口的响应时间超过了这个阈值,那么程序会自动取消此次调用并返回一个异常。
例如上述场景,customer 服务依赖 template 服务来读取优惠券模板的信息,如果想要对 template 的远程服务调用添加超时判定配置,那么我们可以在 customer 模块下的 application.yml 文件中添加下面的配置项。
feign:
client:
config:
# 全局超时配置
default:
# 网络连接阶段1秒超时
connectTimeout: 1000
# 服务请求响应阶段5秒超时
readTimeout: 5000
# 针对某个特定服务的超时配置
coupon-template-service:
connectTimeout: 1000
readTimeout: 2000
从上面这段代码中可以看出,所有超时配置都放在 feign.client.config 路径之下,在这个路径下面声明了两个节点:default 和 coupon-template-service。
default 节点配置了全局层面的超时判定规则,它的生效范围是所有 OpenFeign 发起的远程调用。
coupon-template-service下面配置的超时规则只针对向 template 服务发起的远程调用。如果想要对某个特定服务配置单独的超时判定规则,那么可以用同样的方法,在 feign.client.config 下添加目标服务名称和超时判定规则。
如果同时配置了全局超时规则和针对某个特定服务的超时规则,那么后者的配置会覆盖全局配置,并且优先生效。
在超时判定的规则中定义了两个属性:connectTimeout 和 readTimeout。
其中,connectTimeout 的超时判定作用于“建立网络连接”的阶段;而 readTimeout 的超时判定则作用于“服务请求响应”的阶段(在网络连接建立之后)。我们常说的 RT(即服务响应时间)受后者影响比较大。另外,这两个属性对应的超时时间单位都是毫秒。
降级逻辑是在远程服务调用发生超时或者异常(比如 400、500 Error Code)的时候,自动执行的一段业务逻辑。可以根据具体的业务需要编写降级逻辑,比如执行一段兜底逻辑将服务请求从失败状态中恢复,或者发送一个失败通知到相关团队提醒它们来线上排查问题。
这里借助 OpenFeign 实现 Client 端的服务降级。尽管它的功能远不如 Sentinel 强大,但它相比于 Sentinel 而言更加轻量级且容易实现,足以满足一些简单的服务降级业务需求。
OpenFeign 对服务降级的支持是借助 Hystrix 组件实现的,由于 Hystrix 已经从 Spring Cloud 组件库中被移除,所以我们需要在 customer模块的 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 工厂。
这是最为简单的一种途径,如果想要为 TemplateService 这个 FeignClient 接口指定一段降级流程,那么可以定义一个降级类并实现 TemplateService 接口。如下TemplateServiceFallback 类
@Slf4j
@Component
public class TemplateServiceFallback implements TemplateService {
@Override
public CouponTemplateInfo getTemplate(Long id) {
log.info("fallback getTemplate");
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) { TemplateServiceFallback templateServiceFallback = new TemplateServiceFallback (); templateServiceFallback.setCause(throwable); return templateServiceFallback; } } @Slf4j @Component public class TemplateServiceFallback implements TemplateService { @Setter private Throwable cause; @Override public CouponTemplateInfo getTemplate(Long id) { log.info("fallback getTemplate{}", cause); return null; } }
从上面的代码中可以看出,抽象工厂 create 方法的入参是一个 Throwable 对象。这样一来,在降级方法中就可以获取到原始请求的具体报错异常信息了。
最后还需要将这个工厂类添加到 TemplateService 注解中,这个过程和指定 fallback 类的过程有一点不一样,需要借助 FeignClient 注解的 fallbackFactory 属性来完成。
@FeignClient(value = "coupon-template-serv", path = "/template",
// 通过抽象工厂来定义降级逻辑
fallbackFactory = TemplateServiceFallbackFactory.class)
public interface TemplateService {
// ... 省略方法定义
}
实践注意点:
在日志打印方面,OpenFeign 的日志信息是测试开发联调过程中的好帮手,但是在生产环境中是用不上的,因为几乎所有公司的生产环境都不会使用 Debug 级别的日志,最多是 Info 级别。
在超时判定方面,有时候在线上会使用多维度的超时判定,比如 OpenFeign + 网关层超时判定 + Sentinel 等等判定。它们可以互相作为兜底方案,一旦某个环节突然发生故障,另一个可以顶上去。但这就形成了一个木桶理论,也就是几种判定规则中最严格的那个规则会优先生效。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。