实习积累-微服务本地调用模式

177 阅读7分钟

问题描述

在微服务项目中,以微服务A调用微服务B为例,调用大概要经历以下过程:

  • A获取到B的服务名
  • A从注册中心(nacos)中获取到B的ip地址和端口号
  • A发起调用

这在线上环境是没有问题的,但是如果是调试环境呢?

例如我有A、B、C三个微服务,我需要对A进行修改,A依赖于B和C来完成服务。

在完成对A的修改之后,往往需要在本地先进行功能测试。

这时候就衍生出第一个问题,我是否需要在本地启动nacos:

  1. 启动
    1. 本地启动nacos,那么同时也需要启动B和C两个微服务,让他们也注册到nacos上,这样A就能从本地nacos中获取到本地两个微服务的地址,完成调用。
    2. 但是当微服务数量过多,本地开发机器性能有限时,可能没办法一下子启动全部的微服务,这就造成问题。
  2. 不启动,使用开发环境的nacos
    1. 开发环境肯定也部署了ABC三个服务,我只需要正常调用即可。
    2. 但是这又导致新的问题,如果我同时需要对A和B进行修改来完成一个业务操作,在不能保证功能本地测试通过之前,是不能提交代码部署在开发环境的,那么我就没办法保证A不去调用开发环境的B。

因此,在微服务数量较少的情况下,可以考虑在本地启动全部的微服务和nacos来实现,在微服务数量较多的情况下,就需要考虑 开发环境和本地环境冲突的问题。

解决方案

Feign配置文件

首先复习一下Feign的使用,一般我们都是在接口上加上@FeignClient的注解,注解主要有以下两个参数:

  • value/name 指定FeignClient名称 如果使用了注册中心,会作为微服务名称用于服务发现
  • url 指定后,api地址不会从注册中心获取

也就是说,我们要避免服务从注册中心获取api地址,那我们可以在local环境的配置文件下,指定url地址,dev环境下,不指定,则默认为空。

这样就可以启动本地微服务调用了。

这样虽然能解决问题啊,但是不够智能,如果我在本地进行配置了,那么我就一定需要启动该微服务,或者删除掉该配置。

那么有没有一个办法,能实现,我本地启动了该微服务,则从本地调用,如果我本地未启动该微服务,则从注册中心获取呢

微服务本地联调

再次强调我们目前的需求,我们希望在进行微服务间调用时,如果本地未启动被调用的微服务,则从注册中心获取并调用开发环境,如果本地启动了被调用的微服务,则直接调用本地微服务。

也就是说,我们需要知道,本地微服务是否启动。

我们可以通过开放一个接口,使用HTTP工具来访问接口,通过能否访问来判断该服务是否启动,因为我的系统集成了swagger,所以我是通过测试swagger接口来实现的。

在能判断本地微服务是否启动后,我们遍可以通过判断依据来进行下一步操作。

再次强调上一小节我们说过的:如果@FeignClient中配置了url属性,则不会从注册中心获取url

也就是说,我们只需要对判断已启动的微服务,对修改配置的属性即可。而这遍可以通过BeanFactoryPostProcessor实现,因为它是聚焦于:对beanDefinition的属性进行修改调整。

这里需要复习两个知识:

  1. ApplicationContextAware
    1. 这是一个回调接口,用于获取应用程序上下文(ApplicationContext)。
    2. 当该类被初始化并注入到Spring容器中时,Spring会自动调用setApplicationContext方法,将应用程序上下文对象传递给该类。
    3. 以便在需要时可以通过该对象访问Spring容器中的其他bean。
  2. BeanFactoryPostProcessor
    1. 是一个Bean后置处理器接口,用于在Spring容器加载Bean的定义后,对Bean进行额外的处理。
    2. 当Spring容器实例化并配置所有的Bean定义后,会调用postProcessBeanFactory方法,允许对BeanFactory进行修改或执行其他的自定义逻辑。

因此我们可以大概获取到以下流程:

  1. 首先检查是否为本地环境且开启本地调用模式
  2. 获取配置文件中配置的服务名和端口
  3. 循环配置的服务名+端口
    1. 判断是否已启动
    2. 设置url

现在来展示代码实现:

先定义好类和配置文件,在本测试用例中:

  1. 定义了三个Feign调用类:FeginClient1、FeginClient2、FeginClient3
  2. 定义配置文件:application.yml和application-local.yml,并在appliaction.yml中配置active为local
  3. 在application-local中配置必要属性,如下:
spring:
  application:
    name: test
local:
  mode: true
  microservice:
    - name: test1
      port: 8081
    - name: test2
      port: 8082
server:
  port: 8080

定义类本次的重点实现类,并实现接口:

@Component
public class FeignDevHelper implements ApplicationContextAware, BeanFactoryPostProcessor {


    ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }


    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory configurableListableBeanFactory) throws BeansException {

    }

}

接下来在postProcessBeanFactory中进行进一步处理。

