2.4.Options

    本文主要讲解Options,它是Vert.x中的一个经典结构,很多地方都会使用到,虽然不同的使用场景中,它的名字有所差异,但在整个Vert.x中,不同种类的Options结构是近似的。

1. 基本介绍

    Vert.x中很多组件都搭载了Options的结构,它定义了Vert.x中许多组件使用的配置信息,如前文中看到的io.vertx.core.VertxOptions类,本文我会带大家去看看整个Vert.x框架中常用的Options。

    先看看下边代码(来自官方):

    // 直接创建
    Vertx vertx = Vertx.vertx();
    // 使用 VertxOptions 的创建
    Vertx vertx = Vertx.vertx(new VertxOptions().setWorkerPoolSize(40));

    上述代码是官方创建Vertx实例的介绍代码,实际上第一行代码的内部调用如:

// 位于:io.vertx.core.impl.VertxFactoryImpl 类中
  @Override
  public Vertx vertx() {
    return vertx(new VertxOptions());
  }

    简单说,VertxOptions 提供了Vertx实例需要使用的所有配置信息,如果开发人员不提供自定义的VertxOptions,那么Vert.x会使用内置默认的VertxOptions来实例化Vertx,这也是Options结构带了的福利,任何在Vert.x中运行的组件,不论是Router、Verticle还是HttpServer,都自带了一套默认的运行配置,开发人员甚至可以理解成零配置启动。但整个Vertx框架中Options的种类繁多,初学者往往会被不同的Options吓到。

2. 从VertxOptions出发

    开发人员不用去认识每一种不同的Options,只要理解了Options结构,就可以在任意场景随心所欲地使用不同类型的Options了,本章节对io.vertx.core.VertxOptions的结构进行解析,通过解析让开发人员理解如何去解读Vert.x中的Options结构并且彻底掌握它的使用原理。

2.1. Vertx中的codegen

    Vertx中的大部分Options都会引入了下边定义片段:

import io.vertx.codegen.annotations.DataObject;
// ... 其他 import

@DataObject(generateConverter = true, publicConverter = false)
public class VertxOptions {
    // ... 内容代码
}

    这个注解是Vert.x中的另外一个子项目vertx-codegen[^1]中的内容,Vert.x框架支持多种编程语言,它提供了一个很方便生成API的项目vertx-codegen,使用它可以简化多语言平台开发。Vert.x的官方介绍中,它有一个特性Ployglot,而这个项目就是实现Ployglot的桥梁,使用该项目很容易让开发的API支持多语言架构,这也是Vert.x中的一个亮点。如果包含了上边的注解@DataObject,则io.vertx.core.VertxOptionsConverter会自动生成,参考下边注释:

    // io.vertx.core.VertxOptionsConverter
    /**
     * Converter for {@link io.vertx.core.VertxOptions}.
     * NOTE: This class has been automatically generated from the 
     * {@link io.vertx.core.VertxOptions} original class using Vert.x codegen.
     */

「注」vertx-codegen的使用需要配置才能启用,而不是自动识别。

2.2. 基本结构

       Vert.x中的Options结构近似于JavaBean的基本规范,包含了set/get基本的API,其中get的API是类似的,直接返回里面配置的每个属性项,而setAPI会在原始的JavaBean规范之中有所改动。参考下边Java代码对比:

    // 普通 JavaBean 中的 set 方法
    public void setName(final String name){
        this.name = name;
    }

    // Options 中的 set 方法
    // 有些API中使用了 Fluent 注解,而有些未使用,主要和 codegen 的生成有关
    @Fluent
    public VertxOptions setName(final String name){
        this.name = name;
        return this;
    }

    上述代码段演示了两种不同风格的set方法的区别,而在Vert.x中所有的Options方法在设置数据时都使用了第二种,第一种是通用的JavaBean规范,而第二种就是Vert.x中的Fluent风格。

2.3. 构造和默认

    Options中的构造函数主要有三个(读者可理解成是绝对统一的,几乎所有的Options都包含这三个构造函数。)

默认无参构造函数

    public VertxOptions() {}

拷贝类型的构造函数

    public VertxOptions(VertxOptions other) {}

自动生成类构造函数(JsonObject作参数)

    public VertxOptions(JsonObject json) {
        this();
        VertxOptionsConverter.fromJson(json, this);
    }

