跳到主要内容

Seata配置管理原理解析

· 阅读需 13 分钟

说到Seata中的配置管理,大家可能会想到Seata中适配的各种配置中心,其实今天要说的不是这个,虽然也会简单分析Seata和各配置中心的适配过程,但主要还是讲解Seata配置管理的核心实现

在讲配置中心之前,先简单介绍一下Server端的启动流程,因为这一块就涉及到配置管理的初始化,核心流程如下:

  1. 程序入口在Server#main方法中
  2. 获取port的几种形式:从容器中获取;从命令行获取;默认端口
  3. 解析命令行参数:host、port、storeMode等参数,这个过程可能涉及到配置管理的初始化
  4. Metric相关:无关紧要,跳过
  5. NettyServer初始化
  6. 核心控制器初始化:Server端的核心,还包括几个定时任务
  7. NettyServer启动

代码如下,删除了非核心代码

public static void main(String[] args) throws IOException {
// 获取port的几种形式:从容器中获取;从命令行获取;默认端口, use to logback.xml
int port = PortHelper.getPort(args);
System.setProperty(ConfigurationKeys.SERVER_PORT, Integer.toString(port));

// 解析启动参数,分容器和非容器两种情况
ParameterParser parameterParser = new ParameterParser(args);

// Metric相关
MetricsManager.get().init();

// NettyServer初始化
NettyRemotingServer nettyRemotingServer = new NettyRemotingServer(workingThreads);

// 核心控制器初始化
DefaultCoordinator coordinator = new DefaultCoordinator(nettyRemotingServer);
coordinator.init();

// NettyServer启动
nettyRemotingServer.init();
}

为社么说步骤3中肯能涉及到配置管理的初始化呢?核心代码如下:

if (StringUtils.isBlank(storeMode)) {
storeMode = ConfigurationFactory.getInstance().getConfig(ConfigurationKeys.STORE_MODE,
SERVER_DEFAULT_STORE_MODE);
}

如果在启动参数中没有特别指定storeMode,就会通过ConfigurationFactory相关API去获取该配置,在ConfigurationFactory.getInstance()这行代码中,涉及到两部分内容:ConfigurationFactory静态代码初始化和Configuration初始化。接下来我们重点分析这部分内容

配置管理初始化

ConfigurationFactory初始化

我们知道在Seata中有两个关键配置文件:一个是registry.conf,这是核心配置文件,必须要有;另一个是file.conf,只有在配置中心是File的情况下才需要用到。ConfigurationFactory静态代码块中,其实主要就是加载registry.conf文件,大概如下:

1、在没有手动配置的情况,默认读取registry.conf文件,封装成一个FileConfiguration对象,核心代码如下:

Configuration configuration = new FileConfiguration(seataConfigName,false);
FileConfigFactory.load("registry.conf", "registry");
FileConfig fileConfig = EnhancedServiceLoader.load(FileConfig.class, "CONF", argsType, args);

2、配置增强:类似代理模式,获取配置时,在代理对象里面做一些其他处理,下面详细介绍

Configuration extConfiguration = EnhancedServiceLoader.load(ExtConfigurationProvider.class).provide(configuration);

3、将步骤2中的代理对象赋值给CURRENT_FILE_INSTANCE引用,在很多地方都直接用到了CURRENT_FILE_INSTANCE这个静态引用

CURRENT_FILE_INSTANCE = extConfiguration == null ? configuration : extConfiguration;

可以简单的认为:CURRENT_FILE_INSTANCE对应的就是registry.conf里面的内容。我认为registry.conf这个文件名取的不太好,歧义太大,叫做bootstrap.conf是不是更好一些?

Configuration初始化

Configuration其实就是对应配置中心,Seata目前支持的配置中心很多,几乎主流的配置中心都支持,如:apollo、consul、etcd、nacos、zk、springCloud、本地文件。当使用本地文件作为配置中心的时候,涉及到file.conf的加载,当然这是默认的名字,可以自己配置。到这里,大家也基本上知道了registry.conffile.conf的关系了。

Configuration作为单例放在ConfigurationFactory中,所以Configuration的初始化逻辑也是在ConfigurationFactory中,大概流程如下: 1、先从registry.conf文件中读取config.type属性,默认就是file

configTypeName = CURRENT_FILE_INSTANCE.getConfig(ConfigurationKeys.FILE_ROOT_CONFIG + ConfigurationKeys.FILE_CONFIG_SPLIT_CHAR+ ConfigurationKeys.FILE_ROOT_TYPE);

