问题描述
在微服务项目中,以微服务A调用微服务B为例,调用大概要经历以下过程:
- A获取到B的服务名
- A从注册中心(nacos)中获取到B的ip地址和端口号
- A发起调用
这在线上环境是没有问题的,但是如果是调试环境呢?
例如我有A、B、C三个微服务,我需要对A进行修改,A依赖于B和C来完成服务。
在完成对A的修改之后,往往需要在本地先进行功能测试。
这时候就衍生出第一个问题,我是否需要在本地启动nacos:
- 启动
- 本地启动nacos,那么同时也需要启动B和C两个微服务,让他们也注册到nacos上,这样A就能从本地nacos中获取到本地两个微服务的地址,完成调用。
- 但是当微服务数量过多,本地开发机器性能有限时,可能没办法一下子启动全部的微服务,这就造成问题。
- 不启动,使用开发环境的nacos
- 开发环境肯定也部署了ABC三个服务,我只需要正常调用即可。
- 但是这又导致新的问题,如果我同时需要对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的属性进行修改调整。
这里需要复习两个知识:
- ApplicationContextAware
- 这是一个回调接口,用于获取应用程序上下文(ApplicationContext)。
- 当该类被初始化并注入到Spring容器中时,Spring会自动调用setApplicationContext方法,将应用程序上下文对象传递给该类。
- 以便在需要时可以通过该对象访问Spring容器中的其他bean。
- BeanFactoryPostProcessor
- 是一个Bean后置处理器接口,用于在Spring容器加载Bean的定义后,对Bean进行额外的处理。
- 当Spring容器实例化并配置所有的Bean定义后,会调用postProcessBeanFactory方法,允许对BeanFactory进行修改或执行其他的自定义逻辑。
因此我们可以大概获取到以下流程:
- 首先检查是否为本地环境且开启本地调用模式
- 获取配置文件中配置的服务名和端口
- 循环配置的服务名+端口
- 判断是否已启动
- 设置url
现在来展示代码实现:
先定义好类和配置文件,在本测试用例中:
- 定义了三个Feign调用类:FeginClient1、FeginClient2、FeginClient3
- 定义配置文件:application.yml和application-local.yml,并在appliaction.yml中配置active为local
- 在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;
}
}