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是类似的,直接返回里面配置的每个属性项,而set
API会在原始的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
项目中):
组件 | 父类 | Options | 备注 |
---|---|---|---|
Vertx | x | VertxOptions | Vertx实例专用配置 |
Verticle | x | DeploymentOptions | Verticle实例专用配置 |
DnsClient | x | DnsClientOptions | 网络DNS客户端配置 |
x | x | AddressResolverOptions | 网络DNS内部寻址配置 |
FileSystem | x | CopyOptions | 文件拷贝配置 |
FileSystem | x | OpenOptions | 文件打开配置 |
FileResolver | x | FileSystemOptions | 文件系统配置 |
VertxMetrics | x | MetricsOptions | 监控指标配置 |
x | x | NetworkOptions | 网络顶层配置 |
DatagramSocket | NetworkOptions | DatagramSocketOptions | UDP专用配置 |
x | NetworkOptions | TCPSSLOptions | TCP和SSL专用配置 |
EventBus | TCPSSLOptions | EventBusOptions | EventBus对应配置 |
x | TCPSSLOptions | ClientOptionsBase | 客户端抽象配置 |
HttpClient | ClientOptionsBase | HttpClientOptions | HTTP客户端配置 |
NetClient | ClientOptionsBase | NetClientOptions | 网络客户端配置 |
NetServer | TCPSSLOptions | NetServerOptions | 网络服务器配置 |
HttpServer | NetServerOptions | HttpServerOptions | HTTP服务器配置 |
x | TCPSSLOptions | ServerOptionsBase | 新版空配置(保留) |
HttpClient | x | RequestOptions | HTTP请求配置 |
WebSocket | RequestOptions | WebSocketConnectOptions | WebSocket配置 |
MessageX | x | DeliveryOptions | 消息传输专用配置 |
x | x | TrustOptions(接口) | 可信任客户端配置 |
x | x | KeyCertOptions(接口) | 客户端证书配置 |
x | x | JksOptions | Jks证书配置 |
x | x | PfxOptions | Pfx证书配置 |
x | x | PemTrustOptions | 受信任配置 |
x | x | SSLEngineOptions | SSL引擎配置 |
x | SSLEngineOptions | JdkSSLEngineOptions | JDK ssl引擎实现配置 |
x | SSLEngineOptions | OpenSSLEngineOptions | Open ssl引擎实现配置 |
上边枚举了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.clazz
和Ut.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()
的方式构造,这样的系统往往会变得灵活,但付出的代价就是牺牲代码的健壮性,而此时只能通过开发人员自身来保证代码质量。主要问题如:
- 类名异常:开发人员有可能因为拼写错而导致常见的
ClassNotFound
的异常,如果这个类名配置是必须的则比较好处理,抛出异常也能提示开发人员这里有问题,直接将异常信息打印出来都可行;但是如果这个类名配置仅仅是可选配置,就意味着即使这里发生了ClassNotFound
,系统可以直接提供默认行为忽略该配置,而不是直接抛出异常信息,这种情况下,自定义代码逻辑的优势就很明显了。 - 接口冲突:在配置这种带反射信息的数据时,还容易犯的一个错就是接口实现问题,有可能你配置的类本身并没有实现你所期望的接口,这种情况下,即使类加载成功了,在实例化过程中通常会因为类型原因导致实例化失败。但是,我们通常期望在实例化失败时,系统依旧可以运行,系统是忽略还是抛出异常取决于本身的业务场景。
在真实项目开发中,约定确实是个好东西,但它毕竟不是约束,再完美的约定都无法保证人为去破坏它,从编程角度讲这是可以的,但从系统角度讲,这样会破坏系统的严谨性。为了让系统在任何场景下都可以适配配置数据,就需要在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
的内部逻辑。
4. 总结
1. Vert.x API Generation, https://github.com/vert-x3/vertx-codegen, Vert.x的codegen
子项目。 ↩