vlambda博客
学习文章列表

读书笔记《hands-on-cloud-native-applications-with-java-and-quarkus》高级应用程序开发

Advanced Application Development

在本章中,我们将探索 Quarkus 的一些高级特性,这些特性将帮助您设计和编写尖端的 Quarkus 应用程序。我们将学习的主题将涵盖 Quarkus API 的不同领域,从高级配置选项到控制 Quarkus 应用程序的生命周期以及使用 Quarkus 调度程序触发基于时间的事件。

在本章结束时,您将能够利用以下高级功能:

  • Using advanced MicroProfile configuration options
  • Controlling the life cycle events of your services
  • Scheduling periodic tasks in your services

Using advanced configuration options

正如我们已经了解到的,Quarkus 依赖 MicroProfile Config 规范将配置属性注入我们的应用程序。到目前为止,我们已经使用默认配置文件(名为 application.properties)为应用程序的初始设置提供初始值。

让我们回顾一下如何注入属性的基本示例,包括属性的默认值:

@Inject
@ConfigProperty(name="tempFileName", defaultValue="file.tmp")
String fileName;

在前面的代码中,我们将应用程序属性注入到 fileName 变量中。请注意,应仔细规划属性名称,因为 Quarkus 附带了一组广泛的系统属性,可用于管理其环境。幸运的是,您不需要手头有文档来检查所有可用的系统属性。事实上,您可以使用 Maven 的 generate-config 命令根据您当前安装的扩展列出所有内置系统属性:

mvn quarkus:generate-config

此命令将在 src/main/resources 文件夹下创建一个名为 application.properties.example 的文件。如果打开此文件,您将看到它包含所有可用配置选项的注释列表,这些选项位于 quarkus 命名空间下。这是它的简短摘录:

# The name of the application.
# If not set, defaults to the name of the project.
#
#quarkus.application.name=
 
# The version of the application.
# If not set, defaults to the version of the project
#
#quarkus.application.version=

作为旁注,您可以通过添加 -Dfile= 选项为 generate-command 选择不同的文件名。

在接下来的部分中,我们将使用位于 本书的 GitHub 存储库的 Chapter08/advanced-config 文件夹中的示例作为参考,了解一些高级配置练习。我们建议您在继续之前将项目导入您的 IDE。

Multiple configuration sources

application.properties 文件不是设置应用程序属性的唯一选项。根据 MicroProfile 的 Config 规范,您还可以使用以下内容:

  • Java system properties: Java system properties can be read/written programmatically by means of the System.getProperty() and System.setProperty() APIs. As an alternative, you can set a property on the command line with the -D option, as follows:
java -Dquarkus.http.port=8180 app.jar
  • Environment variables: This requires setting an environment variable for the property, as follows:
export QUARKUS_HTTP_PORT=8180

您可能已经注意到,匹配的环境变量名称已设置为大写,点已替换为下划线。

请注意,在当前版本的 Quarkus 中,还需要在 application.properties 以便它可以被环境变量覆盖。

最后,还可以通过向我们的应用程序添加新的配置源来从外部源收集我们的配置。下一节将向我们展示如何做到这一点。

Configuring custom configuration sources

在到目前为止我们创建的所有示例中,我们假设应用程序配置是从 src/main/resources/application.properties 文件中获取的,这是 Quarkus 应用程序的默认设置。尽管如此,由于 Quarkus 完全支持 MicroProfile Config 规范,因此完全可以从其他来源加载配置,这可能是外部文件系统、数据库或任何可以由 Java 应用程序加载的东西!

为此,您必须实现 org.eclipse.microprofile.config.spi.ConfigSource 接口,该接口公开了一组用于加载属性的方法 (getProperties) ,检索属性的名称 (getPropertyNames),并检索相应的值 (getValue)。

作为概念证明,请查看 Chapter08/advanced-config 项目中的以下实现:

public class FileConfigSource implements ConfigSource {
    private final String CONFIG_FILE = "/tmp/config.properties";
    private final String CONFIG_SOURCE_NAME = "ExternalConfigSource";
    private final int ORDINAL = 900;

