微服务实践之通信(OpenFeign)详解-SpringCloud(2021.0.x)-6

[版权申明] 非商业目的注明出处可自由转载
出自:shusheng007

首发于:shusheng007.top

文章目录

  • 概述
  • OpenFeign简介
  • 基本使用
    • 新建provider与consumer两个服务
    • provider服务
    • consumer服务
      • 引入依赖
      • 声明OpenFeign接口
      • 开启OpenFeign
  • 原理
  • 配置
    • 日志配置
    • 更换Http客户端
    • 负载均衡
      • spring-cloud-loadbalancer源码
      • 使用nacos负载均衡器
    • 断路器
    • 拦截器(Interceptors)
  • 总结
  • 源码

概述

本文是微服务系列总结的第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 payment(@RequestBody PaymentReq paymentReq){
        return ResultUtil.ok(orderService.paymentOrder(paymentReq.getOrderId())) ;
    }
}

consumer服务

新建一个消费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
        
      

声明OpenFeign接口

声明一个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")

开启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接口然后就可以注入实例来操作数据库了,对啦,这里也用到了动态代理,所以说这个动态代理在写框架时真是必不可少的存在啊。

微服务实践之通信(OpenFeign)详解-SpringCloud(2021.0.x)-6_第1张图片

配置

前面那个是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配置文件配置。我们就看下如何在配置文件配置这种方式吧。

  1. 调整你项目的日志级别为: DEBUG

    由于OpenFeign的输出到控制台的日志级别为debug,所以首先需要调整项目的日志级别,让其可以输出到控制台。

logging:
  level:
    # 将top.shusheng007.goodsservice包里的日志级别调整为debug
    top.shusheng007.goodsservice: DEBUG  
  1. 配置OpenFeign的日志级别
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下不止可以配置日志级别,还可以配置很多东西,我们慢慢来看。

更换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依赖
 
     io.github.openfeign
     feign-okhttp
 
  • 激活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);
    }
}
  • 配置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 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作为服务发现与注册中心,而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的了。

微服务实践之通信(OpenFeign)详解-SpringCloud(2021.0.x)-6_第2张图片
测试:

我们向order-service服务发起了5次调用,其中4次落在了7101的实例,1次落在了7102的实例上,和我们预想的一样。
微服务实践之通信(OpenFeign)详解-SpringCloud(2021.0.x)-6_第3张图片
微服务实践之通信(OpenFeign)详解-SpringCloud(2021.0.x)-6_第4张图片

断路器

微服务架构要拥有降级熔断等服务治理能力,而我们使用了openfeign后就面临着怎么将这些功能集成到它上面去。以前一般会使用Hystrix,但现在使用resilience4j或者阿里巴巴Sentinel,我们这里使用resilience4j吧。

  1. 打开断路器开关
feign:
  circuitbreaker:
    enabled: true
  1. 引入断路器依赖

        
            org.springframework.cloud
            spring-cloud-starter-circuitbreaker-resilience4j
        
  1. 提供fallback方法

当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状态时,请求瞬间就返回了,不会去真的调用远端服务的。

拦截器(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 "";
    }
}
  • 配置

将其配置到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,星星点一点,猿猿不迷路…

你可能感兴趣的:(SpringCloud,分布式,spring,cloud,微服务,java,openfeign)