3.4.一切都是Handler
Handler是Vert.x中最常见的一种行为结构,也是Vert.x中为什么最小JDK支持为8.0的主要原因——没错,它使用了JDK 8.0引入的lambda语法,您在官方文档中也许看到最多的代码就如下:
vertx.setPeriodic(1000, id -> {
// This handler will get called every second
System.out.println("timer fired!");
});
从此处开始,您就打开了Handler的大门,它的格式类似JS中常用的回调函数,也是前文提到的Vert.x中启动周期和请求周期的一个分水岭,在上述示例中,内部打印语句并不会立即执行,您可以理解vertx.setPeriodic
在部署代码,部署好之后每隔一秒lambda函数内的打印语句会执行一次,这种思路充斥着整个Vert.x官方教程,如果单纯依靠这种写法也容易陷入代码中的回调地狱,本章节我会带领大家一起感受Handler的各种玩法。
使用Handler之前,我们先看看Vert.x中的基本定义,理解了基本定义后,回过头来看Handler的基本用法会更加得心应手,读者需要牢记几个思考基础:
Handler在主代码过程中通常只用于绑定(函数的延迟调用原理),并不会执行函数内部代码。
Handler的内部代码会在满足条件时执行,一般是触发式的(这点很重要,您的断点打在什么位置是最需要您理解的)。
Vert.x中大部分Handler是异步调用,读者需要区分延迟调用和异步调用,二者在某些地方是等价而在某些地方是不等价的,Vert.x中的异步调用通常会追加
AsyncResult
作数据的封装容器。
1. Handler接口
Handler的主接口定义如下:
@FunctionalInterface
public interface Handler<E> {
/**
* Something has happened, so handle it.
*
* @param event the event to handle
*/
void handle(E event);
}
它是一个函数式接口(@FunctionalInterface
修饰),基本接口十分简单,此处我们讨论下这个方法背后的设计。在整个Vert.x中,实现了这个接口的类不少,您可以将它理解成Vert.x框架中行为的神经系统,由于这个方法的返回值是void
,意味着它可以支持异步也可支持同步,Vert.x内部的核心系统基础实际是回调模式,您可以在该方法的实现中直接书写类似:
@Override
public void mount(final Route route, final RRecord record) {
route.handler(res -> {
// 异步回调:Handler内部代码,该代码无返回值
});
route.blockingHandler(res -> {
// 同步回调
})
}
遵循Vert.x的黄金法则,如果您要绑定同步的Handler,则尽可能找到类似blockingHandler
的方法来绑定,该绑定内部由Vert.x执行调度,不会让您的程序出现阻塞;您若直接在handler
方法中绑定同步代码,很有可能导致线程阻塞问题;若您的执行代码已经处理过同步转异步的操作,则可无视上述法则——禁止直接绑定同步代码依然是在Vert.x开发过程中牢记的法则。
所以Handler接口的实现类大部分执行方式如下图(参考上边例子):

