Vert.x服务代理
在编写Vert.x应用程序时,您可能希望将某个功能独立出来,并使其对应用程序的其余部分可用。这就是服务代理的主要目的。它允许您在事件总线上公开服务,因此,任何其他Vert.x组件都可以使用该服务,只要它们知道服务发布的地址。
服务由一个遵循异步模式方法的Java接口描述。在底层,消息通过事件总线发送以调用服务并获取响应。但为了便于使用,它会生成一个代理,您可以直接调用该代理(使用服务接口的API)。
使用Vert.x服务代理
要使用Vert.x服务代理,请在构建描述符的dependencies部分添加以下依赖项:
Maven(在您的pom.xml中):
<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-service-proxy</artifactId>
<version>5.0.0</version>
</dependency>
Gradle(在您的build.gradle文件中):
compile 'io.vertx:vertx-service-proxy:5.0.0'
要实现服务代理,还需添加:
Maven(在您的pom.xml中):
<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-codegen</artifactId>
<version>5.0.0</version>
<classifier>processor</classifier>
<scope>provided</scope>
</dependency>
Gradle < 5(在您的build.gradle文件中):
compileOnly 'io.vertx:vertx-codegen:5.0.0'
Gradle >= 5(在您的build.gradle文件中):
annotationProcessor 'io.vertx:vertx-codegen:5.0.0:processor'
annotationProcessor 'io.vertx:vertx-service-proxy:5.0.0'
请注意,由于服务代理机制依赖于代码生成,因此对服务接口的修改需要重新编译源文件以重新生成代码。
要生成不同语言的代理,您需要添加语言依赖项,例如Groovy的vertx-lang-groovy。
服务代理简介
让我们看一下服务代理及其有用的原因。假设您有一个在事件总线上公开的数据库服务,您应该这样做:
JsonObject message = new JsonObject();
message
.put("collection", "mycollection")
.put("document", new JsonObject().put("name", "tim"));
DeliveryOptions options = new DeliveryOptions().addHeader("action", "save");
vertx.eventBus()
.request("database-service-address", message, options)
.onSuccess(msg -> {
// 完成
}).onFailure(err -> {
// 失败
});
创建服务时,需要编写一定数量的样板代码来监听事件总线上的传入消息,将它们路由到适当的方法,并在事件总线上返回结果。
使用Vert.x服务代理,您可以避免编写所有这些样板代码,而专注于编写服务。
您可以将服务编写为Java接口,并使用@ProxyGen注释对其进行注释,例如:
@ProxyGen
public interface SomeDatabaseService {
// 用于创建实例和代理的几个工厂方法
static SomeDatabaseService create(Vertx vertx) {
return new SomeDatabaseServiceImpl(vertx);
}
static SomeDatabaseService createProxy(Vertx vertx, String address) {
return new SomeDatabaseServiceVertxEBProxy(vertx, address);
}
// 实际的服务操作在这里...
Future<Void> save(String collection, JsonObject document);
}
您还需要在定义接口的包中(或上方)的某个位置有一个package-info.java文件。该包需要使用@ModuleGen注释,以便Vert.x CodeGen可以识别您的接口并生成适当的EventBus代理代码。
package-info.java:
@io.vertx.codegen.annotations.ModuleGen(groupPackage = "io.vertx.example", name = "services")
package io.vertx.example;
有了这个接口,Vert.x将生成通过事件总线访问服务所需的所有样板代码,它还将为您的服务生成一个客户端代理,因此您的客户端可以使用丰富的惯用API来访问您的服务,而不必手动构造要发送的事件总线消息。客户端代理将不管您的服务实际在事件总线上的什么位置(可能在不同的机器上)都能工作。
这意味着您可以像这样与服务交互:
SomeDatabaseService service = SomeDatabaseService
.createProxy(vertx, "database-service-address");
// 在数据库中保存一些数据 - 这次使用代理
service.save(
"mycollection",
new JsonObject().put("name", "tim")).onComplete(
res2 -> {
if (res2.succeeded()) {
// 完成
}
});
您还可以将@ProxyGen与语言API代码生成(@VertxGen)结合使用,以便在Vert.x支持的任何语言中创建服务存根-这意味着您可以用Java编写一次服务,并通过惯用的其他语言API与它交互,而不管服务是位于本地还是事件总线上的其他位置。为此,请不要忘记在构建描述符中添加对您的语言的依赖项:
@ProxyGen // 生成服务代理
@VertxGen // 生成客户端
public interface SomeDatabaseService {
// ...
}
异步接口
为了被服务代理生成使用,服务接口必须遵守几个规则。首先,它应该遵循异步模式。要返回结果,该方法应声明Future返回类型。ResultType可以是另一个代理(因此代理可以是其他代理的工厂)。
让我们看一个例子:
@ProxyGen
public interface SomeDatabaseService {
// 用于创建实例和代理的几个工厂方法
static SomeDatabaseService create(Vertx vertx) {
return new SomeDatabaseServiceImpl(vertx);
}
static SomeDatabaseService createProxy(Vertx vertx, String address) {
return new SomeDatabaseServiceVertxEBProxy(vertx, address);
}
// 通知完成但没有结果(void)的方法
Future<Void> save(String collection, JsonObject document);
// 提供结果(JsonObject)的方法
Future<JsonObject> findOne(String collection, JsonObject query);
// 创建连接
Future<MyDatabaseConnection> createConnection(String shoeSize);
}
以及:
@ProxyGen
@VertxGen
public interface MyDatabaseConnection {
void insert(JsonObject someData);
Future<Void> commit();
@ProxyClose
void close();
}
您还可以通过使用@ProxyClose注释来声明某个方法取消注册代理。调用此方法时,代理实例将被销毁。
下面描述了服务接口的更多约束。
安全性
服务代理可以使用简单的拦截器执行基本安全性。必须提供身份验证提供程序,可以选择添加授权,在这种情况下,还必须存在授权提供程序。请注意,身份验证基于从auth-token标头中提取的令牌。
SomeDatabaseService service = new SomeDatabaseServiceImpl();
// 注册处理程序
new ServiceBinder(vertx)
.setAddress("database-service-address")
// 保护传输中的消息
.addInterceptor(
"action",
// 将使用JWT身份验证验证令牌
AuthenticationInterceptor.create(
JWTAuth.create(vertx, new JWTAuthOptions())))
.addInterceptor(
AuthorizationInterceptor.create(JWTAuthorization.create("permissions"))
// 可选地,我们也可以保护权限:
// 管理员
.addAuthorization(RoleBasedAuthorization.create("admin"))
// 可以打印
.addAuthorization(PermissionBasedAuthorization.create("print")))
.register(SomeDatabaseService.class, service);
代码生成
使用@ProxyGen注释的服务会触发生成服务帮助器类:
- 服务代理:编译时生成的代理,使用EventBus通过消息与服务交互
- 服务处理程序:编译时生成的EventBus处理程序,对代理发送的事件做出反应
生成的代理和处理程序以服务类命名,例如,如果服务名为MyService,则处理程序称为MyServiceProxyHandler,代理称为MyServiceEBProxy。
此外,Vert.x Core提供了一个生成器,用于创建数据对象转换器,以便在服务代理中轻松使用数据对象。这种转换器为JsonObject构造函数和toJson()方法提供了基础,这是在服务代理中使用数据对象所必需的。
codegen注释处理器在编译时生成这些类。这是Java编译器的一个特性,因此不需要额外的步骤,只需正确配置您的构建即可:
只需在构建中添加io.vertx:vertx-codegen:processor和io.vertx:vertx-service-proxy依赖项。
以下是Maven的配置示例:
<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-codegen</artifactId>
<version>5.0.0</version>
<classifier>processor</classifier>
</dependency>
<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-service-proxy</artifactId>
<version>5.0.0</version>
</dependency>
此功能也可以在Gradle中使用:
compile "io.vertx:vertx-codegen:5.0.0:processor"
compile "io.vertx:vertx-service-proxy:5.0.0"
IDE通常提供对注释处理器的支持。
codegen处理器分类器通过META-INF/services插件机制向jar添加服务代理注释处理器的自动配置。
如果需要,您也可以将其与常规jar一起使用,但需要显式声明注释处理器,例如在Maven中:
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessors>
<annotationProcessor>io.vertx.codegen.CodeGenProcessor</annotationProcessor>
</annotationProcessors>
</configuration>
</plugin>
公开您的服务
有了服务接口后,编译源代码以生成存根和代理。然后,您需要一些代码来在事件总线上“注册”您的服务:
SomeDatabaseService service = new SomeDatabaseServiceImpl();
// 注册处理程序
new ServiceBinder(vertx)
.setAddress("database-service-address")
.register(SomeDatabaseService.class, service);
这可以在Verticle中或代码中的任何位置完成。
注册后,服务即可访问。如果您在集群上运行应用程序,该服务可从任何主机访问。
要取消服务注册,请使用unregister方法:
ServiceBinder binder = new ServiceBinder(vertx);
// 创建服务实现的实例
SomeDatabaseService service = new SomeDatabaseServiceImpl();
// 注册处理程序
MessageConsumer<JsonObject> consumer = binder
.setAddress("database-service-address")
.register(SomeDatabaseService.class, service);
// ....
// 取消注册服务。
binder.unregister(consumer);
代理创建
现在服务已公开,您可能需要使用它。为此,您需要创建一个代理。可以使用ServiceProxyBuilder类创建代理:
ServiceProxyBuilder builder = new ServiceProxyBuilder(vertx)
.setAddress("database-service-address");
SomeDatabaseService service = builder.build(SomeDatabaseService.class);
// 或使用交付选项:
SomeDatabaseService service2 = builder.setOptions(options)
.build(SomeDatabaseService.class);
第二种方法接受DeliveryOptions的实例,您可以在其中配置消息传递(例如超时)。
或者,您可以使用生成的代理类。代理类的名称是服务接口类名称后跟VertxEBProxy。例如,如果您的服务接口名为SomeDatabaseService,代理类名为SomeDatabaseServiceVertxEBProxy。
通常,服务接口包含一个createProxy静态方法来创建代理。但这不是必需的:
@ProxyGen
public interface SomeDatabaseService {
// 创建代理的方法。
static SomeDatabaseService createProxy(Vertx vertx, String address) {
return new SomeDatabaseServiceVertxEBProxy(vertx, address);
}
// ...
}
错误处理
服务方法可以通过向方法的Handler传递包含ServiceException实例的失败Future来向客户端返回错误。ServiceException包含一个int失败代码、一条消息和一个可选的JsonObject,其中包含任何认为重要的要返回给调用者的额外信息。为了方便起见,ServiceException.fail工厂方法可用于创建已经包装在失败Future中的ServiceException实例。例如:
public class SomeDatabaseServiceImpl implements SomeDatabaseService {
private static final BAD_SHOE_SIZE = 42;
private static final CONNECTION_FAILED = 43;
// 创建连接
public Future<MyDatabaseConnection> createConnection(String shoeSize) {
if (!shoeSize.equals("9")) {
return Future.failedFuture(ServiceException.fail(BAD_SHOE_SIZE, "鞋码必须为9!",
new JsonObject().put("shoeSize", shoeSize)));
} else {
return doDbConnection().recover(err -> Future.failedFuture(ServiceException.fail(CONNECTION_FAILED, result.cause().getMessage())));
}
}
}
然后,客户端可以检查从失败的Future接收到的Throwable是否是ServiceException,如果是,则检查其中的特定错误代码。它可以使用此信息来区分业务逻辑错误和系统错误(如未在Event Bus中注册服务),并确定发生了确切的业务逻辑错误。
public Future<JsonObject> foo(String shoeSize) {
SomeDatabaseService service = SomeDatabaseService.createProxy(vertx, SERVICE_ADDRESS);
server.createConnection("8")
.compose(connection -> {
// 执行成功操作。
return doSuccessStuff(connection);
})
.recover(err -> {
if (err instanceof ServiceException) {
ServiceException exc = (ServiceException) err;
if (exc.failureCode() == SomeDatabaseServiceImpl.BAD_SHOE_SIZE) {
return Future.failedFuture(
new InvalidInputError("你提供了一个错误的鞋码: " +
exc.getDebugInfo().getString("shoeSize")));
} else if (exc.failureCode() == SomeDatabaseServiceImpl.CONNECTION) {
return Future.failedFuture(new ConnectionError("连接数据库失败"));
}
} else {
// 必须是系统错误(例如,代理没有注册服务)
return Future.failedFuture(new SystemError("发生意外错误: + " result.cause().getMessage()));
}
});
}
如果需要,服务实现还可以返回ServiceException的子类,只要为其注册了默认的MessageCodec。例如,给定以下ServiceException子类:
class ShoeSizeException extends ServiceException {
public static final BAD_SHOE_SIZE_ERROR = 42;
private final String shoeSize;
public ShoeSizeException(String shoeSize) {
super(BAD_SHOE_SIZE_ERROR, "收到无效的鞋码: " + shoeSize);
this.shoeSize = shoeSize;
}
public String getShoeSize() {
return extra;
}
public static <T> Future<T> fail(int failureCode, String message, String shoeSize) {
return Future.failedFuture(new MyServiceException(failureCode, message, shoeSize));
}
}
只要注册了默认的MessageCodec,服务实现就可以直接向调用者返回自定义异常:
public class SomeDatabaseServiceImpl implements SomeDatabaseService {
public SomeDataBaseServiceImpl(Vertx vertx) {
// 在服务端注册。如果使用本地事件总线,这就是所需要的全部,因为代理端将共享同一个Vertx实例。
SomeDatabaseService service = SomeDatabaseService.createProxy(vertx, SERVICE_ADDRESS);
vertx.eventBus().registerDefaultCodec(ShoeSizeException.class,
new ShoeSizeExceptionMessageCodec());
}
// 创建连接
Future<MyDatabaseConnection> createConnection(String shoeSize) {
if (!shoeSize.equals("9")) {
return ShoeSizeException.fail(shoeSize);
} else {
// 在这里创建连接
return Future.succeededFuture(myDbConnection);
}
}
}
最后,客户端现在可以检查自定义异常:
public Future<JsonObject> foo(String shoeSize) {
// 如果此代码在集群中的不同节点上运行,ShoeSizeExceptionMessageCodec也需要在此节点上的Vertx实例中注册。
SomeDatabaseService service = SomeDatabaseService.createProxy(vertx, SERVICE_ADDRESS);
service.createConnection("8")
.compose(connection -> {
// 执行成功操作。
return doSuccessStuff(connection);
})
.recover(err -> {
if (result.cause() instanceof ShoeSizeException) {
ShoeSizeException exc = (ShoeSizeException) result.cause();
return Future.failedFuture(
new InvalidInputError("你提供了一个错误的鞋码: " + exc.getShoeSize()));
} else {
// 必须是系统错误(例如,代理没有注册服务)
return Future.failedFuture(
new SystemError("发生意外错误: + " result.cause().getMessage())
);
}
});
}
请注意,如果您正在集群Vertx实例,则需要在集群中的每个Vertx实例中注册自定义异常的MessageCodec。
服务接口的限制
服务方法中可以使用的类型和返回值有限制,以便于在事件总线消息