ribbon & feign个性化配置

代码例子

项目地址,见ribbon-example、feign-example

原理基本介绍

一个微服务中可能会调用多个微服务提供的服务,ribbon和feign允许都具体某一个微服务进行配置,这基于Spring中父子容器这一概念实现。比如服务A即调用了服务B的方法,又调用了服务C的方法。假定服务B、服务C的服务名依次为serviceB、serviceC。那么会为serviceB、serviceC各自创建一个ApplicationContext,互不干扰。当然应用中本身的ApplicationContext作为它们的父容器,子容器可以访问父容器的bean,而父容器不能访问子容器的bean,对于ribbon来说,则体现在SpringClientFactory类,它继承了NamedContextFactory类,是一个创建父子容器的工具设施。

ribbon原生使用

当引入spring-cloud-starter-netflix-ribbon依赖时,在spring.factories文件中加一个自动配置类RibbonAutoConfiguration,会为应用中注入一个LoadBalancerClient的实现RibbonLoadBalancerClient,我们可以使用该接口来选择服务,具体怎么选择,交给具体的负载均衡策略实现。

package com.wangtao.ribbonexample.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.net.URI;

@RequestMapping("/ribbon")
@RestController
public class RibbonController {

    @Autowired
    private LoadBalancerClient loadBalancerClient;

    @GetMapping("/chooseServer")
    public String chooseServer() {
        ServiceInstance serviceInstance = loadBalancerClient.choose("nacos-producer");
        System.out.println("getInstanceId: " + serviceInstance.getInstanceId());
        System.out.println("getServiceId: " + serviceInstance.getServiceId());
        System.out.println("getHost: " + serviceInstance.getHost());
        System.out.println("getPort: " + serviceInstance.getPort());
        System.out.println("getScheme: " + serviceInstance.getScheme());
        System.out.println("getUri: " + serviceInstance.getUri());
		
        // 将nacos-producer换成具体的IP+端口
        URI originalUri = URI.create("https://nacos-producer/demo");
        System.out.println(loadBalancerClient.reconstructURI(serviceInstance, originalUri));
        return serviceInstance.getUri().toString();
    }
}

ribbon配置负载均衡策略

IRule是ribbon负载均衡策略的顶级接口,只要在容器中创建一个实现类型,即可自定义ribbon的负载均衡策略。ribbon提供了很多的实现,比如随机策略(RandomRule)、轮询策略(RoundRobinRule)、区域可用策略(ZoneAvoidanceRule)等,具体可使用IDE查看IRule的实现类。其中ZoneAvoidanceRule是ribbon的默认策略,它会经过ZoneAvoidancePredicate、AvailabilityPredicate过滤之后,对于剩下的服务列表,使用轮询的规则。

下面就来实现这么一个需求,调用nacos-producer服务中的方法使用随机策略,其它服务中的方法使用轮询策略,特指RoundRobinRule。

第一种配置方式

@Configuration
public class RibbonGlobalConfig {

    /**
     * 随机策略
     */
    @Bean
    public IRule randomRule() {
        return new RandomRule();
    }
}

public class RibbonNacosProducerConfig {

    /**
     * 线性轮询策略
     * @Primary很重要,不然在创建ILoadBalancer会发生错误,依赖IRule实现,因为无法找到唯一的实现
     * 看下文解析
     */
    @Primary
    @Bean
    public IRule roundRobinRule() {
        return new RoundRobinRule();
    }
}

@RibbonClients(
        value = {@RibbonClient(value = "nacos-producer", configuration = RibbonNacosProducerConfig.class)}
)
@EnableDiscoveryClient
@SpringBootApplication
public class RibbonExampleApplication {

    public static void main(String[] args) {
        SpringApplication.run(RibbonExampleApplication.class, args);
    }
}

其中RibbonGlobalConfig作为全局策略,因为它注册到了应用的容器中,被@Configuration标记了,并且能被扫描到。而RibbonNacosProducerConfig仅仅作为nacos-producer子容器的bean,只对nacos-producer有效,它没有被@Configuration标记,也就意味着没有注册到应用中的容器里,而是通过@RibbonClient注解将它放入了nacos-producer子容器中。当然你也可以把RibbonNacosProducerConfig放到一个不会被应用扫描到的包中,然后加上@Configuration,表示这是一个配置类。只要不被应用中的容器扫描到。

如果要验证配置是否生效,可以在具体策略的类中choose方法加入一个断点debug即可。

其中这里还有一个坑,如果说没有配置RibbonGlobalConfig这个全局的,那么会很正常的工作,这里执行的时候将会报一个bean创建一个异常,原因是无法找到一个唯一的IRule实现。因为我以为如果在子容器中找到了IRule的实现,就不会再去父容器中找了,但是事实上会例举出所有的实现,包括父容器。因此在RibbonNacosProducerConfig的配置上我加了一个@Primary

关于这个问题,可以用下面这段代码来复现该问题

