第十四章 VERT.X5 服务代理

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。

服务接口的限制

服务方法中可以使用的类型和返回值有限制,以便于在事件总线消息

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

err2008

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值