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


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

    首发于shusheng007.top

    概述

    本文是微服务系列总结的第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())) ;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    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
            
          
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    声明OpenFeign接口

    声明一个interface,使用@FeignClient标记,如果使用了服务发现与注册中心,那么其value写要调用的服务名称即可。

    注意,OpenFeign服务名称不支持下划线_,这是一个坑

    @FeignClient(value = "order-service")
    public interface OrderServiceFeign {
    
        @PostMapping(value = "/order/payment")
        public BaseResponse payment(@RequestBody PaymentReq paymentReq);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    这个接口里面声明要调用的服务order-service 的API,其签名要求与order-service 里的API完全一致。注意那个返回值虽然这里写的都是同一个类型BaseResponse,但与order-service 里的那个BaseResponse可不是同一个,他们是以json交互的,只要对的上即可。

    如果没有使用服务发现与注册中心,这在微服务架构中是不可能发生的,但是我们确实也存在需要直接调用某个url的情形,使用如下配置即可,不过那个value是强制需要的,你可以谁便起个名字。

    @FeignClient(value = "order-service" ,url = "https://xxxxx")
    
    • 1

    开启OpenFeign

    当写完上面的接口,我们还的使用一个@EnableFeignClients注解将其打开

    @SpringBootApplication
    ...
    @EnableFeignClients
    public class GoodsServiceApplication {
        public static void main(String[] args) {
            SpringApplication.run(GoodsServiceApplication.class, args);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    经过以上三步后,奇迹发生了,我们可以当调用本地方法一样调用远程方法了,如下所示。

    @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());
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    原理

    写个接口就把远程方法调用了?我们都没写实现类哎!想想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
      }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    那我们如何修改其日志级别呢,和其他SpringBoot程序一样,这里有两种方式,一种是代码配置,一种是yaml配置文件配置。我们就看下如何在配置文件配置这种方式吧。

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

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

    logging:
      level:
        # 将top.shusheng007.goodsservice包里的日志级别调整为debug
        top.shusheng007.goodsservice: DEBUG  
    
    • 1
    • 2
    • 3
    • 4
    1. 配置OpenFeign的日志级别
    feign:
      client:
        config:
          default: # 项目全局
            loggerLevel: HEADERS
          order-service: #访问某个服务的特定的feign
            loggerLevel: FULL
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    输出结果如下:

     [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)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    这里需要注意的是,既可以给你项目里的所有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
     
    
    • 1
    • 2
    • 3
    • 4
    • 激活okhttp

    在配置文件中激活OKhttp即可

    feign:
      client:
        config:
          xxx
      okhttp:
        enabled: true
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    至此其实已经配置好了,但是使用的是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();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    负载均衡

    最新版本的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?
    
    • 1

    我们这里就直接使用spring-cloud-loadbalancer演示

    
    
        org.springframework.cloud
        spring-cloud-starter-loadbalancer
    
    
    • 1
    • 2
    • 3
    • 4
    • 5

    只要引入上面的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);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 配置openfeign

    使用@LoadBalancerClient标记openfeign客户端,将OpenFeignLoadBalancerConfig类作为参数传递给@LoadBalancerClient注解

    @FeignClient(value = "order-service")
    @LoadBalancerClient(value = "order-service",configuration = OpenFeignLoadBalancerConfig.class)
    public interface OrderServiceFeign {}
    
    • 1
    • 2
    • 3

    经过上面两步,我们已经将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);
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    然后进入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();
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    我们自己切换负载均衡算法也就是在提供自定义的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);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    配置完了发起请求,打断点可见负载均衡器已经成功切换为nacos的了。

    在这里插入图片描述
    测试:

    我们向order-service服务发起了5次调用,其中4次落在了7101的实例,1次落在了7102的实例上,和我们预想的一样。
    在这里插入图片描述
    在这里插入图片描述

    断路器

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

    1. 打开断路器开关
    feign:
      circuitbreaker:
        enabled: true
    
    • 1
    • 2
    • 3
    1. 引入断路器依赖
    
            
                org.springframework.cloud
                spring-cloud-starter-circuitbreaker-resilience4j
            
    
    • 1
    • 2
    • 3
    • 4
    • 5
    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("支付失败");
        }
       ...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    将其配置给@FeignClient

    @FeignClient(value = "order-service",fallback = OrderServiceFeignFallback.class)
    
    • 1

    经过以上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();
                        }
                    });
                }
            };
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34

    测试:

    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)
    
    • 1
    • 2
    • 3

    可见在断路器处于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 "";
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 配置

    将其配置到yaml文件中即可,当然也可以使用@Bean的方式,然后如果是针对某一个openfeign客户端的就配置到@FeignClient(value = "order-service",configuration = {FeignTokenInterceptor.class}) ,针对全局的就配置@EnableFeignClients(defaultConfiguration = {FeignTokenInterceptor.class})

    feign:
      client:
        config:
          default:
            requestInterceptors:
              - top.shusheng007.goodsservice.api.FeignTokenInterceptor
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    总结

    声明式编程使得编程变简单了还是变复杂了呢?你说他变简单了吧,内部非常复杂,外部非常简单,以至于不了解内部的话,出了问题就抓瞎。你说他变复杂了吧,应用上却非常简单。

    所以,软件行业最后可能会演变成10%的专家领着90%的码工干活的情形…哎,怎么哪里都逃不开二八定律呢?

    源码

    一如既往,你可以从Github获得本文的源码 master-microservice,星星点一点,猿猿不迷路…

  • 相关阅读:
    腾讯春招C++面试题大解析:最全面!最详细!2024年必备攻略,99%的开发者已收藏!
    Ceph块存储的安装部署和使用
    机器学习库实战:DL4J与Weka在Java中的应用
    二维列表对应一维列表的内容子串查询,并返回下标
    Android11去掉Settings中的网络和互联网一级菜单
    C# OCR服务测试程序
    springboot 整合使用redis发布订阅功能
    vue3环境搭建
    使用dockerfile制作定时执行任务镜像
    No.13软件集成技术
  • 原文地址:https://blog.csdn.net/ShuSheng0007/article/details/127522601