<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-service-proxy</artifactId>
<version>5.2.1</version>
</dependency>
Vert.x 服务代理(Service Proxy)
在构建 Vert.x 应用程序时,你可能希望将某项功能隔离在某处,并让应用程序的其余部分可以使用它。这正是服务代理(Service Proxy)的主要目的。它允许你在 EventBus 上暴露一个 服务(service),这样任何其他 Vert.x 组件只要知道发布该服务的 地址(address),就可以消费它。
一个 服务 通过包含遵循 异步模式(async pattern) 方法的 Java 接口进行描述。在其底层实现中,消息在 EventBus 上发送以调用服务并获取响应。但为了便于使用,它会生成一个 代理(proxy),你可以直接调用它(使用来自服务接口的 API)。
使用 Vert.x 服务代理
要 使用 Vert.x Service Proxy,请在构建描述文件的 dependencies 部分添加以下依赖:
-
Maven (in your
pom.xml):
-
Gradle (in your
build.gradlefile):
compile 'io.vertx:vertx-service-proxy:5.2.1'
要 实现 服务代理,还需要添加:
-
Maven (in your
pom.xml):
<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-codegen</artifactId>
<version>5.2.1</version>
<classifier>processor</classifier>
<scope>provided</scope>
</dependency>
-
Gradle < 5 (in your
build.gradlefile):
compileOnly 'io.vertx:vertx-codegen:5.2.1'
-
Gradle >= 5 (in your
build.gradlefile):
annotationProcessor 'io.vertx:vertx-codegen:5.2.1:processor'
annotationProcessor 'io.vertx:vertx-service-proxy:5.2.1'
请注意,由于 Service Proxy 机制依赖于代码生成,因此对 服务接口 的修改需要重新编译源码以重新生成代码。
要在不同的语言中生成代理,你需要添加对应的 语言 依赖,例如针对 Groovy 的 vertx-lang-groovy。
服务代理简介
让我们了解一下服务代理以及它们为什么有用。假设你在 EventBus 上暴露了一个 数据库服务(database service),通常你需要编写类似这样的代码:
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 -> {
// 失败
});
创建服务时,通常需要编写一定量的模板代码(boilerplate code)来在 EventBus 上监听传入的消息、将其路由到合适的方法并在 EventBus 上返回结果。
借助 Vert.x Service Proxy,你可以避免编写所有这些模板代码,专注于编写你的服务。
你将服务编写为一个 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 代理代码。
@io.vertx.codegen.annotations.ModuleGen(groupPackage = "io.vertx.example", name = "services")
package io.vertx.example;
有了该接口后,Vert.x 将生成通过 EventBus 访问服务所需的所有模板代码,并且还会为你的服务生成一个 客户端代理(client side proxy),因此你的客户端可以为服务使用丰富且符合习惯的 API,而无需手动构建并发送 EventBus 消息。无论你的服务实际运行在 EventBus 的何处(甚至可能在另一台机器上),客户端代理都能正常工作。
这意味着你可以像这样与服务进行交互:
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 支持的任何语言中创建服务存根(service stub)——这意味着你可以用 Java 编写一次服务,然后通过其他语言中符合习惯的 API 与之交互,而无需关心该服务是运行在本地还是完全位于 EventBus 的其他地方。为此,请不要忘记在构建描述文件中添加对应语言的依赖:
@ProxyGen // 生成服务代理
@VertxGen // 生成客户端
public interface SomeDatabaseService {
// ...
}
异步接口
要用于服务代理生成,服务接口 必须遵守几条规则。首先,它应该遵循异步模式。为了返回结果,方法应声明 Future<ResultType> 返回类型。
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 注解标注特定方法,声明该方法用于注销代理。当调用此方法时,代理实例将被销毁(disposed)。
有关 服务接口 的更多约束详见下文。
安全
Service Proxy 可以使用一个简单的拦截器(interceptor)执行基本安全控制。必须提供一个认证提供者(authentication provider),可选地还可以添加 Authorization,在这种情况下还必须提供 AuthorizationProvider。请注意,认证基于从 auth-token 请求头中提取的令牌(token)工作。
SomeDatabaseService service = new SomeDatabaseServiceImpl();
// 注册 Handler
new ServiceBinder(vertx)
.setAddress("database-service-address")
// 保护传输中的消息
.addInterceptor(
"action",
// 令牌将使用 JWT 认证进行验证
AuthenticationInterceptor.create(
JWTAuth.create(vertx, new JWTAuthOptions())))
.addInterceptor(
AuthorizationInterceptor.create(JWTAuthorization.create("permissions"))
// 可选地,我们也可以保护权限:
// admin 角色
.addAuthorization(RoleBasedAuthorization.create("admin"))
// 且拥有 print 权限
.addAuthorization(PermissionBasedAuthorization.create("print")))
.register(SomeDatabaseService.class, service);
代码生成
带有 @ProxyGen 注解的服务会触发生成服务辅助类:
-
服务代理(service proxy):在编译期生成的代理,它使用
EventBus通过消息与服务进行交互 -
服务处理器(service handler):在编译期生成的
EventBus处理器,用于对代理发送的事件做出响应
生成的代理与处理器以服务类命名,例如如果服务名为 MyService,则处理器名为 MyServiceProxyHandler,代理名为 MyServiceEBProxy。
此外,Vert.x Core 提供了一个生成器来创建数据对象转换器(data object converter),以简化在 Service Proxy 中使用数据对象(DataObject)。该转换器为在 Service Proxy 中使用数据对象所必需的 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.2.1</version>
<classifier>processor</classifier>
</dependency>
<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-service-proxy</artifactId>
<version>5.2.1</version>
</dependency>
在 Gradle 中也可以使用该特性:
compile "io.vertx:vertx-codegen:5.2.1:processor"
compile "io.vertx:vertx-service-proxy:5.2.1"
IDE 通常都支持注解处理器。
codegen 的 processor classifier 通过 META-INF/services 插件机制将 Service Proxy 注解处理器的自动配置添加到 JAR 中。
如果你愿意,也可以将其与普通 JAR 一起使用,但此时你需要显式声明注解处理器,例如在 Maven 中:
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessors>
<annotationProcessor>io.vertx.codegen.CodeGenProcessor</annotationProcessor>
</annotationProcessors>
</configuration>
</plugin>
暴露服务
准备好 服务接口 后,编译源代码以生成存根和代理。接着,你需要编写一些代码将你的服务“注册”到 EventBus 上:
SomeDatabaseService service = new SomeDatabaseServiceImpl();
// 注册 Handler
new ServiceBinder(vertx)
.setAddress("database-service-address")
.register(SomeDatabaseService.class, service);
这可以在 Verticle 中完成,也可以在你代码中的任何地方完成。
一旦注册,该服务就变得可以访问。如果你的应用程序运行在集群上,则该服务可以从任何主机访问。
要撤销(注销)你的服务,请使用 unregister 方法:
ServiceBinder binder = new ServiceBinder(vertx);
// 创建服务实现实例
SomeDatabaseService service = new SomeDatabaseServiceImpl();
// 注册 Handler
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);
}
// ...
}
错误处理
服务方法可以通过向方法的调用方传递包含 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, "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;如果是,则检查其内部的具体错误代码。客户端可以利用此信息来区分业务逻辑错误与系统错误(例如服务未在 EventBus 上注册),并精确确定发生了哪种业务逻辑错误。
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("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 {
// 必定是系统错误(例如:没有为代理注册服务)
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) {
// 在服务侧注册。如果使用本地 EventBus,只需要这些操作即可,
// 因为代理侧将共享同一个 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) {
// 如果此代码运行在集群中的不同节点上,
// 也需要在该节点上的 Vertx 实例中注册 ShoeSizeExceptionMessageCodec。
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("You provided a bad shoe size: " + exc.getShoeSize()));
} else {
// 必定是系统错误(例如:没有为代理注册服务)
return Future.failedFuture(
new SystemError("An unexpected error occurred: + " result.cause().getMessage())
);
}
});
}
请注意,如果你将 Vertx 实例进行集群化,则需要在集群中的每个 Vertx 实例上都注册自定义异常的 MessageCodec。
服务接口的限制
在服务方法中可以使用的类型和返回值存在一定限制,以便它们易于在 EventBus 消息中进行编组(marshall),并能够以异步方式使用。具体限制如下:
数据类型
令 JSON = JsonObject | JsonArray
令 PRIMITIVE = 任意基本类型或装箱基本类型
参数可以是以下任意一种:
-
JSON -
PRIMITIVE -
List<JSON> -
List<PRIMITIVE> -
Set<JSON> -
Set<PRIMITIVE> -
Map<String, JSON> -
Map<String, PRIMITIVE> -
任意 枚举(Enum) 类型
-
带有
@DataObject注解的任意类
异步结果表示为 Future<R>
R 可以是以下任意一种:
-
JSON -
PRIMITIVE -
List<JSON> -
List<PRIMITIVE> -
Set<JSON> -
Set<PRIMITIVE> -
任意 枚举(Enum) 类型
-
带有
@DataObject注解的任意类 -
另一个代理
重载方法
服务方法不得重载(即 无论方法签名如何,不得存在多个同名方法)。
通过 EventBus 调用服务的约定(不使用代理)
Service Proxy 假定 EventBus 消息遵循特定格式,以便能够用于调用服务。
当然,如果你不想使用客户端代理来访问远程服务,也 不是必须 使用它。直接通过 EventBus 发送消息与其交互是完全可以的。
为了让服务能够以一致的方式进行交互,任何 Vert.x 服务都 必须使用 以下消息格式。
该格式非常简单:
-
应该包含一个名为
action的请求头(header),给出要执行的操作名称。 -
消息体应该是一个
JsonObject,对象中应为操作所需的每个参数提供一个字段。
例如,调用名为 save 的操作,该操作需要一个 String 类型的 collection 和一个 JsonObject 类型的 document:
Headers:
"action": "save"
Body:
{
"collection", "mycollection",
"document", {
"name": "tim"
}
}
无论是否使用 Service Proxy 来创建服务,都应该使用上述约定,因为这样可以确保服务之间能够一致地进行交互。
在使用 Service Proxy 的情况下,“action” 的值应映射到服务接口中操作方法的名称,并且消息体中的每个 [key, value] 应映射到操作方法中的 [arg_name, arg_value]。
对于返回值,服务应使用 message.reply(…) 方法发送返回值——它可以是 EventBus 支持的任何类型。要指示失败,应使用 message.fail(…) 方法。
如果你正在使用 Service Proxy,生成的代码会自动为你处理这一切。