厌倦了魔术表演?

在我休假前不久,有人对我说:“我不喜欢魔术。” 在这个语境下,魔术指的是 Quarkus 为简化起见在后台进行的那些隐藏的操作。它包括依赖注入、注解等等。

这已经不是我第一次收到这类评论了,而且来自 Vert.x 项目,这说得通。Vert.x (几乎)没有魔术,而且有充分的理由:太多的魔术可能会很糟糕,而且会使生产环境的调优变得异常昂贵。有时您想拥有更多的控制权并避免意外行为:执行您编写的代码,仅此而已。

但是魔术本身并非不好。魔术是一种可以被善用也可以被滥用的力量。毕竟,您的应用程序运行在一个由微码魔术驱动的硅片上,而微码又由抽象魔术驱动的操作系统驱动,操作系统又由即时魔术驱动的 Java 虚拟机驱动。魔术总是存在的,只是我们对其了解(或信任)的程度不同。

您可能会认为 Quarkus 有很多魔术。从某种意义上说,这是真的,但它易于理解,并且在内存优化、启动时间优化或最重要的开发者体验改进方面具有显著优势。您可以决定您想要多少魔术以及您觉得舒适的控制量。如果您更喜欢自己动手,不必使用依赖注入或托管客户端。

在这篇文章中,我们将介绍减少魔术量的三种不同方法。我们将从几乎没有魔术到仅提供足够的魔术以获得良好的开发者体验。本文的示例可在 GitHub 上找到。

几乎没有魔术的方法

Quarkus 应用程序是 Java 应用程序。所以,某个地方会有 public static void main(String…​ args)。虽然在使用 Quarkus 时您不需要编写该方法,但它仍然很方便,并且可以为您提供更多关于应用程序启动的控制权。它也是直接从 IDE 启动 Quarkus 应用程序的一个好技巧。

例如,我们将实现一个简单的 HTTP 应用程序。没什么特别的

package me.escoffier.quarkus.nomagic;

import io.quarkus.runtime.Quarkus;
import io.quarkus.runtime.QuarkusApplication;
import io.quarkus.runtime.annotations.QuarkusMain;
import io.vertx.core.Vertx;
import io.vertx.core.http.HttpServer;
import io.vertx.ext.web.Router;
import org.eclipse.microprofile.config.ConfigProvider;

@QuarkusMain
public class Main implements QuarkusApplication {

    public static void main(String... args) {
        Quarkus.run(Main.class, args);
    }

    @Override
    public int run(String... args) {
        Vertx vertx = Vertx.vertx();
        Router router = Router.router(vertx);

        String message = ConfigProvider.getConfig().getValue("message", String.class);

        router.get("/").handler(rc -> rc.response().end(message));
        router.get("/bye").handler(rc -> {
            rc.response().end("bye");
            Quarkus.asyncExit();
        });

        HttpServer server = vertx.createHttpServer()
                .requestHandler(router)
                .listen(8080);

        Quarkus.waitForExit();

        server.close();
        return 0;
    }
}

完整的源代码可以在 此处 找到。不要期望更多;该应用程序只有一个 Java 类,但让我们看一下。

@QuarkusMain 表明 Quarkus 应该使用该类作为应用程序的主入口点run 方法包含您的应用程序逻辑。稍后我们会回到这个逻辑。首先,看看 public static void main(String…​ args) 方法。它只是启动应用程序。您可以直接从 IDE 使用此入口点。是的,Quarkus.run 后面仍然有一点魔术;在那里会发生扩展初始化——这与包括 Vert.x 在内的任何框架初始化没有什么不同。由于此应用程序不使用任何扩展,因此不会发生太多事情。

此应用程序 直接依赖 于 Vert.x Web 和 Vert.x Core。唯一的 Quarkus 依赖是 Arc(不直接使用但必需)

<dependencies>
    <dependency>
        <groupId>io.quarkus</groupId>
        <artifactId>quarkus-arc</artifactId>
    </dependency>

    <dependency>
        <groupId>io.vertx</groupId>
        <artifactId>vertx-web</artifactId>
        <version>3.9.5</version>
    </dependency>
</dependencies>

让我们回到 run 方法。它包含应用程序逻辑,在这里是一个虚拟的 Vert.x 应用程序。它创建 Vertx 实例,一个 Router,注册了几个路由,然后启动 HTTP 服务器。因为我们不希望应用程序立即停止,所以我们等待退出/bye 请求处理程序说明了如何以编程方式触发应用程序关闭。

此应用程序几乎没有魔术,只有一个注解和一个常规的 Java 入口点。您可能想知道为什么不使用纯 Java 程序?即使以这种方式使用,Quarkus 也能提供优势。例如,您可以访问内置的配置支持,如代码片段所示

String message = ConfigProvider.getConfig().getValue("message", String.class);

配置位于 application.properties 文件中。