    @Override
    public Map getProperties() {

        try(InputStream in = new FileInputStream( CONFIG_FILE )){

            Properties properties = new Properties();
            properties.load( in );

            Map map = new HashMap();
            properties.stringPropertyNames()
                    .stream()
                    .forEach(key-> map.put(key, 
                     properties.getProperty(key)));

            return map;

        } catch (IOException e) {
            e.printStackTrace();
        }

        return null;
    }

    @Override
    public Set getPropertyNames() {

        try(InputStream in = new FileInputStream( CONFIG_FILE )){

            Properties properties = new Properties();
            properties.load( in );

            return properties.stringPropertyNames();

        } catch (IOException e) {
            e.printStackTrace();
        }

        return null;
    }

    @Override
    public int getOrdinal() {
        return ORDINAL;
    }

    @Override
    public String getValue(String s) {

        try(InputStream in = new FileInputStream( CONFIG_FILE )){
            Properties properties = new Properties();
            properties.load( in );
            return properties.getProperty(s);

        } catch (IOException e) {
            e.printStackTrace();
        }

        return null;
    }

    @Override
    public String getName() {
        return CONFIG_SOURCE_NAME;
    }
}

如果您熟悉 java.io API,则代码本身非常简单。 FileConfigSource 类尝试从文件系统的 /tmp/config.properties 路径加载外部配置。值得一提的是,已经设置了一个 ORDINAL 变量来指定此 ConfigSource 类的顺序,以防某些属性从多个源加载。

ConfigSource 的默认值设置为 100,如果该属性是跨多个源定义的,则具有最高序数值的源具有优先权。以下是可用配置源的排名:

Config source Value
application.properties 100
Environment variables 300
System properties 400

由于我们在示例中将 ORDINAL 变量设置为 900,因此它将优先于其他配置源(如果有)。

一旦自定义的 ConfigSource 在项目中可用,我们需要注册这个类。为此,我们在项目的 resources/META-INF/services 文件夹下添加了一个名为 org.eclipse.microprofile.config.spi.ConfigSource 的文件。这是项目的树视图,位于 resources 文件夹下:

│   └── resources
│       └── META-INF
│               └── services
│                   ├── org.eclipse.microprofile.config.spi.ConfigSource

在此文件中,我们指定了 ConfigSource 的完全限定名称。在我们的例子中,如下所示:

com.packt.chapter8.FileConfigSource

现在,一旦应用程序启动,自定义 ConfigSource 将被加载,其属性将优先于相同属性的其他潜在副本。

在您的项目的 AdvancedConfigTest 类中,您会发现一个断言,它验证是否已从外部 FileConfigSource 类加载了一个属性:

given()
        .when().get("/hello")
        .then()
        .statusCode(200)
        .body(is("custom greeting"));

关于 AdvancedConfigTest 类的更多细节将在本章后面讨论。

Using converters in your configuration

为了讨论配置转换器,让我们以这个简单的配置示例为例:

year=2019
isUser=true

在这里,将前面的属性注入到我们的代码中是非常好的:

@ConfigProperty(name = "year", defaultValue = "2020")
Integer year;

@ConfigProperty(name = "isUser", defaultValue = "false")
Boolean isUser;

在底层,MicroProfile Config API 为不仅仅是纯字符串的值提供了类型安全的转换。

另外,请注意,我们可以为属性提供默认值,如果该属性尚未在我们的配置中定义,则将使用该默认值。

这是通过在配置模型中提供转换器来实现的。默认情况下,MicroProfile Config API 已经提供了一些开箱即用的转换器。以下是内置转换器的列表:

  • boolean and java.lang.Boolean. The following values are converted into Booleans (case-insensitive): true, YES, Y, 1, and ON. Any other value will be false.
  • byte and java.lang.Byte.
  • short and java.lang.Short.
  • int and java.lang.Integer.
  • long and java.lang.Long.
  • float and java.lang.Float. A dot . is used to separate the fractional digits.
  • double and java.lang.Double. A dot . is used to separate the fractional digits.
  • char and java.lang.Character.
  • java.lang.Class. This is based on the result of Class.forName.

还支持数组、列表和集合。为了将这些集合之一注入到类变量中,您可以使用逗号 (,) 字符作为分隔符并使用 \ 作为转义字符。例如,采取以下配置:

students=Tom,Pat,Steve,Lucy

以下代码会将上述配置注入到 java.util.List 元素中:

@ConfigProperty(name = "students")
List<String> studentList;

以同样的方式,您可以使用内置转换器从值列表生成 Array。看一下下面的配置示例:

pets=dog,cat,bunny

可以将上述配置注入到字符串数组中,如下所示:

@ConfigProperty(name = "pets")
String[] petsArray;

甚至类也可以作为配置的一部分注入:

myclass=TestClass

在运行时,类将由类加载器搜索并使用 Class.forName 构造创建。我们可以把它放在我们的代码中,如下所示:

@ConfigProperty(name = "myclass")
TestClass clazz;

最后,值得一提的是,您可以注入整个 Config 对象并在每次需要时检索单个属性:

@Inject
Config config;

@GET
@Produces(MediaType.TEXT_PLAIN)
public String hello() {
    Integer y = config.getValue("year", Integer.class);
    return "Year is " +y;
}

现在,让我们探索一些更高级的创建类型转换器的策略。

Adding custom converters

如果内置转换器列表不够用,您仍然可以通过实现通用接口,即org.eclipse.microprofile.config.spi.Converter来创建自定义转换器。接口的Type参数是字符串转换成的目标类型:

public class MicroProfileCustomValueConverter implements Converter<CustomConfigValue> {

    public MicroProfileCustomValueConverter() {
    }

    @Override
    public CustomConfigValue convert(String value) {
        return new CustomConfigValue(value);
    }
}

以下代码用于目标 Type 参数,它派生自我们已包含在配置中的纯 Java 字符串:

public class CustomConfigValue {
    
    private final String email;
    private final String user;

    public CustomConfigValue(String value) {

        StringTokenizer st = new StringTokenizer(value,";");
        this.user = st.nextToken();
        this.email = st.nextToken();       
    }

    public String getEmail() {
        return email;
    }

    public String getUser() {
        return user;
    }

您必须在名为 resources/META-INF/services/org.eclipse.microprofile.config.spi.Converter 的文件中注册您的转换器。包括自定义实现的完全限定类名。例如,在我们的例子中,我们添加了以下行:

com.packt.chapter8.MicroProfileCustomValueConverter

现在,让我们学习如何在实践中使用我们的自定义转换器。为此,我们将以下行添加到 application.properties 文件中,该文件使用 CustomConfigValue 类的构造函数中编码的模式:

customconfig=john;[email protected]

现在,自定义转换器可以作为类属性注入到我们的代码中:

@ConfigProperty(name = "customconfig")
CustomConfigValue value;

@Path("/email")
@GET
@Produces(MediaType.TEXT_PLAIN)
public String getEmail() {
    return value.getEmail();
}

尽管前面的示例没有什么花哨的,但它向我们展示了如何根据类定义创建自定义属性。

Testing advanced configuration options

在本章的 Chapter08/advanced-config/src/test 文件夹中,您会找到一个名为 AdvancedConfigTest 的测试类,它将验证我们所学的关键概念大约到目前为止。

要成功运行所有这些测试,请将 customconfig.properties 文件复制到驱动器的 /tmp 文件夹中,否则将使用 AdvancedConfigTest< 中包含的断言之一/kbd> 类将失败:

cp Chapter08/customconfig.properties /tmp

然后,只需运行 install 目标,这将触发测试的执行:

mvn install

您应该看到 AdvancedConfigTest 中包含的所有测试都通过了。

Configuration profiles

我们刚刚学习了如何使用内置转换器创建复杂的配置,对于要求最苛刻的自定义转换器。如果我们需要在不同的配置之间切换,例如从开发环境转移到生产环境时,该怎么办?在这里,您可以复制您的配置。然而,IT 项目并不总是欢迎配置文件的激增。让我们学习如何使用配置文件来处理这个问题。

简而言之,配置配置文件允许我们在配置中为配置文件指定命名空间,以便我们可以将每个属性绑定到同一文件中的特定配置文件。

Quarkus 开箱即用,附带以下配置文件:

  • dev: This is triggered when running in development mode (that is, quarkus:dev).
  • test: This is triggered when running tests.
  • prod: This is picked up when we're not running in development or test mode.
除了上述配置文件,您还可以定义自己的自定义配置文件,这些配置文件将根据我们在 激活配置文件部分。

您可以使用以下语法将配置参数绑定到特定配置文件:

%{profile}.config.key=value

为了查看这方面的实际示例,我们将浏览本书 GitHub 存储库的 Chapter08/profiles 文件夹中的源代码。我们建议您在继续之前将项目导入您的 IDE。

让我们从检查它的 application.properties 配置文件开始,它定义了多个配置文件:

%dev.quarkus.datasource.url=jdbc:postgresql://localhost:5432/postgresDev
%test.quarkus.datasource.url=jdbc:postgresql://localhost:6432/postgresTest
%prod.quarkus.datasource.url=jdbc:postgresql://localhost:7432/postgresProd
 
quarkus.datasource.driver=org.postgresql.Driver
quarkus.datasource.username=quarkus
quarkus.datasource.password=quarkus
 
quarkus.datasource.initial-size=1
quarkus.datasource.min-size=2
quarkus.datasource.max-size=8

%prod.quarkus.datasource.initial-size=10
%prod.quarkus.datasource.min-size=10
%prod.quarkus.datasource.max-size=20

在前面的配置中,我们为数据源连接指定了三个不同的 JDBC URL。每个都绑定到不同的配置文件。我们还为生产配置文件设置了特定的连接池设置,以便授予更多的数据库连接。在下一节中,我们将学习如何激活每个配置文件。

Activating profiles

让我们以上述配置中的prod配置文件为例,了解如何激活特定配置文件。首先,我们需要启动一个名为 postgresProd 的 PostgreSQL 实例并将其绑定到端口 7432

docker run --ulimit memlock=-1:-1 -it --rm=true --memory-swappiness=0 --name quarkus_Prod -e POSTGRES_USER=quarkus -e POSTGRES_PASSWORD=quarkus -e POSTGRES_DB=postgresProd -e PGPORT=7432 -p 7432:7432 postgres:10.5

然后,我们需要在package阶段提供profile信息,如下:

mvn clean package -Dquarkus.profile=prod -DskipTests=true

运行应用程序时,它将获取您在 package 阶段指定的配置文件:

java -jar target/profiles-demo-1.0-SNAPSHOT-runner.jar

作为替代方案,也可以使用 QUARKUS_PROFILE 环境变量指定配置文件,如下所示:

export QUARKUS_PROFILE=dev
java -jar target/profiles-demo-1.0-SNAPSHOT-runner.jar

最后,值得一提的是,同样的策略可用于定义非标准配置文件。例如,假设我们要为需要在生产前检查的应用程序添加 staging 配置文件:

%staging.quarkus.datasource.url=jdbc:postgresql://localhost:8432/postgresStage

在这里,我们可以应用与其他配置文件相同的策略,即,我们可以在应用程序启动时使用 Java 系统属性 (quarkus-profile) 指定配置文件,也可以添加必要的信息到 QUARKUS_PROFILE 环境变量。

Automatic profile selection

为了简化开发和测试,devtest 配置文件可以由 Maven 插件自动触发。因此,例如,如果您在开发模式下执行 Quarkus,最终将使用 dev 配置文件:

mvn quarkus:dev

以同样的方式,test 配置文件将在执行测试时被激活,例如,在 install 生命周期阶段:

mvn quarkus:install

test 配置文件将在您执行 Maven test 目标时激活。此外,值得了解的是,您可以通过 maven-surfire-plugin 在其系统属性中为您的测试设置不同的配置文件:

<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>${surefire-plugin.version}</version>
<configuration>
    <systemPropertyVariables>
        <quarkus.test.profile>custom-test</quarkus.test.profile>
        <buildDirectory>${project.build.directory}</buildDirectory>
    </systemPropertyVariables>
</configuration>

在本节中,我们浏览了应用程序配置文件。在下一节中,我们将学习如何控制 Quarkus 应用程序的生命周期。

Controlling the application life cycle

控制应用程序生命周期是服务能够引导一些外部资源或验证组件状态的常见要求。一种从 Java Enterprise API 借用的简单策略是包含 Undertow 扩展(或任何上层,例如休息服务),以便您可以利用 ServletContextListener,它在创建或销毁 Web 应用程序时收到通知。这是它的最小实现:

public final class ContextListener implements ServletContextListener {