**「注」**上述的第三种构造函数就是codegen自动生成的构造函数,它会在生成过程中生成Converter类,并且使用它对统一的JsonObject方法执行转换。

    Options中有默认无参的构造函数,而这些构造函数在构造Options对象时使用的默认值不是Java中的类型默认值,而是自定义的值,每个Options中都定义了静态公有变量对每个配置项提供默认值,定义片段如下:

  /**
   * The default value of quorum size = 1
   */
  public static final int DEFAULT_QUORUM_SIZE = 1;

  /**
   * The default value of Ha group is "__DEFAULT__"
   */
  public static final String DEFAULT_HA_GROUP = "__DEFAULT__";

  /**
   * The default value of HA enabled = false
   */
  public static final boolean DEFAULT_HA_ENABLED = false;

2.4. 常用Options

    到这里,Vert.x中的Options基本结构就分析清楚了,读者可以按照这种思路去解读其他所有的Options,在整个Vert.x框架中,Options的基础结构是一致的,这里列举常用的Options给读者参考(vertx-core项目中):

    上边枚举了vertx-core项目中所有涉及的Options结构的常用类和相关说明,对应的结构图如下:

    本章并不打算给读者讲解每一个Options的内容细节,主要是通过对Options结构的解读让读者掌握完整的Options结构的分析和理解方式,同时提供Vert.x中的标准的Options的整体关系,让读者从高处俯瞰整个Options部分的内容。

3. 开发Options

    看完了Options的整体结构,本章节我们来学习开发自定义的Options,为了让读者更加了解Options的结构,这里不使用codegen,而采用最原始的方式来编写Options部分的代码。Options的主要目的是提供配置项信息,它的一切都是以配置为基础,先看下边的配置片段:

zero:
    vertx:
        clustered:
            enabled: true
            manager: ""
            options: 
                key1: "value1"        # 临时添加,用于演示

    上述代码取自Zero内部集群管理器,读者不要误解,这里将要开发的并不是单独针对options配置项,而是clustered节点配置项,按照前天提到过的结构,先开发一个Converter,参考下边代码:

final class ClusterOptionsConverter {

    private ClusterOptionsConverter() {
    }

    static void fromJson(final JsonObject json, final ClusterOptions obj) {
        // 是否启用集群模式
        if (json.getValue("enabled") instanceof Boolean) {
            obj.setEnabled(json.getBoolean("enabled"));
        }

        // 如果包含 options 则直接转换成集群管理器所需配置,结构为 JsonObject
        if (json.getValue("options") instanceof JsonObject) {
            obj.setOptions(json.getJsonObject("options"));
        }

        // manager节点中的内容无法直接转换,它是一个Java类全名,定义了当前系统所使用的
        // ClusterManager的实现类
        final Object managerObj = json.getValue("manager");
        Fn.safeNull(() -> {
            final Class<?> clazz = Ut.clazz(managerObj.toString());
            Fn.safeNull(() -> {

                // 如果反射生成的Class<?>不为空,则实例化集群管理器
                final ClusterManager manager = Ut.instance(clazz);
                obj.setManager(manager);
            }, clazz);
        }, managerObj);
    }
}

    其实这个Converter中由于包含了类名,并且要实例化成该类对应的对象,所以也是无法直接使用codegen生成代码的原因,上述代码的基本格式参考了Vert.x中内置生成的Converter格式,而最后的manager节点的处理则是自定义逻辑,Fn.safeNull是非空安全执行类,保证在处理过程中不会读取到任何null的值,而Ut.clazzUt.instance底层则是直接使用反射执行类加载和对象实例化的操作。写好了Converter的代码后,再来看看ClusterOptions部分:

public class ClusterOptions implements Serializable {

    private static final boolean ENABLED = false;
    private static final ClusterManager MANAGER = new HazelcastClusterManager();
    private static final JsonObject OPTIONS = new JsonObject();

    private boolean enabled;
    private ClusterManager manager;
    private JsonObject options;

    public ClusterOptions() {
        this.enabled = ENABLED;
        this.manager = MANAGER;
        this.options = OPTIONS;
    }

    public ClusterOptions(final ClusterOptions other) {
        this.enabled = other.isEnabled();
        this.manager = other.getManager();
        this.options = other.getOptions();
    }

    public ClusterOptions(final JsonObject json) {
        this();
        ClusterOptionsConverter.fromJson(json, this);
    }

    public boolean isEnabled() {
        return this.enabled;
    }

    @Fluent
    public ClusterOptions setEnabled(final boolean enabled) {
        this.enabled = enabled;
        return this;
    }

    public ClusterManager getManager() {
        return this.manager;
    }