interface A {}

class A1 implements A {}

class A2 implements A {}

class B {
    private A a;
    
    public B(A a) {
        this.a  = a;
    }
} 

/**
 * 该代码模拟一个父子容器,在创建B时将发生错误,因为无法确定唯一的bean
 * 但是如果去掉B的注册
 * child.getBean(A.Class)是可以拿到A2的实现的
 * 仅仅是依赖注入时会例举出所有的实现,说实话这点不是很能理解Spring的做法,为啥不和getBean方法一样
 * 优先子容器的bean。
 */
public void test() {
        AnnotationConfigApplicationContext parent = new AnnotationConfigApplicationContext();
        parent.register(A1.class);
        parent.refresh();

        AnnotationConfigApplicationContext child = new AnnotationConfigApplicationContext();
        child.setParent(parent);
        child.register(A2.class);
        child.register(B.class);
        child.refresh();
 }

第二种配置方式

上述的配置是把全局的配置文件注入到应用容器中,@RibbonClients注解也提供了一个defaultConfiguration属性,可以注册默认配置到子容器中,子容器中配置注册顺序如下(参考NamedContextFactory类的createContext方法)

  • @RibbonClient注解configuration属性指定的配置,为子容器特有的配置。
  • @RibbonClients注解defaultConfiguration属性指定的配置,注意默认配置会被注册到每一个子容器中
  • SpringClientFactory指定的全局配置RibbonClientConfiguration也会被注册到每一个子容器中

注意:NamedContextFactory创建的容器是允许bean覆盖的,需要小心beanName重复。

public class DefaultRibbonConfig {

    /**
     * 随机策略
     * 默认的,容器没有才生效
     */
    @ConditionalOnMissingBean
    @Bean
    public IRule randomRule() {
        return new RandomRule();
    }
}
public class RibbonNacosProducerConfig {

    /**
     * 线性轮询策略
     * 不再需要@Primary注解
     */
    @Bean
    public IRule roundRobinRule() {
        return new RoundRobinRule();
    }
}
@RibbonClients(
        value = {@RibbonClient(value = "nacos-producer", configuration = RibbonNacosProducerConfig.class)},
        defaultConfiguration = DefaultRibbonConfig.class
)
@EnableDiscoveryClient
@SpringBootApplication
public class RibbonExampleApplication {

    public static void main(String[] args) {
        SpringApplication.run(RibbonExampleApplication.class, args);
    }
}

对于nacos-producer子容器中,存在RibbonNacosProducerConfigDefaultRibbonConfigRibbonClientConfiguration3个配置类,而其它子容器只有DefaultRibbonConfigRibbonClientConfiguration2个配置类。

DefaultRibbonConfigRibbonClientConfiguration的配置类中的IRule bean都有@ConditionalOnMissingBean条件注解。

因此对于nacos-producer子容器,只有RibbonNacosProducerConfigIRule会被注册到容器中,而对于其它容器则只有DefaultRibbonConfigIRule会被注册到容器中。

feign特殊化配置

其中feign和ribbon一样,也是有父子容器的配置概念,就不多赘述了。feign体现在FeignContext这个类,当然了肯定也是继承了NamedContextFactory

/**
 * 类上不加@Configuration注解, 是不想让该配置全局生效
 */
public class NacosProducerFeignConfig {

    /**
     * 配置feign的转换日志格式, 默认不打印
     * 注: 还需要打开logback的日志级别, feign接口是UserService
     * 所以将UserService的logback级别配成debug
     */
    @Bean
    public Logger.Level feginLoggerLevel() {
        return Logger.Level.FULL;
    }

    /**
     * 配置超时时间
     */
    @Bean
    public Request.Options requestOption() {
        return new Request.Options(10, TimeUnit.SECONDS, 60, TimeUnit.SECONDS, true);
    }
}

然后在@FeginClient中引入

/**
 * 其中value或name属性为服务名称
 * contextId为子容器的名字, 默认等于value
 * FeignClient注解的configuration属性指定的配置类都是在子容器创建的
 *
 * 会创建两个bean
 * 1. beanName=[contextId].FeignClientSpecification
 *    class=org.springframework.cloud.openfeign.FeignClientSpecification
 * 2. beanName=com.wangtao.feignexample.feign.UserService
 *    class=org.springframework.cloud.openfeign.FeignClientFactoryBean
 *    FeignClientFactoryBean是一个FactoryBean, 从容器通过getObject方法可以获取
 *    到UserService的实例, 且该bean会被@Primary修饰(@FeignClient注解的primary默认为true),
 *    因此存在fallback实现类时, 注入时不用指定名称也不会报错
 */
@FeignClient(value = "nacos-producer", path = "/api/users", configuration = NacosProducerFeignConfig.class)
public interface UserService {

}
posted on 2022-06-26 20:34  wastonl  阅读(325)  评论(0编辑  收藏  举报