2.5.Store

    前文讲解了Vert.xOptions结构的用法,本文在Options的基础之上来看另外一种结构:Store。其实我一直在思考用什么方式来翻译这个词,由于它在整个框架中的特殊性,我没有更好的方式给它一个中文命名,所以最终还是用Store贯穿全文,而不改变它的本来含义。

    本章涉及的Store这个结构并不在vertx-core项目中,它存在于其中一个子项目vertx-config里面[^1]。

1. 基本介绍

    vertx-config项目主要为您编写的Vert.x的应用提供配置信息,它是为微服务设计的一个子项目,您可以将它理解成Vert.x的核心配置管理器,在官方定义中,它支持四种主要功能:

  • 提供多种格式的配置数据,如JSON、属性文件(properties)、YAML、Hocon等。

  • 提供多种不同的存储(Store)实体,如:文件系统、目录、HTTP、Git、Redis、系统属性、环境变量等。

  • 您可以自己定义配置的存储顺序以及加载顺序。

  • 除此之外,您还可以在运行过程中重配置Vert.x中编写好的组件。

虽然本文不会详细介绍vertx-config这个子项目,但为了让读者更容易理解Store这种数据结构,那么我们去扒一扒这个子项目也是有必要的。

    学习之前,读者可以先思考一个问题:为什么要关注配置?其实不论是Vert.x还是Spring以及其他应用框架,配置是少不了的一部分。零配置从真正的实战看起来,只是一个宣传的噱头,真正能够做到零配置上生产的框架,几乎凤毛菱角,而我个人理解中的零配置并不该表没有配置,相反:由框架本身提供系统可以在生产环境运行的比较优化的配置信息,让开发人员结合实际的生产运行环境,去探索这个环境中的最优配置,以达到应用在生产环境稳定、长久、可靠地运行。

    相信有一定经验的开发人员心有感触:配置给编写应用带了的福利远比编码带了的福利更直接,包括很多公司在研发业务型产品的时:使用可定制性去应付业务需求的变化,一直是产品的核心目标,而可定制型的基础,就是如何玩配置。

    Java的初学者对配置的入门篇莫过于JDBC的学习,但凡学习Java的开发人员,在完成了基础语法学习过后,必过关卡就是JDBC,相信很多开发人员对下边这段代码都有久违的感觉:

    try{
        Class.forName("com.mysql.jdbc.Driver");
        Connection con =
DriverManager.getConnection("jdbc:mysql://localhost:3306/DB_TEST","root","******");
    }catch(Exception ex){
        ex.printStackTrace();
    }

    当然在今天这个时代,上述代码是很少出现在生产环境的,最少里面的参数部分绝对会被提取出来,存放在一个配置文件中,如:

jdbc.host=localhost
jdbc.port=3306
jdbc.username=root
jdbc.password=******

    此时,您就已经在使用配置 + 开发的方式玩转你的系统了——为什么如此做?请读者思考一个最简单的问题:您的代码是不是想要访问不同的数据库?以后的迁移是不是希望在不重新编译代码的基础上平滑迁移到另外一个数据库上边?那么……相信读者都有了自己的答案。反思生活,其实程序和配置就像衣服和人一样,人可以在出席不同场合的时候去换衣服,而不是将整个人的生命重来一次,这就是配置的隐喻。回到本章的主题,vertx-config这个项目,就是给您编写的Vert.x应用提供的最好的配置管理器,而Store就是存储这些配置的机制,这个机制在底层的实现可以多样化,您可以将您的配置存储在不同的地方,想要使用的时候可以通过它实现很方便的调用。

2. ConfigRetriever的初遇

2.1. 示例

    接下来我们先来看一段示例代码,看看如何使用ConfigRetriever这个类,先在你的 Maven 环境中加入以下配置(Gradle部分可参考官方站点):

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

    参考下边Java代码:

package io.vertx.up._02.store;

import io.vertx.config.ConfigRetriever;
import io.vertx.config.ConfigRetrieverOptions;
import io.vertx.config.ConfigStoreOptions;
import io.vertx.core.Vertx;
import io.vertx.core.json.JsonObject;

