[版权申明] 非商业目的注明出处可自由转载
出自:shusheng007
概述
本文是微服务系列总结的第6篇,一起来看看微服务之间通信时用到的OpenFeign组件。
OpenFeign简介
我们在SpringCloud中使用的一般是 spring-cloud-openfeign,它是SpringCloud团队基于feign封装的一个变体,支持了SpringMvc里的各种注解,例如@RequestBody
之类的。
Feign是一个类似于Retrofit (OkHttp的一个封装)的一个声明式的Http客户端包装器。
是不是还不好理解,一会看到例子就懂了,此时就将其理解为和RestTemplate
类似的东西好了,只不过它是声明式的。你不会又要问啥是声明式吧?假设你妈让你去打二斤酱油,至于你是去超市还是小卖部,腿儿着去还是骑共享单车去,现金付款还是扫码付款,你妈一概不关心,她只要酱油这个结果,这就是声明式。
基本使用
学习一个框架或者三方技术,千万不要一上来就一头扎入其内部一顿捣鼓,把自己弄得灰头土脸的,完了还似懂非懂。第一步就是要熟练的使用,优秀的框架和三方技术组件的API都封装的非常好,处处展示了其设计思想,所以熟练使用后再去探究其是怎么实现的就会事半功倍
我们知道,OpenFeign是一个用来发起http请求的库,所以我们需要两个服务,一个提供API(provider),一个消费API(consumer)。
新建provider与consumer两个服务
provider服务
新建一个对外提供API的SpringBoot的web程序order-service。
@RequiredArgsConstructor
@RestController
@RequestMapping("/order")
public class OrderController {
private final OrderService orderService;
@PostMapping(value = "/payment")
public BaseResponse<OrderDetail> payment(@RequestBody PaymentReq paymentReq){
return ResultUtil.ok(orderService.paymentOrder(paymentReq.getOrderId())) ;
}
}
consumer服务
新建一个消费order-service 的API的服务:goods-service,我们要在此服务中使用OpenFeign调用order-service服务提供的API
引入依赖
在goods-service服务中引入OpenFeign的依赖,注意 spring-cloud的版本要和你当前使用的springboot的版本匹配上。
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--依赖管理-->
<dependencyManagement>
<dependencies>
<!--SpringCloud依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependencyManagement>
声明OpenFeign接口
声明一个interface,使用@FeignClient
标记,如果使用了服务发现与注册中心,那么其value
写要调用的服务名称即可。
注意,OpenFeign服务名称不支持下划线_,这是一个坑
@FeignClient(value = "order-service")
public interface OrderServiceFeign {
@PostMapping(value = "/order/payment")
public BaseResponse<OrderDetail> payment(@RequestBody PaymentReq paymentReq);
}
在这个interface里面声明要调用的服务order-service 的API,其签名要求与order-service 里的API完全一致。注意那个返回值虽然这里写的都是同一个类型BaseResponse,但与order-service 里的那个BaseResponse不是同一个,他们是以json交互的,只要对的上即可。
如果没有使用服务发现与注册中心,虽然理论上说这在微服务架构中是不可能发生的,但有时我们就是没有,你说怎么办吧?这种情况我们就需要使用服务地址的形式来调用了了使用如下配置即可,不过那个value是强制需要的,还是建议设置为目标服务的名称。
@FeignClient(value = "order-service" ,url = "https://shusheng007.top/order-service")
开启OpenFeign
当写完上面的接口,我们还的使用一个@EnableFeignClients
注解将其打开
@SpringBootApplication
...
@EnableFeignClients
public class GoodsServiceApplication {
public static void main(String[] args) {
SpringApplication.run(GoodsServiceApplication.class, args);
}
}
经过以上三步后,奇迹发生了,我们可以像调用本地方法一样调用远程方法了,如下所示。
@Slf4j
@RequiredArgsConstructor
@Service
public class PaymentServiceImpl implements PaymentService {
private final OrderServiceFeign orderServiceFeign;
@Override
public String payment(String orderId) {
OrderDetail result = orderServiceFeign.payment(PaymentReq.builder()
.orderId(orderId)
.build())
.getData();
return String.format("你已经成功购买:%s",result.getGoodsName());
}
}
原理
写个接口就把远程方法调用了?我们都没写实现类哎!想想Mybatis,是不是也就是写了个mapper接口然后就可以注入实例来操作数据库了?此刻你是不是该想到我们框架的老朋友动态代理了,所以说动态代理在框架中真是必不可少的存在啊。
配置
前面的方法之所以可以工作,那是因为springboot给我们提供了开箱即用的默认配置,但是一个优秀的组件怎么可能没有自定义配置的功能呢?openfeign当然也不例外拉。
日志配置
我们使用OpenFeign发起网络调用,有时需要查看日志来定位问题,OpenFeign提供了4种日志级别,如下所示:
public enum Level {
/**
* No logging.
*/
NONE,
/**
* Log only the request method and URL and the response status code and execution time.
*/
BASIC,
/**
* Log the basic information along with request and response headers.
*/
HEADERS,
/**
* Log the headers, body, and metadata for both requests and responses.
*/
FULL
}
那我们如何修改其日志级别呢,和其他SpringBoot程序一样,有两种方式。一种是代码配置,一种是yaml
配置文件配置。我们就看下如何在配置文件配置这种方式吧。
- 调整项目的日志级别为: DEBUG
由于OpenFeign的输出到控制台的日志级别为debug,所以首先需要调整项目的日志级别,让其可以输出到控制台。
logging:
level:
# 将top.shusheng007.goodsservice包里的日志级别调整为debug
top.shusheng007.goodsservice: DEBUG
- 配置OpenFeign的日志级别
feign:
client:
config:
default: #配置作用于整个项目中的feign客户端,
loggerLevel: HEADERS
order-service: #配置作用于某个特定的feign客户端
loggerLevel: FULL
输出结果如下:
[OrderServiceFeign#payment] ---> POST http://order-service/order/payment HTTP/1.1
[OrderServiceFeign#payment] Content-Length: 21
[OrderServiceFeign#payment] Content-Type: application/json
[OrderServiceFeign#payment]
[OrderServiceFeign#payment] {"orderId":"177hhh4"}
[OrderServiceFeign#payment] ---> END HTTP (21-byte body)
[OrderServiceFeign#payment] <--- HTTP/1.1 200 (5ms)
[OrderServiceFeign#payment] connection: keep-alive
[OrderServiceFeign#payment] content-type: application/json
[OrderServiceFeign#payment] date: Sun, 06 Nov 2022 06:16:41 GMT
[OrderServiceFeign#payment] keep-alive: timeout=60
[OrderServiceFeign#payment] transfer-encoding: chunked
[OrderServiceFeign#payment]
[OrderServiceFeign#payment] {"code":0,"errorMessage":"","data":{"orderId":"177hhh4","goodsName":"设计模式","price":50,"deliveryState":"delivery"}}
[OrderServiceFeign#payment] <--- END HTTP (122-byte body)
这里需要注意的是,既可以给你项目里的所有openfeign客户端,就是那个些使用@FeignClient
标记的接口,统一配置,也可以针对具体某个openfeign客户端配置。例如我这边default
配置了Headers级别,而order-service
这个客户端则配置了Full级别。和你想的一样,其他没有特别配置的openfeign客户端使用默认配置,特殊配置了的使用自己的配置。
也许你已经猜到了,那些config下不止可以配置日志级别,还可以配置很多东西,我们慢慢来看。
更换Http客户端
你有没有想过是谁帮OpenFeign发起的网络请求的呢?那是谁帮RestTemplate
发起的网络请求呢?
他两默认都是Java自带的URLConnection
了,但是其功能与流行的Http客户端相比显得稍微有点弱,例如不支持连接池,所以我们如果遇到不能满足需求的情况时也可以采用流行Http客户端。例如Apache的HttpClident,或者OkHttp,其是Android开发中网络请求的事实标准,但是不要误会,人家不仅可以用在Android中。
OkHttp非常优秀,下面是其几个亮眼的特性:
- HTTP/2 support allows all requests to the same host to share a socket.
- Connection pooling reduces request latency (if HTTP/2 isn’t available).
- Transparent GZIP shrinks download sizes.
- Response caching avoids the network completely for repeat requests.
让我们OpenFeign的客户端更换为OkHttp 。
- 引入okhttp依赖
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-okhttp</artifactId>
</dependency>
- 激活okhttp
在配置文件中激活OKhttp即可
feign:
okhttp:
enabled: true
至此其实已经配置好了,但是使用的是okhttp的默认配置,如果你有对okhttp配置的自定义需求就可以写一个配置类,里面有非常多的配置项,可以到okhttp的官网查看如何配置
一般只要配置openfeign就可以了,但是我们知道openfeign是对底层http客户端的一个抽象,它不可能将具体的客户端的能力全部抽象出来,只能提供一些通用的,所以如果有那种特殊的openfeign没有提供的配置,就需要直接去配置其内部的http客户端。
@Configuration
public class OpenFeignOkHttpConfig {
@Bean
public okhttp3.OkHttpClient okHttpClient(){
return new OkHttpClient.Builder()
.retryOnConnectionFailure(false)//连接失败不进行重试
.build();
}
@Bean
public Client feignClient() {
return new feign.okhttp.OkHttpClient(okHttpClient());
}
}
一旦提供了自定义的okhttp3.OkHttpClient
必须提供自定义的Client
,因为自动装配会检查类路径中是否有okhttp3.OkHttpClient
,有的话自动装配就不起作用了,所以也就不会自动装配Feign的Client。
负载均衡
最新版本的OpenFeign已经去除了对Ribbon的依赖,依赖了SpringCloud团队自己抽象出来的spring-cloud-commons
,然后提供了一个spring-cloud-starter-loadbalancer
,如果你引入了openfeign,但是不提供loadbalancer客户端程序会报错的。
Caused by: java.lang.IllegalStateException: No Feign Client for loadBalancing defined. Did you forget to include spring-cloud-starter-loadbalancer?
我们这里就直接使用spring-cloud-loadbalancer
演示
<!--负载均衡-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
只要引入上面的starter即可,openfeign默认使用RoundRobin算法,也就是你一个我一个...。那我们如何切换负载均衡算法呢?
- 负载均衡配置类
例如我们要将其切换为随机访问算法,其中RandomLoadBalancer
是spring-cloud-loadbalancer提供的,我们可以参考它实现自己的负载均衡器。
public class OpenFeignLoadBalancerConfig {
@Bean
public ReactorLoadBalancer<ServiceInstance> reactorServiceInstanceLoadBalancer(Environment environment,
LoadBalancerClientFactory loadBalancerClientFactory) {
String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
return new RandomLoadBalancer(
loadBalancerClientFactory.getLazyProvider(name, ServiceInstanceListSupplier.class), name);
}
}
注意,此配置类不需要@Configuration
标记
- 配置openfeign
使用@LoadBalancerClient
标记openfeign客户端,将OpenFeignLoadBalancerConfig
类作为参数传递给@LoadBalancerClient
注解
@FeignClient(value = "order-service")
@LoadBalancerClient(value = "order-service",configuration = OpenFeignLoadBalancerConfig.class)
public interface OrderServiceFeign {}
经过上面两步,我们已经将openfeign请求从轮询算法切换到了随机算法。
spring-cloud-loadbalancer源码
稍微讲一点点源码,不感兴趣的可以跳过:
请求进入FeignBlockingLoadBalancerClient
类的execute
方法,其中最重要的就是使用loadBalancerClient去获取服务实例
@Override
public Response execute(Request request, Request.Options options) throws IOException {
final URI originalUri = URI.create(request.url());
String serviceId = originalUri.getHost();
String hint = getHint(serviceId);
DefaultRequest<RequestDataContext> lbRequest = new DefaultRequest<>(
new RequestDataContext(buildRequestData(request), hint));
// 获取服务实例,所以主要看那个loadBalancerClient怎么写了
ServiceInstance instance = loadBalancerClient.choose(serviceId, lbRequest);
}
然后进入public class BlockingLoadBalancerClient implements LoadBalancerClient
类的choose
方法获取服务实例。
@Override
public <T> ServiceInstance choose(String serviceId, Request<T> request) {
//获取负载均衡器,例如用于轮询的 RoundRobinLoadBalancer
ReactiveLoadBalancer<ServiceInstance> loadBalancer = loadBalancerClientFactory.getInstance(serviceId);
...
Response<ServiceInstance> loadBalancerResponse = Mono.from(loadBalancer.choose(request)).block();
...
return loadBalancerResponse.getServer();
}
我们自己切换负载均衡算法也就是在提供自定义的ReactiveLoadBalancer
。
使用nacos负载均衡器
我们使用了nacos作为服务发现与注册中心,而nacos提供了配置每个服务实例的访问权重的功能,如下图所示
两个服务实例不同的权重,7101的服务实例应该会承担更大的流量,那么我们怎么启用这个功能呢?
需要将负载均算法切换到nacos上,nacos提供了一个负载均衡器
com.alibaba.cloud.nacos.loadbalancer.NacosLoadBalancer
,我们只需要切换成它才能启用这个功能
public class OpenFeignLoadBalancerConfig {
@Bean
public ReactorLoadBalancer<ServiceInstance> nacosServiceInstanceLoadBalancer(Environment environment,
LoadBalancerClientFactory loadBalancerClientFactory,
NacosDiscoveryProperties nacosDiscoveryProperties) {
String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
return new NacosLoadBalancer(
loadBalancerClientFactory.getLazyProvider(name, ServiceInstanceListSupplier.class), name,nacosDiscoveryProperties);
}
}
配置完了发起请求,打断点可见负载均衡器已经成功切换为nacos的了。
测试:
我们向order-service服务发起5次调用,其中4次落在了7101的实例,1次落在了7102的实例上,和我们预想的一样。
断路器
微服务架构要拥有降级熔断等服务治理能力,而我们使用了openfeign后就面临着怎么将这些功能集成到它上面去的问题。以前一般会使用Hystrix,但现在使用resilience4j或者阿里巴巴Sentinel,我们这里使用resilience4j吧。
- 打开断路器开关
feign:
circuitbreaker:
enabled: true
- 引入断路器依赖
<!--断路器-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-circuitbreaker-resilience4j</artifactId>
</dependency>
- 提供fallback方法
当openfeign请求远程服务失败后,可以调用fallback方法进行降级。
提供一个实现了openfeign客户端接口的实现类,里面写fallback方法。
@Component
@Slf4j
public class OrderServiceFeignFallback implements OrderServiceFeign{
@Override
public BaseResponse<OrderDetail> payment(PaymentReq paymentReq) {
log.info("支付fallback:{}",paymentReq.toString());
return ResultUtil.error("支付失败");
}
...
}
将其配置给@FeignClient
@FeignClient(value = "order-service",fallback = OrderServiceFeignFallback.class)
经过以上3步就可以了,但是我们使用的是resilience4j的默认配置,断路器功能好像也没有体现。详情可以查看微服务实践之网关详解的断路器部分
如果对断路器不熟悉,配置起来还是非常困难的,因为涉及到的概念太多了。下面是个示例,当然里面的配置都有默认值,我们这里为了学习故意自己配置了很多项,真是环境中药根据自己的需求进行配置。
@Configuration
public class OpenFeignCircuitBreakerConfig {
@Bean
public Customizer<Resilience4JCircuitBreakerFactory> defaultCustomizer(){
return new Customizer<Resilience4JCircuitBreakerFactory>() {
@Override
public void customize(Resilience4JCircuitBreakerFactory factory) {
CircuitBreakerConfig circuitBreakerConfig = CircuitBreakerConfig.custom()
.slidingWindowType(CircuitBreakerConfig.SlidingWindowType.COUNT_BASED) // 滑动窗口的类型为请求个数
.slidingWindowSize(10) // 时间窗口的大小为10个
.minimumNumberOfCalls(1) // 在单位时间窗口内最少需要1次调用才能开始进行统计计算
.failureRateThreshold(50) // 在单位时间窗口内调用失败率达到50%后会启动断路器
.enableAutomaticTransitionFromOpenToHalfOpen() // 允许断路器自动由打开状态转换为半开状态
.waitDurationInOpenState(Duration.ofSeconds(2)) // 断路器打开状态转换为半开状态需要等待2秒
.permittedNumberOfCallsInHalfOpenState(2) // 在半开状态下允许进行正常调用的次数
.recordExceptions(Throwable.class) // 所有异常都当作失败来处理
.build();
TimeLimiterConfig timeLimiterConfig = TimeLimiterConfig.custom()
.timeoutDuration(Duration.ofMillis(500))//接口500毫秒没有响应就认为失败了
.build();
factory.configureDefault(new Function<String, Resilience4JConfigBuilder.Resilience4JCircuitBreakerConfiguration>() {
@Override
public Resilience4JConfigBuilder.Resilience4JCircuitBreakerConfiguration apply(String id) {
return new Resilience4JConfigBuilder(id)
.timeLimiterConfig(timeLimiterConfig)
.circuitBreakerConfig(circuitBreakerConfig)
.build();
}
});
}
};
}
}
测试:
2022-11-06 20:30:04.306 INFO [goods-service,bb263baffae6df28,af063e602711b874] 17627 --- [nio-7001-exec-4] t.s.g.api.OrderServiceFeignFallback : 支付fallback:PaymentReq(orderId=5)
2022-11-06 20:30:05.019 INFO [goods-service,6ce43edd0f0445c1,afdfd1de4860317a] 17627 --- [nio-7001-exec-5] t.s.g.api.OrderServiceFeignFallback : 支付fallback:PaymentReq(orderId=5)
2022-11-06 20:30:05.668 INFO [goods-service,49b5d5afb8c3c356,6365268689d0d527] 17627 --- [nio-7001-exec-6] t.s.g.api.OrderServiceFeignFallback : 支付fallback:PaymentReq(orderId=5)
可见在断路器处于OPEN状态时,请求瞬间就返回了,不会去真的调用远端服务的。
拦截器(Interceptors)
拦截器大家应该不陌生了,因为其可以完成通用性的操作,所以很多框架和库都会设计这个能力,openfeign也不例外。例如实现各个服务互相调用时在请求头里面携带token这个需求就可以使用interceptor完成。
- 实现一个拦截器类
@Slf4j
public class FeignTokenInterceptor implements RequestInterceptor {
private static final String TOKEN = "token";
@Override
public void apply(RequestTemplate template) {
String token = getToken();
log.info("拦截token:{}",token);
template.header(TOKEN, token);
}
private String getToken() {
try {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
return Optional.ofNullable(request.getHeader(TOKEN)).orElse("");
} catch (Exception exception) {
log.error("获取request失败",exception);
}
return "";
}
}
- 配置
配置的话还是有两种方法,一种是通过配置文件,一种是通过代码
通过配置文件:
feign:
client:
config:
"order-service":
request-interceptors:
- top.shusheng007.goodsservice.api.FeignTokenInterceptor
通过代码配置
定义一个配置类,注意不要添加@Configuration
,不然它会作用到项目中的所有feign客户端上。
public class OrderServiceFeignConfig {
@Bean
public RequestInterceptor authRestInterceptor(){
return new FeignTokenInterceptor();
}
}
我们这里要配置"order-service"服务的eign客户端,所以将其配置到@FeignClient(value = "order-service",configuration = {OrderServiceFeignConfig.class})
即可。
如果要对全局的feign客户端生效,则配置@FeignClients(defaultConfiguration = {OrderServiceFeignConfig.class})
至此文章已经很长了,就让我们结束它吧,其实OpenFeign还有很多自定义的功能有待你的发现,本文就当抛砖引玉了
总结
声明式编程使得编程变简单了还是变复杂了呢?你说他变简单了吧,内部非常复杂,外部非常简单,以至于不了解内部的话,出了问题就抓瞎。你说他变复杂了吧,应用上却非常简单。
所以,软件行业最后可能会演变成10%的专家领着90%的码工干活的情形...哎,怎么哪里都逃不开二八定律呢?
源码
一如既往,你可以从Github获得本文的源码 master-microservice,星星点一点,猿猿不迷路...
文章评论