Feign
简介
v.假装,人如其名,是一个伪 RPC 客户端,是一种声明式、模板化的 HTTP 客户端。在 Spring Cloud 中使用 Feign,可以做到使用 HTTP 请求访问远程服务,就像调用本地方法一样,开发者完全感知不到这是在调用远程方法,更感知不到在访问 HTTP 请求,Feign 的具体特性如下:
- 可插拔的注解支持,包括 Feign 注解和 JAX-RS 注解。
- 支持可插拔的 HTTP 编码器和解码器
- 支持 Hystrix 和 Hystrix 的 Fallback 机制。
- 支持 Ribbon 的负载均衡。
- 支持 HTTP 请求和响应的压缩。
Feign 是一个声明式的 Web Service 客户端,它的目的就是让 Web Service 调用更加简单。它整合了 Ribbon 和 Hystrix,避免了开发者需要针对 Feign 进行二次的整合。Feign 提供了 HTTP 请求模版,通过编写简单的接口和注解,就可以定义好 HTTP 请求的参数、格式、地址等信息,Feign 会完全代理 HTTP 的请求,在使用过程开发者只需要依赖注入 Bean,然后调用对应的方法并传递参数即可。
传送门
入门案例
添加 Maven 依赖
1<dependency>
2 <groupId>org.springframework.cloud</groupId>
3 <artifactId>spring-cloud-starter-openfeign</artifactId>
4 </dependency>
5
6 <dependency>
7 <groupId>io.github.openfeign</groupId>
8 <artifactId>feign-httpclient</artifactId>
9 </dependency>
启动类添加注解 @EnableFeignClients
其中的 @EnableFeignClients 注解表示当程序启动时,会进行扫描,扫描
所有带 @FeignClient 的注解的类并进行处理。
1@SpringBootApplication
2@EnableFeignClients
3public class OrderServiceApplication {
4 public static void main(String[] args) {
5 SpringApplication.run(OrderServiceApplication.class, args);
6 }
7}
编写 FeignClient 所需的接口类
1//商品服务客户端
2@FeignClient(name = "product-service")
3public interface ProductClient {
4 @GetMapping("/api/v1/product/find")
5 String findById(@RequestParam(value = "id") int id);
6}
使用 FeignClient 进行服务的消费
1@Service
2public class ProductOrderServiceImpl implements ProductOrderService {
3 @Autowired
4 private ProductClient productClient;
5
6 @Override
7 public ProductOrder save(int userId, int productId) {
8
9 String response = productClient.findById(productId);
10
11 JsonNode jsonNode = JsonUtils.str2JsonNode(response);
12
13 ProductOrder productOrder = new ProductOrder();
14 productOrder.setCreateTime(new Date());
15 productOrder.setUserId(userId);
16 productOrder.setTradeNo(UUID.randomUUID().toString());
17 productOrder.setProductName(jsonNode.get("name").toString());
18 productOrder.setPrice(Integer.parseInt(jsonNode.get("price").toString()));
19 return productOrder;
20 }
21}
注意事项
- FeignClient 接口类的注解路径必须与提供者的方法一致 @GetMapping("/api/v1/product/find")
- 应用名称必须与提供者一致:@FeignClient(name = "product-service")
- 调用方使用 @RequestBody 时(参数是 POJO),应该使用 @PostMapping("/api/v1/product/find"),而不是 @GetMapping("/api/v1/product/find")
。 - 多个参数的时候,通过(@RequestParam("id") int id)方式调用, FeignClient 方法上参数名必须和提供者方法上的参数名一致,这里指的是 int id.
Feign 的工作原理
- 启动类添加注解 @EnableFeignClients 注解开启对带有 @FeignClient 注解的接口进行扫描和处理。
- 当程序启动时,会将所有 @FeignClient 接口类注入到 SpringICO 容器中。当接口中的方法被调用时,通过 JDK 代理的方式,来生成具体的 RequestTemplate。在生成代理时,Feign 会为每个接口方法创建一个 RequestTemplate 对象,该对象封装了 HTTP 请求需要的全部信息,如请求参数、请求方式等信息都是在这个过程中确定的。
- 然后由 RequestTemplate 生成 Request,然后把 Request 交给 Client 去处理,这里指的 Client 可以是 JDK 原生的 URLConnection、或是 Http Client,也可以是 Okhttp。
- 最后 Client 被封装到了 LoadBalanceClient 类,这个类结合 Ribbon 负载均衡发起服务之间的调用。
Feign VS Robbin
Feign 集成了 Robbin,直接使用 Robbin 进行开发的痛点如下:
- 需要手动维护 RequestTemplate 对象
- 在 Service 层封装 HTTP 请求的信息需要以硬编码的方式,不利于阅读和维护。
Feign 的优势如下:
- 默认集成了 ribbon,底层可以使用 ribbon 的负载均衡机制。
- 面向接口编程,思路清晰、调用方便。
- 采用注解方式进行配置,配置熔断等方式方便。
Feign 请求/响应压缩
1feign:
2 compression:
3 request: #配置请求GZIP压缩
4 mime-types: text/xml,application/xml,application/json #配置压缩支持的MIME TYPE
5 enabled: true
6 min-request-size: 2048 #配置压缩数据大小的下限
7 response:
8 enabled: true #配置响应GZIP压缩
替换默认的 HTTP Client
Feign 默认情况下使用的 JDK 远程的 URLConnection 发送 HTTP 请求,所以没有连接池,但对于每个地址会保持一个长连接(利用 HTTP 的 persistence connection),有效的使用 HTTP 可以使应用访问速度变得更快,更节省带宽。因此可以使用第三方的 HTTP 客户端进行替换,okhttp 是一款出色的 HTTP 客户端,具有以下功能和特性:
- 支持 SPDY,可以合并多个到同一个主机的请求。
- 使用连接池技术减少请求的延迟(如果 SPDY 是可以的话)。
- 使用 GZIP 压缩减少传输的数据量。
- 缓存响应避免重复的网络请求。
引入 okhttp 的 Maven 依赖
1<!--okhttp依赖-->
2 <dependency>
3 <groupId>io.github.openfeign</groupId>
4 <artifactId>feign-okhttp</artifactId>
5 </dependency>
开启 okhttp 为 Feign 默认的 Client
1feign:
2 httpclient:
3 enabled: false
4 okhttp:
5 enable: true
okHttpClient 是 okhttp 的核心功能执行者,可通过如下配置类来构建 OkHttpClient 对象,在构建过程中可按需设置常用的配置项。
1package net.test.order_service.config;
2import feign.Feign;
3import okhttp3.ConnectionPool;
4import okhttp3.OkHttpClient;
5import org.springframework.boot.autoconfigure.AutoConfigureBefore;
6import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
7import org.springframework.cloud.openfeign.FeignAutoConfiguration;
8import org.springframework.context.annotation.Bean;
9import org.springframework.context.annotation.Configuration;
10
11import java.util.concurrent.TimeUnit;
12
13@Configuration
14@ConditionalOnClass(Feign.class)
15@AutoConfigureBefore(FeignAutoConfiguration.class)
16public class FeignOkHttpConfig {
17 @Bean
18 public OkHttpClient okHttpClient(){
19 return new OkHttpClient.Builder()
20 //设置连接超时
21 .connectTimeout(60, TimeUnit.SECONDS)
22 //设置读超时
23 .readTimeout(60, TimeUnit.SECONDS)
24 //设置写超时
25 .writeTimeout(60, TimeUnit.SECONDS)
26 //是否自动重连
27 .retryOnConnectionFailure(true)
28 //连接池
29 .connectionPool(new ConnectionPool())
30 .build();
31 }
32}
参数传递
多参数传递
多个参数的时候,通过(@RequestParam(“id”) int id)方式调用, FeignClient 方法上参数名必须和提供者方法上的参数名一一对应:这里指的是 int id.
参数类型 POJO
- 默认情况下 GET 方法不支持传递 POJO,因此调用方使参数中使用 @RequestBody 时(参数是 POJO),应该使用 @PostMapping(“/api/v1/product/find”),而不是 @GetMapping(“/api/v1/product/find”) 。
- GET 方法传参 POJO,需要使用 @SpringQueryMap
POJO 类
1// Params.java
2public class Params {
3 private String param1;
4 private String param2;
5 // [Getters and setters omitted for brevity]
6}
FeignClient 接口类中方法参数使用 @SpringQueryMap 注解
1@FeignClient("demo")
2public class DemoTemplate {
3 @GetMapping(path = "/demo")
4 String demoEndpoint(@SpringQueryMap Params params);
5}
负载均衡策略
Feign 集成了 Ribbon,用于实现微服务的"横向扩展"能力。因此只需修改 Ribbon 服务均衡策略即可,Ribbon 默认的负载均衡策略是轮询。
Robbin 的负载均衡机制来源于 @LoadBalanced,而此注解常常作用于容器管理的 RestTemplate 对象,以开启负载均衡机制。
1@Bean
2 @LoadBalanced #开启负载均衡机制
3 public RestTemplate restTemplate() {
4 return new RestTemplate();
5 }
Robbin 负载均衡的原理(分析 @LoadBalanced 源码)
不同于服务端负载均衡(Nginx、F5),Robbin 属于客户端服务再均衡。
- 首先从注册中心获取 provider 的列表。
- 通过本地指定的负载均衡策略计算出一个选中的节点。
- 然后被选中节点的信息返回给 restTemplate 调用。
自定义负载均衡策略
1product-service: # Provider的名称。
2 ribbon:
3 NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule #配置规则 随机
4 # NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RoundRobinRule #配置规则 轮询
5 # NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RetryRule #配置规则 重试
6 # NFLoadBalancerRuleClassName: com.netflix.loadbalancer.WeightedResponseTimeRule #配置规则 响应时间权重
7 # NFLoadBalancerRuleClassName: com.netflix.loadbalancer.BestAvailableRule #配置规则 最空闲连接策略
8 ConnectTimeout: 500 #请求建立连接超时
9 ReadTimeout: 1000 #请求处理的超时
10 OkToRetryOnAllOperations: true #对所有请求都进行重试
11 MaxAutoRetriesNextServer: 2 #切换实例的重试次数
12 MaxAutoRetries: 1 #对当前实例的重试次数
超时设置
Feign 集成了 Ribbon 和 Hystrix,因此 Feign 的调用分两层,即 Ribbon 的调用和 Hystrix 的调用。
- hystrix 默认是 1 秒超时,优先以 hystrix 为准,Ribbon 次之。
- 除非显示定义 Feign 的调用超时时间,此时以显示设置的为准,否则以 Hystrix 为准,Ribbon 次之。
设置 Feign 调用超时时间
1feign:
2 client:
3 config:
4 default:
5 connectTimeout: 2000 #请求建立连接超时
6 readTimeout: 2000 #请求处理的超时
Ribbon 的饥饿加载
Ribbon 在进行客户端负载均衡的时候并不是在启动就加载上下文,而是实际请求的时候才去创建,因此这个特性往往会导致在第一次调用显得不够迅速,严重的时候甚至会导致调用超时。因此可以通过制定 Ribbon 具体客户端的名称来开启饥饿接在,即在启动的时候便加载所有配置项的应用程序上下文:
1ribbon:
2 eager-load:
3 enabled: true
4 clients: product-service
FeignClient 接口类更多写法参考
CoffeeService
1package geektime.spring.springbucks.customer.integration;
2
3import geektime.spring.springbucks.customer.model.CoffeeOrder;
4import geektime.spring.springbucks.customer.model.NewOrderRequest;
5import org.springframework.cloud.openfeign.FeignClient;
6import org.springframework.http.MediaType;
7import org.springframework.web.bind.annotation.GetMapping;
8import org.springframework.web.bind.annotation.PathVariable;
9import org.springframework.web.bind.annotation.PostMapping;
10import org.springframework.web.bind.annotation.RequestBody;
11
12@FeignClient(name = "waiter-service", contextId = "coffeeOrder")
13public interface CoffeeOrderService {
14 @GetMapping("/order/{id}")
15 CoffeeOrder getOrder(@PathVariable("id") Long id);
16
17 @PostMapping(path = "/order/", consumes = MediaType.APPLICATION_JSON_VALUE,
18 produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
19 CoffeeOrder create(@RequestBody NewOrderRequest newOrder);
20}
CoffeeOrderService
1package geektime.spring.springbucks.customer.integration;
2
3import geektime.spring.springbucks.customer.model.CoffeeOrder;
4import geektime.spring.springbucks.customer.model.NewOrderRequest;
5import org.springframework.cloud.openfeign.FeignClient;
6import org.springframework.http.MediaType;
7import org.springframework.web.bind.annotation.GetMapping;
8import org.springframework.web.bind.annotation.PathVariable;
9import org.springframework.web.bind.annotation.PostMapping;
10import org.springframework.web.bind.annotation.RequestBody;
11
12@FeignClient(name = "waiter-service", contextId = "coffeeOrder")
13public interface CoffeeOrderService {
14 @GetMapping("/order/{id}")
15 CoffeeOrder getOrder(@PathVariable("id") Long id);
16
17 @PostMapping(path = "/order/", consumes = MediaType.APPLICATION_JSON_VALUE,
18 produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
19 CoffeeOrder create(@RequestBody NewOrderRequest newOrder);
20}
CustomerRunner
1package geektime.spring.springbucks.customer;
2
3import geektime.spring.springbucks.customer.integration.CoffeeOrderService;
4import geektime.spring.springbucks.customer.integration.CoffeeService;
5import geektime.spring.springbucks.customer.model.Coffee;
6import geektime.spring.springbucks.customer.model.CoffeeOrder;
7import geektime.spring.springbucks.customer.model.NewOrderRequest;
8import lombok.extern.slf4j.Slf4j;
9import org.springframework.beans.factory.annotation.Autowired;
10import org.springframework.boot.ApplicationArguments;
11import org.springframework.boot.ApplicationRunner;
12import org.springframework.stereotype.Component;
13
14import java.util.Arrays;
15import java.util.List;
16
17@Component
18@Slf4j
19public class CustomerRunner implements ApplicationRunner {
20 @Autowired
21 private CoffeeService coffeeService;
22 @Autowired
23 private CoffeeOrderService coffeeOrderService;
24
25 @Override
26 public void run(ApplicationArguments args) throws Exception {
27 readMenu();
28 Long id = orderCoffee();
29 queryOrder(id);
30 }
31
32 //代码清爽:避免复杂的RestTemplate的方法调用
33 private void readMenu() {
34 List<Coffee> coffees = coffeeService.getAll();
35 coffees.forEach(c -> log.info("Coffee: {}", c));
36 }
37
38 private Long orderCoffee() {
39 NewOrderRequest orderRequest = NewOrderRequest.builder()
40 .customer("Li Lei")
41 .items(Arrays.asList("capuccino"))
42 .build();
43 CoffeeOrder order = coffeeOrderService.create(orderRequest);
44 log.info("Order ID: {}", order.getId());
45 return order.getId();
46 }
47
48 private void queryOrder(Long id) {
49 CoffeeOrder order = coffeeOrderService.getOrder(id);
50 log.info("Order: {}", order);
51 }
52}
CustomerServiceApplication
1package geektime.spring.springbucks.customer;
2
3import geektime.spring.springbucks.customer.support.CustomConnectionKeepAliveStrategy;
4import lombok.extern.slf4j.Slf4j;
5import org.apache.http.impl.client.CloseableHttpClient;
6import org.apache.http.impl.client.HttpClients;
7import org.springframework.boot.SpringApplication;
8import org.springframework.boot.autoconfigure.SpringBootApplication;
9import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
10import org.springframework.cloud.openfeign.EnableFeignClients;
11import org.springframework.context.annotation.Bean;
12
13import java.util.concurrent.TimeUnit;
14
15@SpringBootApplication
16@Slf4j
17@EnableDiscoveryClient
18@EnableFeignClients
19public class CustomerServiceApplication {
20
21 public static void main(String[] args) {
22 SpringApplication.run(CustomerServiceApplication.class, args);
23 }
24
25 //配置httpclient
26 @Bean
27 public CloseableHttpClient httpClient() {
28 return HttpClients.custom()
29 .setConnectionTimeToLive(30, TimeUnit.SECONDS)
30 .evictIdleConnections(30, TimeUnit.SECONDS)
31 .setMaxConnTotal(200)
32 .setMaxConnPerRoute(20)
33 .disableAutomaticRetries()
34 .setKeepAliveStrategy(new CustomConnectionKeepAliveStrategy())
35 .build();
36 }
37}