2、基于config.type属性值加载配置中心,比如默认是:file,则先从registry.conf文件中读取config.file.name读取本地文件配置中心对应的文件名,然后基于该文件名创建FileConfiguration对象,这样就将file.conf中的配置加载到内存中了。同理,如果配置的是其他配置中心,则会通过SPI初始化其他配置中心。还有一点需要注意的是,在这阶段,如果配置中心是本地文件,则会为其创建代理对象;如果不是本地文件,则通过SPI加载对应的配置中心

if (ConfigType.File == configType) {
String pathDataId = String.join("config.file.name");
String name = CURRENT_FILE_INSTANCE.getConfig(pathDataId);
configuration = new FileConfiguration(name);
try {
// 配置增强 代理
extConfiguration = EnhancedServiceLoader.load(ExtConfigurationProvider.class).provide(configuration);
} catch (Exception e) {
LOGGER.error("failed to load extConfiguration:{}", e.getMessage(), e);
}
} else {
configuration = EnhancedServiceLoader
.load(ConfigurationProvider.class, Objects.requireNonNull(configType).name()).provide();
}

3、基于步骤2创建的Configuration对象,为其再创建一层代理,这个代理对象有两个作用:一个是本地缓存,不需要每次获取配置的时候都从配置中获取;另一个是监听,当配置发生变更会更新它维护的缓存。如下:

if (null != extConfiguration) {
configurationCache = ConfigurationCache.getInstance().proxy(extConfiguration);
} else {
configurationCache = ConfigurationCache.getInstance().proxy(configuration);
}

到这里,配置管理的初始化就完成了。Seata通过先先加载registry.conf文件获取对应的配置中心信息、注册中心,然后再根据获取到的的对应信息初始化配置中心。在使用本地文件作为配置中心的情况下,默认是加载file.conf文件。然后再为对应的配置中心创建对应的代理对象,使其支持本地缓存和配置监听

整理流程还是比较简单,在这里我要抛出几个问题:

  1. 什么是配置增强?Seata中的配置增强是怎么做的?
  2. 如果使用本地文件作为配置中心,就必须要将配置定义在file.conf文件中。如果是Spring应用,能不能直接将对应的配置项定义在application.yaml中?
  3. 在上面说的步骤2中,为什么在使用本地文件配置中心的情况下,要先为Configuration创建对应配置增强代理对象,而其他配置中心不用?

这3个问题都是紧密联系的,都和Seata的配置增加相关。下面详细介绍

配置管理增强

配置增强,简单来说就是为其创建一个代理对象,在执行目标独对象的目标方法时候,执行代理逻辑,从配置中心的角度来讲,就是在通过配置中心获取对应配置的时候,执行代理逻辑。

  1. 通过ConfigurationFactory.CURRENT_FILE_INSTANCE.getgetConfig(key)获取配置
  2. 加载registry.conf文件创建FileConfiguration对象configuration
  3. 基于SpringBootConfigurationProviderconfiguration创建代理对象configurationProxy
  4. configurationProxy中获取配置中心的连接信息file zk nacos 等
  5. 基于连接信息创建配中心Configuration对象configuration2
  6. 基于SpringBootConfigurationProviderconfiguration2创建代理对象configurationProxy2
  7. 执行configurationProxy2的代理逻辑
  8. 基于key找到对应的SpringBean
  9. 执行SpringBean的getXxx方法

配置增强实现

上面也简单提到过配置增强,相关代码如下:

EnhancedServiceLoader.load(ExtConfigurationProvider.class).provide(configuration);
  1. 首先通过SPI机获取一个ExtConfigurationProvider对象,在Seata中默认只有一个实现:SpringBootConfigurationProvider
  2. 通过ExtConfigurationProvider#provider方法为Configuration创建代理对象

核心代码如下:

public Configuration provide(Configuration originalConfiguration) {
return (Configuration) Enhancer.create(originalConfiguration.getClass(), new MethodInterceptor() {
@Override
public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy)
throws Throwable {
if (method.getName().startsWith("get") && args.length > 0) {
Object result = null;
String rawDataId = (String) args[0];
if (args.length == 1) {
result = get(convertDataId(rawDataId));
} else if (args.length == 2) {
result = get(convertDataId(rawDataId), args[1]);
} else if (args.length == 3) {
result = get(convertDataId(rawDataId), args[1], (Long) args[2]);
}
if (result != null) {
//If the return type is String,need to convert the object to string
if (method.getReturnType().equals(String.class)) {
return String.valueOf(result);
}
return result;
}
}

return method.invoke(originalConfiguration, args);
}
});
}

