<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-service-proxy</artifactId>
<version>5.0.1</version>
</dependency>
Vert.x 服务代理
当你编写 Vert.x 应用程序时,你可能希望隔离某个功能,并将其提供给应用程序的其他部分使用。这就是服务代理的主要目的。它允许你在事件总线上公开一个*服务*,这样,任何其他 Vert.x 组件都可以使用它,只要它们知道服务发布的*地址*。
*服务*通过一个 Java 接口描述,其中包含遵循*异步模式*的方法。在底层,消息通过事件总线发送,以调用服务并获取响应。但为了易于使用,它生成一个*代理*,你可以直接调用(使用服务接口提供的 API)。
使用 Vert.x 服务代理
要**使用** Vert.x 服务代理,请在构建描述符的 *dependencies* 部分添加以下依赖:
-
Maven(在您的
pom.xml
中)
-
Gradle(在您的
build.gradle
文件中)
compile 'io.vertx:vertx-service-proxy:5.0.1'
要**实现**服务代理,还需要添加:
-
Maven(在您的
pom.xml
中)
<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-codegen</artifactId>
<version>5.0.1</version>
<classifier>processor</classifier>
<scope>provided</scope>
</dependency>
-
Gradle < 5(在你的 `build.gradle` 文件中)
compileOnly 'io.vertx:vertx-codegen:5.0.1'
-
Gradle >= 5(在你的 `build.gradle` 文件中)
annotationProcessor 'io.vertx:vertx-codegen:5.0.1:processor'
annotationProcessor 'io.vertx:vertx-service-proxy:5.0.1'
请注意,由于服务代理机制依赖于代码生成,因此对*服务接口*的修改需要重新编译源代码以重新生成代码。
要在不同的语言中生成代理,你需要添加*语言*依赖,例如 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 -> {
// done
}).onFailure(err -> {
// failure
});
创建服务时,需要编写一定量的样板代码,用于监听事件总线上的传入消息、将它们路由到相应的方法并在事件总线上返回结果。
使用 Vert.x 服务代理,你可以避免编写所有这些样板代码,并专注于编写你的服务。
你将服务编写为 Java 接口,并使用 `@ProxyGen` 注解对其进行标注,例如:
@ProxyGen
public interface SomeDatabaseService {
// A couple of factory methods to create an instance and a proxy
static SomeDatabaseService create(Vertx vertx) {
return new SomeDatabaseServiceImpl(vertx);
}
static SomeDatabaseService createProxy(Vertx vertx,
String address) {
return new SomeDatabaseServiceVertxEBProxy(vertx, address);
}
// Actual service operations here...
Future<Void> save(String collection, JsonObject document);
}
你还需要在定义接口的包中(或其上级包中)放置一个 `package-info.java` 文件。该包需要用 `@ModuleGen` 注解进行标注,以便 Vert.x CodeGen 能够识别你的接口并生成相应的 EventBus 代理代码。
@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");
// Save some data in the database - this time using the proxy
service.save(
"mycollection",
new JsonObject().put("name", "tim")).onComplete(
res2 -> {
if (res2.succeeded()) {
// done
}
});
你还可以将 `@ProxyGen` 与语言 API 代码生成 (`@VertxGen`) 结合使用,以便在 Vert.x 支持的任何语言中创建服务存根——这意味着你可以用 Java 编写一次服务,并通过惯用的其他语言 API 与其交互,无论服务是在本地还是完全在事件总线的其他地方。为此,请不要忘记在构建描述符中添加你的语言依赖:
@ProxyGen // Generate service proxies
@VertxGen // Generate the clients
public interface SomeDatabaseService {
// ...
}
异步接口
为了被服务代理生成机制使用,*服务接口*必须遵循一些规则。首先,它应该遵循异步模式。要返回结果,方法应声明一个 `Future
让我们看一个例子:
@ProxyGen
public interface SomeDatabaseService {
// A couple of factory methods to create an instance and a proxy
static SomeDatabaseService create(Vertx vertx) {
return new SomeDatabaseServiceImpl(vertx);
}
static SomeDatabaseService createProxy(Vertx vertx, String address) {
return new SomeDatabaseServiceVertxEBProxy(vertx, address);
}
// A method notifying the completion without a result (void)
Future<Void> save(String collection, JsonObject document);
// A method providing a result (a json object)
Future<JsonObject> findOne(String collection, JsonObject query);
// Create a connection
Future<MyDatabaseConnection> createConnection(String shoeSize);
}
附带
@ProxyGen
@VertxGen
public interface MyDatabaseConnection {
void insert(JsonObject someData);
Future<Void> commit();
@ProxyClose
void close();
}
你还可以通过使用 `@ProxyClose` 注解来声明某个特定方法注销代理。当此方法被调用时,代理实例将被销毁。
有关*服务接口*的更多约束如下所述。
安全性
服务代理可以使用简单的拦截器执行基本安全功能。必须提供一个认证提供者,可以选择添加 `Authorization`,在这种情况下,`AuthorizationProvider` 也必须存在。请注意,认证是基于令牌的,令牌从 `auth-token` 头中提取。
SomeDatabaseService service = new SomeDatabaseServiceImpl();
// Register the handler
new ServiceBinder(vertx)
.setAddress("database-service-address")
// Secure the messages in transit
.addInterceptor(
"action",
// Tokens will be validated using JWT authentication
AuthenticationInterceptor.create(
JWTAuth.create(vertx, new JWTAuthOptions())))
.addInterceptor(
AuthorizationInterceptor.create(JWTAuthorization.create("permissions"))
// optionally we can secure permissions too:
// an admin
.addAuthorization(RoleBasedAuthorization.create("admin"))
// that can print
.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.1</version>
<classifier>processor</classifier>
</dependency>
<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-service-proxy</artifactId>
<version>5.0.1</version>
</dependency>
此功能也可在 Gradle 中使用
compile "io.vertx:vertx-codegen:5.0.1:processor"
compile "io.vertx:vertx-service-proxy:5.0.1"
IDE 通常支持注解处理器。
codegen 的 `processor` 分类器通过 `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();
// Register the handler
new ServiceBinder(vertx)
.setAddress("database-service-address")
.register(SomeDatabaseService.class, service);
这可以在 verticle 中完成,也可以在你的代码的任何地方完成。
注册后,服务即可访问。如果你的应用程序运行在集群上,则该服务可从任何主机访问。
要撤销你的服务,请使用 `unregister` 方法:
ServiceBinder binder = new ServiceBinder(vertx);
// Create an instance of your service implementation
SomeDatabaseService service = new SomeDatabaseServiceImpl();
// Register the handler
MessageConsumer<JsonObject> consumer = binder
.setAddress("database-service-address")
.register(SomeDatabaseService.class, service);
// ....
// Unregister your service.
binder.unregister(consumer);
代理创建
服务公开后,你可能想要使用它。为此,你需要创建一个代理。可以使用 `ServiceProxyBuilder` 类创建代理:
ServiceProxyBuilder builder = new ServiceProxyBuilder(vertx)
.setAddress("database-service-address");
SomeDatabaseService service = builder.build(SomeDatabaseService.class);
// or with delivery options:
SomeDatabaseService service2 = builder.setOptions(options)
.build(SomeDatabaseService.class);
第二个方法接受一个 `DeliveryOptions` 实例,你可以在其中配置消息传递(例如超时)。
或者,你可以使用生成的代理类。代理类名是*服务接口*类名后跟 `VertxEBProxy`。例如,如果你的*服务接口*名为 `SomeDatabaseService`,则代理类名为 `SomeDatabaseServiceVertxEBProxy`。
通常,*服务接口*包含一个用于创建代理的 `createProxy` 静态方法。但这不是必需的:
@ProxyGen
public interface SomeDatabaseService {
// Method to create the proxy.
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;
// Create a connection
public Future<MyDatabaseConnection> createConnection(String shoeSize) {
if (!shoeSize.equals("9")) {
return Future.failedFuture(ServiceException.fail(BAD_SHOE_SIZE, "The shoe size must be 9!",
new JsonObject().put("shoeSize", shoeSize)));
} else {
return doDbConnection().recover(err -> Future.failedFuture(ServiceException.fail(CONNECTION_FAILED, result.cause().getMessage())));
}
}
}
客户端可以检查从失败 `Future` 接收到的 `Throwable` 是否为 `ServiceException`,如果是,则检查其中的特定错误代码。它可以使用此信息区分业务逻辑错误和系统错误(例如服务未在事件总线注册),并精确确定发生了哪个业务逻辑错误。
public Future<JsonObject> foo(String shoeSize) {
SomeDatabaseService service = SomeDatabaseService.createProxy(vertx, SERVICE_ADDRESS);
server.createConnection("8")
.compose(connection -> {
// Do success stuff.
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("You provided a bad shoe size: " +
exc.getDebugInfo().getString("shoeSize")));
} else if (exc.failureCode() == SomeDatabaseServiceImpl.CONNECTION) {
return Future.failedFuture(new ConnectionError("Failed to connect to the DB"));
}
} else {
// Must be a system error (e.g. No service registered for the proxy)
return Future.failedFuture(new SystemError("An unexpected error occurred: + " 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, "In invalid shoe size was received: " + 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) {
// Register on the service side. If using a local event bus, this is all
// that's required, since the proxy side will share the same Vertx instance.
SomeDatabaseService service = SomeDatabaseService.createProxy(vertx, SERVICE_ADDRESS);
vertx.eventBus().registerDefaultCodec(ShoeSizeException.class,
new ShoeSizeExceptionMessageCodec());
}
// Create a connection
Future<MyDatabaseConnection> createConnection(String shoeSize) {
if (!shoeSize.equals("9")) {
return ShoeSizeException.fail(shoeSize);
} else {
// Create the connection here
return Future.succeededFuture(myDbConnection);
}
}
}
最后,客户端现在可以检查自定义异常:
public Future<JsonObject> foo(String shoeSize) {
// If this code is running on a different node in the cluster, the
// ShoeSizeExceptionMessageCodec will need to be registered with the
// Vertx instance on this node, too.
SomeDatabaseService service = SomeDatabaseService.createProxy(vertx, SERVICE_ADDRESS);
service.createConnection("8")
.compose(connection -> {
// Do success stuff.
return doSuccessStuff(connection);
})
.recover(err -> {
if (result.cause() instanceof ShoeSizeException) {
ShoeSizeException exc = (ShoeSizeException) result.cause();
return Future.failedFuture(
new InvalidInputError("You provided a bad shoe size: " + exc.getShoeSize()));
} else {
// Must be a system error (e.g. No service registered for the proxy)
return Future.failedFuture(
new SystemError("An unexpected error occurred: + " result.cause().getMessage())
);
}
});
}
请注意,如果你正在集群 `Vertx` 实例,你需要为集群中的每个 `Vertx` 实例注册自定义异常的 `MessageCodec`。
服务接口限制
服务方法中可使用的类型和返回值存在限制,以便它们易于通过事件总线消息进行编组,并且可以异步使用。它们是:
数据类型
令 `JSON` = `JsonObject | JsonArray` 令 `PRIMITIVE` = 任何原始类型或包装原始类型
参数可以是以下任何一种:
-
JSON
-
PRIMITIVE
-
List<JSON>
-
List<PRIMITIVE>
-
Set<JSON>
-
Set<PRIMITIVE>
-
Map<String, JSON>
-
Map<String, PRIMITIVE>
-
任何 *枚举* 类型
-
任何用 `@DataObject` 注解标注的类
异步结果建模为 `Future
`R` 可以是以下任何一种:
-
JSON
-
PRIMITIVE
-
List<JSON>
-
List<PRIMITIVE>
-
Set<JSON>
-
Set<PRIMITIVE>
-
任何 *枚举* 类型
-
任何用 `@DataObject` 注解标注的类
-
另一个代理
重载方法
服务方法不得重载。(*即* 具有相同名称的方法不能多于一个,无论签名如何)。
通过事件总线调用服务(不使用代理)的约定
服务代理假设事件总线消息遵循特定格式,以便它们可以用于调用服务。
当然,如果你不想,你**不必**使用客户端代理来访问远程服务。通过事件总线发送消息来与它们交互是完全可以接受的。
为了服务能够以一致的方式进行交互,Vert.x 服务**必须使用**以下消息格式:
格式非常简单:
-
应该有一个名为 `action` 的头部,它给出要执行的操作的名称。
-
消息体应该是一个 `JsonObject`,对象中应该为操作所需的每个参数包含一个字段。
例如,要调用一个名为 `save` 的操作,它需要一个字符串集合和一个 JsonObject 文档:
Headers: "action": "save" Body: { "collection", "mycollection", "document", { "name": "tim" } }
无论是否使用服务代理来创建服务,都应使用上述约定,因为它允许服务以一致的方式进行交互。
在使用服务代理的情况下,“action”值应映射到服务接口中操作方法的名称,并且主体中的每个 `[key, value]` 应映射到操作方法中的 `[arg_name, arg_value]`。
对于返回值,服务应使用 `message.reply(…)` 方法发送返回值——这可以是事件总线支持的任何类型。要发出失败信号,应使用 `message.fail(…)` 方法。
如果你正在使用服务代理,生成的代码将自动为你处理此问题。