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;
}
}
注意事项
- 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 请求/响应压缩
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 属于客户端服务再均衡。
- 首先从注册中心获取 provider 的列表。
- 通过本地指定的负载均衡策略计算出一个选中的节点。
- 然后被选中节点的信息返回给 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();
}
}