目录

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 依赖

<dependency>
		<groupId>org.springframework.cloud</groupId>
		<artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>

	<dependency>
		<groupId>io.github.openfeign</groupId>
		<artifactId>feign-httpclient</artifactId>
	</dependency>

启动类添加注解@EnableFeignClients
 其中的@EnableFeignClients 注解表示当程序启动时,会进行扫描,扫描
所有带@FeignClient 的注解的类并进行处理。

@SpringBootApplication
@EnableFeignClients
public class OrderServiceApplication {
    public static void main(String[] args) {
        SpringApplication.run(OrderServiceApplication.class, args);
    }
}

编写 FeignClient 所需的接口类

//商品服务客户端
@FeignClient(name = "product-service")
public interface ProductClient {
    @GetMapping("/api/v1/product/find")
    String findById(@RequestParam(value = "id") int id);
}

使用 FeignClient 进行服务的消费

@Service
public class ProductOrderServiceImpl implements ProductOrderService {
    @Autowired
    private ProductClient productClient;

    @Override
    public ProductOrder save(int userId, int productId) {

        String response = productClient.findById(productId);

        JsonNode  jsonNode = JsonUtils.str2JsonNode(response);

        ProductOrder productOrder = new ProductOrder();
        productOrder.setCreateTime(new Date());
        productOrder.setUserId(userId);
        productOrder.setTradeNo(UUID.randomUUID().toString());
        productOrder.setProductName(jsonNode.get("name").toString());
        productOrder.setPrice(Integer.parseInt(jsonNode.get("price").toString()));
        return productOrder;
    }
}

注意事项

  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 请求/响应压缩

feign:
  compression:
    request:  #配置请求GZIP压缩
      mime-types: text/xml,application/xml,application/json #配置压缩支持的MIME TYPE
      enabled: true
      min-request-size: 2048 #配置压缩数据大小的下限
    response:
      enabled: true #配置响应GZIP压缩

替换默认的 HTTP Client

 Feign 默认情况下使用的 JDK 远程的 URLConnection 发送 HTTP 请求,所以没有连接池,但对于每个地址会保持一个长连接(利用 HTTP 的 persistence connection),有效的使用 HTTP 可以使应用访问速度变得更快,更节省带宽。因此可以使用第三方的 HTTP 客户端进行替换,okhttp 是一款出色的 HTTP 客户端,具有以下功能和特性:

  • 支持 SPDY,可以合并多个到同一个主机的请求。
  • 使用连接池技术减少请求的延迟(如果 SPDY 是可以的话)。
  • 使用 GZIP 压缩减少传输的数据量。
  • 缓存响应避免重复的网络请求。

引入 okhttp 的 Maven 依赖

<!--okhttp依赖-->
        <dependency>
            <groupId>io.github.openfeign</groupId>
            <artifactId>feign-okhttp</artifactId>
        </dependency>

开启 okhttp 为 Feign 默认的 Client

feign:
  httpclient:
    enabled: false
  okhttp:
    enable: true

okHttpClient 是 okhttp 的核心功能执行者,可通过如下配置类来构建 OkHttpClient 对象,在构建过程中可按需设置常用的配置项。

package net.test.order_service.config;
import feign.Feign;
import okhttp3.ConnectionPool;
import okhttp3.OkHttpClient;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.cloud.openfeign.FeignAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.concurrent.TimeUnit;

@Configuration
@ConditionalOnClass(Feign.class)
@AutoConfigureBefore(FeignAutoConfiguration.class)
public class FeignOkHttpConfig {
    @Bean
    public OkHttpClient okHttpClient(){
        return new OkHttpClient.Builder()
                //设置连接超时
                .connectTimeout(60, TimeUnit.SECONDS)
                //设置读超时
                .readTimeout(60, TimeUnit.SECONDS)
                //设置写超时
                .writeTimeout(60, TimeUnit.SECONDS)
                //是否自动重连
                .retryOnConnectionFailure(true)
                //连接池
                .connectionPool(new ConnectionPool())
                .build();
    }
}

参数传递

多参数传递
 多个参数的时候,通过(@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 类

// Params.java
public class Params {
    private String param1;
    private String param2;
    // [Getters and setters omitted for brevity]
}

FeignClient 接口类中方法参数使用@SpringQueryMap 注解

@FeignClient("demo")
public class DemoTemplate {
    @GetMapping(path = "/demo")
    String demoEndpoint(@SpringQueryMap Params params);
}

负载均衡策略

Feign 集成了 Ribbon,用于实现微服务的"横向扩展"能力。因此只需修改 Ribbon 服务均衡策略即可,Ribbon 默认的负载均衡策略是轮询。
Robbin 的负载均衡机制来源于@LoadBalanced,而此注解常常作用于容器管理的 RestTemplate 对象,以开启负载均衡机制。

@Bean
    @LoadBalanced #开启负载均衡机制
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }

Robbin 负载均衡的原理(分析@LoadBalanced 源码)
 不同于服务端负载均衡(Nginx、F5),Robbin 属于客户端服务再均衡。

  1. 首先从注册中心获取 provider 的列表。
  2. 通过本地指定的负载均衡策略计算出一个选中的节点。
  3. 然后被选中节点的信息返回给 restTemplate 调用。

自定义负载均衡策略