public class ConfigFirst {
    public static void main(final String[] args) {
        final Vertx vertx = Vertx.vertx();

        // 构造 Store Option
        final ConfigStoreOptions storeOptions = new ConfigStoreOptions()
                .setType("file")
                .setConfig(new JsonObject().put("path", "data.json"));

        // 构造 Retriever Option
        final ConfigRetrieverOptions options = new ConfigRetrieverOptions()
                .addStore(storeOptions);

        // 创建 Retriever
        final ConfigRetriever retriever = ConfigRetriever.create(vertx, options);
        retriever.getConfig(handler -> {
            if (handler.succeeded()) {
                final JsonObject result = handler.result();
                System.out.println(result.encodePrettily());
            }
        });
    }
}

    简单总结一下使用步骤:

  1. 在环境中拿到 Vertx 实例(这个过程在前边章节已经提过,此处不重复)。

  2. 先创建不同的 ConfigStoreOptions(此处使用的就是前文提到的Options对象),示例代码中只使用了一个,这一步可以创建多个 ConfigStoreOptions,如果创建了多个,则可以在addStore的时候依次添加。

  3. 创建 Retriever 所需的 ConfigRetrieverOptions 对象。

  4. 调用ConfigRetriever.create创建所需的ConfigRetriever对象。

    上边代码演示了使用它的基本流程,运行过后就可以在控制台看到如下输出:

{
  "config" : "Hello Store"
}

    输出内容则是data.json文件的内容。

2.2. 分析

    ConfigRetriever本身是接口,不是类,底层实现类默认使用了io.vertx.config.impl.ConfigRetrieverImpl类,该接口提供了一个静态方法(接口中定义静态方法是在 Java 8.0 引入的特性),该方法定义如下:

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

    static ConfigRetriever create(Vertx vertx) {
        return create(vertx, (new ConfigRetrieverOptions()).setIncludeDefaultStores(true));
    }

       如果不传入ConfigRetrieverOptions对象,那么将使用默认场景下的配置,默认场景下,Vert.x中会使用三个 Store 对象:

                ((List)stores).add((new ConfigStoreOptions()).setType("json")
                        .setConfig(vertx.getOrCreateContext().config()));
                ((List)stores).add((new ConfigStoreOptions()).setType("sys"));
                ((List)stores).add((new ConfigStoreOptions()).setType("env"));

    读者也许好奇,它将打印什么信息?这里我截取一部分:

      "...": "...",
      "awt.toolkit" : "sun.lwawt.macosx.LWCToolkit",
      "java.vm.info" : "mixed mode",
      "java.version" : "1.8.0_262",
      "...": "...",
      "LESS" : "-R",
      "LC_CTYPE" : "UTF-8",
      "HOME" : "/Users/lang"

    实际上默认情况内置使用了三个对象:

  • io.vertx.config.impl.spi.EnvVariablesConfigStore:读取系统环境变量。

  • io.vertx.config.impl.spi.SystemPropertiesConfigStore:读取JVM环境中的系统属性——调用 System.getProperties()。

  • io.vertx.config.impl.spi.JsonConfigStore:读取 Vertx 中的配置属性——调用vertx.getOrCreateContext().config()获得配置信息。

    该对象会将三者的信息合并到一起统一输出,所以读者就可以看到上述相关片段,不仅仅如此,影响该输出的还有一个配置,该配置会指定一个文件路径,然后实例化一个io.vertx.config.impl.spi.FileConfigStore类来读取某个文件路径下的相关配置信息,配置部分检测代码如下:

        String value = System.getenv("VERTX_CONFIG_PATH");
        if (value == null || value.trim().isEmpty()) {
            value = System.getProperty("vertx-config-path");
        }

        if (value != null && !value.trim().isEmpty()) {
            return value.trim();
        } else {
            File file = ((VertxInternal)this.vertx).resolveFile(DEFAULT_CONFIG_PATH);
            boolean exists = file != null && file.exists();
            return exists ? file.getAbsolutePath() : null;
        }

    它会先检查VERTX_CONFIG_PATH环境变量,如果没有设置,则依次检查vertx-config-path的系统属性,检查都失败时,会直接去读取静态变量DEFAULT_CONFIG_PATH中存储的内容,该路径默认指向:conf/config.json,但开发人员一旦输入了自己的ConfigStoreOptions对象,上述默认流程就会直接失效,控制默认配置的方法为:setIncludeDefaultStores

    那么到这里,请读者思考一个问题?所有的配置都通过addStore的方法添加到环境中,而不同的 Store 最终返回的数据格式都是Json格式,而且最终的result只有一个 JsonObject 对象。如果两个相同的 Store 中出现了同名键,Vert.x会怎么处理呢?——这就是官方文档中出现Overloading Rules章节的原因。这里的规则很简单:谁最后添加,那么冲突的配置就会使用最后添加的 Store 的!这里使用官方的例子,如系统中存在两个 Store 配置:

    a的配置如:

{
    "a": "value1",
    "b": 1
}

    b的配置如:

{
    "a": "value2",
    "c": 2
}

    如果addStore的顺序是A、B,那么最终结果就是:

{
    "a": "value2",
    "b": 1,
    "c": 2
}

    反之,如果addStore的顺序是B、A,那么最终结果就是:

{
    "a": "value1",
    "b": 1,
    "c": 2
}

    理解了上述讲解过后,读者应该对ConfigRetriever有一定的了解了,读者可以自己去环境中运行一下该例子,并且深入源代码去一探究竟。

3. 配置消费

    看完了上边关于ConfigRetriever部分的内容,本节开始,我们就正式进入配置数据的消费流程来仔细看看如何在Vert.x中消费读取到的配置信息。到这里其实可以简单给Store一个定义了:Store是Vert.x中抽象出来的用来定义配置数据存储机制的一种数据结构,它定义了配置数据的来源、格式、加载顺序等相关信息,扮演了 Vert.x中组件组件配置的桥梁。由于Store是一个抽象结构,使用它来定义配置有几个实战上的优势:

  • 消费端使用统一的JsonObject格式实现多源异构配置消费,而Store统一了不同数据来源的数据格式。

  • 在编写Vert.x组件过程中,组件本身只需要消费JsonObject配置数据信息,而不需要考虑底层数据格式是否真正意义上的Json格式(如读取yaml、hocon等),从Vert.x中组件调用Store这套机制开始,它拿到的配置数据就是格式统一的。

    结合前文中提到的Options结构,读者可以按照下图理解:

    在这种场景下,所有的Vert.x框架中的组件在读取配置数据时变得更加单一,而且易于构造。细心的读者会发现,在创建Vert.x中组件时通常使用了工厂模式创建,通过传入Vertx实例来完成整个组件的实例化过程,并且在实例化过程中,一般会提供第二参数的重载方法,类似前边提到的create,而第二参要么是JsonObject,要么是 2.4 章节中提到的Options——这是贯穿整个Vert.x框架中的结构,类似一种约定。

我在开发Zero时,所有的底层插件扩展也采用了这样的模式,通过Vertx实例和Options来完成插件的开发,以保证整个插件和框架本身的一致性,遗憾的是那个时候对Store机制不熟,所以在读取配置数据时依旧使用了传统的IO读取模式,支持的格式主要包括Yaml格式、Json格式。

3.1. ConfigStore 分类

    进入消费场景之前,先看看ConfigStore的默认支持类型,上述示例代码中使用了setType来定义底层Store类型,实际上这里是在选择Store的实现。Vert.x框架中所有的Store实现类如下:

  • io.vertx.config.impl.spi.DirectoryConfigStore

  • io.vertx.config.impl.spi.EnvVariablesConfigStore

  • io.vertx.config.impl.spi.EventBusConfigStore

  • io.vertx.config.impl.spi.FileConfigStore

  • io.vertx.config.impl.spi.HttpConfigStore

  • io.vertx.config.impl.spi.JsonConfigStore

  • io.vertx.config.impl.spi.SystemPropertiesConfigStore

    这里不解释上述每个Store的具体用途,读者可以通过名字来判断,主要提一下setType的参数应该怎么写,方便读者在开发过程中尽可能少犯错。这个地方想吐槽?其实Vert.x在设计这部分参数时如果要使用系统内置,可直接采用Java中的枚举参数,以确认开发人员不会因为拼写原因而犯错,很遗憾它采用了字符串的参数格式,不同的字符串对应不同底层Store的选择。——真是这样么?其实不是,这种设计的好处是易于扩展,Vert.x框架中在实例化Store时大部分使用了工厂模式,而匹配的值调用了name()方法来实现,如果您要编写属于自己的Store时可以参考原生实现,通过定义自己的名字就完成了直接扩展,如果这里使用了枚举值,就会导致扩展Store时被限定了,只能再开第二扩展配置来完成Store的整体扩展开发——所以,枚举类型的设计在此处反而不易于扩展开发,所以此处是没有槽点的。

    常见代码如下:

ConfigStoreOptions store = new ConfigStoreOptions()
  .setType("file")
  .setFormat("hocon")
  .setConfig(new JsonObject()
    .put("path", "my-config.conf")
  );

    setType方法的参数决定了底层的Store默认实现,读者可以参考下边表格:

**「注」**推荐读者简单记忆一下上述几种不同的参数值,保证在使用默认Vert.x框架的Store时不去犯一些拼写级别的错,在实战过程中,最好的方式是写一个静态类,用该类来实例化想要的 Store 实现以完成代码的无错映射,——主要是防止拼写错(前车之鉴)。

3.2. 实例化组件

    配置数据的存在目的是提供给组件消费用,前一个章节了解了Vert.x框架中默认使用的底层Store实现,接下来先看几个官方站点提供的示例信息:

3.2.1. 创建Vertx实例

    在您的环境中先创建一份配置文件:

{
    "haGroup": "__HA_DEFAULT__",
    "haEnabled": true,
    "workerPoolSize": 30
}

    然后执行下边代码来创建Vertx实例

package io.vertx.up._02.store;

import io.vertx.config.ConfigRetriever;
import io.vertx.config.ConfigRetrieverOptions;
import io.vertx.config.ConfigStoreOptions;
import io.vertx.core.Vertx;
import io.vertx.core.VertxOptions;
import io.vertx.core.json.JsonObject;

public class ConfigDeploy {

    public static void main(final String[] args) {
        // 构造 Store Option
        final Vertx vertx = Vertx.vertx();
        final ConfigStoreOptions storeOptions = new ConfigStoreOptions()
                .setType("file")
                .setConfig(new JsonObject().put("path", "vertx.json"));

        // 构造 Retriever Option
        final ConfigRetrieverOptions options = new ConfigRetrieverOptions()
                .addStore(storeOptions);

        // 创建 Retriever
        final ConfigRetriever retriever = ConfigRetriever.create(vertx, options);
        retriever.getConfig(handler -> {
            if (handler.succeeded()) {
                final JsonObject config = handler.result();
                // 关闭原始 vertx 实例
                System.out.println("Will close old. " + vertx.hashCode());
                vertx.close();
                // 创建一个新的
                final VertxOptions vertxOptions = new VertxOptions(config);
                final Vertx vertxNew = Vertx.vertx(vertxOptions);
                // 调用发布代码发布 Verticle
                // .......
                System.out.println("Vertx has been created. " + vertxNew.hashCode());
            }
        });
    }
}

    代码执行后,您不仅可以看到相关输出,还可以看到控制台会有一句警告:

You're already on a Vert.x context, are you sure you want to create a new Vertx instance?

    也许读者会问,为什么呢?这里看到这句警告的主要原因是因为Vert.x中大量方法使用了异步调用模式,包括这里的close()方法,虽然该方法调用了,但由于方法本身是异步的,所以在原始的Vertx实例没有关闭完成时,下边的代码就已经在执行了。

警告在编程过程中,不是什么好东西,在时间允许的情况下,希望读者养成一个系统,就是消除系统中所有的警告信息,让整个系统运行在零警告的环境中,写出高质量的程序。

    为了不看见上述警告日志,将代码做一定修改:

                // 关闭原始 vertx 实例
                System.out.println("Will close old. " + vertx.hashCode());
                vertx.close(closed -> {
                    // 创建一个新的
                    final VertxOptions vertxOptions = new VertxOptions(config);
                    final Vertx vertxNew = Vertx.vertx(vertxOptions);
                    // 调用发布代码发布 Verticle
                    // .......
                    System.out.println("Vertx has been created. " + vertxNew.hashCode());
                });

    这样就只能看到正常的输出信息了(您的 hashCode 值可能和我这边有所不同):

Will close old. 199698313
Vertx has been created. 274710202

    示例中读取了Store里存储的配置数据信息,然后使用该配置实例化一个VertxOptions对象,并且用该对象创建了一个新的 Vertx实例,这里我们使用的Store是FileConfigStore,该实现会从文件路径中去读取信息。这里的文件路径并不是Maven项目中的src/main/resources路径,这里的文件路径就是执行该程序的路径。