首先获取Enviroment对象,判断本地环境和本地微服务调试模式是否启动:

               Environment environment = applicationContext.getEnvironment();
        String[] profiles = environment.getActiveProfiles();
        if (!"local".equals(profiles[0]))
        {
            return;
        }
        boolean localmode = Boolean.valueOf(environment.getProperty("local.mode"));
        if(!localmode){
            return;
        }

判断启动成功后,下一步就是获取配置的microservice对象数组了:

        String property1 = environment.getProperty("local.microservice");
        System.out.println(property1);

注意,这是一个错误示例

在 Spring Framework 中,Environment 接口通常用于获取应用程序的配置信息,但是对于复杂类型的配置项(例如数组),它的支持是有限的。通常情况下,直接使用 environment.getProperty("key") 获取复杂类型的配置项可能会返回 null。

因此,我们需要通过配置类的形式来实现需求:


@Configuration
@ConfigurationProperties(prefix = "local")
public class MicroserviceConfig {

    private boolean mode;
    private List<Microservice> microservice;

    // Getters and Setters
    public boolean isMode() {
        return mode;
    }

    public void setMode(boolean mode) {
        this.mode = mode;
    }

    public List<Microservice> getMicroservice() {
        return microservice;
    }

    public void setMicroservice(List<Microservice> microservice) {
        this.microservice = microservice;
    }
}
public class Microservice {
    private String name;
    private int port;

    // Getters and Setters
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getPort() {
        return port;
    }

    public void setPort(int port) {
        this.port = port;
    }
}

然后在 postProcessBeanFactory 中通过 ApplicationContext 获取bean对象。

但是注意!这同样也是一个错误示例

因为postProcessBeanFactory是在读取Beandefination的配置属性之后,此时Bean对象还未被初始化,读取不到属性。

因此我们还是只能回到Environment下手,既然他无法读取到复杂对象,我们直接读取简单对象即可:

        for (int i = 0;; i++) {
            String name = environment.getProperty("local.microservice[" + i + "].name");
            int port = environment.getProperty("local.microservice[" + i + "].port", Integer.class, 0);
            if(name==null||port==0){
                break;
            }
            // 处理微服务信息
            System.out.println(name);
            System.out.println(port);
        }

这便是正确示例了。

获取到配置信息后,下一步便是验证微服务是否启动,由于我的项目中使用到了swagger,所以这里用swagger进行演示:

 String localSwaggerUrl = "http://localhost:" + port + "/" + name + "/doc.html";
            boolean localMicroServiceUp = testSwagger(localSwaggerUrl);
            if (!localMicroServiceUp)
            {
                return;
            }

验证微服务启动完成之后,便到了最重要的一步,对验证成功的微服务进行url属性的注入。

注意这里要获取name属性,因为在@FeignClient的源码中使用了AliasFor。

  String url = "http://localhost" + ":" + port;
            String[] beanNames = applicationContext.getBeanDefinitionNames();
            for (String beanName : beanNames) {
                BeanDefinition beanDefinition = beanFactory.getBeanDefinition(beanName);
                MutablePropertyValues mutablePropertyValues = beanDefinition.getPropertyValues();
                PropertyValue propertyValue = mutablePropertyValues.getPropertyValue("name");
                if (null != propertyValue && name.equals(propertyValue.getValue())) {
                    mutablePropertyValues.removePropertyValue("url");
                    mutablePropertyValues.addPropertyValue("url", url);
                }
            }

这样遍完成啦,只需要记得启动顶层服务前,先把底层依赖的微服务启动就好咯。

完整代码如下:

@Component
public class FeignDevHelper implements ApplicationContextAware, BeanFactoryPostProcessor {


    ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }


    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
        Environment environment = (ConfigurableEnvironment) applicationContext.getEnvironment();
        String[] profiles = environment.getActiveProfiles();
        if (!"local".equals(profiles[0])) {
            return;
        }
        boolean localmode = Boolean.valueOf(environment.getProperty("local.mode"));
        if (!localmode) {
            return;
        }

        for (int i = 0; ; i++) {
            String name = environment.getProperty("local.microservice[" + i + "].name");
            int port = environment.getProperty("local.microservice[" + i + "].port", Integer.class, 0);
            if (name == null || port == 0) {
                break;
            }
            // 处理微服务信息

            String localSwaggerUrl = "http://localhost:" + port + "/" + name + "/doc.html";
            boolean localMicroServiceUp = testSwagger(localSwaggerUrl);
            if (!localMicroServiceUp) {
                return;
            }
            String url = "http://localhost" + ":" + port;
            String[] beanNames = applicationContext.getBeanDefinitionNames();
            for (String beanName : beanNames) {
                BeanDefinition beanDefinition = beanFactory.getBeanDefinition(beanName);
                MutablePropertyValues mutablePropertyValues = beanDefinition.getPropertyValues();
                PropertyValue propertyValue = mutablePropertyValues.getPropertyValue("name");
                if (null != propertyValue && name.equals(propertyValue.getValue())) {
                    mutablePropertyValues.removePropertyValue("url");
                    mutablePropertyValues.addPropertyValue("url", url);
                    System.out.println(url);
                }
            }
        }

    }


    private boolean testSwagger(String localSwaggerUrl) {
        return true;
    }

}