【每日鲜蘑】Vert.x 随笔之vertx-config

19,423 阅读5分钟

vertx-config介绍

vertx-config主要负责vert.x的配置文件导入,可以导入jsonproperties等等格式的配置文件,配置文件的来源可以是本地文件http获取配置中心等等。

使其可用

<dependency>
    <groupId>io.vertx</groupId>
    <artifactId>vertx-config</artifactId>
</dependency>

SPI机制

如果你看过vert.x的源码,就会很清楚什么是SPI了,vert.x的模块大多是基于SPI实现的。这里简单介绍下SPI。

SPI 全称为 (Service Provider Interface)。

SPI 是JDK内置的一种服务提供发现机制。

SPI 是一种动态替换发现的机制。

比如有个接口,想运行时动态的给它添加实现,你只需要添加一个实现。我们经常遇到的就是java.sql.Driver接口,其他不同厂商可以针对同一接口做出不同的实现,mysqlpostgresql都有不同的实现提供给用户,而Java的SPI机制可以为某个接口寻找服务实现。

vertx-config的SPI

1. ConfigStore

ConfigStore是一个SPI的接口,用于指定配置文件来源,而具体实现都在io.vertx.config.impl.spi中,该package中可以看到很多实现类,如下

  • DirectoryConfigStore 从本地目录加载配置文件;
  • EventBusConfigStoreEventBus加载配置文件;
  • FileConfigStore 加载指定的配置文件;
  • HttpConfigStore 通过http请求获取配置文件;
  • JsonConfigStore 加载json格式的配置文件;
public interface ConfigStore {

  /**
   * Closes the configuration store.
   *
   * @param completionHandler handler called when the cleanup has been completed
   */
  default void close(Handler<Void> completionHandler) {
    completionHandler.handle(null);
  }

  /**
   * Retrieves the configuration store in this store.
   *
   * @param completionHandler the handler to pass the configuration
   */
  void get(Handler<AsyncResult<Buffer>> completionHandler);

}

vertx-config提供了常用的一些配置文件来源,在此基础上,我们也可以自己去扩展,比如从git获取配置文件,很简单的三个类就可以轻松实现,详细可以参考 github.com/vert-x3/ver…

2. ConfigProcessor

ConfigProcessor也是一个SPI的接口,用于指定配置文件解析的方式,而具体实现都在io.vertx.config.impl.spi中,该package中可以看到很多实现类,如下

  • JsonProcessorjson形式解析配置文件;
  • PropertiesConfigProcessorproperties形式解析配置文件;
  • RawProcessorraw形式解析配置文件;
public class JsonProcessor implements ConfigProcessor {

  @Override
  public void process(Vertx vertx, JsonObject configuration, Buffer input, Handler<AsyncResult<JsonObject>> handler) {
    try {
      JsonObject json = input.toJsonObject();
      if (json == null) {
        json = new JsonObject();
      }
      handler.handle(Future.succeededFuture(json));
    } catch (Exception e) {
      handler.handle(Future.failedFuture(e));
    }
  }

  @Override
  public String name() {
    return "json";
  }
}

如上是JsonProcessor的实现,看起来非常简单,将获取的文件内容Buffer直接转化成JsonObject并返回。

vert.x提倡异步,对外提供的服务也都是异步的,返回一个AsyncResult等待后续处理。

3. ConfigStoreFactory

ConfigStoreFactory 也是一个SPI的接口,是ConfigStore的工厂,而具体实现都在io.vertx.config.impl.spi中,该package中可以看到很多实现类,比如DirectoryConfigStoreFactory是用于创建DirectoryConfigStore实现类的工厂。

public class DirectoryConfigStoreFactory implements ConfigStoreFactory {
  @Override
  public String name() {
    return "directory";
  }

  @Override
  public ConfigStore create(Vertx vertx, JsonObject configuration) {
    return new DirectoryConfigStore(vertx, configuration);
  }
}

具体实现也非常简单,首先做了一个name标示是directory,其次实现了创建ConfigStore的方法。

4.总结

vertx-config对扩展是开放的,同时实现了部分常用的场景。我们在自定义业务时,只需要这三个SPI,即可将我们的业务嵌入其中,很简单,很快乐。

怎么运行起来的?

1. ConfigRetriever 加载

retriever = ConfigRetriever.create(vertx, options);
retriever.getConfig(ar -> {
    //...
});

ConfigRetriever是配置文件加载的入口,我们在Verticle中构建ConfigRetriever即可轻松实现配置文件的加载。

2. 定时刷新

static ConfigRetriever create(Vertx vertx, ConfigRetrieverOptions options) {
    ConfigRetrieverImpl retriever = new ConfigRetrieverImpl(vertx, options);
    retriever.initializePeriodicScan();
    return retriever;
}

ConfigRetrieverImplConfigRetriever的实现类,创建实现类后,同时通过initializePeriodicScan()初始化了定时刷新。

定时刷新的默认配置是5秒。

private static final long SCAN_PERIOD_DEFAULT = 5000L;