    private ServletContext context = null;

    public void contextInitialized(ServletContextEvent event) {
        context = event.getServletContext();
        System.out.println("Web application started!");

    }
    public void contextDestroyed(ServletContextEvent event) {
       context = event.getServletContext();
       System.out.println("Web application stopped!");

    }
}

尽管在 Quarkus Web 应用程序中重用此策略非常好,但建议将此方法用于任何类型的 Quarkus 服务。这可以通过观察 io.quarkus.runtime.StartupEventio.quarkus.runtime.ShutdownEvent 事件来完成。此外,在 CDI 应用程序中,您可以使用 @Initialized(ApplicationScoped.class) 限定符观察事件,当应用程序上下文被初始化。这对于引导资源(例如数据库)特别有用,在 Quarkus 读取配置之前需要这些资源。

要查看这方面的实际示例,请查看本书 GitHub 存储库的 Chapter08/lifecycle 文件夹中的源代码。像往常一样,建议您在继续之前将项目导入您的 IDE。此示例的目的是向您展示如何在我们的客户服务中将 PostgreSQL 数据库替换为 H2 数据库(https: //www.h2database.com/)。

从配置开始,生命周期项目不再包含 PostgreSQL JDBC 依赖项。为了替换它,已包含以下内容:

<dependency>
  <groupId>io.quarkus</groupId>
  <artifactId>quarkus-jdbc-h2</artifactId>
</dependency>

为了测试我们的客户服务,我们包含了两个 H2 数据库配置文件:一个绑定到 dev 配置文件,一个绑定到 test 配置文件:

%dev.quarkus.datasource.url=jdbc:h2:tcp://localhost:19092/mem:test
%test.quarkus.datasource.url=jdbc:h2:tcp://localhost/mem:test
quarkus.datasource.driver=org.h2.Driver

要在应用上下文启动之前绑定 H2 数据库,我们可以使用下面的 DBLifeCycleBean 类:

@ApplicationScoped
public class DBLifeCycleBean {

    protected final Logger log = 
     LoggerFactory.getLogger(this.getClass());

    // H2 Database
    private Server tcpServer;

    public void observeContextInit(@Observes 
     @Initialized(ApplicationScoped.class) Object event) {
        try {
            tcpServer =  Server.createTcpServer("-tcpPort",
             "19092", "-tcpAllowOthers").start();
            log.info("H2 database started in TCP server 
            mode on Port 19092");
        } catch (SQLException e) {

            throw new RuntimeException(e);

        }
    }
    void onStart(@Observes StartupEvent ev) {
        log.info("Application is starting");
    }

    void onStop(@Observes ShutdownEvent ev) {
        if (tcpServer != null) {
            tcpServer.stop();
            log.info("H2 database was shut down");
            tcpServer = null;
        }
    }
}

此类能够拦截以下事件:

  • Context startup: This is captured through the observeContextInit method. The database is bootstrapped in this method.
  • Application startup: This is captured through the onStart method. We are simply performing some logs when this event is fired.
  • Application shutdown: This is captured through the onStop method. We are shutting down the database in this method.

现在,您可以像往常一样使用以下命令在 dev 配置文件中启动 Quarkus:

mvn quarkus:dev

当应用启动时,会通知我们H2数据库已经启动:

INFO  [com.pac.qua.cha.DBLifeCycleBean] (main) H2 database started in TCP server mode on Port 19092

然后,我们将在应用程序启动时收到更多通知,其中我们可以包含一些要完成的额外任务:

[com.pac.qua.cha.DBLifeCycleBean] (main) Application is starting

最后,当我们停止应用程序时,资源将被解除,如以下控制台日志所示:

[com.pac.qua.cha.DBLifeCycleBean] (main) H2 database was shut down

在关闭数据库之前,您可以使用一个很小的内存数据库层来运行您的客户服务示例。

Activating a database test resource

作为奖励提示,我们将向您展示如何在测试生命周期中激活 H2 数据库。这可以通过向您的测试类添加一个注释为 @QuarkusTestResource 的类来完成,同时将 H2DatabaseTestResource 类作为属性传递。

这是一个例子:

@QuarkusTestResource(H2DatabaseTestResource.class)
public class TestResources {
}

H2DatabaseTestResource 基本上执行与 DBLifeCycleBean 相同的操作,在我们的测试被触发之前。请注意,已将以下依赖项添加到项目中以运行上述测试类:

<dependency>
  <groupId>io.quarkus</groupId>
  <artifactId>quarkus-test-h2</artifactId>
  <scope>test</scope>
</dependency>

现在,您可以使用以下命令安全地针对 test 配置文件运行测试:

mvn install

请注意,在执行我们的测试之前,以下日志将确认 H2 数据库已在我们的可用 IP 地址之一上启动:

[INFO] H2 database started in TCP server mode; server status: TCP server  running at tcp://10.5.126.52:9092 (only local connections)

引导外部资源确实是生命周期管理器的常见用例。另一个常见的用例包括在应用程序启动阶段调度事件。在下一节中,我们将讨论如何使用 Quarkus 的调度程序触发事件。

Firing events with the Quarkus scheduler

Quarkus 包含一个名为 scheduler 的扩展,可用于调度任务以进行单次或重复执行。我们可以使用 cron 格式来指定调度程序触发事件的次数。

以下示例的源代码位于 本书的 GitHub 存储库的 Chapter08/scheduler 文件夹中。如果您检查 pom.xml 文件,您会注意到已添加以下扩展名:

<dependency>
      <groupId>io.quarkus</groupId>
      <artifactId>quarkus-scheduler</artifactId>
</dependency>

我们的示例项目每 30 秒生成一个随机令牌(为简单起见,使用随机字符串)。负责生成随机令牌的类是以下 TokenGenerator 类:

@ApplicationScoped
public class TokenGenerator {

    private String token;

    public String getToken() {
        return token;
    }

    @Scheduled(every="30s")
    void generateToken() {
        token= UUID.randomUUID().toString();
        log.info("New Token generated"); 
    }

}

现在,我们可以将令牌注入内置的 REST 端点,如下所示:

@Path("/token")
public class Endpoint {

    @Inject
    TokenGenerator token;

    @GET
    @Produces(MediaType.TEXT_PLAIN)
    public String getToken() {

        return token.getToken();
    }
}

使用以下命令像往常一样启动应用程序:

mvn quarkus:dev

您会注意到,每隔 30 秒,控制台日志中会打印以下消息:

[INFO] New Token generated

然后,通过请求 /token URL,将返回随机生成的字符串:

curl http://localhost:8080/token
 3304a8de-9fd7-43e7-9d25-6e8896ca67dd

Using the cron scheduler format

除了使用时间表达式(s=seconds、m=minutes、h=hours、d=days),您可以选择更紧凑的 cron 调度程序表达式。因此,如果您想每秒触发一次事件,那么您可以使用以下 cron 表达式:

@Scheduled(cron="* * * * * ?")
void generateToken() {
    token= UUID.randomUUID().toString();
    log.info("New token generated");
}

查看 cron 主页以获取有关 cron 格式的更多信息:http:// man7.org/linux/man-pages/man5/crontab.5.html

Firing one-time events

如果你需要执行一次性事件,那么你可以直接将 io.quarkus.scheduler.Scheduler 类注入你的代码并使用 startTimer 方法,该方法会触发在单独的线程中执行操作。这可以在以下示例中看到:

@Inject
Scheduler scheduler;

public void oneTimeEvnt() {

    scheduler.startTimer(300, () -> oneTimeAction());
    
}

public void oneTimeAction() {
    // Do something
}

在这个简短的摘录中,我们可以看到将在 oneTimeAction() 方法中执行的单个事件如何在 300 毫秒后触发一次性操作。

Summary

在本章中,我们介绍了一些高级技术,我们可以使用这些技术来使用转换器和配置文件来管理我们的配置。我们还演示了如何注入不同的配置源并优先于标准配置文件。在本章的第二部分,我们了解了如何捕获应用程序生命周期的事件以及如何安排未来任务的执行。

为了使我们的应用程序更具可扩展性,在下一章中,我们将讨论如何构建事件驱动和非阻塞的反应式应用程序。紧紧抓住!