**「注」**如果您使用的是 IDEA,那么您可以在Edit Configurations...窗口中直接设置Working directory作为程序运行路径,我们的配置文件就是放在这个路径下的。

    还有一点值得思考的地方是,这段代码的使用场景是什么?其实从整个代码流程可以看到,我们的目的是创建一个新的Vertx实例,在整个Vert.x环境中,最好的解决方案是维持一个Vertx实例,如果要创建一个新的,你必然会收到前文提到的警告信息,那么什么情况下我们会去创建一个新的Vertx实例呢?我能想到的一个场景是在我们目前生产环境中的一个场景,就是做调参热部署。我们在客户的生产环境部署了一套基于Zero框架的系统,该系统用于执行集成接口的对端请求,而在执行过程中,我们加入了一个组件,该组件会记录下来运行状况,并且根据不同集成端的压力去修正VertxOptions的数据,每次修正完成过后,会关闭原始的Vertx实例,然后创建一个新的Vertx实例,并且在生产环境不通过编码的方式替换原始Vertx实例。替换完成过后,Zero会运行在新的Vertx实例中,而原来的Vertx实例会直接被替换掉。

3.2.2. 创建Verticle实例

    Vert.x框架编程中有一个重要任务就是开发各种不同的Verticle以完成不同职责的组件,Verticle组件依赖Vertx实例来发布,一旦发布完成,它就会运行在Vertx实例中了,本节我们来看看如何借用Store机制来创建Verticle实例。为了让读者有更深入的了解,我把Store进行了分离,让它从文件系统中读取不同格式的配置信息。

    要运行本节的示例,需要在环境中引入下边的依赖:

    <dependency>
        <groupId>io.vertx</groupId>
        <artifactId>vertx-config-hocon</artifactId>
        <version>4.3.3</version>
    </dependency>
    <dependency>
        <groupId>io.vertx</groupId>
        <artifactId>vertx-config-yaml</artifactId>
        <version>4.3.3</version>
    </dependency>

    先创建三个配置文件:verticle.json,verticle.yaml,verticle.conf,三个文件的文件内容如下:

**「注」**Verticle的Options结构使用的是DeploymentOptions,这个在前文提到过,这里强调一下帮助读者记忆,防止写成了VerticleOptions(Vert.x框架中是没有这个类的)。

verticle.json

{
    "config2": {
        "worker": true,
        "instance": 10
    }
}

verticle.conf

config1 {
    worker = false
    instance = 12
}

verticle.yaml

config3:
    worker: true
    instance: 11

    将文件放在对应的目录中,运行下边Java代码:

package io.vertx.up._02.store;

import io.vertx.config.ConfigRetriever;
import io.vertx.config.ConfigRetrieverOptions;
import io.vertx.config.ConfigStoreOptions;
import io.vertx.core.DeploymentOptions;
import io.vertx.core.Vertx;
import io.vertx.core.json.JsonObject;

public class ConfigMore {

    public static void main(final String[] args) {
        final Vertx vertx = Vertx.vertx();

        // 构造 Store
        final ConfigStoreOptions configStore1 = new ConfigStoreOptions()
                .setType("file")
                .setFormat("hocon")
                .setConfig(new JsonObject().put("path", "verticle.conf"));
        final ConfigStoreOptions configStore2 = new ConfigStoreOptions()
                .setType("file")
                .setConfig(new JsonObject().put("path", "verticle.json"));
        final ConfigStoreOptions configStore3 = new ConfigStoreOptions()
                .setType("file")
                .setFormat("yaml")
                .setConfig(new JsonObject().put("path", "verticle.yaml"));

        // 构造 Retriever Option
        final ConfigRetrieverOptions options = new ConfigRetrieverOptions()
                .addStore(configStore1)
                .addStore(configStore2)
                .addStore(configStore3);
        // 创建 Retriever
        final ConfigRetriever retriever = ConfigRetriever.create(vertx, options);
        retriever.getConfig(handler -> {
            if (handler.succeeded()) {
                final JsonObject config = handler.result();
                System.out.println(config.encodePrettily());
                // ......
                final DeploymentOptions options1 =
                        new DeploymentOptions(config.getJsonObject("config1"));
                final DeploymentOptions options2 =
                        new DeploymentOptions(config.getJsonObject("config2"));
                final DeploymentOptions options3 =
                        new DeploymentOptions(config.getJsonObject("config3"));

                vertx.deployVerticle(ConfigVerticle.class, options1);
                vertx.deployVerticle(ConfigVerticle.class, options2);
                vertx.deployVerticle(ConfigVerticle.class, options3);
            }
        });
    }
}

    上边代码执行后,您会在控制台看到配置信息的输出:

{
  "config1" : {
    "instance" : 12,
    "worker" : false
  },
  "config2" : {
    "worker" : true,
    "instance" : 10
  },
  "config3" : {
    "worker" : true,
    "instance" : 11
  }
}

    这样,您就可以直接使用这些配置去发布Verticle了,如果在Verticle的start()方法中打印线程信息,您可以看到如下输出:

vert.x-eventloop-thread-4
vert.x-worker-thread-2
vert.x-worker-thread-3

    上述输出可以证明配置生效。关于其他几类不同Store的用法,读者可以参考官方教程中的示例代码去体会,这里就不啰嗦了,演示部分代码的目的是让读者更深入理解Store在整个Vert.x框架中扮演的角色,并且可以得心应手地使用vertx-config项目,为自己项目中的配置部分奠定基础。

4. 从源码看自定义

    本章的最后一个小节,我带大家去分析一下源代码,来看实战项目中如何自定义一个Store,它的具体步骤如下(可以二选一):

添加ConfigProcessor支持不同格式。 添加ConfigStoreFactory支持底层的Store配置实现。

    简单说,如果只是做格式层面的处理,您可以只扩展ConfigProcessor,而不去自定义Store整个结构,受到影响的将会是setFormat方法;但如果您要切换存储机制,那么就不得不去实现新的ConfigStoreFactory接口,将整个Store重新实现,此时受到影响的就会是setType方法。

4.1. 整体结构

    Vert.x框架中的Factory接口定义如下:

package io.vertx.config.spi;

import io.vertx.core.Vertx;
import io.vertx.core.json.JsonObject;

public interface ConfigStoreFactory {
    String name();

    ConfigStore create(Vertx var1, JsonObject var2);
}

    该接口有两个函数,name()函数返回的就是setType的参数,而create方法返回的就是一个ConfigStore对象,除此之外,您还需要定义另外一个实现对象Store:

package io.vertx.config.spi;

import io.vertx.core.AsyncResult;
import io.vertx.core.Handler;
import io.vertx.core.buffer.Buffer;

public interface ConfigStore {
    default void close(Handler<Void> completionHandler) {
        completionHandler.handle((Object)null);
    }

    void get(Handler<AsyncResult<Buffer>> var1);
}

    一旦实现了上述两个接口过后,您就可以在实现层编写自己的Store来完成Store的定制。

4.2. type = file

    本文不打算分析所有框架内的内置Store源代码,这里只是针对Store机制的自定义实现进行抛砖引玉,让读者更理解底层细节,最终可以定义自己的Store。系统内置的文件存储读取由两个类来实现。

FileConfigtoreFactory源码

这里不是拼写错,应该是开发人员少写了个S导致的这个名字,所以读者不要觉得奇怪。

package io.vertx.config.impl.spi;

import io.vertx.config.spi.ConfigStore;
import io.vertx.config.spi.ConfigStoreFactory;
import io.vertx.core.Vertx;
import io.vertx.core.json.JsonObject;

public class FileConfigtoreFactory implements ConfigStoreFactory {
    public FileConfigtoreFactory() {
    }

    public String name() {
        return "file";
    }

    public ConfigStore create(Vertx vertx, JsonObject configuration) {
        return new FileConfigStore(vertx, configuration);
    }
}

    Factory部分的代码很简单,这里的name()方法就返回了前文中的setType("file")使用的参数,而另外一个方法创建了一个新的ConfigStore对象。

FileConfigStore源码

package io.vertx.config.impl.spi;

import io.vertx.config.spi.ConfigStore;
import io.vertx.core.AsyncResult;
import io.vertx.core.Handler;
import io.vertx.core.Vertx;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.json.JsonObject;

public class FileConfigStore implements ConfigStore {
    private Vertx vertx;
    private String path;

    public FileConfigStore(Vertx vertx, JsonObject configuration) {
        this.vertx = vertx;
        this.path = configuration.getString("path");
        if (this.path == null) {
            throw new IllegalArgumentException("The `path` configuration is required.");
        }
    }