private Object get(String dataId) throws IllegalAccessException, InstantiationException {
String propertyPrefix = getPropertyPrefix(dataId);
String propertySuffix = getPropertySuffix(dataId);
ApplicationContext applicationContext = (ApplicationContext) ObjectHolder.INSTANCE.getObject(OBJECT_KEY_SPRING_APPLICATION_CONTEXT);
Class<?> propertyClass = PROPERTY_BEAN_MAP.get(propertyPrefix);
Object valueObject = null;
if (propertyClass != null) {
try {
Object propertyBean = applicationContext.getBean(propertyClass);
valueObject = getFieldValue(propertyBean, propertySuffix, dataId);
} catch (NoSuchBeanDefinitionException ignore) {

}
} else {
throw new ShouldNeverHappenException("PropertyClass for prefix: [" + propertyPrefix + "] should not be null.");
}
if (valueObject == null) {
valueObject = getFieldValue(propertyClass.newInstance(), propertySuffix, dataId);
}

return valueObject;
}

1、如果方法是以get开头,并且参数个数为1/2/3,则执行其他的获取配置的逻辑,否则执行原生Configuration对象的逻辑 2、我们没必要纠结为啥是这样的规则,这就是Seata的一个约定 3、其他获取配置的逻辑,就是指通过Spring的方式获取对应配置值

到这里已经清楚了配置增强的原理,同时,也可以猜测得出唯一的ExtConfigurationProvider实现SpringBootConfigurationProvider,肯定是和Spring相关

配置增强与Spring

在介绍这块内容之前,我们先简单介绍一下Seata的使用方式:

  1. 非Starter方式:引入依赖 seata-all, 然后手动配置几个核心的Bean
  2. Starter方式: 引入依赖seata-spring-boot-starter,全自动准配,不需要自动注入核心Bean

SpringBootConfigurationProvider就在seata-spring-boot-starter模块中,也就是说,当我们通过引入seata-all的方式来使用Seata时,配置增强其实没有什么作用,因为此时根本找不到ExtConfigurationProvider实现类,自然就不会增强。

seata-spring-boot-starter是如何将这些东西串联起来的?

1、首先,在seata-spring-boot-starter模块的resources/META-INF/services目录下,存在一个spring.factories文件,内容分如下

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
io.seata.spring.boot.autoconfigure.SeataAutoConfiguration,\

# 暂时不管
io.seata.spring.boot.autoconfigure.HttpAutoConfiguration

2、在SeataAutoConfiguration文件中,会创建以下Bean: GlobalTransactionScanner 、SeataDataSourceBeanPostProcessor、SeataAutoDataSourceProxyCreator、SpringApplicationContextProvider。前3个和我们本文要讲的内容不相关,主要关注SpringApplicationContextProvider,核心代码非常简单,就是将ApplicationContext保存下来:

public class SpringApplicationContextProvider implements ApplicationContextAware {
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
ObjectHolder.INSTANCE.setObject(OBJECT_KEY_SPRING_APPLICATION_CONTEXT, applicationContext);
}
}

3、然后,在SeataAutoConfiguration文件中,还会将一些xxxProperties.Class和对应的Key前缀缓存到PROPERTY_BEAN_MAP中。``xxxProperties就简单理解成application.yaml`中的各种配置项:

static {
PROPERTY_BEAN_MAP.put(SEATA_PREFIX, SeataProperties.class);
PROPERTY_BEAN_MAP.put(CLIENT_RM_PREFIX, RmProperties.class);
PROPERTY_BEAN_MAP.put(SHUTDOWN_PREFIX, ShutdownProperties.class);
...省略...
}

至此,整个流程其实已经很清晰,在有SpringBootConfigurationProvider配置增强的时候,我们获取一个配置项的流程如下:

  1. 先根据p配置项Key获取对应的xxxProperties对象
  2. 通过ObjectHolder中的ApplicationContext获取对应xxxProperties的SpringBean
  3. 基于xxxProperties的SpringBean获取对应配置的值
  4. 至此,通过配置增强,我们成功的获取到application.yaml中的值