服务工厂与辅助工具
你当前正在浏览历史版本 Vert.x 4.2.7 的文档。 访问本页面的 最新版本。

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):

<dependency>
  <groupId>io.vertx</groupId>
  <artifactId>vertx-service-proxy</artifactId>
  <version>5.0.12</version>
</dependency>
  • Gradle (in your build.gradle file):

compile 'io.vertx:vertx-service-proxy:5.0.12'

要 实现 服务代理,还需要添加:

  • Maven (in your pom.xml):

<dependency>
  <groupId>io.vertx</groupId>
  <artifactId>vertx-codegen</artifactId>
  <version>5.0.12</version>
  <classifier>processor</classifier>
  <scope>provided</scope>
</dependency>
  • Gradle < 5 (in your build.gradle file):

compileOnly 'io.vertx:vertx-codegen:5.0.12'
  • Gradle >= 5 (in your build.gradle file):

annotationProcessor 'io.vertx:vertx-codegen:5.0.12:processor'
annotationProcessor 'io.vertx:vertx-service-proxy:5.0.12'

请注意,由于 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 代理代码。

package-info.java
@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.0.12</version>
  <classifier>processor</classifier>
</dependency>
<dependency>
  <groupId>io.vertx</groupId>
  <artifactId>vertx-service-proxy</artifactId>
  <version>5.0.12</version>
</dependency>

在 Gradle 中也可以使用该特性:

compile "io.vertx:vertx-codegen:5.0.12:processor"
compile "io.vertx:vertx-service-proxy:5.0.12"

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,生成的代码会自动为你处理这一切。