    @Fluent
    public ClusterOptions setManager(final ClusterManager manager) {
        this.manager = manager;
        return this;
    }

    public JsonObject getOptions() {
        return this.options;
    }

    @Fluent
    public ClusterOptions setOptions(final JsonObject options) {
        this.options = options;
        return this;
    }

    @Override
    public String toString() {
        return "ClusterOptions{enabled=" + this.enabled
                + ", manager=" +
                ((null == this.manager) ? "null" : this.manager.getClass().getName())
                + ", options="
                + this.options.encode() + '}';
    }
}

    按照Options的结构,一个完整的ClusterOptions就开发好了。在自定义Options的过程中,这里我把Class<?>压到底层作为了配置项,这是没有直接使用codegen的原因,它包含了部分自定义的反射逻辑,并且为整个程序拿到ClusterManager的引用。从配置这个概念来讲,这种做法比较混淆,但是从实际使用效果上看来,这样做也有好处。

    在真实系统中,如果使用面向接口编程,那么实现类会自然引入可配置的特性,一旦可配置,真正代码中就不可能通过new X()的方式构造,这样的系统往往会变得灵活,但付出的代价就是牺牲代码的健壮性,而此时只能通过开发人员自身来保证代码质量。主要问题如:

  1. 类名异常:开发人员有可能因为拼写错而导致常见的ClassNotFound的异常,如果这个类名配置是必须的则比较好处理,抛出异常也能提示开发人员这里有问题,直接将异常信息打印出来都可行;但是如果这个类名配置仅仅是可选配置,就意味着即使这里发生了ClassNotFound,系统可以直接提供默认行为忽略该配置,而不是直接抛出异常信息,这种情况下,自定义代码逻辑的优势就很明显了。

  2. 接口冲突:在配置这种带反射信息的数据时,还容易犯的一个错就是接口实现问题,有可能你配置的类本身并没有实现你所期望的接口,这种情况下,即使类加载成功了,在实例化过程中通常会因为类型原因导致实例化失败。但是,我们通常期望在实例化失败时,系统依旧可以运行,系统是忽略还是抛出异常取决于本身的业务场景。

    约定确实是个好东西,但它毕竟不是约束,再完美的约定都无法保证人为去破坏它,从编程角度讲这是可以的,但从系统角度讲,这样会破坏系统的严谨性。为了让系统在任何场景下都可以适配配置数据,就需要在Options的构造过程中尽可能保证不出错,这也是Options存在于Vert.x中的意义,如果读者仔细去研究所有Options的代码,它都存在默认值,这个目的就是为了保证任何情况下,组件本身拥有一套可运行的配置。在项目开发中,配置数据是面向开发人员而不是用户,不论是静态配置还是动态配置,都需要做到完美约定——即在任何情况下,配置数据都不能出错,一旦出错系统就会变得不稳定,所以作为开发人员在书写自定义Options的过程中,需要仔细思考、设计和编码,阻击所有有可能的错误发生,做到底层稳定(所以读者才会看到上述代码中繁琐的Fn.safeNull,既检查输入又保证输出)。

    开发了自定义的Options过后,您就可以在您的代码中使用了,最后提供一段消费Options的代码,读者慢慢去体会一下:

        // 读取集群配置
        final ClusterOptions cluster = ZeroGrid.getClusterOption();
        if (cluster.isEnabled()) {
            final ClusterManager manager = cluster.getManager();
            logger.info(Info.APP_CLUSTERD, manager.getClass().getName(),
                    manager.getNodeID(), manager.isActive());
            // 集群启动器函数
            fnCluster.accept(manager, consumer);
        } else {
            // 单独启动器函数
            fnSingle.accept(consumer);
        }

    早在第一个章节我们演示了如何开发一个完整的启动器,这里不累赘介绍fnCluster/fnSingle的内部逻辑,一旦自定义了Options过后,您就可以在自己的接口中定义对应类型,实现接口约定式的开发和调用了。

4. 总结

    本章节我们主要学习了Vert.x中的Options架构,除了了解原生框架提供的基本结构以外,还使用例子告诉读者如何定义自己的Options来实现Vert.x组件对配置数据的消费。从我的使用经验可以知道,这种结构在很多场景下比直接使用JsonObject让系统更具有结构性,所以在适当的组件开发过程中,使用Options衔接组件和配置也是一种比较不错的策略。

[^1]: Vert.x API Generation, https://github.com/vert-x3/vertx-codegen, Vert.x的codegen子项目。

最后更新于