[版权申明] 非商业目的注明出处可自由转载
出自:shusheng007
首发于:shusheng007.top
本文是微服务系列总结的第6篇,一起来看看微服务之间通信时用到的OpenFeign组件。
我们在SpringCloud中使用的一般是 spring-cloud-openfeign,它是SpringCloud团队基于feign封装的一个变体,支持了SpringMvc里的各种注解,例如@RequestBody
之类的。
Feign是一个类似于Retrofit (对OkHttp的一个封装)的一个声明式的Http客户端包装器。
是不是还不好理解,一会看到例子就懂了,此时就将其理解为和RestTemplate
类似的东西好了,只不过它是声明式的。你不会又要问啥是声明式吧?假设你妈让你去打二斤酱油,至于你是去超市还是小卖部,腿儿着去还是骑共享单车去,现金付款还是扫码付款,你妈完全不关心整个实现过程,这就是声明式。
学习一个框架或者三方技术,千万不要一上来就一头扎入其内部一顿捣鼓,把自己弄得灰头土脸的,完了还似懂非懂。第一步就是要熟练的使用,优秀的框架和三方技术组件的API都封装的非常好,处处展示了其设计思想,所以熟练使用后再去探究其是怎么实现的就会事半功倍
我们知道,OpenFeign呢是一个用来发起http请求的库,所以我们需要两个服务,一个提供API(provider),一个调用API(consumer)。
新建一个对外提供API的SpringBoot的web程序order-service。
@RequiredArgsConstructor
@RestController
@RequestMapping("/order")
public class OrderController {
private final OrderService orderService;
@PostMapping(value = "/payment")
public BaseResponse payment(@RequestBody PaymentReq paymentReq){
return ResultUtil.ok(orderService.paymentOrder(paymentReq.getOrderId())) ;
}
}
新建一个消费order-service 的API的服务:goods-service,我们要在此服务中使用OpenFeign调用order-service服务提供的API
在goods-service服务中引入OpenFeign的依赖
org.springframework.cloud
spring-cloud-starter-openfeign
org.springframework.cloud
spring-cloud-dependencies
${spring-cloud.version}
pom
import
声明一个interface,使用@FeignClient
标记,如果使用了服务发现与注册中心,那么其value写要调用的服务名称即可。
注意,OpenFeign服务名称不支持下划线_,这是一个坑
@FeignClient(value = "order-service")
public interface OrderServiceFeign {
@PostMapping(value = "/order/payment")
public BaseResponse payment(@RequestBody PaymentReq paymentReq);
}
这个接口里面声明要调用的服务order-service 的API,其签名要求与order-service 里的API完全一致。注意那个返回值虽然这里写的都是同一个类型BaseResponse,但与order-service 里的那个BaseResponse可不是同一个,他们是以json交互的,只要对的上即可。
如果没有使用服务发现与注册中心,这在微服务架构中是不可能发生的,但是我们确实也存在需要直接调用某个url的情形,使用如下配置即可,不过那个value是强制需要的,你可以谁便起个名字。
@FeignClient(value = "order-service" ,url = "https://xxxxx")
当写完上面的接口,我们还的使用一个@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
feign:
client:
config:
default: # 项目全局
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下不止可以配置日志级别,还可以配置很多东西,我们慢慢来看。
你有没有想过是谁帮OpenFeign发起的网络请求的呢?那是谁帮RestTemplate
发起的网络请求呢?
他两默认都是Java自带的URLConnection
了,但是其功能与流行的Http客户端相比显得稍微有点弱,例如不支持连接池,所以我们如果遇到不能满足需求的情况时也可以采用流行Http客户端。例如Apache的HttpClident,或者OkHttp,其是Android开发中网络请求的事实标准,但是不要误会,人家不仅可以用在Android中。
OkHttp非常优秀,下面是其几个亮眼的特性:
让我们OpenFeign的客户端更换为OkHttp 。
io.github.openfeign
feign-okhttp
在配置文件中激活OKhttp即可
feign:
client:
config:
xxx
okhttp:
enabled: true
至此其实已经配置好了,但是使用的是okhttp的默认配置,如果你有对okhttp配置的自定义需求就可以写一个配置类,里面有非常多的配置项,可以到okhttp的官网查看如何配置
一般只要配置openfeign就可以了,但是我们知道openfeign是对底层http客户端的一个抽象,它不可能将具体的客户端的能力全部抽象出来,只能提供一些通用的,所以如果有那种特殊的openfeign没有提供的配置,就需要直接去配置其内部的http客户端。
@Configuration
public class OpenFeignOkHttpConfig {
@Bean
public okhttp3.OkHttpClient okHttpClient(OkHttpClient.Builder builder){
return builder
.retryOnConnectionFailure(false)//连接失败不进行重试
.build();
}
}
最新版本的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
演示
org.springframework.cloud
spring-cloud-starter-loadbalancer
只要引入上面的starter即可,openfeign默认使用RoundRobin算法,也就是你一个我一个…。那我们如何切换负载均衡算法呢?
这里我我们将其切换为随机访问,其中RandomLoadBalancer
是spring-cloud-loadbalancer提供的,你也可以参考它实现自己的负载均衡器。
public class OpenFeignLoadBalancerConfig {
@Bean
public ReactorLoadBalancer reactorServiceInstanceLoadBalancer(Environment environment,
LoadBalancerClientFactory loadBalancerClientFactory) {
String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
return new RandomLoadBalancer(
loadBalancerClientFactory.getLazyProvider(name, ServiceInstanceListSupplier.class), name);
}
}
使用@LoadBalancerClient
标记openfeign客户端,将OpenFeignLoadBalancerConfig
类作为参数传递给@LoadBalancerClient
注解
@FeignClient(value = "order-service")
@LoadBalancerClient(value = "order-service",configuration = OpenFeignLoadBalancerConfig.class)
public interface OrderServiceFeign {}
经过上面两步,我们已经将openfeign请求从轮询算法切换到了随机算法。
稍微讲一点点源码,不感兴趣的可以跳过:
请求进入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 lbRequest = new DefaultRequest<>(
new RequestDataContext(buildRequestData(request), hint));
// 获取服务实例,所以主要看那个loadBalancerClient怎么写了
ServiceInstance instance = loadBalancerClient.choose(serviceId, lbRequest);
}
然后进入public class BlockingLoadBalancerClient implements LoadBalancerClient
类的choose
方法获取服务实例。
@Override
public ServiceInstance choose(String serviceId, Request request) {
//获取负载均衡器,例如用于轮询的 RoundRobinLoadBalancer
ReactiveLoadBalancer loadBalancer = loadBalancerClientFactory.getInstance(serviceId);
...
Response loadBalancerResponse = Mono.from(loadBalancer.choose(request)).block();
...
return loadBalancerResponse.getServer();
}
我们自己切换负载均衡算法也就是在提供自定义的ReactiveLoadBalancer
。
由于我们使用了nacos作为服务发现与注册中心,而nacos给我提供了配置每个服务实例的访问权重的功能,如下图所示
两个服务实例不同的权重,7101的服务实例应该会承担更大的流量,那么我们怎么启用这个功能呢?
需要将负载均算法切换到nacos上,nacos提供了一个负载均衡器com.alibaba.cloud.nacos.loadbalancer.NacosLoadBalancer
,我们只需要切换到它上面即可
public class OpenFeignLoadBalancerConfig {
@Bean
public ReactorLoadBalancer 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
org.springframework.cloud
spring-cloud-starter-circuitbreaker-resilience4j
当openfeign请求的远程服务失败后,可以调用我们的fallback方法(算降级)。
写一个实现了openfeign客户端接口的实现类,里面写fallback方法。
@Component
@Slf4j
public class OrderServiceFeignFallback implements OrderServiceFeign{
@Override
public BaseResponse 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 defaultCustomizer(){
return new Customizer() {
@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() {
@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状态时,请求瞬间就返回了,不会去真的调用远端服务的。
拦截器大家应该不陌生了,因为其可以完成通用性的操作,所以很多框架和库都会设计这个能力,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 "";
}
}
将其配置到yaml文件中即可,当然也可以使用@Bean的方式,然后如果是针对某一个openfeign客户端的就配置到@FeignClient(value = "order-service",configuration = {FeignTokenInterceptor.class})
,针对全局的就配置@EnableFeignClients(defaultConfiguration = {FeignTokenInterceptor.class})
feign:
client:
config:
default:
requestInterceptors:
- top.shusheng007.goodsservice.api.FeignTokenInterceptor
声明式编程使得编程变简单了还是变复杂了呢?你说他变简单了吧,内部非常复杂,外部非常简单,以至于不了解内部的话,出了问题就抓瞎。你说他变复杂了吧,应用上却非常简单。
所以,软件行业最后可能会演变成10%的专家领着90%的码工干活的情形…哎,怎么哪里都逃不开二八定律呢?
一如既往,你可以从Github获得本文的源码 master-microservice,星星点一点,猿猿不迷路…