目录

Life in Flow

知不知,尚矣;不知知,病矣。
不知不知,殆矣。

X

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}

注意事项

  1. FeignClient 接口类的注解路径必须与提供者的方法一致 @GetMapping("/api/v1/product/find")
  2. 应用名称必须与提供者一致:@FeignClient(name = "product-service")
  3. 调用方使用 @RequestBody 时(参数是 POJO),应该使用 @PostMapping("/api/v1/product/find"),而不是 @GetMapping("/api/v1/product/find")
  4. 多个参数的时候,通过(@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 属于客户端服务再均衡。

  1. 首先从注册中心获取 provider 的列表。
  2. 通过本地指定的负载均衡策略计算出一个选中的节点。
  3. 然后被选中节点的信息返回给 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}

作者:Soulboy