    public void get(Handler<AsyncResult<Buffer>> completionHandler) {
        this.vertx.fileSystem().readFile(this.path, completionHandler);
    }
}

    FileConfigStore类只有一段代码主逻辑,它直接读取传入路径(path参数)中的文件数据,转换成字节流,存储在Buffer对象中,然后Vert.x中的异步处理器会执行该结果。前文已经提到过,Store和Factory会对setType的参数产生影响,它的自定义流程会定义不同的Store,而Processor可以说是为FileConfigStore量身打造的格式处理组件,定制它会直接影响setFormat的参数。如果要开发一个自定义的Processor则需要实现ConfigProcessor接口:

package io.vertx.config.spi;

import io.vertx.core.AsyncResult;
import io.vertx.core.Handler;
import io.vertx.core.Vertx;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.json.JsonObject;

public interface ConfigProcessor {
    String name();

    void process(Vertx var1, JsonObject var2, 
                 Buffer var3, Handler<AsyncResult<JsonObject>> var4);
}

    ConfigProcessor接口中总共定义了四个参数,前边两个参数和Store以及Factory有些雷同,第一参数传入Vertx实例引用,第二参数传入JsonObject格式的配置数据。这里讲讲后两个参数。前文中的FileConfigStore的get方法传入了一个Handler<AsyncResult<Buffer>>,这个类型我们将在下一章节揭开面纱,读者只需要理解一点——最终真实返回的数据类型是Buffer,而延生到Processor中,这里的第三参就是Store中的输出,它会作为Processor的数据输入。

    引用前文中的处理流程图,我们就清楚知道,Store传递给ConfigRetriever的数据结构是Buffer,将Buffer统一成JsonObject就是ConfigRetriever做的事情,这点可以在源代码中得到证明。补充一点:Vert.x框架对开发人员而言,使用最多的数据结构是JsonObject,所以我们常常说Json格式闯天下,但在它内部,很多时候采用的数据格式是Buffer(Stream模式),内部组件相互传输数据的过程多使用了Buffer。

同样的,在Zero框架中,我也使用了Buffer做内部数据传输格式,这种设计对开发人员更友好,在真正的业务场景中,JsonObject是人易于理解的一种结构,往往操作Buffer会比较繁琐,所以一般业务场景中,JsonObject会比Buffer更合适。

    如果您理解了FileConfigStore中的Handler<AsyncResult<Buffer>>参数,那么第四个参数在这里就不用详细解析了,它最终会产生一个JsonObject的输出,这个输出也反映在我们本章最早的示例代码的handler.result()中。

    最后再看看系统内置的JsonProcessor的源代码:

JsonProcessor源码

package io.vertx.config.impl.spi;

import io.vertx.config.spi.ConfigProcessor;
import io.vertx.core.AsyncResult;
import io.vertx.core.Future;
import io.vertx.core.Handler;
import io.vertx.core.Vertx;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.json.JsonObject;

public class JsonProcessor implements ConfigProcessor {
    public JsonProcessor() {
    }

    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 var6) {
            handler.handle(Future.failedFuture(var6));
        }

    }

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

4.3. 及锋而试

    本小节我带大家来尝试写一个自定义Store的框架代码,并且部署到环境中运行起来,让读者及锋而试,加深对Store这个结构的理解,并且学会如何自定义Store实现扩展开发。

4.3.1. 源代码

TestStore源码

package io.vertx.up._02.store;

import io.vertx.config.spi.ConfigStore;
import io.vertx.core.AsyncResult;
import io.vertx.core.Future;
import io.vertx.core.Handler;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.json.JsonObject;

import java.util.Objects;

public class TestStore implements ConfigStore {
    private final JsonObject configuration;

    public TestStore(final JsonObject configuration) {
        if (Objects.isNull(configuration)) {
            this.configuration = new JsonObject();
        } else {
            this.configuration = configuration;
        }
    }

    public void get(final Handler<AsyncResult<Buffer>> completionHandler) {
        System.out.println("传入配置:" + this.configuration.encodePrettily());
        final Buffer buffer = Buffer.buffer();
        final JsonObject output = new JsonObject();
        final String node = this.configuration.getString("node");
        if (Objects.nonNull(node)) {
            output.put(node, "测试配置输出");
        }
        completionHandler.handle(Future.succeededFuture(output.toBuffer()));
    }
}

TestFactory源码

package io.vertx.up._02.store;