product-service:  # Provider的名称。
  ribbon:
    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule  #配置规则 随机
      #    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RoundRobinRule #配置规则 轮询
      #    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RetryRule #配置规则 重试
      #    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.WeightedResponseTimeRule #配置规则 响应时间权重
      #    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.BestAvailableRule #配置规则 最空闲连接策略
    ConnectTimeout: 500 #请求建立连接超时
    ReadTimeout: 1000 #请求处理的超时
    OkToRetryOnAllOperations: true #对所有请求都进行重试
    MaxAutoRetriesNextServer: 2 #切换实例的重试次数
    MaxAutoRetries: 1 #对当前实例的重试次数

超时设置

Feign 集成了 Ribbon 和 Hystrix,因此 Feign 的调用分两层,即 Ribbon 的调用和 Hystrix 的调用。

  • hystrix 默认是 1 秒超时,优先以 hystrix 为准,Ribbon 次之。
  • 除非显示定义 Feign 的调用超时时间,此时以显示设置的为准,否则以 Hystrix 为准,Ribbon 次之。

设置 Feign 调用超时时间

feign:
  client:
    config:
      default:
        connectTimeout: 2000   #请求建立连接超时
        readTimeout: 2000   #请求处理的超时

Ribbon 的饥饿加载

 Ribbon 在进行客户端负载均衡的时候并不是在启动就加载上下文,而是实际请求的时候才去创建,因此这个特性往往会导致在第一次调用显得不够迅速,严重的时候甚至会导致调用超时。因此可以通过制定 Ribbon 具体客户端的名称来开启饥饿接在,即在启动的时候便加载所有配置项的应用程序上下文:

ribbon:
    eager-load:
      enabled: true
      clients: product-service

FeignClient 接口类更多写法参考

CoffeeService

package geektime.spring.springbucks.customer.integration;

import geektime.spring.springbucks.customer.model.CoffeeOrder;
import geektime.spring.springbucks.customer.model.NewOrderRequest;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;

@FeignClient(name = "waiter-service", contextId = "coffeeOrder")
public interface CoffeeOrderService {
    @GetMapping("/order/{id}")
    CoffeeOrder getOrder(@PathVariable("id") Long id);

    @PostMapping(path = "/order/", consumes = MediaType.APPLICATION_JSON_VALUE,
            produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
    CoffeeOrder create(@RequestBody NewOrderRequest newOrder);
}

CoffeeOrderService

package geektime.spring.springbucks.customer.integration;

import geektime.spring.springbucks.customer.model.CoffeeOrder;
import geektime.spring.springbucks.customer.model.NewOrderRequest;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;

@FeignClient(name = "waiter-service", contextId = "coffeeOrder")
public interface CoffeeOrderService {
    @GetMapping("/order/{id}")
    CoffeeOrder getOrder(@PathVariable("id") Long id);

    @PostMapping(path = "/order/", consumes = MediaType.APPLICATION_JSON_VALUE,
            produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
    CoffeeOrder create(@RequestBody NewOrderRequest newOrder);
}

CustomerRunner

package geektime.spring.springbucks.customer;

import geektime.spring.springbucks.customer.integration.CoffeeOrderService;
import geektime.spring.springbucks.customer.integration.CoffeeService;
import geektime.spring.springbucks.customer.model.Coffee;
import geektime.spring.springbucks.customer.model.CoffeeOrder;
import geektime.spring.springbucks.customer.model.NewOrderRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;

import java.util.Arrays;
import java.util.List;

@Component
@Slf4j
public class CustomerRunner implements ApplicationRunner {
    @Autowired
    private CoffeeService coffeeService;
    @Autowired
    private CoffeeOrderService coffeeOrderService;

    @Override
    public void run(ApplicationArguments args) throws Exception {
        readMenu();
        Long id = orderCoffee();
        queryOrder(id);
    }

    //代码清爽:避免复杂的RestTemplate的方法调用
    private void readMenu() {
        List<Coffee> coffees = coffeeService.getAll();
        coffees.forEach(c -> log.info("Coffee: {}", c));
    }

    private Long orderCoffee() {
        NewOrderRequest orderRequest = NewOrderRequest.builder()
                .customer("Li Lei")
                .items(Arrays.asList("capuccino"))
                .build();
        CoffeeOrder order = coffeeOrderService.create(orderRequest);
        log.info("Order ID: {}", order.getId());
        return order.getId();
    }

    private void queryOrder(Long id) {
        CoffeeOrder order = coffeeOrderService.getOrder(id);
        log.info("Order: {}", order);
    }
}

CustomerServiceApplication

package geektime.spring.springbucks.customer;

import geektime.spring.springbucks.customer.support.CustomConnectionKeepAliveStrategy;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.Bean;

import java.util.concurrent.TimeUnit;

@SpringBootApplication
@Slf4j
@EnableDiscoveryClient
@EnableFeignClients
public class CustomerServiceApplication {

	public static void main(String[] args) {
		SpringApplication.run(CustomerServiceApplication.class, args);
	}

	//配置httpclient
	@Bean
	public CloseableHttpClient httpClient() {
		return HttpClients.custom()
				.setConnectionTimeToLive(30, TimeUnit.SECONDS)
				.evictIdleConnections(30, TimeUnit.SECONDS)
				.setMaxConnTotal(200)
				.setMaxConnPerRoute(20)
				.disableAutomaticRetries()
				.setKeepAliveStrategy(new CustomConnectionKeepAliveStrategy())
				.build();
	}
}

作者:Soulboy