而检查更新的方法也非常简单,将新的配置文件覆盖到current中。(current = ar.result();

private void scan() {
    if (beforeScan != null) {
      beforeScan.handle(null);
    }
    compute(ar -> {
      if (ar.failed()) {
        streamOfConfiguration.fail(ar.cause());
        LOGGER.error("Error while scanning configuration", ar.cause());
      } else {
        synchronized (ConfigRetrieverImpl.this) {
          // Check for changes
          if (!current.equals(ar.result())) {
            JsonObject prev = current;
            current = ar.result();

            listeners.forEach(l -> l.handle(new ConfigChange(prev, current)));
            try {
              streamOfConfiguration.handle(current);
            } catch (Throwable e) {
              // Report the error on the context exception handler.
              if (vertx.exceptionHandler() != null) {
                vertx.exceptionHandler().handle(e);
              } else {
                throw e;
              }
            }
          }
        }
      }
    });
  }

在这个配置刷新功能中,还提供了监听配置前后变化的功能,如下

retriever.listen(change -> {
      JsonObject json = change.getNewConfiguration();
      vertx.eventBus().publish("new-configuration", json);
    });

3. ConfigRetrieverImpl 的创建

第一步,先将所有的ConfigStoreFactory工厂加载进来,按照键值组成一个nameToImplMap,供后面使用。
    ServiceLoader<ConfigStoreFactory> storeImpl =
        ServiceLoader.load(ConfigStoreFactory.class,
            ConfigStoreFactory.class.getClassLoader());

    Map<String, ConfigStoreFactory> nameToImplMap = new HashMap<>();
    storeImpl.iterator().forEachRemaining(factory -> nameToImplMap.put(factory.name(), factory));
    if (nameToImplMap.isEmpty()) {
      throw new IllegalStateException("No configuration store implementations found on the classpath");
    }
第二步,整理ConfigStoreOptions中配置信息。
    List<ConfigStoreOptions> stores = options.getStores();
    if (options.isIncludeDefaultStores()) {
      stores = new ArrayList<>();
      stores.add(
        new ConfigStoreOptions().setType("json")
          .setConfig(vertx.getOrCreateContext().config()));
      stores.add(new ConfigStoreOptions().setType("sys"));
      stores.add(new ConfigStoreOptions().setType("env"));

      // Insert the default config if configured.
      String defaultConfigPath = getDefaultConfigPath();
      if (defaultConfigPath != null  && ! defaultConfigPath.trim().isEmpty()) {
        String format = extractFormatFromFileExtension(defaultConfigPath);
        LOGGER.info("Config file path: " + defaultConfigPath + ", format:" + format);
        stores.add(new ConfigStoreOptions()
          .setType("file").setFormat(format)
          .setOptional(true)
          .setConfig(new JsonObject().put("path", defaultConfigPath)));
      }
      stores.addAll(options.getStores());
    }

vert.x设置了默认的ConfigStoreOptions,主要用于加载环境配置信息和vert.x的默认加载的配置文件。

第三步,根据配置信息,加载ConfigStoreConfigProcessor
    providers = new ArrayList<>();
    for (ConfigStoreOptions option : stores) {
      String type = option.getType();
      if (type == null) {
        throw new IllegalArgumentException(
            "the `type` entry is mandatory in a configuration store configuration");
      }

      ConfigStoreFactory factory = nameToImplMap.get(type);
      if (factory == null) {
        throw new IllegalArgumentException("unknown configuration store implementation: " +
            type + " (known implementations are: " + nameToImplMap.keySet() + ")");
      }

      JsonObject config = option.getConfig();
      if (config == null) {
        config = new JsonObject();
      }
      ConfigStore store = factory.create(vertx, config);

      String format = option.getFormat() != null ? option.getFormat() : "json";
      ConfigProcessor processor = Processors.get(format);
      if (processor == null) {
        throw new IllegalArgumentException("unknown configuration format: " + format + " (supported formats are: " +
            Processors.getSupportedFormats());
      }
      providers.add(new ConfigurationProvider(store, processor, option.getConfig(), option.isOptional()));
    }

通过源码可以看到ConfigStoreOptions中需要配置的信息

  • type是必填的;
  • type必须要有对应的ConfigStoreFactory实现;
  • config是选填的,如果为null,则会给初始化new JsonObject();
  • format是选填的,如果为null,则会给初始化json;
  • format必须要有对应的ConfigProcessor实现;

一般项目配置

一般配置ConfigRetrieverOptions时,看业务需求,可以选择禁用配置刷新缓存等等。 下面是我项目中的一个简单的配置,仅供参考。

private ConfigRetrieverOptions initOptions() {
        // 使用默认ConfigStore
        ConfigRetrieverOptions options = new ConfigRetrieverOptions().setIncludeDefaultStores(true);
        // 禁用配置刷新
        options.setScanPeriod(-1);
        // 导入需要加载的Verticle
        options.addStore(new ConfigStoreOptions()
            .setType("file")
            .setFormat("json")
            .setOptional(true)
            .setConfig(new JsonObject().put("path", "verticles.json")));
        // 导入SQL脚本
        options.addStore(new ConfigStoreOptions()
            .setType("file")
            .setFormat("json")
            .setOptional(true)
            .setConfig(new JsonObject().put("path", "sql.json")));
        // 导入阿里云基础配置
        options.addStore(new ConfigStoreOptions()
            .setType("file")
            .setFormat("json")
            .setOptional(true)
            .setConfig(new JsonObject().put("path", "conf/aliyun.json")));
        // 禁用缓存
        options.getStores().forEach(store -> {
            store.getConfig().put("cache", "false");
        });
        return options;
    }

总结

vertx-config 这个模块结构简单明了,SPI机制简明易懂,算是日常学习的好材料。其实在读vert.x的过程中,深深得体会到了vert.x的简洁。

vert.x给我的印象就是高效而不晦涩