import io.vertx.config.spi.ConfigStore;
import io.vertx.config.spi.ConfigStoreFactory;
import io.vertx.core.Vertx;
import io.vertx.core.json.JsonObject;

public class TestStoreFactory implements ConfigStoreFactory {
    public TestStoreFactory() {

    }

    public String name() {
        return "test";
    }

    public ConfigStore create(final Vertx vertx, final JsonObject configuration) {
        return new TestStore(configuration);
    }
}

TestFirst执行代码

package io.vertx.up._02.store;

import io.vertx.config.ConfigRetriever;
import io.vertx.config.ConfigRetrieverOptions;
import io.vertx.config.ConfigStoreOptions;
import io.vertx.core.Vertx;
import io.vertx.core.json.JsonObject;

public class TestFirst {
    public static void main(final String[] args) {
        final Vertx vertx = Vertx.vertx();

        // 构造 Store Option
        final ConfigStoreOptions storeOptions = new ConfigStoreOptions()
                // 自定义类型
                .setType("test")
                // 传入配置
                .setConfig(new JsonObject().put("node", "store"));

        // 构造 Retriever Option
        final ConfigRetrieverOptions options = new ConfigRetrieverOptions()
                .addStore(storeOptions);

        // 创建 Retriever
        final ConfigRetriever retriever = ConfigRetriever.create(vertx, options);
        retriever.getConfig(handler -> {
            if (handler.succeeded()) {
                final JsonObject result = handler.result();
                System.out.println(result.encodePrettily());
            }
        });
    }
}

4.3.2. 执行分析

    上述代码可以正常执行?如果这样想你就错了,不出意外,你会收到下边这段异常信息:

unknown configuration store implementation: test \
    (known implementations are: [event-bus, file, json, http, env, sys, directory])

    这并不是偶然,而是因为整个应用中少写了一份配置,这个配置牵涉到Java中SPI(Service Provider Interface)的内容;在您的项目类路径下创建META-INF/services/io.vertx.config.spi.ConfigStoreFactory文件(无后缀名),它的内容如下:

io.vertx.up._02.store.TestStoreFactory

    然后再执行上边的代码,就会得到以下输出:

传入配置:{
  "node" : "store"
}
{
  "store" : "测试配置输出"
}

    将上边的执行代码修改一下:

        // 构造 Store Option
        final ConfigStoreOptions storeOptions = new ConfigStoreOptions()
                // 自定义类型
                .setType("test")
                // Format
                .setFormat("test")
                // 传入配置
                .setConfig(new JsonObject().put("node", "store"));

    然后在环境中创建一个Processor

package io.vertx.up._02.store;

import io.vertx.config.spi.ConfigProcessor;
import io.vertx.core.AsyncResult;
import io.vertx.core.Future;
import io.vertx.core.Handler;
import io.vertx.core.Vertx;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.json.JsonObject;

public class TestProcessor implements ConfigProcessor {
    @Override
    public String name() {
        return "test";
    }

    @Override
    public void process(final Vertx vertx, final JsonObject jsonObject,
                        final Buffer buffer, final Handler<AsyncResult<JsonObject>> handler) {
        final JsonObject data = buffer.toJsonObject();
        data.put("processor", "执行Processor");
        handler.handle(Future.succeededFuture(data));
    }
}

    运行过后您就会得到新的输出:

{
      "store" : "测试配置输出",
      "processor" : "执行Processor"
}

    同样Vert.x的Processor也是基于SPI设计的,所以在执行之前需要配置:META-INF/services/io.vertx.config.spi.ConfigProcessor文件才可确保执行。

    到这里,我们已经自定义了vertx-config中的核心组件Factory, Store和Processor,并且在环境中成功运行起来,自定义部分的旅途也可以告一段落,最后通过一张图再让我们回顾一下它们相互之间的关系:

5. 总结

    本章主要讲解了Store这种数据结构,通过对Store本身的介绍以及部分源代码的结构分析,让读者对Vert.x中的Store有一个直观的认识,并且以点带面,透过Store详细讲解了vertx-config这个子项目的各个方面,虽然没有提供实战场景,但根据本章的案例代码,读者应该可以掌握这部分的设计和实现思路,对Vert.x中的配置管理有更深入的认识。

[^1]: Vertx Config的Java版官方手册, https://vertx.io/docs/vertx-config/java/, vertx-config子项目。

最后更新于