这种第一种方法有一些缺点。它无法从 Quarkus 的构建时处理中受益。构建时执行的逻辑包含在扩展中,在这种情况下,我们不使用扩展(Arc 除外)。另一个问题是,将此应用程序编译为原生可执行文件将会失败,因为扩展在原生编译过程中也涉及其中。最后,热重载将无法工作,但您可以直接从 IDE 重新启动应用程序。

使用托管的 Vert.x 实例

Quarkus 大量使用 Vert.x。quarkus-vertx-core 扩展管理着 Quarkus 使用的 Vert.x 实例。您可以直接使用该实例,避免创建 Vert.x 实例。如果您需要配置该实例,可以从 application.properties 进行配置。它还支持原生打包(因为该扩展包含了将 Vert.x 应用程序编译为原生的指令)。

在您的 pom.xml 文件中,只需添加以下依赖

<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-vertx-core</artifactId>
</dependency>

有了这个,run 方法就变成了

@Override
public int run(String... args) {
    Vertx vertx = CDI.current().select(Vertx.class).get();
    Router router = Router.router(vertx);

    String message = ConfigProvider.getConfig().getValue("message", String.class);

    router.get("/").handler(rc -> rc.response().end(message + " world!"));
    router.get("/stop").handler(rc -> {
        rc.response().end("bye");
        Quarkus.asyncExit();
    });

    HttpServer server = vertx.createHttpServer()
            .requestHandler(router)
            .listen(8080);

    Quarkus.waitForExit();

    server.close();
    return 0;
}

请注意它是如何检索托管的 Vert.x 实例的。虽然您可以使用 @Inject,但也可以通过编程方式检索它,其余代码保持不变。看到了吗?对您来说没有魔术!我们仍然可以使用main方法从 IDE 启动它。

如果您不包含 quarkus-vertx-core 扩展(或任何依赖于它的扩展),Quarkus 将不会创建 Vert.x 实例。

使用扩展可以为您提供一些属性连接,以及构建时优化和原生镜像编译

> mvn package -Dnative
...
> ./target/managed-vertx-example-1.0-SNAPSHOT-runner

但是,仍然没有热重载 😿。

使用托管的 HTTP 服务器

除了仅使用 quarkus-vertx-core 扩展,我们还可以选择将 HTTP 服务器的委托交给 Quarkus。您可能认为这是失去控制,但实际上,我们很少围绕它做太多事情,而且,如果需要,您仍然可以从 application.properties 文件进行配置。

使用 quarkus-vertx-http 替代 quarkus-vertx-core

<dependencies>
    <dependency>
        <groupId>io.quarkus</groupId>
        <artifactId>quarkus-vertx-http</artifactId>
    </dependency>
</dependencies>

无需直接依赖 Vert.x Web,它已包含在内。

您仍然会注册您的路由,但使用的是托管的 Router

@Override
public int run(String... args) {
    Router router = CDI.current().select(Router.class).get();

    String message = ConfigProvider.getConfig().getValue("message", String.class);

    router.get("/").handler(rc -> rc.response().end(message));
    router.get("/bye").handler(rc -> {
        rc.response().end("bye");
        Quarkus.asyncExit();
    });

    Quarkus.waitForExit();
    return 0;
}

这种方法通过拦截 HTTP 请求来启用 Quarkus 的热重载。您仍然可以控制与应用程序逻辑相关的所有内容。

您可以使用以下方式启动热重载

> mvn quarkus:dev

最后的魔术触动

现在的问题是,我们离一个普通的 Quarkus 应用程序还有多远?实际上,相当近了。使用 RESTEasy Reactive 的等效应用程序将是这样的

@Path("/")
public class MyResource {

    @Inject @ConfigProperty("message") String message;

    @GET
    public String hello() {
        return message;
    }

}

与以前的方法不同,这种方法利用了声明式(基于注解)的模型。在后台,它与最后一种方法没有什么不同。Quarkus 在路由器上注册一个路由,该路由在接收到匹配的请求时调用 hello 方法。路由器在 Quarkus.run 方法期间初始化。不需要main端点,但您仍然可以使用一个,这在 IDE 中通常很方便。

总结

我们与魔术的关系取决于我们的背景和经验。Quarkus 让您决定接受多少魔术。本文介绍了四种配置,从几乎没有魔术到普通的 Quarkus 代码。每种方法都有其优缺点

控制 构建时优化 原生可执行文件 热重载

几乎没有魔术

完全

🥵

🥵

🥵

使用托管的 Vert.x 实例

除了 Vert.x

😀,对于 Vert.x

😀

🥵

使用托管的 HTTP 服务器

除了 Vert.x 和 HTTP 服务器

😀,对于 Vert.x 和 HTTP

😀

😀

普通的 Quarkus

由 Quarkus 管理的端点

😀

😀

😀

选择适合您需求的方法。此外,正如我们在配置中所见,大多数 Quarkus 服务也可以通过编程方式使用。因此,如果您倾向于避免托管对象,请随意使用提供的 API。