OpenFeign 简介
官方文档
Feign是一个声明式的HTTP客户端,它的目的就是让远程调用更简单。它是Spring Cloud组件中的一个轻量级RESTful的HTTP服务客户端。使用Feign能让编写Web Service客户端更加简单。它的使用方法是定义一个服务接口然后在上面添加注解。使用Feign调用API就像调用本地方法一样,从避免了调用目标微服务时,需要不断的解析/封装JSON数据的繁琐。
Feign也支持可拔插式的编码器和解码器。Spring Cloud对Feign进行了封装,使其支持了Spring MVC标准注解和HttpMessageConverters
。Feign可以与Eureka和Ribbon组合使用以支持负载均衡。Github地址
Feign集成了Ribbon和RestTemplate。Ribbon+Eureka是面向微服务编程,而Feign是面向接口编程。
Feign 能干什么
Feign旨在使编写Java HTTP客户端变得更容易。
在使用Ribbon + RestTemplate
进行服务调用时(见文章【Spring Cloud】Ribbon),利用RestTemplate
对HTTP请求的封装处理,形成了一套模版化的调用方法。但是在实际开发中,由于对服务依赖的调用可能不止一处,往往一个接口会被多处调用,所以通常都会针对每个微服务自行封装一些客户端类来包装这些依赖服务的调用。
所以,Feign在此基础上做了进一步封装,由他来帮助我们定义和实现依赖服务接口的定义。在Feign的实现下,我们只需创建一个接口并使用注解的方式来配置它(类比Dao接口上面标注@Mapper
注解,现在是一个微服务接口上面标注一个@FeignClient
注解),即可完成对服务提供方的接口绑定,简化了使用Spring Cloud Ribbon时封装服务调用客户端的开发量。
Feign利用动态代理机制对Ribbon和RestTemplate进行了封装,使用接口的方式实现负载均衡和服务调用。
Feign 集成了 Ribbon
利用Ribbon维护了Payment的服务列表信息,并且通过轮询实现了客户端的负载均衡。而与Ribbon不同的是,通过Feign只需要定义服务绑定接口且以声明式的方法,优雅而简单地实现了服务调用。
Feign 和 OpenFeign 两者区别
Feign是Spring Cloud组件中的一个轻量级RESTful的HTTP服务客户端。Feign内置了Ribbon,用来做客户端负载均衡,去调用服务注册中心的服务。Feign的使用方式是:使用Feign的注解定义接口,调用这个接口,就可以调用服务注册中心的服务。
1 2 3 4
| <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-feign</artifactId> </dependency>
|
OpenFeign是Spring Cloud在Feign的基础上支持了SpringMVC的注解,如@RequesMapping
等等。OpenFeign的@FeignClient
可以解析Spring MVC的@RequestMapping
注解下的接口,并通过动态代理的方式产生实现类,代理实现类中做负载均衡并调用其他服务。
1 2 3 4
| <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency>
|
1 2 3 4 5 6 7
| @Component @FeignClient(value = "CLOUD-PAYMENT-SERVICE") public interface PaymentFeignService { @GetMapping(value = "/payment/get/{id}") public CommonResult<Payment> getPaymentById(@PathVariable("id") Long id); }
|
OpenFeign 远程服务调用
接口 + 注解:微服务调用接口 + @FeignClient
- 新建
cloud-consumer-feign-order80
- 导入依赖
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64
| <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <artifactId>cloud2021</artifactId> <groupId>com.zhao.springcloud</groupId> <version>1.0.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion>
<artifactId>cloud-consumer-feign-order80</artifactId>
<dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId> </dependency> <dependency> <groupId>com.zhao.springcloud</groupId> <artifactId>cloud-api-commons</artifactId> <version>${project.version}</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> <optional>true</optional> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies>
</project>
|
- 配置文件
1 2 3 4 5 6 7 8 9
| server: port: 80
spring: cloud: nacos: discovery: server-addr: localhost:8848
|
- 主启动类使用
@EnableFeignClients
开启远程调用
1 2 3 4 5 6 7 8 9 10 11 12 13
| import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.openfeign.EnableFeignClients;
@SpringBootApplication @EnableDiscoveryClient
@EnableFeignClients(basePackage = "com.zhao.mall.member.feign") public class OrderFeignMain80 { public static void main(String[] args) { SpringApplication.run(OrderFeignMain80.class, args); } }
|
- 业务类:业务逻辑接口 +
@FeignClient
配置调用provider服务。新建 PaymentFeignService
接口并新增注解@FeignClient
,该接口将通过动态代理的方式产生实现类,实现类中做负载均衡并调用其他服务。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| import com.zhao.springcloud.entities.CommonResult; import com.zhao.springcloud.entities.Payment; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.stereotype.Component; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable;
@Component @FeignClient(value = "CLOUD-PAYMENT-SERVICE") public interface PaymentFeignService { @GetMapping(value = "/payment/get/{id}") public CommonResult<Payment> getPaymentById(@PathVariable("id") Long id); @PostMapping(value = "/payment/save") public CommonResult<Payment> savePayment(@RequestBody Entity entity); }
|
- 控制层Controller调用该代理对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| import com.zhao.springcloud.entities.CommonResult; import com.zhao.springcloud.entities.Payment; import com.zhao.springcloud.service.PaymentFeignService; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RestController; import javax.annotation.Resource;
@RestController @Slf4j public class OrderFeignController { @Resource private PaymentFeignService paymentFeignService;
@GetMapping(value = "/consumer/payment/get/{id}") public CommonResult<Payment> getPaymentById(@PathVariable("id") Long id) { return paymentFeignService.getPaymentById(id); } @PostMapping(value = "/consumer/payment/save") public CommonResult<Payment> savePayment(@RequestBody Entity entity) { return paymentFeignService.savePayment(entity); } }
|
注意:远程调用的返回结果如果是 Java 实体类对象,则 OpenFeign 会自动将网络间传送的 JSON 数据填充到该实体类对象中,无需额外转换。
但若是返回 Map 类型对象,则该 Map 中保存的 Java 实体类对象无法被自动转换,直接 get()
返回的是 Object
类型。此时需要先将该对象转换成 JSON 字符串,然后再解析成对应类型:
1 2 3 4 5
| String json = JSONObject.toJSONString(r.get("skuInfo"));
SkuInfoVo skuInfo = JSONObject.parseObject(json, new TypeReference<SkuInfoVo>() { });
|
OpenFeign 远程服务调用原理
以下面接口为例说明 OpenFeign 远程服务调用的原理:
1 2 3 4 5
| @FeignClient("mall-coupon") public interface CouponFeignService { @PostMapping("/coupon/spubounds/save") R saveSpuBounds(@RequestBody SpuBoundTo spuBoundTo); }
|
商品服务 mall-proudct
调用该接口远程调用 mall-coupon
服务的 /coupon/spubounds/save
请求时,传入一个 SpuBoundTo
类型的对象,其将转换成 JSON 字符串发送到目标服务的目标方法处,并被同样解析为 SpuBoundTo
类型的对象进行保存。具体过程:
- 接口中的
@RequestBody
注解会将这个对象转为 JSON 字符串(key
对应对象中的每个属性名)
- 在注册中心找到
mall-coupon
服务,向 /coupon/spubounds/save
发送请求,并将上一步转换的 JSON 字符串放到请求体中
mall-coupon
服务在收到请求后,将解析请求体中的 JSON 字符串,然后再根据对方接口方法中 @RequestBody
修饰的类的属性名,与 JSON 的 key
进行映射(哪怕对方修饰的类与之前的不同,只要每个属性名相同就能映射),即可得到商品服务远程传来的参数
- 该方法执行完毕后,同样将返回值以 JSON 形式返回给商品服务,对方仍然使用上述方式解析返回值
只要 JSON 数据的接收类的属性名是相等的,双方服务无需使用同一个 To 类
OpenFeign 超时控制
OpenFeign默认等待1秒钟,超过后报错。YML文件里需要开启OpenFeign客户端超时控制:
1 2 3 4 5 6
| ribbon: ReadTimeout: 5000 ConnectTimeout: 5000
|
OpenFeign 日志增强
日志打印功能
Feign提供了日志打印功能,我们可以通过配置来调整日志级别,从而了解Feign中Http请求的细节。即对Feign接口的调用情况进行监控和输出
日志级别
NONE
:默认的,不显示任何日志;
BASIC
:仅记录请求方法、URL、响应状态码及执行时间;
HEADERS
:除了BASIC
中定义的信息之外,还有请求和响应的头信息;
FULL
:除了HEADERS
中定义的信息之外,还有请求和响应的正文及元数据。
配置日志 Bean
1 2 3 4 5 6 7 8 9 10 11
| import feign.Logger; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration;
@Configuration public class FeignConfig { @Bean Logger.Level feignLoggerLevel() { return Logger.Level.FULL; } }
|
YML文件里需要开启日志的Feign客户端
1 2 3 4
| logging: level: com.zhao.springcloud.service.PaymentFeignService: debug
|