上图中Route对象就是编程过程中调用handler
的对象,而此时执行的绑定(并不执行Handler内部代码),即Vert.x中Web流程的启动周期;而下边触发事件时的虚线数据流才会执行Handler的内部代码,即Vert.x中Web流程的请求周期,两部分代码不是同时执行的,这一点需读者牢记。
比较有意思的是,如果调用blockingHandler
执行了同步绑定,这些看似在Route对象内部的代码最终都会执行Context
上下文对象中的同步调度块,而不在组件内部,整个Vert.x框架中的大部分组件都基于此原则——大部分调度代码最终由Context
统一完成。读者可以直接跟踪route.blockingHandler
的源代码如下:
// 代码文件:io.vertx.core.impl.ContextImpl
PoolMetrics metrics = workerPool.metrics();
Object queueMetric = metrics != null ? metrics.submitted() : null;
Promise<T> promise = context.promise();
Future<T> fut = promise.future();
try {
Runnable command = () -> {
Object execMetric = null;
if (metrics != null) {
execMetric = metrics.begin(queueMetric);
}
context.dispatch(promise, f -> {
try {
blockingCodeHandler.handle(promise);
} catch (Throwable e) {
promise.tryFail(e);
}
});
if (metrics != null) {
metrics.end(execMetric, fut.succeeded());
}
};
Executor exec = workerPool.executor();
if (queue != null) {
queue.execute(command, exec);
} else {
exec.execute(command);
}
} catch (RejectedExecutionException e) {
// Pool is already shut down
if (metrics != null) {
metrics.rejected(queueMetric);
}
throw e;
}
return fut;
细心的读者会发现,Vert.x整个框架中的组件在初始化时都包含了初始化组件的API函数,该函数的参数签名一般是(Vertx,JsonObject)
格式或(Vertx,Options)
格式,Vertx实例的引用就是通过它传入到组件内部,让所有组件共享的。阅读上述源代码之前,您需回顾一下两个基本概念:函数引用和函数调用:
函数引用:通常构造如:
var fn = xxx
,构造出来的fn
本身是一个函数,如Java8之后的java.util.function.Function
对象,在构造该函数时,您只是使用fn
变量获取了一个函数引用,而此时函数没有执行。函数调用:这个概念最简单,就是我们通常说的方法调用。
代码中的blockingCodeHandler
就是一个函数引用,它的类型就是本小节提到的Handler
,所以在下边这行代码之前,函数内部的绑定代码都不会运行:
blockingCodeHandler.handle(promise);
如果您区分了函数引用和函数调用的概念,理解Handler就轻而易举了,最后说明一点:是否异步执行取决于函数调用时的上下文(代码块),而不是函数引用的上下文,在Vert.x环境中对比如下:
// 假设此处的 data 是普通Java对象,并非Vert.x中的Future或Promise
// 同步执行
blockingCodeHandler.handle(data);
// 异步执行,等待当前future完成之后
future.onComplete(res -> {
blockingCodeHandler.handle(data);
})
上述概念代码就是Handler的核心原理,至于blockingCodeHandler
引用函数的内部代码是同步还是异步取决于函数内部的实现代码,回到本小节最初提到的第三点:如果您已经拿到了一个函数引用,那么该函数就具备了延迟调用的功能,因为这个函数会在您触发它的时候执行(如执行handler.handle(xx)
),而触发之前环境中只是维持了该函数引用;若函数内代码本身是同步的,那么只是单纯的延迟调用,若函数内代码是异步的,那么它才是异步调用。之所以会将延迟调用和异步调用混淆,是因为二者具备极高的相似性,它们都是延迟得到执行结果,但从原理上分析,二者存在本质的区别。
2. AsyncResult接口
看完了Handler之后,我们继续屠城。
如果说Handler的设计是用来区分函数引用和函数调用,那么另外一个接口io.vertx.core.AsyncResult
就是用来区分同步调用和异步调用,该接口的定义如下:
public interface AsyncResult<T> {
// 其他方法定义
}
理解接口之前先看一个图示:

上图描述了AsyncResult
接口中所有API的概念图,它是一个典型的Monad容器,容器内数据类型为T
,包含了两种状态:成功和失败,每种状态提供了判断函数以及读取数据的函数,失败时读取的数据为一个Java语言中的Throwable
对象;由于map
和otherwise
是绑定的函数,函数中实现同步或异步由开发人员自定。
参考下边的简单代码:
package io.vertx.up._03.handler;
import io.vertx.core.Future;
public class AsyncMain {
public static void main(final String[] args) {
first().map(AsyncMain::add5).onSuccess(res -> {
if (res.succeeded()) {
System.out.println(res.result() + "," + Thread.currentThread().getName());
}
});
System.out.println("-------------------------");
first().map(AsyncMain::addx).onFailure(res -> {
res.printStackTrace();
System.out.println("Error, " + Thread.currentThread().getName());
});
System.out.println("-------------------------");
first().map(AsyncMain::addDefaultX)
.otherwise(AsyncMain::addDefault).onComplete(res -> {
if (res.succeeded()) {
System.out.println("Default, " + res.result());
} else {
System.out.println("Other Error");
}
});
}
private static Integer addDefault(final Throwable error) {
error.printStackTrace();
System.out.println("Default, 3 " + Thread.currentThread().getName());
return 3;
}
private static Integer addDefaultX(final int seed) {
throw new RuntimeException("Default, " + seed);
}
private static Future<Integer> addx(final int seed) {
throw new RuntimeException("Err, " + seed);
}
private static Future<Integer> add5(final int seed) {
System.out.println("Seed, " + seed + " " + Thread.currentThread().getName());
return Future.succeededFuture(5 + seed);
}
private static Future<Integer> first() {
System.out.println("First, 10 " + Thread.currentThread().getName());
return Future.succeededFuture(10);
}
}
运行该代码您会得到如下信息输出:
First, 10 main
Seed, 10 main
15,main
-------------------------
First, 10 main
Error, main
-------------------------
First, 10 main
Default, 3 main
java.lang.RuntimeException: Err, 10
at io.vertx.up._03.handler.AsyncMain.addx(AsyncMain.java:40)
at io.vertx.core.impl.future.Mapping.onSuccess(Mapping.java:35)
at io.vertx.core.impl.future.FutureBase.emitSuccess(FutureBase.java:60)
at io.vertx.core.impl.future.SucceededFuture.addListener(SucceededFuture.java:88)
at io.vertx.core.impl.future.FutureBase.map(FutureBase.java:108)
at io.vertx.core.impl.future.SucceededFuture.map(SucceededFuture.java:27)
at io.vertx.up._03.handler.AsyncMain.main(AsyncMain.java:14)
java.lang.RuntimeException: Default, 10
at io.vertx.up._03.handler.AsyncMain.addDefaultX(AsyncMain.java:36)
at io.vertx.core.impl.future.Mapping.onSuccess(Mapping.java:35)
at io.vertx.core.impl.future.FutureBase.emitSuccess(FutureBase.java:60)
at io.vertx.core.impl.future.SucceededFuture.addListener(SucceededFuture.java:88)
at io.vertx.core.impl.future.FutureBase.map(FutureBase.java:108)
at io.vertx.core.impl.future.SucceededFuture.map(SucceededFuture.java:27)
at io.vertx.up._03.handler.AsyncMain.main(AsyncMain.java:19)
Default, 3
上述代码中有三个例子:
第一个例子最简单,全程没有任何异常抛出,所以最终调用了
onSuccess
方法打印了最终结果,它的数据变化如:10 -> 15
,最终输出结果。第二个例子中,强制性在
addx
方法内抛出了一个异常,最终调用onFailure
方法捕捉异常发生后的情况。第三个例子中绑定了otherwize函数,并返回了同步结果
Integer
,最终调用onComplete
方法来捕捉结果。
开发人员在使用AsyncResult<T>
一定要注意此处的T
类型,在关注类型时,T
类型本身很简单的,比较复杂的是转换之后的类型。代码中的第三个例子在调用map
时绑定的是同步函数,也就意味着最终转换出来的结果类型已经是Integer
,此时如果您直接使用onSuccess
,那么都不用执行下边的检查代码:
if (res.succeeded()) {
System.out.println("Default, " + res.result());
} else {
System.out.println("Other Error");
}
此处根据map/otherwise
两个方法的定义来理解:
default <U> AsyncResult<U> map(Function<T, U> mapper)
default AsyncResult<T> otherwise(Function<Throwable, T> mapper)
两个函数在执行过程中,不会去理睬返回值(map
中的T
,otherwise
中的U
)是一个异步结构还是一个同步结构,通常开发人员可以选择两种方式捕捉结果:
onSuccess/onFailure
配对,这种模式下函数参数就已经是内部结果T
或U
了。onComplete
,这种模式下您拿到的其实是原始结果,它并没有执行任何数据提取操作。
发现问题没?其实map
和otherwize
构造出来的实际是类似Java中lambda的map
效果,它是一个函数链,并不是字面理解的异步数据流(我们所有的例子此时都还是同步的),如果您把第一个例子中的onSuccess
改成onComplete
,您会发现res的数据结构形如AsyncResult<Future<Integer>>
,和期望的AsyncResult<Integer>
不同,这就是map
带来的效果——函数返回什么内容,那么它就解析成什么类型,并对此类型执行AsyncResult封装,至于该类型是同步还是异步,它不关心。map
和otherwize
构造了如下的一种数据结构:

所有的函数执行都会有成功和失败两种状态,而Java语言中的函数本身不支持多返回值,于是有了AsyncResult<T>
对函数执行结果的数据和状态进行封装,如此,函数即使出现异常,也只会生成一个失败状态的AsyncResult<T>
而不是以异常的方式抛出,这也遵循了函数式语言中Monad的特性,所以从这点意义上讲,AsyncResult<T>
本质就是函数式编程中最子元的Monad结构,它最大的改动就是让您的函数可以返回双态。
您也许会问,既如此,为何AsyncResult<T>
的字面名字会叫异步结果?主要原因是AsyncResult
在Vert.x中是以接口的形式定义,它的API形如Monad,而最终这个Monad的实现是同步还是异步取决于实现类,Vert.x中最常用的两个实现类是FutureImpl和PromiseImpl,这两货内部实现都是异步数据流,因此这个名字就实至名归了。
3. 再谈Future<T>/Promise<T>
Future<T>/Promise<T>
紧接前一章节提到的AsyncResult<T>
,本章我们再来温习两个可爱的小宝贝:Future和Promise,只是这次我们换个视角,从整体结构来解读Vert.x中这部分内容:
3.1. 整体结构
[I]标记是接口,[A]标记是抽象类。

从结构图上可知:
Future
的类型是前文提到的AsyncResult<T>
。Promise
的类型则是Handler<AsyncResult<T>>
。
Future<T>
接口定义中存在几个和Handler
直接相关的方法:
// 成功时回调
Future<T> onSuccess(Handler<T> handler)
// 失败时回调
Future<T> onFailure(Handler<Throwable> handler)
// 完成时回调(双态)
Future<T> onComplete(Handler<AsyncResult<T>> handler)
这三个方法最早是没有的,3.x中原始版本最常用的方法是setHandler
,该方法等价于onComplete
,从3.8
开始该方法被标记为废弃(@Deprecated
),4.x
之后就被移除了,这种设计是因为早期代码经常会是如下写法:
future.setHandler(res -> {
if(res.succeeded()){
// 成功返回
}else{
// 失败返回
}
})
虽然这种写法会使得函数本身更趋近于全函数,但在开发过程中,我们的业务层面有时候只关心单边状态,如成功时如何,又或者失败时如何,如果一直使用setHandler
绑定函数,那么系统中会到处充斥着上述结构的代码,这样会显得冗余,所以取而代之提供了onSuccess
和onFailure
的绑定模式。
也就是说上述示例代码:
first().map(AsyncMain::add5).onSuccess(res -> {
if (res.succeeded()) {
System.out.println(res.result() + "," + Thread.currentThread().getName());
}
});
也可直接取消res.succeeded()
的判断,直接写成:
first().map(AsyncMain::add5).onSuccess(res -> {
System.out.println(res.result() + "," + Thread.currentThread().getName());
});
此处牵涉一个简单的编程思维问题:Handler是否有必要处处都检查?这个问题可根据实际情况而定,我们在编程过程中通常会有A调用B的模式,是否检查取决于函数的内部实现,如果内部实现是可信任的,即使用了类似前文提到的otherwise
方法绑定异常情况,那么就没有必要检查,而若内部实现会访问网络、数据库、文件系统等无法预知的资源,除了使用try-catch
执行Checked
异常转换,也可以让部分Runtime
的异常抛出,丢给Handler来检查(res.failed()
)。也许有人会说处处检查更完美,但完美的东西不一定实用,况且对代码本身的维护也是一种成本,所以根据分析,您就可以择优而从,因地制宜了。
3.2. 三态
前文一直提到函数执行要么成功,要么失败,此处的三态如何讲呢?其实还有一种就是Handler中同时包含了成功和失败,因为FutureBase
有三个直接子类,三者的统计如下:
FutureImpl
onComplete
(default)
同时支持两种状态。
SucceededFuture
onSuccess
public
成功时专用。
FailedFuture
onFailure
public
失败时专用。
由于FutureImpl类的访问域是default的,所以基本不会使用,但内部是如此设计,若您要理解Vert.x中的结构,不可越过它;在3.8.x
版本之前,Future类有一个直接构造方法Future.future()
,FutureImpl类就是为它量身打造的,而之后的版本进行了细分设计,该方法就被废弃了,取而代之的就是Promise.promise()
。开发人员在使用Future中常用的代码如:
// 成功
Future.succeededFuture(t);
// 失败
Future.failedFuture(throwable);
上述两个方法会构造SucceededFuture实例和FailedFuture实例,这两个类的出现是细粒度设计的体现,二者一个职责是表示成功,另外一个职责就是表示失败,它们的区别如下:
二者构造函数不同
// 成功 public SucceededFuture(T result) // 失败 public FailedFuture(Throwable t)
置留的空方法不同,由于这两个类都是单态,所以对另外一种状态会存在忽略不计的情况,若您在书写时使用了反向绑定(如SucceededFuture调用了onFailure绑定)可能什么也不会发生,这也是开发过程中容易被忽视的点。
// SucceededFuture中的 onFailure 方法 public Future<T> onFailure(Handler<Throwable> handler) { return this; } // FailedFuture中的 onSuccess 方法 public Future<T> onSuccess(Handler<T> handler){ return this; }
FutureBase中有两个只有子类(protected
域)才能访问的主方法emitSuccess(T,Listener<T>)
和emitFailure(Throwable,Listener<T>)
,这两个方法是整个Future/Promise的核心逻辑,FutureBase的构造函数如下:
// 未绑定Vert.x中的上下文对象Context
FutureBase() {
this(null);
}
// 绑定了Vert.x中的上下文对象Context
FutureBase(ContextInternal context) {
this.context = context;
}
到此处,相信读者对下边几个方法已经有答案了:
Future什么时候是异步的,什么时候是同步的?
Future和Promise的本质是什么?
在Vert.x中,Future本身是同时支持同步和异步的,在同步模式中,它可以不和Context上下文环境产生任何关系,此种模式下可直接触发绑定的Handler实现同步转异步的操作(只是代码模式转成了异步,是否异步同样取决于绑定Handler的内部实现);若Future和Context上下文环境相关联,那么它就具备了先天性的异步特征。
Future和Promise不同的点是继承的接口,前者从AsyncResult<T>
继承,后者则从Handler<AsyncResult<T>>
继承。Future本质上是一种数据结构,您可以将它理解成异步容器,它将数据结果封装在容器内部,提供给函数链消费,而其本身就是标准的Monad结构;Promise本质上是一种行为,它在3.8.x
之前都是不存在的,它是从原版的Future中剥离出来的。——二者实则是异步数据和异步行为的一种分离设计,各司其职。
之所以一直对Future/Promise念念不忘,重复讲解,主因是二者不论在Vert.x内部还是Future风格的开发中随处可见,若您不掌握二者的用法,可能在开发过程中举步维艰。
4. 其他Handler
上述提到的Handler是整个Vert.x中的Handler主结构,Vert.x中多数API都使用了该结构,到这里,相信您再去阅读Vert.x的源码就更加轻车熟路了,我们跟着目前所见之初瞅瞅它,让读者对一切都是Handler有更深入的印象。
4.1. Vertx初始化
Vertx实例在初始化时支持两种模式:单机模式和集群模式,单机模式的启动是同步创建,集群模式则是异步实现,二者函数签名如:
// VertxBuilder中的代码
// 单机模式
public Vertx vertx();
// 集群模式
public void clusteredVertx(Handler<AsyncResult<Vertx>> handler)
于是就有了在初始化集群时的官方代码:
/*
* Vertx中的clusteredVertx方法内部封装如
* new VertxBuilder(options).init().clusteredVertx(resultHandler);
* VertxBuilder会优先读取配置并执行组件初始化,再调用上边提到的构造函数实例化Vertx
*/
VertxOptions options = new VertxOptions();
Vertx.clusteredVertx(options, res -> {
if (res.succeeded()) {
Vertx vertx = res.result();
EventBus eventBus = vertx.eventBus();
System.out.println("We now have a clustered event bus: " + eventBus);
} else {
System.out.println("Failed: " + res.cause());
}
});
4.2. Verticle的部署
Vertx实例从部署Verticle开始就已经是异步部署了,部署组件没有同步模式,它的API形如:
// Future封装
Future<String> deployVerticle(Verticle verticle, DeploymentOptions options)
// Handler模式
void deployVerticle(Verticle verticle, DeploymentOptions options,
Handler<AsyncResult<String>> completionHandler)
官方有一段代码:
// 标准模式
vertx.deployVerticle("com.mycompany.MyOrderProcessorVerticle", res -> {
if (res.succeeded()) {
System.out.println("Deployment id is: " + res.result());
} else {
System.out.println("Deployment failed!");
}
});
// 小坑代码
DeploymentOptions options = new DeploymentOptions().setInstances(16);
vertx.deployVerticle("com.mycompany.MyOrderProcessorVerticle", options);
之所以说此处代码是小坑是因为在部署Verticle组件时,我们本身就会等待部署结果,无关部署完成后去执行此操作,Future<String>
的返回值并不代表代码本身不执行,而是随后会执行,执行完成待定。它提供了这样一种视角:一个函数返回Future
,并不代表它不会执行,而是它不会立即执行,若您不关心返回值,则无需设置对应的Handler,当然也不需要调用onSuccess/onFailure
执行绑定,等待结果即可。若您想要在执行完成后有对应操作,则必须设置Handler代码,Handler的另外一种本质就是类似JS中的回调,它采用了Callback风格的代码让您可以在异步操作执行完成之后再处理其他事情。图示如下:

图中的灰色区域是开发人员无法触碰的区域,主代码一旦调用了deployVerticle之后
,它的执行就不受主代码管控了,能等待它执行结果的地方就只有异步回调代码了,而主代码中的return
操作在此时不起任何作用,这和JS中的异步如出一辙,而灰色区域实则是开发过程中的盲区。
4.3. Verticle启动
我们一般写Verticle代码,只会去重写start()
方法,通常写法如:
public class OptionVerticle extends AbstractVerticle {
@Override
public void start() {
final HttpServer server = this.vertx.createHttpServer();
server.requestHandler(handler -> {
System.out.println(Thread.currentThread().getName());
handler.response()
.putHeader("content-type", "text/plain")
.end("Hello Direct Server!");
});
server.listen(1023, res -> {
System.out.println("Server Started");
});
}
}
而AbstractVerticle中包含了异步方法函数如下:
@Override
public void start(Promise<Void> startPromise) throws Exception {
start();
startPromise.complete();
}
这里有个小细节:异步实现中只是单纯调用了start()
函数,如果该函数本身包含了异步行为,那么这个行为您可以在函数内部书写回调,Verticle组件本身无法捕捉回调内容,也就是说,Verticle的默认异步启动只是发起了异步请求,并没等待您的start()方法执行完成,若要让您的start()方法在执行完成后再标识Verticle部署完成(示例中等待监听结果),您可以重写start(Promise<Void>)
而不是start()
,代码修改如下:
@Override
public void start(final Promise<Void> startPromise) throws Exception {
final HttpServer server = this.vertx.createHttpServer();
server.requestHandler(handler -> {
System.out.println(Thread.currentThread().getName());
handler.response()
.putHeader("content-type", "text/plain")
.end("Hello Direct Server!");
});
server.listen(1023, res -> {
if (res.succeeded()) {
System.out.println("Server Started");
startPromise.complete();
} else {
startPromise.fail(res.cause());
}
});
}
如此:Verticle的部署完成就一定会发生在HttpServer监听成功之后了!所幸的是大部分情况下,部署Verticle是无需如此操作,异步部署已经是很优秀的解决办法了。配合下边主代码:
public class AsyncLauncher {
public static void main(final String[] args) {
// 选择单点模式
final Launcher launcher = new SingleLauncher();
launcher.start(vertx -> {
// 发布
vertx.deployVerticle(OptionVerticle::new,
new DeploymentOptions().setInstances(1), res -> {
if (res.succeeded()) {
System.out.println("Verticle Completed");
}
});
});
}
}
打印结果:
# 同步启动打印结果,重写 start(Promise)
Server Started
Verticle Completed
# 异步启动打印结果,重写 start()
Verticle Completed
Server Started
同步打印结果是固定的,每次打印结果都会按顺序输出,异步打印结果理论上讲由于Verticle的start()方法要耗费时间,所以Verticle Completed
先打印,但如果小概率出现了Verticle组件先执行完,那么有可能Server Started
也先打印,这点读者尤其注意,两种打印结果前者是固定顺序,后者并不是固定顺序,而是无序的,您理解了这点,那么对异步数据流就有了更深的理解。
5. 总结
本章主要讲解了Vert.x中Handler部分主体内容,也解析了其常用结构和用法,在涉入Vert.x的Web开发之前,这里属于必经之路。您若仔细去阅读Vert.x的源代码,会在整个框架内部看到很多类似Handler<AsyncResult<T>>
的定义,阅读了本章内容,那么您更容易理解Handler,面对很多API的调用以及相关写法,就不会疑惑了。
最后更新于