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.1</version>
</dependency>
  • 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 代理代码。

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");

// 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` 返回类型。`ResultType` 可以是另一个代理(因此代理可以是其他代理的工厂)。

让我们看一个例子:

@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(…​)` 方法。

如果你正在使用服务代理,生成的代码将自动为你处理此问题。