本手册正在编写中,目前尚未完善。
如果您想帮助改进它,我们希望您能这样做,请查看 README

1 介绍

Ratpack 是一组 Java 库,它们可以帮助构建快速、高效、可扩展且经过良好测试的 HTTP 应用。它构建于高性能且高效的 Netty 事件驱动的网络引擎之上。

Ratpack 纯粹是一个运行时。它没有可安装的软件包,也没有耦合的构建工具(如 Rails、Play、Grails)。要构建 Ratpack 应用,您可以使用任何 JVM 构建工具。Ratpack 项目通过插件为 Gradle 提供了特定的支持,但也可以使用任何其他构建工具。

Ratpack 作为一组库 JAR 发布。ratpack-core 库是唯一严格需要的库。其他库,例如 ratpack-groovyratpack-guiceratpack-jacksonratpack-test 等,都是可选的。

1.1 目标

Ratpack 的目标是

  1. 快速、可扩展且高效
  2. 允许应用在复杂性方面发展而不会妥协
  3. 利用非阻塞编程的优势并降低成本
  4. 在集成其他工具和库方面灵活且不强求
  5. 允许应用轻松且彻底地测试

Ratpack 的目标 **不是**

  1. 成为一个完全集成的“全栈”解决方案
  2. 提供您可能需要的所有功能
  3. 为“业务逻辑”提供架构或框架

2.1 关于本手册

Ratpack 的文档分散在本手册和 Javadoc API 参考 中。本手册从高层次介绍主题和概念,并链接到 Javadoc 以提供详细的 API 信息。大多数信息包含在 Javadoc 中。预期的是,一旦您理解了核心 Ratpack 概念,本手册将变得不那么有用,Javadoc 将变得更有用。

1.2.1 代码示例

文档中的所有代码示例都经过测试,大多数是完整的程序,您可以复制/粘贴并自行运行(给定正确的类路径等)。

大多数示例都以微小的嵌入式 Ratpack 应用形式给出,在测试中。以下是以 Ratpack 代码示例的“Hello World”为例。

import ratpack.test.embed.EmbeddedApp;
import static org.junit.jupiter.api.Assertions.assertEquals;
 
public class Example {
  public static void main(String... args) throws Exception {
    EmbeddedApp.fromHandler(ctx -> 
      ctx.render("Hello World!")
    ).test(httpClient -> 
      assertEquals("Hello World!", httpClient.getText())
    );
  }
}

为了清晰起见,import 语句默认情况下是折叠的。单击它们以显示/隐藏它们。

此示例是一个完整的 Ratpack 应用。但是,EmbeddedApp 不是通常用于正式应用的入口点(有关典型入口点的详细信息,请参见 启动章节)。EmbeddedApp 是面向测试的。它便于在大型应用期间启动/停止非常小的(或完全成熟的)应用,并提供了一种方便的方式来对应用进行 HTTP 请求。它在示例中用于将引导量降到最低,以便专注于要演示的 API。

在此示例中,我们正在使用默认配置在短暂端口上启动一个 Ratpack 服务器,它以纯文本字符串“Hello World”响应所有 HTTP 请求。这里使用的 test() 方法为给定函数提供了一个 TestHttpClient,它被配置为对正在测试的服务器进行请求。此示例以及所有类似的示例都对 Ratpack 服务器进行 HTTP 请求。EmbeddedAppTestHttpClient 作为 Ratpack 的一部分提供 测试支持.

在许多示例中使用的另一个关键测试实用程序是 ExecHarness.

import com.google.common.io.Files;
import ratpack.test.exec.ExecHarness;
import ratpack.exec.Blocking;

import java.io.File;
import java.nio.charset.StandardCharsets;

import static org.junit.jupiter.api.Assertions.assertEquals;

public class Example {
  public static void main(String... args) throws Exception {
    File tmpFile = File.createTempFile("ratpack", "test");
    Files.asCharSink(tmpFile, StandardCharsets.UTF_8).write("Hello World!");
    tmpFile.deleteOnExit();

    String content = ExecHarness.yieldSingle(e ->
        Blocking.get(() -> Files.asCharSource(tmpFile, StandardCharsets.UTF_8).read())
    ).getValueOrThrow();

    assertEquals("Hello World!", content);
  }
}

EmbeddedApp 支持创建整个 Ratpack 应用时,ExecHarness 仅提供 Ratpack 执行模型的基础设施。它通常用于对使用 Ratpack 结构(如 Promise)的异步代码进行单元测试(有关执行模型的更多信息,请参见 “异步和非阻塞” 章节)。ExecHarness 也作为 Ratpack 的一部分提供 测试支持.

1.1.2.1 Java 8 风格

Ratpack 建立在 Java 8 之上,并且需要 Java 8。代码示例广泛地使用 Java 8 结构,例如 lambda 表达式和方法引用。如果您熟悉 Java 但不熟悉 Java 8 中的新结构,您可能会发现这些示例“奇特”。

2 快速入门

本章提供有关如何启动并运行 Ratpack 应用以进行操作的说明。

1.2 使用 Groovy 脚本

Ratpack 应用可以实现为单个 Groovy 脚本。这是一种用 Ratpack 和 Groovy 进行实验的有用方法。

首先,安装 Groovy

创建具有以下内容的文件 ratpack.groovy

@Grapes([
  @Grab('io.ratpack:ratpack-groovy:2.0.0-rc-1'),
  @Grab('org.slf4j:slf4j-simple:1.7.36')
])
import static ratpack.groovy.Groovy.ratpack

ratpack {
    handlers {
        get {
            render "Hello World!"
        }
        get(":name") {
            render "Hello $pathTokens.name!"
        }
    }
}

现在,您可以在命令行上运行以下命令来启动应用

groovy ratpack.groovy

服务器将通过 http://localhost:5050/ 可用。

handlers() 方法 接受一个闭包,该闭包委托给 GroovyChain 对象。“Groovy 处理器链 DSL”用于构建响应处理策略。

开发过程中对文件的更改是实时的。您可以编辑该文件,更改将在下次请求时生效。

2.2 使用 Gradle 插件

我们建议使用 Gradle 构建系统 来构建 Ratpack 应用。Ratpack 不需要 Gradle;任何构建系统都可以使用。

以下说明假设您已经安装了 Gradle。有关安装说明,请参见 Gradle 用户指南

Ratpack 项目提供了两个 Gradle 插件

  1. io.ratpack.ratpack-java - 用于用 Java 实现的 Ratpack 应用
  2. io.ratpack.ratpack-groovy - 用于用 Groovy 实现的 Ratpack 应用

有关 Gradle 构建支持的更详细解释,请参见 专用章节

1.2.2 使用 Gradle Java 插件

创建具有以下内容的文件 build.gradle

buildscript {
  repositories {
    mavenCentral()
  }
  dependencies {
    classpath "io.ratpack:ratpack-gradle:2.0.0-rc-1"
  }
}

apply plugin: "io.ratpack.ratpack-java"
apply plugin: "idea"

repositories {
  mavenCentral()
}

dependencies {
  runtimeOnly "org.slf4j:slf4j-simple:1.7.36"
}

mainClassName = "my.app.Main"

创建具有以下内容的文件 src/main/java/my/app/Main.java

package my.app;

import ratpack.core.server.RatpackServer;

public class Main {
  public static void main(String... args) throws Exception {
    RatpackServer.start(server -> server 
      .handlers(chain -> chain
        .get(ctx -> ctx.render("Hello World!"))
        .get(":name", ctx -> ctx.render("Hello " + ctx.getPathTokens().get("name") + "!"))     
      )
    );
  }
}

现在,您可以通过使用 Gradle 执行 run 任务(即在命令行上运行 gradle run)来启动应用,也可以通过将项目导入到 IDE 中并执行 my.app.Main 类来启动应用。

运行后,服务器将通过 http://localhost:5050/ 可用。

handlers() 方法 接受一个函数,该函数接收一个 Chain 对象。“处理器链 API”用于构建响应处理策略。

Ratpack Gradle 插件支持 Gradle 的持续构建功能。使用它可以使源代码的更改自动应用到正在运行的应用。

有关使用 Ratpack 与 Groovy 的更多信息,请参见 Gradle 章节。

2.2.2 使用 Gradle Groovy 插件

创建具有以下内容的文件 build.gradle

buildscript {
  repositories {
    mavenCentral()
  }
  dependencies {
    classpath "io.ratpack:ratpack-gradle:2.0.0-rc-1"
  }
}

apply plugin: "io.ratpack.ratpack-groovy"
apply plugin: "idea"

repositories {
  mavenCentral()
}

dependencies {
  runtimeOnly "org.slf4j:slf4j-simple:1.7.36"
}

创建具有以下内容的文件 src/ratpack/ratpack.groovy

import static ratpack.groovy.Groovy.ratpack

ratpack {
    handlers {
        get {
            render "Hello World!"
        }
        get(":name") {
            render "Hello $pathTokens.name!"
        }
    }
}

现在,您可以通过使用 Gradle 执行 run 任务(即在命令行上运行 gradle run)来启动应用,也可以通过将项目导入到 IDE 中并执行 ratpack.groovy.GroovyRatpackMain 类来启动应用。

运行后,服务器将通过 http://localhost:5050/ 可用。

handlers() 方法 接受一个闭包,该闭包委托给 GroovyChain 对象。“Groovy 处理器链 DSL”用于构建响应处理策略。

Ratpack Gradle 插件支持 Gradle 的持续构建功能。使用它可以使源代码的更改自动应用到正在运行的应用。

有关使用 Ratpack 与 Groovy 的更多信息,请参见 Groovy 章节。

有关使用 Ratpack 与 Groovy 的更多信息,请参见 Gradle 章节。

3 架构

本章从高层次描述 Ratpack 应用。

1.3 强类型

Ratpack 是强类型的。除了用 Java(一种强类型语言)实现之外,它的 API 还包含类型。例如,Registry 的概念在 Ratpack 中被广泛使用。Registry 可以看作是一个使用类型作为键的映射。

这对于用 Groovy 实现其应用的 Ratpack 用户来说可能最有趣。Ratpack 的 Groovy 适配器 使用最新的 Groovy 功能来完全支持静态类型,同时保持惯用的简洁的 Groovy API。

2.3 非阻塞

Ratpack 的核心是一个基于事件的(即非阻塞的)HTTP IO 引擎,以及一个使构建响应逻辑变得容易的 API。与“传统”阻塞 Java API 相比,非阻塞会强加不同的 API 风格,因为 API 必须是异步的

Ratpack 旨在为 HTTP 应用简化这种编程风格。它提供了对构建异步代码的支持(请参见 “异步和非阻塞” 章节),并使用了一种创新的方法将请求处理结构化为一个自构建的、异步遍历的函数图(它不像听起来那么复杂)。

3.3 组成部分

在以下部分中,“引号”用于表示 Ratpack 关键术语和概念。

Ratpack 应用从“启动配置”开始,正如您所预期的那样,它提供了启动应用所需的配置。Ratpack “服务器”可以仅从“启动配置”构建和启动。“服务器”一旦启动,就会开始监听请求。有关这方面的更多详细信息,请参见 “启动” 章节。

提供给“启动配置”的一个关键配置是“处理器工厂”,它创建“处理器”。“处理器”被要求对每个请求做出响应。处理器可以执行以下三件事之一

  1. 响应请求
  2. 委托给“下一个”处理器
  3. “插入”处理器并立即委托给它们

所有请求处理逻辑只是处理器的组合(有关这方面的更多详细信息,请参见 Handlers 章节)。重要的是,处理不受线程的约束,可以异步完成。“处理器”API 支持这种异步组合。

处理程序在“上下文”中操作。“上下文”代表处理程序图中特定点的请求处理状态。它的关键功能之一是充当“注册表”,可用于按类型检索对象。这允许处理程序通过公共类型从“上下文”中检索策略对象(通常只是实现关键接口的对象)。当处理程序将其他处理程序插入处理程序图时,它们可以为上下文注册表做出贡献。这允许处理程序将代码(作为策略对象)贡献给下游处理程序。有关更多详细信息,请参见“上下文”章节,以及以下关于此上下文注册表如何在实践中使用的部分。

这是一个关于 Ratpack 应用程序的高级抽象描述。目前可能尚不清楚所有这些是如何转换为实际代码的。本手册的其余部分以及附带的 API 参考将提供详细说明。

4.3 通过注册表实现插件和扩展性

Ratpack 没有插件的概念。但是,添加的与 Google Guice 集成通过 Guice 模块促进了某种插件系统。Guice 是一个依赖注入容器。Guice 模块定义要作为依赖注入容器一部分的对象。Guice 模块可以通过提供关键 Ratpack 接口的实现来充当插件,这些实现由处理程序使用。在使用 Guice 集成时,所有 Guice 知晓的对象(通常通过 Guice 模块)都可以通过“上下文注册表”获得。也就是说,处理程序可以通过类型检索它们。

为了了解这为什么有用,我们将使用将对象渲染为 JSON 的需求来响应。“上下文”对象传递给“处理程序”具有render(Object)方法。此方法的实现只是在上下文注册表中搜索可以渲染给定类型对象的Renderer实现。因为 Guice 可用的对象可通过注册表获得,所以它们可以用于渲染。因此,添加具有针对所需类型的 Renderer 实现的 Guice 模块将允许将其集成到请求处理中。从概念上讲,这与普通的依赖注入没有区别。

虽然我们在上面的示例中使用了 Guice 集成,但这种方法并不绑定到 Guice(Guice 不是 Ratpack 核心 API 的一部分)。另一个依赖注入容器(例如 Spring)可以轻松使用,或者根本不使用容器。任何对象源都可以适应 Ratpack 的Registry接口(也有一个构建器)。

5.3 服务和业务逻辑

Ratpack 对如何构建与请求处理无关的代码(即业务逻辑)没有意见。我们将使用术语“服务”作为执行某种业务逻辑的对象的总称。

处理程序当然可以自由地使用它们需要的任何服务。从处理程序访问服务的两种主要模式

  1. 在构造处理程序时向其提供服务
  2. 从上下文注册表中检索服务

4 启动

本章描述了 Ratpack 应用程序是如何启动的,有效地详细说明了 Ratpack API 的入口点。

1.4 RatpackServer

RatpackServer类型是 Ratpack 的入口点。您编写自己的主类,使用此 API 启动应用程序。

package my.app;

import ratpack.core.server.RatpackServer;
import ratpack.core.server.ServerConfig;
import java.net.URI;

public class Main {
  public static void main(String... args) throws Exception {
    RatpackServer.start(server -> server
      .serverConfig(ServerConfig.embedded().publicAddress(new URI("http://company.org")))
      .registryOf(registry -> registry.add("World!"))
      .handlers(chain -> chain
        .get(ctx -> ctx.render("Hello " + ctx.get(String.class)))
        .get(":name", ctx -> ctx.render("Hello " + ctx.getPathTokens().get("name") + "!"))     
      )
    );
  }
}

应用程序定义为传递给该接口的 of()start() 静态方法的函数。该函数接受一个RatpackServerSpec,该函数可用于指定 Ratpack 应用程序的三个基本方面(即服务器配置、基本注册表、根处理程序)。

本手册和 API 参考中的大多数示例都使用EmbeddedApp而不是 RatpackServer 来创建应用程序。这是由于示例的“测试”性质。有关代码示例的更多信息,请参见本节

1.1.4 服务器配置

ServerConfig定义了启动服务器所需的配置设置。ServerConfig 的静态方法可用于创建实例。

1.1.1.4 基本目录

服务器配置的一个重要方面是基本目录。基本目录实际上是应用程序文件系统的根目录,提供了一个可移植的文件系统。所有在运行时解析为文件的相对路径将相对于基本目录进行解析。静态资源(例如图像、脚本)通常通过基本目录使用相对路径进行提供。

baseDir(Path)方法允许将基本目录设置为某个已知位置。为了在必要时跨环境实现可移植性,调用此方法的代码负责确定给定运行时的基本目录应该是哪个。

更常见的是使用BaseDir.find(),它支持在类路径上查找基本目录,从而提供更好的跨环境可移植性。此方法在类路径上路径为 "/.ratpack" 的位置搜索资源。

要使用与 /.ratpack 默认值不同的路径,请使用BaseDir.find(String)方法。

标记文件的内容完全被忽略。它仅用于查找封闭目录,该目录将用作基本目录。该文件可能位于类路径上的 JAR 中,也可能位于类路径上的目录中。

以下示例演示了如何使用 BaseDir.find() 从类路径中发现基本目录。

import ratpack.core.server.ServerConfig;
import ratpack.test.embed.EphemeralBaseDir;
import ratpack.test.embed.EmbeddedApp;

import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Path;

import static org.junit.jupiter.api.Assertions.assertEquals;

public class Example {
  public static void main(String... args) throws Exception {
    EphemeralBaseDir.tmpDir().use(baseDir -> {
      baseDir.write("mydir/.ratpack", "");
      baseDir.write("mydir/assets/message.txt", "Hello Ratpack!");
      Path mydir = baseDir.getRoot().resolve("mydir");

      ClassLoader classLoader = new URLClassLoader(new URL[]{mydir.toUri().toURL()});
      Thread.currentThread().setContextClassLoader(classLoader);

      EmbeddedApp.of(serverSpec -> serverSpec
        .serverConfig(c -> c.baseDir(mydir))
        .handlers(chain ->
          chain.files(f -> f.dir("assets"))
        )
      ).test(httpClient -> {
        String message = httpClient.getText("message.txt");
        assertEquals("Hello Ratpack!", message);
      });
    });
  }
}

上面的示例中EphemeralBaseDir的使用以及新的上下文类加载器的构造纯粹是为了使示例自包含。真实的主方法将简单地调用 BaseDir.find(),依赖于启动 Ratpack 应用程序 JVM 的任何东西都已使用适当的类路径启动。

Ratpack 通过 Java 7 的Path API 访问基本目录,允许透明地使用 JAR 内容作为文件系统。

2.1.1.4 端口

port(int)方法允许设置用于连接到服务器的端口。如果未配置,则默认值为 5050。

3.1.1.4 SSL

默认情况下,Ratpack 服务器将在配置端口上侦听 HTTP 流量。要启用 HTTPS 流量,ssl(SslContext)方法允许使用 SSL 证书和密钥。

从 v2.0 开始,Ratpack 还支持使用服务器名称指示 (SNI) 根据请求的主机选择 SSL 配置。该ssl(SslContext, Action)用于指定默认 SSL 配置和任何使用备用 SSL 配置的附加域映射。映射中指定的域支持DNS 通配符,并且最多将在域层次结构中匹配一层(例如 *.ratpack.io 将匹配 api.ratpack.io 但不匹配 docs.api.ratpack.io)。

通过系统属性或环境变量配置 SSL 设置需要特殊处理才能指定域名。下表显示了如何指定默认 SSl 配置和子域配置。

| 系统属性 | 环境变量 | 描述 | |————————————————|————————————————–|———————————————————————————| | ratpack.server.ssl.keystoreFile | RATPACK_SERVER__SSL__KEYSTORE_FILE | 指定包含服务器证书和私钥的 JKS 的路径 | | ratpack.server.ssl.keystorePassword | RATPACK_SERVER__SSL__KEYSTORE_PASSWORD | 指定密钥库 JKS 的密码 | | ratpack.server.ssl.truststoreFile | RATPACK_SERVER__SSL__TRUSTSTORE_FILE | 指定包含受信任证书的 JKS 的路径 | | ratpack.server.ssl.truststorePassword | RATPACK_SERVER__SSL__TRUSTSTORE_PASSWORD | 指定信任库 JKS 的密码 | | ratpack.server.ssl.ratpack_io.keystoreFile | RATPACK_SERVER__SSL__RATPACK_IO__KEYSTORE_FILE | 指定域 ratpack.io 的密钥库路径 | | ratpack.server.ssl.*_ratpack_io.kyestoreFile | RATPACK_SERVER__SSL___RATPACK_IO_KEYSTORE_FILE | 指定域 *.ratpack.io 的密钥库路径 |

请注意以下特殊规则:1. 在系统属性和环境变量中,域名分隔符(.)都转换为下划线(_) 2. 在环境变量中,域通配符字符(*)使用下划线(_)指定。这会导致在域名之前有 3 个下划线(___RATPACK_IO)。

2.1.4 注册表

一个registry是按类型存储对象的存储区。应用程序中可能存在许多不同的注册表,但所有应用程序都由“服务器注册表”支持。服务器注册表只是用于支持应用程序并定义在启动时的注册表的名称。

3.1.4 处理程序

服务器处理程序接收所有传入的 HTTP 请求。处理程序是可组合的,并且很少有应用程序实际上只包含一个处理程序。大多数应用程序的服务器处理程序是一个复合处理程序,通常是通过使用handlers(Action)方法创建的,该方法使用Chain DSL 来创建复合处理程序。

4.1.4 启动和停止操作

Service接口允许连接到应用程序生命周期。在接受任何请求之前,Ratpack 将通知所有服务并允许它们执行任何初始化。相反,当应用程序停止时,Ratpack 将通知所有服务并允许它们执行任何清理或终止。

5 处理程序

本章介绍处理程序,它是 Ratpack 应用程序的基本组件。

1.5 什么是处理程序?

从概念上讲,处理程序(Handler)只是一个作用于处理上下文(Context)的函数。

“hello world”处理程序看起来像这样…

import ratpack.core.handling.Handler;
import ratpack.core.handling.Context;

public class Example implements Handler {
  public void handle(Context context) {
      context.getResponse().send("Hello world!");
  }
}

正如我们在上一章中看到的,强制的启动配置属性之一是 HandlerFactory 实现,它提供主处理程序。此工厂创建的处理程序实际上是应用程序。

这似乎很有限,直到我们认识到处理程序不必是端点(即,它可以做的事情不仅仅是生成 HTTP 响应)。处理程序还可以以多种方式委托给其他处理程序,更多地充当路由功能。事实上,在框架级别(即类型)上,路由步骤和端点之间没有区别,这提供了很大的灵活性。这意味着可以通过组合处理程序来构建任何类型的自定义请求处理管道。这种组合方法是 Ratpack 作为工具包而不是神奇框架的哲学的典型示例。

本章的其余部分将讨论处理程序的某些方面,这些方面超出了 HTTP 级别的问题(例如读取标头、发送响应等),这些问题将在HTTP 章节中介绍。

2.5 处理程序委托

如果处理程序不打算生成响应,则它必须委托给另一个处理程序。它可以插入一个或多个处理程序,或者简单地推迟到下一个处理程序。

考虑一个根据请求路径路由到两个不同处理程序之一的处理程序。这可以实现为…

import ratpack.core.handling.Handler;
import ratpack.core.handling.Context;

public class FooHandler implements Handler {
  public void handle(Context context) {
    context.getResponse().send("foo");
  }
}

public class BarHandler implements Handler {
  public void handle(Context context) {
    context.getResponse().send("bar");
  }
}

public class Router implements Handler {
  private final Handler fooHandler = new FooHandler();
  private final Handler barHandler = new BarHandler();

  public void handle(Context context) {
    String path = context.getRequest().getPath();
    if (path.equals("foo")) {
      context.insert(fooHandler);
    } else if (path.equals("bar")) {
      context.insert(barHandler);
    } else {
      context.next();
    }
  }
}

委托的关键在于 context.insert() 方法,它将控制权传递给一个或多个链接的处理程序。 context.next() 方法将控制权传递给下一个链接的处理程序。

考虑以下示例…

import ratpack.core.handling.Handler;
import ratpack.core.handling.Context;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class PrintThenNextHandler implements Handler {
  private final String message;
  private final static Logger LOGGER = LoggerFactory.getLogger(PrintThenNextHandler.class);


  public PrintThenNextHandler(String message) {
    this.message = message;
  }

  public void handle(Context context) {
    LOGGER.info(message);
    context.next();
  }
}

public class Application implements Handler {
  public void handle(Context context) {
    context.insert(
      new PrintThenNextHandler("a"),
      new PrintThenNextHandler("b"),
      new PrintThenNextHandler("c")
    );
  }
}

假设 Application 是主处理程序(即启动配置的 HandlerFactory 返回的处理程序),当此应用程序接收到请求时,以下内容将写入 System.out

a
b
c

那么接下来会发生什么?当“c”处理程序委托给下一个处理程序时会发生什么?最后一个处理程序始终是一个内部处理程序,它会发出 HTTP 404 客户端错误(通过 context.clientError(404),我们将在后面讨论)。

请注意,插入的处理程序本身可以插入更多处理程序…

import ratpack.core.handling.Handler;
import ratpack.core.handling.Context;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class PrintThenInsertOrNextHandler implements Handler {
  private final String message;
  private final Handler[] handlers;
  private final static Logger LOGGER = LoggerFactory.getLogger(PrintThenInsertOrNextHandler.class);

  public PrintThenInsertOrNextHandler(String message, Handler... handlers) {
    this.message = message;
    this.handlers = handlers;
  }

  public void handle(Context context) {
    LOGGER.info(message);
    if (handlers.length == 0) {
      context.next();
    } else {
      context.insert(handlers);
    }
  }
}

public class Application implements Handler {
  public void handle(Context context) {
    context.insert(
      new PrintThenInsertOrNextHandler("a",
        new PrintThenInsertOrNextHandler("a.1"),
        new PrintThenInsertOrNextHandler("a.2"),
      ),
      new PrintThenInsertOrNextHandler("b",
        new PrintThenInsertOrNextHandler("b.1",
          new PrintThenInsertOrNextHandler("b.1.1")
        ),
      ),
      new PrintThenInsertOrNextHandler("c")
    );
  }
}

这将向 System.out 写入以下内容…

a
a.1
a.2
b
b.1
b.1.1
c

这演示了插入处理程序的处理程序的下一个处理程序如何成为插入的处理程序的最后一个处理程序的下一个处理程序。您可能需要多次阅读这句话才能理解。

您应该能够看到一种特定的嵌套功能出现。这对于可组合性很重要,并且对于作用域也很重要,这在本章后面考虑注册表上下文时将很重要。

在这一点上,自然会认为为典型的 Web 应用程序构建处理程序结构需要大量的操作(例如,将与特定请求路径匹配的请求分派到端点)。请继续阅读。

3.5 构建处理程序链

链(Chain)是用于组合(或链接)处理程序的构建器。链本身不会响应请求,而是将请求传递给其附加的处理程序。

再次考虑 Foo-Bar 路由器示例…

import ratpack.core.handling.Chain
import ratpack.core.handling.Handler;
import ratpack.core.handling.Context;
import ratpack.func.Action;

public class FooHandler implements Handler {
    public void handle(Context context) {
        context.getResponse().send("foo");
    }
}

public class BarHandler implements Handler {
    public void handle(Context context) {
        context.getResponse().send("bar");
    }
}

public class RouterChain implements Action<Chain> {
    private final Handler fooHandler = new FooHandler();
    private final Handler barHandler = new BarHandler();

    @Override
    void execute(Chain chain) throws Exception {
        chain.path("foo", fooHandler)
        chain.path("bar", barHandler)
    }
}

这次,我们不需要手动检查路径并处理每个代码分支。但是,结果是相同的。此链最终将被视为处理程序。此处理程序将被设置为从请求中读取路径,并首先将其与“foo”进行比较,然后与“bar”进行比较。如果其中任何一个匹配,它将 context.insert() 给定的处理程序。否则,它将调用 context.next()

与处理程序一样,上下文的目标不是成为一个神奇的工具。相反,它是一个强大的工具,由更灵活的工具(处理程序)构建而成。

1.3.5 添加处理程序和链

因此,链可以最简单地被认为是处理程序列表。向链的列表中添加处理程序最基本的方法是 all(Handler) 方法。“all”表示到达链中这一点的所有请求都将流经给定的处理程序。

如果我们稍微扩展一下思维,将链视为处理程序(一个专门用于插入处理程序的处理程序),那么我们可以将额外的链添加到链中。实际上,我们可以这样做,并且为了与 all(Handler) 方法匹配,您可以使用 insert(Action<Chain>) 方法。同样,这会插入一个链,所有请求都将通过该链进行路由。

现在,如果链只是处理一个处理程序列表,并依次调用每个处理程序,那么它将不是很有用,因此还有一些方法可以执行处理程序和链的条件插入。

2.3.5 注册表

TODO(可以在 Chain javadocs 上找到技术定义)

3.3.5 路径绑定

(例如 /player/:id)

TODO(可以在 Chain javadocs 上找到技术定义)

4.3.5 路径和方法绑定

TODO(可以在 Chain javadocs 上找到技术定义)

6 上下文

Context 类型是 Ratpack 的核心。

它提供以下功能

要直接使用请求/响应,请参阅 HTTP 章节

有关委托的信息,请参阅 处理程序章节

1.6 上下文对象

上下文是一个 注册表。它提供通过类型查找在处理程序管道中上游提供的对象的访问权限。这是 Ratpack 中处理程序之间协作的机制。

考虑以下示例

import ratpack.test.embed.EmbeddedApp;
import ratpack.exec.registry.Registry;

import static org.junit.jupiter.api.Assertions.assertEquals;

public class Example {

  public static interface Person {
    String getId();

    String getStatus();

    String getAge();
  }

  public static class PersonImpl implements Person {
    private final String id;
    private final String status;
    private final String age;

    public PersonImpl(String id, String status, String age) {
      this.id = id;
      this.status = status;
      this.age = age;
    }

    @Override
    public String getId() {
      return id;
    }

    @Override
    public String getStatus() {
      return status;
    }

    @Override
    public String getAge() {
      return age;
    }
  }

  public static void main(String... args) throws Exception {
    EmbeddedApp
      .fromHandlers(chain -> chain
          .prefix("person/:id", (personChain) -> personChain
            .all(ctx -> {
              String id = ctx.getPathTokens().get("id"); // (1)
              Person person = new PersonImpl(id, "example-status", "example-age");
              ctx.next(Registry.single(Person.class, person)); // (2)
            })
            .get("status", ctx -> {
              Person person = ctx.get(Person.class); // (3)
              ctx.render("person " + person.getId() + " status: " + person.getStatus());
            })
            .get("age", ctx -> {
              Person person = ctx.get(Person.class); // (4)
              ctx.render("person " + person.getId() + " age: " + person.getAge());
            }))
      )
      .test(httpClient -> {
        assertEquals("person 10 status: example-status", httpClient.get("person/10/status").getBody().getText());
        assertEquals("person 6 age: example-age", httpClient.get("person/6/age").getBody().getText());
      });
  }
}

(2) 中,我们将 Person 实例推送到注册表中,供下游处理程序使用,在 (3)(4) 中,它们是如何检索它的。我们将创建细节与使用分离,并避免在 statusage 处理程序中重复创建代码。避免重复的好处是显而易见的。稍微微妙一些的是,分离使测试更容易,即使下游处理程序没有实现为匿名类(有关信息,请参阅 测试章节)。

(1) 中,我们也使用上下文对象。 prefix() 链方法绑定到请求路径,可能会捕获标记。如果绑定成功,则将一个 PathBinding 对象与描述绑定结果的上下文注册。这包括在绑定过程中捕获的任何路径标记。在上面的情况下,我们将第二个路径组件捕获为 id。上下文上的 getPathTokens() 方法实际上是同一个上下文上的 get(PathBinding.class).getPathTokens() 的简写。这是使用上下文对象机制进行处理程序间通信的另一个示例。

使用上下文对象的另一个示例是从文件系统访问文件的简写。考虑以下脚本,它使用上下文的 file 方法从文件系统检索静态资产。


import static ratpack.groovy.Groovy.ratpack ratpack { handlers { get { def f = file('../') render f ?: "null-value" } } }

在上面的示例中,上下文的 file() 方法被调用以检索提供的路径的 java.io.File 实例。上下文的 file() 方法是检索注册表中 FileSystemBinding 对象的简写,实际上是 get(FileSystemBinding.class).file(path/to/file) 的简写。上下文始终会解析相对于应用程序根目录的文件资产,因此,在提供绝对路径的情况下,请注意,资产的路径将以应用程序存在的路径为前缀。例如,如果您的应用程序存在于 /home/ratpack/app 中,并且您的处理程序使用 file 方法解析 /etc/passwd,则解析的实际路径将是 /home/ratpack/app/etc/passwd。在无法从应用程序根目录中解析文件的情况下,file() 方法可能会返回 null 值,如上面的示例所示。开发人员有责任处理访问文件可能返回 null 对象的情况。

1.1.6 分区

上下文对象机制通过向不同的分区提供不同的对象来支持应用程序逻辑的分区。这是因为与上下文注册的对象隐式地具有作用域,具体取决于它们的注册方式。使用 next() 方法注册的对象可用于所有作为同一插入的一部分的下游处理程序(即 context.insert() 包括嵌套插入)。使用 insert() 方法注册的对象可用于插入的处理程序和嵌套插入。

这一个典型的用例是使用不同的错误处理策略来处理应用程序的不同部分。

import ratpack.core.error.ServerErrorHandler;
import ratpack.test.embed.EmbeddedApp;

import static org.junit.jupiter.api.Assertions.assertEquals;

public class Example {

  public static void main(String... args) throws Exception {
    EmbeddedApp.fromHandlers(chain -> chain
        .prefix("api", api -> api
            .register(r -> r.add(ServerErrorHandler.class, (context, throwable) ->
                  context.render("api error: " + throwable.getMessage())
              )
            )
            .all(ctx -> {
              throw new Exception("in api - " + ctx.getRequest().getPath());
            })
        )
        .register(r -> r.add(ServerErrorHandler.class, (ctx, throwable) ->
              ctx.render("app error: " + throwable.getMessage())
          )
        )
        .all(ctx -> {
          throw new Exception("in app - " + ctx.getRequest().getPath());
        })
    ).test(httpClient -> {
      assertEquals("api error: in api - api/foo", httpClient.get("api/foo").getBody().getText());
      assertEquals("app error: in app - bar", httpClient.get("bar").getBody().getText());
    });
  }

}

7 基本 HTTP

本章介绍如何处理基本的 HTTP 问题,例如解析请求、呈现响应、内容协商、文件上传等。

1.7 请求和响应

处理程序操作的上下文对象提供 getRequest()getResponse() 方法,用于分别访问 RequestResponse。这些对象提供了您期望的更多或更少的东西。

例如,它们都提供 getHeaders() 方法,该方法返回与请求一起发送的 HTTP 标头的模型以及将与响应一起发送的 HTTP 标头的模型。Request 公开其他元数据属性,例如 HTTP 方法URI 以及 查询字符串参数 的键值模型,以及其他内容。

2.7 重定向

redirect(int, Object) 上下文方法支持发出重定向。此方法从上下文注册表中获取 Redirector 并转发参数。

Ratpack 提供了 默认实现,它支持以下功能

  1. 文字 URL 值
  2. 协议相对 URL 值
  3. 当前应用程序中的绝对路径
  4. 当前应用程序中的相对路径

大多数应用程序不需要提供自定义 Redirector 实现,因为默认行为就足够了。提供自定义重定向器实现的一个原因是将域对象解释为要重定向到的位置。

3.7 读取请求

有几种机制可用于获取请求的主体。对于简单用例,Context.parse(Class<T>) 会将整个类缓冲到内存中,并生成指定类型的对象。当您只需要整个请求的文本或字节视图时,可以使用较低级别的 Request.getBody() 方法。对于高级用途或处理超大型请求,[Request.getBodyStream()] 提供对接收到的各个字节块的访问权限。

1.3.7 解析器

将请求主体转换为对象表示的解析机制。它的工作原理是选择上下文注册表中的 Parser 实现。有关详细信息和更多变体,请参阅 Context.parse(Class<T>)

1.1.3.7 JSON

对处理 JSON 请求主体的支持是开箱即用的,基于 Jackson。有关示例,请参阅 Jackson 解析

2.1.3.7 表单

Ratpack 在核心库中提供了一个用于解析 Form 对象的解析器。这可用于读取 POST(或 PUT 等)的表单,包括 URL 编码和多部分表单(包括文件上传)。

import ratpack.core.handling.Handler;
import ratpack.core.handling.Context;
import ratpack.core.form.Form;
import ratpack.core.form.UploadedFile;

public class MyHandler implements Handler {
  public void handle(Context context) {
    Promise<Form> form = context.parse(Form.class);

    form.then(f -> {
      // Get the first attribute sent with name “foo”
      String foo = form.get("foo");

      // Get all attributes sent with name “bar”
      List<String> bar = form.getAll("bar");

      // Get the file uploaded with name “myFile”
      UploadedFile myFile = form.file("myFile");

      // Send back a response …
    });
  }
}

请查看 FormUploadedFile 获取更多信息和示例。

2.3.7 字节和文本

Request.getBody() 将整个请求读入内存,并提供对数据(以字节或字符串形式)的访问。

此方法默认拒绝大小超过服务器配置的 最大内容长度 的请求。其他变体可用于配置 拒绝操作最大大小

import ratpack.core.http.client.ReceivedResponse;
import ratpack.test.embed.EmbeddedApp;

import static org.junit.jupiter.api.Assertions.assertEquals;

public class Example {
  public static void main(String... args) throws Exception {
    EmbeddedApp
      .fromHandler(ctx -> {
         ctx.getRequest().getBody().then(data -> ctx.render("hello: "+data.getText()));
      })
      .test(httpClient -> {
        ReceivedResponse response = httpClient.request(req->{
          req.method("POST");
          req.getBody().text("world");
        });
        assertEquals("hello: world", response.getBody().getText());
      });
  }
}

3.3.7 字节块流

Request.getBodyStream() 返回接收到的各个块的流。

此方法默认拒绝大小超过服务器配置的 最大内容长度 的请求。其他变体可用于配置 最大大小

请查看 java 文档 获取将请求正文流式传输到文件的示例。

4.7 发送响应

在 Ratpack 中发送 HTTP 响应非常简单、高效且灵活。与 Ratpack 中的大多数操作一样,将响应传输到客户端也是以非阻塞方式完成的。Ratpack 提供了几种发送响应的机制。用于操作响应的方法可以在 ResponseContext 对象中找到。

1.4.7 设置响应状态

设置响应状态与调用 Response#status(int)Response#status(ratpack.core.http.Status) 一样简单。

import ratpack.core.http.Response;
import ratpack.core.http.Status;
import ratpack.test.embed.EmbeddedApp;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;

public class Example {
  public static void main(String... args) throws Exception {
    EmbeddedApp.fromHandlers(chain -> chain
      .all(ctx -> ctx.getResponse().status(202).send("foo"))
    )
    .test(httpClient ->
      assertEquals(202, httpClient.get().getStatusCode())
    );
  }
}

2.4.7 发送响应

有几种方法可以将响应正文发送到客户端。

发送响应的最简短方法是简单地调用 Response#send()。这将发送一个没有响应正文的响应。

import ratpack.core.http.client.ReceivedResponse;
import ratpack.test.embed.EmbeddedApp;

import static org.junit.jupiter.api.Assertions.assertEquals;

public class Example {
  public static void main(String... args) throws Exception {
    EmbeddedApp
      .fromHandler(ctx -> ctx.getResponse().send())
      .test(httpClient -> {
        ReceivedResponse response = httpClient.get();
        assertEquals("", response.getBody().getText());
      });
  }
}

如果您想发送纯文本响应,可以使用 Response#send(String)

import ratpack.core.http.client.ReceivedResponse;
import ratpack.test.embed.EmbeddedApp;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

public class Example {
  public static void main(String... args) throws Exception {
    EmbeddedApp
      .fromHandler(ctx -> ctx.getResponse().send("Ratpack is rad"))
      .test(httpClient -> {
        ReceivedResponse response = httpClient.get();
        assertTrue(response.getHeaders().get("Content-type").startsWith("text/plain;"));
        assertEquals("Ratpack is rad", response.getBody().getText());
      });
  }
}

还有其他 send() 方法允许您发送不同的响应正文有效负载,例如 Stringbyte[]ByteBuf,以及设置 Content-type 标头。有关发送响应的更多信息,请查看 Response

3.4.7 使用渲染器的另一种方法

发送空响应或简单文本响应可能没问题,但您可能会发现自己想要发送更复杂的响应到客户端。 Renderer 是一种机制,可以将给定类型渲染到客户端。更准确地说,它是为 render(Object) 方法提供支持的底层机制,该方法可以在上下文对象上找到。

在以下示例中,我们利用上下文的 render(Object) 方法来渲染类型为 String 的对象。

import ratpack.core.http.client.ReceivedResponse;
import ratpack.test.embed.EmbeddedApp;

import static org.junit.jupiter.api.Assertions.assertEquals;

public class Example {
  public static void main(String... args) throws Exception {
    EmbeddedApp
      .fromHandler(ctx -> ctx.render("Sent using render(Object)!"))
      .test(httpClient -> {
        ReceivedResponse response = httpClient.get();
        assertEquals("Sent using render(Object)!", response.getBody().getText());
      });
  }
}

因为 String 的类型为 CharSequence,所以 Ratpack 会找到并使用 CharSequenceRenderer 来渲染 String。这个 CharSequenceRenderer 来自哪里?Ratpack 提供了许多开箱即用的 Renderer,包括但不限于:CharSequenceRendererRenderableRendererPromiseRendererDefaultFileRenderer

如果您尝试渲染未注册的类型,会导致服务器错误。

import ratpack.core.http.client.ReceivedResponse;
import ratpack.test.embed.EmbeddedApp;

import static org.junit.jupiter.api.Assertions.assertEquals;

public class Example {

  static class Foo {
    public String value;
  }

  public static void main(String... args) throws Exception {
    EmbeddedApp
      .fromHandler(ctx -> {
        Foo foo = new Foo();
        foo.value = "bar";
        ctx.render(foo);
      })
      .test(httpClient -> {
        ReceivedResponse response = httpClient.get();
        assertEquals(500, response.getStatusCode());
      });
  }
}

如果您想实现自己的 Renderer,Ratpack 提供了 RendererSupport,它可以轻松地实现您自己的渲染器。您还必须记住注册您的 Renderer,以便 Ratpack 可以使用它。

import ratpack.core.handling.Context;
import ratpack.exec.registry.Registry;
import ratpack.core.http.client.ReceivedResponse;
import ratpack.core.render.RendererSupport;
import ratpack.test.embed.EmbeddedApp;

import static org.junit.jupiter.api.Assertions.assertEquals;

public class Example {

  static class Foo {
    public String value;
  }

  static class FooRenderer extends RendererSupport<Foo> {
    @Override
    public void render(Context ctx, Foo foo) throws Exception {
      ctx.getResponse().send("Custom type: Foo, value=" + foo.value);
    }
  }

  public static void main(String... args) throws Exception {
    EmbeddedApp
      .fromHandlers(chain -> chain
        .register(Registry.single(new FooRenderer()))
        .all(ctx -> {
          Foo foo = new Foo();
          foo.value = "bar";
          ctx.render(foo);
        })
      )
      .test(httpClient -> {
        ReceivedResponse response = httpClient.get();
        assertEquals(200, response.getStatusCode());
        assertEquals("Custom type: Foo, value=bar", response.getBody().getText());
      });
  }
}

4.4.7 发送 JSON

对将任意对象渲染为 JSON 的支持是基于 Jackson 的。请查看 Jackson 渲染 获取示例。

5.4.7 发送文件

可以使用 sendFile(Path) 发送静态资源(如文件)。

待办事项:介绍 sendFile 方法(指向使用 render(file(«path»))) )。

待办事项:介绍 assets 方法

6.4.7 发送前

Response 对象包含一个方法 beforeSend(Action<? super Response> responseFinalizer),该方法在将响应发送到客户端之前立即被调用。

您不能在 responseFinalizer 回调中调用 Response 上的任何 .send() 方法。

此方法特别适用于修改

在最后一刻。

使用此方法的一个实际用例是在使用 StreamedResponse.forwardTo() 时修改状态码或标头。

例如

import io.netty.handler.codec.http.HttpHeaderNames;
import ratpack.core.http.client.ReceivedResponse;
import ratpack.test.embed.EmbeddedApp;
import ratpack.core.http.Headers;
import ratpack.core.http.Request;
import ratpack.core.http.Status;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

public class Example {
  public static void main(String... args) throws Exception {
    EmbeddedApp
      .fromHandler(ctx -> {
        ctx.getResponse()
          .contentType("application/json")
          .status(Status.OK)
          .beforeSend(response -> {
             response.getHeaders().remove(HttpHeaderNames.CONTENT_LENGTH);
             response.cookie("DNT", "1");
             response.status(Status.of(451, "Unavailable for Legal Reasons"));
             response.contentType("text/plain");
          }).send();
      })
      .test(httpClient -> {
        ReceivedResponse receivedResponse = httpClient.get();

        Headers headers = receivedResponse.getHeaders();
        assertEquals(451, receivedResponse.getStatusCode());
        assertEquals("text/plain", headers.get(HttpHeaderNames.CONTENT_TYPE));
        assertTrue(headers.get(HttpHeaderNames.SET_COOKIE).contains("DNT"));
      });
  }
}

5.7 标头

HTTP 标头信息可从传入请求中获得,就像从传出响应中获得一样。

1.5.7 请求标头

Headers 接口允许您检索与传入请求相关的标头信息。

import ratpack.core.http.client.ReceivedResponse;
import ratpack.test.embed.EmbeddedApp;
import ratpack.core.http.Headers;

import static org.junit.jupiter.api.Assertions.assertEquals;

public class Example {
  public static void main(String... args) throws Exception {
    EmbeddedApp
      .fromHandler(ctx -> {
        Headers headers = ctx.getRequest().getHeaders();
        String clientHeader = headers.get("Client-Header");
        ctx.getResponse().send(clientHeader);
      })
      .test(httpClient -> {
        ReceivedResponse receivedResponse = httpClient
          .requestSpec(requestSpec ->
              requestSpec.getHeaders().set("Client-Header", "From Client")
          ).get();

        assertEquals("From Client", receivedResponse.getBody().getText());
      });
  }
}

2.5.7 响应标头

MutableHeaders 提供的功能使您可以通过响应对象 Response#getHeaders() 操作响应标头。

import ratpack.core.http.MutableHeaders;
import ratpack.core.http.client.ReceivedResponse;
import ratpack.test.embed.EmbeddedApp;

import static org.junit.jupiter.api.Assertions.assertEquals;

public class Example {
  public static void main(String... args) throws Exception {
    EmbeddedApp
      .fromHandler(ctx -> {
        MutableHeaders headers = ctx.getResponse().getHeaders();
        headers.add("Custom-Header", "custom-header-value");
        ctx.getResponse().send("ok");
      })
      .test(httpClient -> {
        ReceivedResponse receivedResponse = httpClient.get();
        assertEquals("custom-header-value", receivedResponse.getHeaders().get("Custom-Header"));
      });
  }
}

此外,您可以 set(CharSequence, Object)remove(CharSequence)clear() 等。

请查看 MutableHeaders 获取更多方法。

6.7 Cookie

与 HTTP 标头一样,Cookie 可用于检查传入请求,也可以用于操作传出响应。

1.6.7 来自传入请求的 Cookie

要检索 Cookie 的值,可以使用 Request#oneCookie(String)

import ratpack.core.http.client.ReceivedResponse;
import ratpack.test.embed.EmbeddedApp;

import static org.junit.jupiter.api.Assertions.assertEquals;

public class Example {
  public static void main(String... args) throws Exception {
    EmbeddedApp.fromHandler(ctx -> {
      String username = ctx.getRequest().oneCookie("username");
      ctx.getResponse().send("Welcome to Ratpack, " + username + "!");
    }).test(httpClient -> {
      ReceivedResponse response = httpClient
        .requestSpec(requestSpec -> requestSpec
          .getHeaders()
          .set("Cookie", "username=hbogart1"))
        .get();

      assertEquals("Welcome to Ratpack, hbogart1!", response.getBody().getText());
    });
  }
}

您还可以通过 Request#getCookies() 检索一组 Cookie。

import io.netty.handler.codec.http.cookie.Cookie;
import ratpack.core.http.client.ReceivedResponse;
import ratpack.test.embed.EmbeddedApp;

import java.util.Set;

import static org.junit.jupiter.api.Assertions.assertEquals;

public class Example {
  public static void main(String... args) throws Exception {
    EmbeddedApp.fromHandler(ctx -> {
      Set<Cookie> cookies = ctx.getRequest().getCookies();
      assertEquals(1, cookies.size());
      Cookie cookie = cookies.iterator().next();
      assertEquals("username", cookie.name());
      assertEquals("hbogart1", cookie.value());
      ctx.getResponse().send("Welcome to Ratpack, " + cookie.value() + "!");
    }).test(httpClient -> {
      ReceivedResponse response = httpClient
        .requestSpec(requestSpec -> requestSpec
          .getHeaders()
          .set("Cookie", "username=hbogart1"))
        .get();

      assertEquals("Welcome to Ratpack, hbogart1!", response.getBody().getText());
    });
  }
}

2.6.7 为传出响应设置 Cookie

您可以使用 Response#cookie(String, String) 设置要与响应一起发送的 Cookie。要检索要与响应一起设置的 Cookie 集,可以使用 Response#getCookies()

import ratpack.core.http.client.ReceivedResponse;
import ratpack.test.embed.EmbeddedApp;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

public class Example {
  public static void main(String... args) throws Exception {
    EmbeddedApp.fromHandler(ctx -> {
      assertTrue(ctx.getResponse().getCookies().isEmpty());
      ctx.getResponse().cookie("whiskey", "make-it-rye");
      assertEquals(1, ctx.getResponse().getCookies().size());
      ctx.getResponse().send("ok");
    }).test(httpClient -> {
      ReceivedResponse response = httpClient.get();
      assertEquals("whiskey=make-it-rye", response.getHeaders().get("Set-Cookie"));
    });
  }
}

如果您想使 Cookie 过期,可以使用 Response#expireCookie()

import ratpack.core.http.client.ReceivedResponse;
import ratpack.test.embed.EmbeddedApp;

import static org.junit.jupiter.api.Assertions.assertTrue;

public class Example {
  public static void main(String... args) throws Exception {
    EmbeddedApp.fromHandler(ctx -> {
      ctx.getResponse().expireCookie("username");
      ctx.getResponse().send("ok");
    }).test(httpClient -> {
      ReceivedResponse response = httpClient
        .requestSpec(requestSpec -> requestSpec
            .getHeaders().set("Cookie", "username=lbacall1")
        )
        .get();

      String setCookie = response.getHeaders().get("Set-Cookie");
      assertTrue(setCookie.startsWith("username=; Max-Age=0"));
    });
  }
}

7.7 内容协商

对渲染资源的不同表示(JSON/XML/HTML、GIF/PNG 等)的支持通过 byContent(Action) 提供。

8.7 会话

无论何时您需要在同一个客户端的多个调用之间保持一致性(换句话说:有状态操作),您可能想要使用会话。Ratpack 提供了一个模块来为您处理会话处理。您可以将会话模块添加到您的项目中,并开始使用 Ratpack 管理的会话。

1.8.7 准备

首先,您需要将所需的依赖项添加到您的项目中。使用 Gradle,您可以通过在依赖项中添加 compile 'io.ratpack:ratpack-session:2.0.0-rc-1' 来添加依赖项。当您刚开始使用 Gradle 文件时,它将如下所示

buildscript {
  repositories {
    mavenCentral()
  }
  dependencies {
    classpath "io.ratpack:ratpack-gradle:2.0.0-rc-1"
  }
}

apply plugin: "io.ratpack.ratpack-groovy"

repositories {
  mavenCentral()
}

dependencies {
  runtimeOnly 'org.slf4j:slf4j-simple:1.7.36'
  implementation group: 'io.ratpack', name: 'ratpack-session', version: '2.0.0-rc-1'

  testImplementation "org.spockframework:spock-core:2.1-groovy-3.0"
}

不要忘记在 Ratpack 中加载模块。

import static ratpack.groovy.Groovy.ratpack
import ratpack.session.SessionModule

ratpack {
	bindings {
		module(SessionModule)
	}
	/* ... */
}

2.8.7 使用会话

您现在已准备好使用会话。以下是一个使用会话的简单应用程序示例。

import ratpack.session.Session

import static ratpack.groovy.Groovy.ratpack
import ratpack.session.SessionModule

ratpack {
	bindings {
		module(SessionModule)
	}

	handlers {
		get('start') { Session session ->
			session.terminate().flatMap {
				session.set('keyForDataStoredInSession', 'Hello Session!').promise()
			}.then {
				response.send('I started a session for you.')
			}
		}
		get { Session session ->
			session.get('keyForDataStoredInSession').then {
				if (it.present) {
					response.send("Message: ${it.get()}")
				} else {
					response.send('I have nothing to say to you')
				}
			}
		}
		get('stop') { Session session ->
			session.terminate().then {
				response.send('The session is dead, dead, dead.')
			}
		}
	}
}

所有会话操作都返回 PromiseOperation。您可以根据所示在您的转换流程中使用它们。

在启动新会话之前,请确保终止旧会话(参见 get('start') 处理程序)。这样,您就可以确保您确实获得了新的会话,而不是仅仅将数据添加到现有会话中。

3.8.7 限制

默认情况下,Ratpack 会话模块使用内存中的会话。它一次最多可以保存 1000 个会话,如果打开新会话,它将删除最旧的会话。如果您预计会有超过 1000 个会话,则应考虑使用与默认模块不同的会话存储。例如,如果您有一个 Redis 服务器,您可以使用 ratpack-session-redis 模块。如果您的有效负载很小,另一个选择是使用 客户端会话模块 将一些会话数据存储在 Cookie 本身中。您的有效负载应该很小,因为一个网站(域)的所有 Cookie 结合起来不能超过 4K。

4.8.7 ratpack-session-redis 模块

要使用 Redis 会话模块,请将依赖项 (compile 'io.ratpack:ratpack-session-redis:2.0.0-rc-1') 添加到您的项目中。

然后,在加载会话模块后配置 Redis。

bindings {
  module(SessionModule)
  RedisSessionModule redisSessionModule = new RedisSessionModule()
  redisSessionModule.configure {
    it.host = 'localhost'
    it.port =  6379
    it.password = 'secret'
  }
  module(redisSessionModule)
}

您可能希望使用 Guice 或其他机制注入 Redis 的配置。除此之外,您已成功配置 Ratpack 使用 Redis 存储会话。确保您的 Redis 服务器正在运行并可用,然后让您的 Ratpack 应用程序运行起来。

5.8.7 评论与有用链接

您现在对如何在 Ratpack 中实现会话有了一个非常粗略的了解。

Ratpack 使用 Cookie 处理会话一致性,特别是包含 UUID 的 JSESSIONID Cookie。无论何时您第一次将数据添加到会话,都会生成 ID,并且该 Cookie 将使用响应中的 Add-Cookie 标头发送到客户端。因此,您需要确保将响应发送到客户端,否则会话将丢失。后续的客户端请求在 Cookie 标头中包含该 Cookie,因此 Ratpack 可以确定客户端的会话。

以下是深入了解 Ratpack 会话处理的一些有用链接: - 会话在 ratpack-session 模块测试 中如何工作的一些示例 - ratpack javadoc 包含大量示例和信息 - 可以通过向会话模块提供自定义的 SessionCookieConfig 来自定义 cookie 行为(例如更改 cookie 名称、使用自定义 ID/过期日期)

6.8.7 使用默认会话存储(内存中)的最后说明:

默认会话存储可能在任何生产环境中都没有用,但对于使用会话的本地测试很有用。调用 session.terminate() 会将会话 cookie 设置为空值。因此,连续的调用将包含一个类似这样的 cookie JSESSIONID=。至少内存中会话存储接受空值作为有效会话。因此,如果您在打算通过向其中添加值来创建新会话之前没有终止会话,您将向具有空 uuid 的现有会话添加会话数据。

8 异步和非阻塞

Ratpack 旨在用于“异步”和“非阻塞”请求处理。其内部 IO(例如 HTTP 请求和响应传输)都是以非阻塞方式执行的(感谢 Netty)。这种方法可以实现更高的吞吐量、更低的资源使用率,以及更重要的是,在负载下的更可预测的行为。由于 Node.js 平台的出现,这种编程模型近年来变得越来越流行。Ratpack 基于与 Node.js 相同的非阻塞、事件驱动模型。

异步编程众所周知很棘手。Ratpack 的关键价值主张之一是它提供了构建和抽象来驯服异步野兽,从而在保持实现简单的情况下提高性能。

1.8 与阻塞框架和容器的比较

构成大多数 JVM Web 框架和容器基础的 Java Servlet API 以及大部分 JDK 从根本上基于同步编程模型。大多数 JVM 程序员都非常熟悉和习惯这种编程模型。在这个模型中,当需要执行 IO 时,调用线程将简单地休眠,直到操作完成并且结果可用。此模型需要一个相当大的线程池。在 Web 应用程序上下文中,这通常意味着每个请求都绑定到大型线程池中的一个线程,并且应用程序可以处理“X”个并行请求,其中“X”是线程池的大小。

Servlet API 的 3.0 版本确实支持异步请求处理。但是,将异步支持作为一种选择加入选项进行改造与完全异步方法是不同的。Ratpack 从一开始就是异步的。

此模型的优点是同步编程毫无争议地“更简单”。与非阻塞模型相比,此模型的缺点是它需要更大的资源使用量,并且吞吐量更低。为了并行服务更多请求,必须增加线程池的大小。这会为计算资源造成更多争用,并且更多周期会浪费在管理这些线程的调度上,更不用说内存消耗的增加。现代操作系统和 JVM 在管理这种争用方面非常出色;但是,它仍然是一个扩展瓶颈。此外,它需要更大的资源分配,这对现代按需付费部署环境来说是一个严重的考虑因素。

异步、非阻塞模型不需要大型线程池。这是可能的,因为线程永远不会阻塞以等待 IO。如果需要执行 IO,调用线程将注册某种回调,该回调将在 IO 完成时被调用。这允许线程在 IO 发生时用于其他处理。在这种模型下,线程池的大小根据可用的处理核心数量确定。由于线程始终忙于计算,因此拥有更多线程毫无意义。

许多 Java API(InputStreamJDBC 等)都依赖于阻塞 IO 模型。Ratpack 提供了一种使用此类 API 的机制,同时最大限度地减少阻塞成本(下面讨论)。

Ratpack 从根本上来说在两个关键方面是异步的……

  1. HTTP IO 是事件驱动的/非阻塞的(感谢 Netty
  2. 请求处理被组织成异步函数的管道

使用 Ratpack 时,HTTP IO 是事件驱动的,这在很大程度上是透明的。Netty 只是做它的事情。

第二点是 Ratpack 的关键特征。它不期望您的代码是同步的。许多具有选择加入异步支持的 Web 框架都存在严重的约束和陷阱,当尝试执行复杂的(即现实世界)异步操作时,这些约束和陷阱会变得明显。Ratpack 从一开始就是异步的。此外,它提供了构建和抽象,可以促进复杂的异步处理。

2.8 执行阻塞操作(例如 IO)

大多数应用程序将不得不执行某种阻塞 IO。许多 Java API 不提供异步选项(例如 JDBC)。Ratpack 提供了一种简单的机制,可以在单独的线程池中执行阻塞操作。这避免了阻塞请求处理(即计算)线程(这是好事),但由于线程争用会导致一些开销。如果您必须使用阻塞 IO API,不幸的是没有其他选择。

让我们考虑一个虚构的数据存储 API。可以想象,与实际数据存储的通信需要 IO(或者,如果它在内存中,那么访问需要等待一个或多个锁,这具有相同的阻塞效果)。无法在请求处理线程上调用 API 方法,因为它们会阻塞。相反,我们需要使用“阻塞”API……

import ratpack.core.handling.InjectionHandler;
import ratpack.core.handling.Context;
import ratpack.exec.Blocking;

import ratpack.test.handling.RequestFixture;
import ratpack.test.handling.HandlingResult;

import java.util.Collections;
import java.io.IOException;

import static org.junit.jupiter.api.Assertions.assertEquals;

public class Example {

  // Some API that performs blocking operations
  public static interface Datastore {
    int deleteOlderThan(int days) throws IOException;
  }

  // A handler that uses the API
  public static class DeletingHandler extends InjectionHandler {
    void handle(final Context context, final Datastore datastore) {
      final int days = context.getPathTokens().asInt("days");
      Blocking.get(() -> datastore.deleteOlderThan(days))
        .then(i -> context.render(i + " records deleted"));
    }
  }

  // Unit test
  public static void main(String... args) throws Exception {
    HandlingResult result = RequestFixture.handle(new DeletingHandler(), fixture -> fixture
        .pathBinding(Collections.singletonMap("days", "10"))
        .registry(r -> r.add(Datastore.class, days -> days))
    );

    assertEquals("10 records deleted", result.rendered(String.class));
  }
}

作为阻塞操作提交的函数以异步方式执行(即 Blocking.get() 方法立即返回一个承诺),在单独的线程池中。它返回的结果将在请求处理(即计算)线程上进行处理。

有关更多详细信息,请参阅 Blocking#get() 方法。

3.8 执行异步操作

用于与异步 API 集成的 Promise#async(Upstream>)。它本质上是一种将第三方 API 适配到 Ratpack 的承诺类型的机制。

import ratpack.test.embed.EmbeddedApp;
import ratpack.exec.Promise;

import static org.junit.jupiter.api.Assertions.assertEquals;

public class Example {
  public static void main(String... args) throws Exception {
    EmbeddedApp.fromHandler(ctx ->
        Promise.async((f) ->
            new Thread(() -> f.success("hello world")).start()
        ).then(ctx::render)
    ).test(httpClient -> {
      assertEquals("hello world", httpClient.getText());
    });
  }
}

4.8 异步组合和避免回调地狱

异步编程的挑战之一在于组合。非平凡的异步编程可以很快地演变成一种被称为“回调地狱”的现象,这个术语用来描述许多嵌套回调的难以理解性。

优雅干净地将异步操作组合成复杂的流程是目前快速创新的领域。Ratpack 不会尝试提供异步组合的框架。相反,它旨在集成和提供与专门用于此任务的工具的适配器。这种方法的一个例子是 Ratpack 与 RxJava 的集成

通常,集成是将 Ratpack 的 Promise 类型与目标框架的组合原语进行适配的问题。

9

Ratpack 以多种方式支持流式传输数据。本章概述了在 Ratpack 中使用数据流的基本原理以及流式传输数据的不同方法。

1.9 Reactive Streams API

通常,Ratpack 中的流式传输基于新兴的 Reactive Streams API 标准。

来自 Reactive Streams 网站

Reactive Streams 是一项旨在为 JVM 提供异步流处理标准的倡议,该标准具有非阻塞背压。

Ratpack 使用 Reactive Streams API 而不是专有 API,以允许用户选择他们选择的反应式工具包。诸如 RxJavaReactor 之类的反应式工具包将在不久的将来支持桥接到 Reactive Streams API。但是,如果您的需求很小,则不需要使用专业的反应式库。Ratpack 通过其 Streams 类提供了一些用于处理流的有用实用程序。

1.1.9 背压

Reactive Streams API 的一个关键原则是在背压的支持下支持流量控制。这允许流订阅者(在 HTTP 服务器应用程序的情况下,通常是 HTTP 客户端)向发布者传达他们可以处理多少数据。在极端情况下,如果没有背压,缓慢消费的客户端会耗尽服务器上的资源,因为数据生产者比消费更快地生产数据,从而可能填满内存缓冲区。背压允许数据生产者将其生产速率与客户端可以处理的速率相匹配。

有关背压重要性的更多信息,请参阅来自 Reactive Streams 项目的文档。

始终通过 Response.sendStream() 方法流式传输响应。有关流式传输数据时背压的更精确语义,请参阅此方法的文档。

2.9 分块传输编码

Ratpack 通过 ResponseChunks 可渲染类型支持对任意数据流进行 分块传输编码

3.9 服务器发送的事件

Ratpack 通过 ServerSentEvents 可渲染类型支持 服务器发送的事件,用于将数据流式传输到主要基于 Javascript 的客户端。

4.9 WebSockets

Ratpack 通过 WebSockets.websocketBroadcast() 方法支持通过 WebSockets 流式传输数据。

Ratpack 还通过 WebSockets 类的其他 websocket 打开方法支持双向 websocket 通信。

10 测试 Ratpack 应用程序

测试是 Ratpack 中的一等公民。ratpack-test 库包含核心支持,而 ratpack-groovy-test 为这些类型提供了一些 Groovy 糖。

ratpackratpack-groovy Gradle 插件会隐式地将这些库添加到测试编译类路径中。

Ratpack 测试支持与使用的测试框架无关。任何框架都可以潜在使用。

许多 Ratpack 用户使用 Spock 测试框架。虽然 Spock 需要用 Groovy 编写测试,但它可以轻松有效地用于测试 Java 代码。

1.10 单元测试

1.1.10 RequestFixture

RequestFixture 类旨在创建模拟请求环境,通常用于测试 Handler 实现。但是,通常使用与其他组件(例如 Parser 实现)集成的请求夹具的临时处理程序。

注意:GroovyRequestFixture 类提供了用于处理请求夹具的 Groovy 语法糖。

2.1.10 ExecHarness

ExecHarness 夹具用于测试在应用程序之外利用 Ratpack 执行机制的代码。如果您需要对使用 Promise 的代码进行单元测试,那么 exec 架具就是您需要的。

2.10 集成测试

Ratpack 集成测试是通过 HTTP 接口测试应用程序组件子集的测试。

EmbeddedApp 夹具用于构建一个临时应用程序,该应用程序可以响应真实的 HTTP 请求。在集成测试的上下文中,它通常用于将应用程序组件的特定组合粘合在一起以进行测试。

因为它构建了一个真实的 Ratpack 应用程序,所以它也可以用于测试 Ratpack 扩展点的实现,例如 RendererParserConfigurableModule

EmbeddedApp 夹具负责启动和停止应用程序,并提供一个 TestHttpClient,它向嵌入式应用程序发出请求。

重要的是,嵌入式应用程序必须在不再需要时关闭,以便释放资源。EmbeddedApp 类型实现了 java.io.AutoCloseable 接口,该接口的 close() 方法可用于停止服务器。这通常可以与所用测试框架的“测试后”生命周期事件相结合,例如 JUnit 的 @After 方法。

注意:EmbeddedApp 夹具也可以“独立”用于在测试其他类型的非 Ratpack 应用程序时创建模拟 HTTP 服务。

3.10 功能测试

Ratpack 功能测试是通过 HTTP 接口测试整个应用程序的测试。

对于定义为 Java 主类的 Ratpack 应用程序,可以使用 MainClassApplicationUnderTest 夹具。对于定义为 Groovy 脚本的 Ratpack 应用程序,可以使用 GroovyRatpackMainApplicationUnderTest 夹具。

如果您有自定义入口点,则可以扩展 ServerBackedApplicationUnderTest 抽象超类以满足您的需求。

这些夹具负责启动和停止应用程序,并提供一个 TestHttpClient,它向嵌入式应用程序发出请求。

重要的是,被测应用程序必须在不再需要时关闭,以便释放资源。CloseableApplicationUnderTest 类型实现了 java.io.AutoCloseable 接口,该接口的 close() 方法可用于停止服务器。这通常可以与所用测试框架的“测试后”生命周期事件相结合,例如 JUnit 的 @After 方法。

1.3.10 施加

Ratpack 提供了一种用于增强被测应用程序的可测试性的机制,称为 施加

通常,施加是通过子类化 MainClassApplicationUnderTest 或类似方法来指定的,并覆盖 addImpositions(ImpositionsSpec) 方法。

2.3.10 浏览器测试

浏览器测试的工作原理与之前在此处称为功能测试的内容类似,不同之处在于使用 Ratpack 的 TestHttpClient 被浏览器自动化所取代。这通常涉及使用 MainClassApplicationUnderTest 启动和停止应用程序,并通过 getAddress() 方法提供被测应用程序的地址。

Ratpack 用户通常使用 Geb 进行浏览器测试,因为它具有表达能力强且与 Spock 的协同作用良好。ratpack.io 网站的 Ratpack/Geb 基于测试的示例可在参考资料中找到。

11 Http 客户端

Ratpack 提供了自己的 HttpClient,可用于进行远程 HTTP 调用。Ratpack 提供的 HttpClient 完全是非阻塞的,并且是 Ratpack 核心库的一部分。与 Ratpack 服务器一样,HttpClient 也在内部使用 Netty,并且实际上共享与 Netty 最佳实践相同的 EventLoopGroup

1.11 基本 GET 请求

import ratpack.core.http.client.HttpClient;
import ratpack.test.embed.EmbeddedApp;

import static org.junit.jupiter.api.Assertions.assertEquals;

public class Example {
  public static void main(String... args) throws Exception {
    try (EmbeddedApp remoteApp = EmbeddedApp.fromHandler(ctx -> ctx.render("Hello from remoteApp"))) {
      EmbeddedApp.fromHandler(ctx -> ctx
          .render(
            ctx
              .get(HttpClient.class)
              .get(remoteApp.getAddress())
              .map(response -> response.getBody().getText())
          )
      ).test(httpClient -> 
        assertEquals("Hello from remoteApp", httpClient.getText())
      );
    }
  }
}

12 静态资产

Ratpack 提供了对将静态文件作为响应进行服务的支持。

1.12 来自目录

Ratpack 应用程序有一个“基本目录”的概念,该目录在启动时指定。这实际上是应用程序所关心的文件系统的根目录。可以使用 Chain.files() 方法提供来自基本目录的文件。

import ratpack.test.embed.EmbeddedApp;
import ratpack.test.embed.EphemeralBaseDir;

import static org.junit.jupiter.api.Assertions.assertEquals;

public class Example {
  public static void main(String... args) throws Exception {
    EphemeralBaseDir.tmpDir().use(baseDir -> {
      baseDir.write("public/some.text", "foo");
      baseDir.write("public/index.html", "bar");

      EmbeddedApp.of(s -> s
        .serverConfig(c -> c.baseDir(baseDir.getRoot()))
        .handlers(c -> c
          .files(f -> f.dir("public").indexFiles("index.html"))
        )
      ).test(httpClient -> {
        assertEquals("foo", httpClient.getText("some.text"));
        assertEquals("bar", httpClient.getText());
        assertEquals(404, httpClient.get("no-file-here").getStatusCode());
      });

    });
  }
}

文件将使用基于文件公布的最后修改时间戳的 Last-Modified 标头进行服务。如果客户端发送 If-Modified-Since 标头,则如果文件自给定值以来未修改,Ratpack 将以 304 响应进行响应。已服务的文件不包含 ETags。

默认情况下,如果客户端请求,文件将通过网络进行 GZIP 压缩。这可以通过调用 Response.noCompress() 方法在每个请求的基础上禁用。这通常通过在文件服务处理程序前面放置一个处理程序来实现,该处理程序检查请求路径(例如文件扩展名)并禁用压缩。

2.12 临时文件

可以通过使用 Context.file()Context.render() 方法来服务单个文件。

import ratpack.test.embed.EmbeddedApp;
import ratpack.test.embed.EphemeralBaseDir;

import static org.junit.jupiter.api.Assertions.assertEquals;

public class Example {
  public static void main(String... args) throws Exception {
    EphemeralBaseDir.tmpDir().use(baseDir -> {
      baseDir.write("some.text", "foo");

      EmbeddedApp.of(s -> s
        .serverConfig(c -> c.baseDir(baseDir.getRoot()))
        .handlers(c -> c
          .get("f", ctx -> ctx.render(ctx.file("some.text")))
        )
      ).test(httpClient ->
        assertEquals("foo", httpClient.getText("f"))
      );

    });
  }
}

如果 Context.file() 返回的文件不存在,则会发出 404

响应的时间戳和压缩方式与 Chain.files() 方法中描述的方式完全相同。

3.12 使用“资产管道”的先进资产服务

资产管道 项目提供了与 Ratpack 的集成。这提供了高级资产捆绑、编译和服务。

13 Google Guice 集成

ratpack-guice 扩展提供了与 Google Guice 的集成。此扩展的主要功能是允许服务器注册表由 Guice 构建。也就是说,Guice Injector 可以作为 Ratpack Registry 呈现。这允许应用程序的布线由 Guice 模块和绑定指定,但仍然允许注册表成为运行时不同 Ratpack 扩展之间的通用集成层。

截至 2.0.0-rc-1 的 ratpack-guice 模块构建在 Guice 5.1.0(以及多绑定扩展)之上,并且依赖于它们。

1.13 模块

Guice 提供了模块的概念,模块是一种提供对象的食谱。有关详细信息,请参阅 Guice 的“入门”文档。

ratpack-guice 库提供了 BindingsSpec 类型来指定应用程序的绑定。

2.13 依赖项注入处理程序

Guice 集成为您提供了一种解耦应用程序组件的方法。您可以将功能分解成独立的(即非 Handler)对象,并从您的处理程序中使用这些对象。这使您的代码更易于维护和测试。这是标准的“依赖注入”或“控制反转”模式。

Guice 类提供了用于创建作为应用程序基础的根处理程序的静态 handler() 工厂方法。这些方法(常用的方法)公开了 Chain 实例,可用于构建应用程序的处理程序链。这些方法公开的实例提供了注册表(通过 getRegistry()),可用于构建依赖注入的处理程序实例。

请参阅 Guice 类的文档以获取示例代码。

3.13 Guice 和上下文注册表

TODO 由 Guice 支持的注册表实现

这提供了一种替代依赖注入的处理程序的方案,因为对象可以按需从上下文中检索。

更有用的是,这意味着可以通过 Guice 模块集成 Ratpack 基础结构。例如,ServerErrorHandler 的实现可以由 Guice 模块提供。由于 Guice 绑定的对象已集成到上下文注册表查找机制中,因此该实现将参与错误处理基础结构。

对于所有通过上下文注册表查找工作的 Ratpack 基础结构来说,情况都是如此,例如 Renderer 实现。

14 Groovy

Groovy 是一种替代的 JVM 编程语言。它与 Java 具有很强的协同作用,并且许多语言和库功能使其成为引人注目的编程环境。Ratpack 通过 ratpack-groovyratpack-groovy-test 库提供了与 Groovy 的强大集成。与 Java 相比,用 Groovy 编写 Ratpack 应用程序通常会通过 Groovy 简洁的语法产生更少的代码,并且能够获得更高效、更愉快的开发体验。但需要明确的是,Ratpack 应用程序不需要用 Groovy 编写。

Groovy 通常被称为动态语言。但是,Groovy 2.0 添加了完整的静态类型和静态编译作为一种选择。Ratpack 的 Groovy 支持严格设计为完全支持“静态 Groovy”,并且还利用了 Groovy 的最新功能来避免引入样板代码以实现此目标。换句话说,Ratpack 的 Groovy 支持不使用任何动态语言功能,并且具有强类型的 API。

TODO: 找到描述静态 Groovy 的合适链接并用在上面

1.14 先决条件

如果您是 Groovy 新手,您可能需要在继续之前研究以下 Groovy 基础主题

  1. 闭包
  2. def 关键字

TODO: 此列表中还应包含什么?另外,需要链接

其他内容

2.14 Ratpack Groovy API

TODO: 解释 Groovy API 如何包装 Java API 并在相应的 ratpack.groovy.* 中镜像它。

1.2.14 @DelegatesTo

@DelegatesTo 旨在记录代码,并在编译时为 IDE 和静态类型检查器/编译器提供更多类型信息。对于 DSL 作者来说,应用此注解尤其有趣。

让我们考虑以下 Ratpack 代码片段

ratpack {
  handlers {
    get {
      render "Hello world!"
    }
  }
}

此代码也被称为用 Ratpack 的“GroovyChain DSL”编写。它本质上与以下代码相同

ratpack({
  handlers({
    get({
      render("Hello world!")
    })
  })
})

调用了 ratpackhandlersgetrender 方法。实际上,没有限制只能调用每个方法一次,例如,代码可以多次调用 get 方法,响应不同的请求 URI

ratpack {
  handlers {
    get {
      render "Hello world!"
    }

    get('foo')  {
      render "bar"
    }
  }
}

请注意 ratpackhandlersget 的调用如何构建层次结构。但它实际上是如何转换为对 Groovy/Java 对象的调用的呢?

这正是委托发挥作用的地方。Groovy 允许更改 Closure 代码块内方法调用的目标。让我们看一个非常基本的例子

class Settings {
   String host
   Integer port

   def host(String h) { this.host = h }
   def port(Integer p) { this.port = p }
}

Settings settings(Closure dsl)  {
    def p = new Settings()
    def code = dsl.clone() // better use: dsl.rehydrate(p, this, this)
    code.delegate = p
    code()
    return p
}

// our DSL starts here and returns a Settings instance
Settings config = settings {
  port 1234
  host 'localhost'
}

assert config.host == 'localhost'
assert config.port == 1234

settings DSL 块中的代码调用了当前词法范围内不存在的方法。在运行时,一旦设置了 delegate 属性,Groovy 就会额外根据给定的委托对象解析该方法,在本例中,为 Settings 实例。

委托通常用于 Groovy DSL 中,例如 Ratpack DSL,以将 DSL 代码与底层对象分离。

这种技术对于 IDE 中的代码补全或 Groovy 2 中添加的静态类型检查器/编译器来说存在问题。在 groovyConsole 中运行以下代码

class Settings {
   String host
   Integer port

   def host(String h) { this.host = h }
   def port(Integer p) { this.port = p }
}

Settings settings(Closure dsl)  {
    def p = new Settings()
    def code = dsl.clone() // better use: dsl.rehydrate(p, this, this)
    code.delegate = p
    code()
    return p
}

@groovy.transform.TypeChecked
void createConfig() {
	Settings config = settings {
	  port 1234
	  host 'localhost'
	}

	assert config.host == 'localhost'
	assert config.port == 1234
}

得到

[Static type checking] - Cannot find matching method ConsoleScript23#port(int). Please check if the declared type is right and if the method exists.
 at line: 20, column: 7

[Static type checking] - Cannot find matching method ConsoleScript23#host(java.lang.String). Please check if the declared type is right and if the method exists.
 at line: 21, column: 7

类型检查器在编译时错过了有关委托类型 Settings 的信息。

这就是 @DelegatesTo 最终发挥作用的地方。它恰恰用于需要为 Closure 方法参数指定此类型信息的情况

// ...

// let's tell the compiler we're delegating to the Settings class
Settings settings(@DelegatesTo(Settings) Closure dsl)  {
    def p = new Settings()
    def code = dsl.clone() // better use: dsl.rehydrate(p, this, this)
    code.delegate = p
    code()
    return p
}

// ...

Ratpack 在使用 Closure 方法参数的任何地方都使用 @DelegatesTo。这不仅有助于代码补全或静态类型检查,还有助于文档目的。

3.14 ratpack.groovy 脚本

TODO: 介绍此文件中使用的 DSL,讨论在开发模式下的重新加载

4.14 handlers {} DSL

TODO: 介绍 GroovyChain DSL,以及作为处理程序的闭包

5.14 测试

Groovy 附带了编写测试的内置支持。除了对 JUnit 的集成支持外,这种编程语言还具有被证明对测试驱动开发非常有价值的功能。其中之一是扩展的 assert 关键字,我们将在下一节中介绍。

如果您正在寻找 Ratpack 特定的测试支持文档,本手册中有一个专门的 Ratpack 测试指南 部分。

1.5.14 断言能力

编写测试意味着使用断言来制定假设。在 Java 中,这可以通过使用在 J2SE 1.4 中添加的 assert 关键字来完成。Java 中的断言语句默认情况下处于禁用状态。

Groovy 带有一个强大的 assert 变体,也称为断言能力语句。Groovy 的断言能力 assert 与 Java 版本的不同之处在于,一旦布尔表达式验证为 false,它的输出就会有所不同

def x = 1
assert x == 2

// Output:
//
// Assertion failed:
// assert x == 2
//        | |
//        1 false

java.lang.AssertionError 包含原始断言错误消息的扩展版本。它的输出显示了从最外层到最内层表达式的所有变量和表达式值。

由于其表达能力,在 Groovy 社区中,使用 assert 语句而不是所选测试库提供的任何 assert* 方法来编写测试用例已成为常识。

assert 语句只是编写测试的众多有用 Groovy 功能之一。如果您正在寻找所有 Groovy 语言测试功能的综合指南,请参阅 Groovy 测试指南

2.5.14 JUnit 3 支持

Groovy 带有嵌入式 JUnit 3 支持和一个自定义的 TestCase 基类:groovy.util.GroovyTestCaseGroovyTestCase 扩展了 TestCase,并添加了有用的实用程序方法。

一个例子是 shouldFail 方法系列。每个 shouldFail 方法都接受一个 Closure 并执行它。如果抛出异常并且代码块失败,shouldFail 会捕获异常,并且测试方法不会失败

import groovy.util.GroovyTestCase

class ListTest extends GroovyTestCase {

  void testInvalidIndexAccess1() {
    def numbers = [1,2,3,4]

    shouldFail {
      numbers.get(4)
    }
  }
}

shouldFail 实际上返回捕获的异常,这允许对异常消息或类型进行断言

import groovy.util.GroovyTestCase

class ListTest extends GroovyTestCase {

  void testInvalidIndexAccess1() {
    def numbers = [1,2,3,4]

    def msg = shouldFail {
      numbers.get(4)
    }

    assert msg == 'Index: 4, Size: 4'
  }
}

此外,还有一种 shouldFail 方法的变体,在 Closure 参数之前带有一个 java.lang.Class 参数

import groovy.util.GroovyTestCase

class ListTest extends GroovyTestCase {

  void testInvalidIndexAccess1() {
    def numbers = [1,2,3,4]

    def msg = shouldFail(IndexOutOfBoundsException) {
      numbers.get(4)
    }

    assert msg == 'Index: 4, Size: 4'
  }
}

如果抛出的异常是另一种类型,shouldFail 将重新抛出异常,使测试方法失败

可以在 JavaDoc 文档 中找到所有 GroovyTestCase 方法的完整概述。

3.5.14 JUnit 4 支持

Groovy 带有嵌入式 JUnit 4 支持。从 Groovy 2.3.0 开始,groovy.test.GroovyAssert 类可以被视为 Groovy 的 GroovyTestCase JUnit 3 基类的补充。GroovyAssert 扩展了 org.junit.Assert,通过添加用于在 Groovy 中编写 JUnit 4 测试的静态实用程序方法来实现这一点。

import static groovy.test.GroovyAssert.shouldFail

class ListTest {
  void testInvalidIndexAccess1() {
    def numbers = [1,2,3,4]
    shouldFail {
      numbers.get(4)
    }
  }
}

可以在 JavaDoc 文档 中找到所有 GroovyAssert 方法的完整概述。

4.5.14 Spock

Spock 是一个用于 Java 和 Groovy 应用程序的测试和规范框架。使其脱颖而出的是它美观且极具表现力的规范 DSL。Spock 规范被编写为 Groovy 类。

Spock 可用于单元测试、集成测试或 BDD(行为驱动开发)测试,它不将自身归入特定类别的测试框架或库。

在以下段落中,我们将初步了解 Spock 规范的结构。它不打算成为完整的文档,而是让你对 Spock 的作用有一个很好的了解。

1.4.5.14 第一步

Spock 允许您编写规范来描述感兴趣的系统所表现出的特征(属性、方面)。“系统”可以是单个类到整个应用程序的任何内容,更高级的术语是被规范的系统。特征描述从系统的特定快照及其协作者开始,此快照称为特征的夹具

Spock 规范类自动从 spock.lang.Specification 派生。具体的规范类可能包含字段、夹具方法、特征方法和辅助方法。

让我们看看一个 Ratack 单元测试规范

import ratpack.groovy.test.GroovyRatpackMainApplicationUnderTest
import ratpack.test.http.TestHttpClient
import ratpack.test.ServerBackedApplicationUnderTest

class SiteSpec {

  ServerBackedApplicationUnderTest aut = new GroovyRatpackMainApplicationUnderTest()
  @Delegate TestHttpClient client = TestHttpClient.testHttpClient(aut)

  def "Check Site Index"() {
    when:
      get("index.html")

    then:
      response.statusCode == 200
      response.body.text.contains('<title>Ratpack: A toolkit for JVM web applications</title>')
  }
}

Spock 特征规范定义为 spock.lang.Specification 类内的 methods。它们通过使用字符串文字而不是方法名称来描述特征。上面的规范使用 "Check Site Index" 来测试对 index.html 的请求的结果。

特征规范使用 whenthen 块。when 块创建了所谓的刺激,它是 then 块的伴随,then 块描述了对刺激的响应。请注意,我们也可以省略 then 块中的 assert 语句。Spock 会正确地解释那里的布尔表达式。setup 块可用于配置仅在特征方法内部可见的局部变量。

2.4.5.14 关于 Spock 的更多信息

Spock 提供了更多高级功能,如数据表或模拟,在本节中我们没有介绍。请随时查阅 Spock GitHub 页面 以获取更多文档。

15 RxJava

优秀的 RxJava 可用于 Ratpack 应用程序以优雅地组合异步操作。

ratpack-rx2 模块提供了 RxRatpack 类,该类提供静态方法用于将 Ratpack 承诺适配为 RxJava 的 Observable

截至 2.0.0-rc-1,ratpack-rx2 模块针对 (并依赖) RxJava 2.2.21 构建。

1.15 初始化

必须调用 RxRatpack.initialize() 以完全启用集成。此方法只需要为 JVM 的生命周期调用一次。

2.15 观察 Ratpack

集成基于 RxRatpack.single()RxRatpack.observe() 静态方法。这些方法将 Ratpack 的承诺类型适配为可观察对象,然后可以使用 RxJava 提供的所有可观察对象运算符。

例如,可以轻松观察阻塞操作。

import ratpack.exec.Promise;
import ratpack.exec.Blocking;
import ratpack.test.handling.HandlingResult;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static ratpack.rx2.RxRatpack.single;
import static ratpack.test.handling.RequestFixture.requestFixture;

public class Example {
  public static void main(String... args) throws Exception {
    HandlingResult result = requestFixture().handle(context -> {
      Promise<String> promise = Blocking.get(() -> "hello world");
      single(promise).map(String::toUpperCase).subscribe(context::render);
    });

    assertEquals("HELLO WORLD", result.rendered(String.class));
  }
}

3.15 隐式错误处理

RxJava 集成的关键特性是隐式错误处理。所有可观察序列都有一个隐式默认错误处理策略,即转发异常到执行上下文错误处理程序。实际上,这意味着很少需要为可观察序列定义错误处理程序。

import ratpack.core.error.ServerErrorHandler;
import ratpack.rx2.RxRatpack;
import ratpack.test.handling.RequestFixture;
import ratpack.test.handling.HandlingResult;
import io.reactivex.Observable;

import static org.junit.jupiter.api.Assertions.assertEquals;

public class Example {
  public static void main(String... args) throws Exception {
    RxRatpack.initialize(); // must be called once per JVM

    HandlingResult result = RequestFixture.requestFixture().handleChain(chain -> {
      chain.register(registry ->
          registry.add(ServerErrorHandler.class, (context, throwable) ->
              context.render("caught by error handler: " + throwable.getMessage())
          )
      );

      chain.get(ctx -> Observable.<String>error(new Exception("!")).subscribe((s) -> {}));
    });

    assertEquals("caught by error handler: !", result.rendered(String.class));
  }
}

在这种情况下,在阻塞操作期间抛出的 throwable 将被转发到当前的 ServerErrorHandler,它可能会将错误页面呈现到响应中。如果订阅者确实实现了错误处理策略,则它将被用于代替隐式错误处理程序。

隐式错误处理适用于在 Ratpack 管理的线程上创建的所有可观察对象。它限于以 Ratpack 承诺为基础的可观察对象。

16 Jackson

Jackson JSON 序列化库 的集成提供了处理 JSON 的能力。这是作为 ratpack-core 的一部分提供的。

截至 Ratpack 2.0.0-rc-1,它是针对 (并依赖) Jackson Core 2.13.1 构建的。

ratpack.core.jackson.Jackson 类提供了大多数与 Jackson 相关的功能。

1.16 编写 JSON 响应

Jackson 集成添加了一个 Renderer,用于将对象呈现为 JSON。

Jackson.json() 方法可用于包装任何对象 (可由 Jackson 序列化) 以与 Context.render() 方法一起使用。

import ratpack.test.embed.EmbeddedApp;
import ratpack.core.http.client.ReceivedResponse;

import static ratpack.core.jackson.Jackson.json;
import static org.junit.jupiter.api.Assertions.*;

public class Example {

  public static class Person {
    private final String name;
    public Person(String name) {
      this.name = name;
    }
    public String getName() {
      return name;
    }
  }

  public static void main(String... args) throws Exception {
    EmbeddedApp.of(s -> s
      .handlers(chain ->
        chain.get(ctx -> ctx.render(json(new Person("John"))))
      )
    ).test(httpClient -> {
      ReceivedResponse response = httpClient.get();
      assertEquals("{\"name\":\"John\"}", response.getBody().getText());
      assertEquals("application/json", response.getBody().getContentType().getType());
    });
  }
}

有关更多示例,包括流和 JSON 事件,请参阅 Jackson 类文档。

2.16 读取 JSON 请求

Jackson 集成添加了一个 解析器,用于将 JSON 请求主体转换为对象。

可以使用 Jackson.jsonNode()Jackson.fromJson() 方法创建要与 Context.parse() 方法一起使用的对象。

import ratpack.guice.Guice;
import ratpack.test.embed.EmbeddedApp;
import ratpack.core.http.client.ReceivedResponse;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.common.reflect.TypeToken;

import java.util.List;

import static ratpack.func.Types.listOf;
import static ratpack.core.jackson.Jackson.jsonNode;
import static ratpack.core.jackson.Jackson.fromJson;
import static org.junit.jupiter.api.Assertions.*;

public class Example {

  public static class Person {
    private final String name;
    public Person(@JsonProperty("name") String name) {
      this.name = name;
    }
    public String getName() {
      return name;
    }
  }

  public static void main(String... args) throws Exception {
    EmbeddedApp.of(s -> s
      .handlers(chain -> chain
        .post("asNode", ctx -> {
          ctx.render(ctx.parse(jsonNode()).map(n -> n.get("name").asText()));
        })
        .post("asPerson", ctx -> {
          ctx.render(ctx.parse(fromJson(Person.class)).map(p -> p.getName()));
        })
        .post("asPersonList", ctx -> {
          ctx.render(ctx.parse(fromJson(listOf(Person.class))).map(p -> p.get(0).getName()));
        })
      )
    ).test(httpClient -> {
      ReceivedResponse response = httpClient.requestSpec(s ->
        s.body(b -> b.type("application/json").text("{\"name\":\"John\"}"))
      ).post("asNode");
      assertEquals("John", response.getBody().getText());

      response = httpClient.requestSpec(s ->
        s.body(b -> b.type("application/json").text("{\"name\":\"John\"}"))
      ).post("asPerson");
      assertEquals("John", response.getBody().getText());

      response = httpClient.requestSpec(s ->
        s.body(b -> b.type("application/json").text("[{\"name\":\"John\"}]"))
      ).post("asPersonList");
      assertEquals("John", response.getBody().getText());
    });
  }
}

该集成添加了一个 无操作解析器,它使使用 Context.parse(Class)Context.parse(TypeToken) 方法成为可能。

import ratpack.test.embed.EmbeddedApp;
import ratpack.core.http.client.ReceivedResponse;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.common.reflect.TypeToken;

import java.util.List;

import static ratpack.func.Types.listOf;
import static org.junit.jupiter.api.Assertions.*;

public class Example {

  public static class Person {
    private final String name;
    public Person(@JsonProperty("name") String name) {
      this.name = name;
    }
    public String getName() {
      return name;
    }
  }

  public static void main(String... args) throws Exception {
    EmbeddedApp.of(s -> s
      .handlers(chain -> chain
        .post("asPerson", ctx -> {
          ctx.parse(Person.class).then(person -> ctx.render(person.getName()));
        })
        .post("asPersonList", ctx -> {
          ctx.parse(listOf(Person.class)).then(person -> ctx.render(person.get(0).getName()));
        })
      )
    ).test(httpClient -> {
      ReceivedResponse response = httpClient.requestSpec(s ->
        s.body(b -> b.type("application/json").text("{\"name\":\"John\"}"))
      ).post("asPerson");
      assertEquals("John", response.getBody().getText());

      response = httpClient.requestSpec(s ->
        s.body(b -> b.type("application/json").text("[{\"name\":\"John\"}]"))
      ).post("asPersonList");
      assertEquals("John", response.getBody().getText());
    });
  }
}

3.16 配置 Jackson

Jackson API 基于 ObjectMapper。Ratpack 会自动将默认实例添加到基本注册表中。要配置 Jackson 行为,请覆盖此实例。

Jackson 功能模块 允许扩展 Jackson 以支持额外的數據类型和功能。例如,JDK8 模块 添加了对 JDK8 类型(如 Optional)的支持。

要使用这些模块,只需将适当配置的 ObjectMapper 添加到注册表即可。

import ratpack.test.embed.EmbeddedApp;
import ratpack.core.http.client.ReceivedResponse;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;

import java.util.Optional;

import static ratpack.core.jackson.Jackson.json;
import static org.junit.jupiter.api.Assertions.*;

public class Example {

  public static class Person {
    private final String name;
    public Person(String name) {
      this.name = name;
    }
    public String getName() {
      return name;
    }
  }

  public static void main(String... args) throws Exception {
    EmbeddedApp.of(s -> s
      .registryOf(r -> r
        .add(ObjectMapper.class, new ObjectMapper().registerModule(new Jdk8Module())) 
      )
      .handlers(chain ->
        chain.get(ctx -> {
          Optional<Person> personOptional = Optional.of(new Person("John"));
          ctx.render(json(personOptional));
        })
      )
    ).test(httpClient -> {
      ReceivedResponse response = httpClient.get();
      assertEquals("{\"name\":\"John\"}", response.getBody().getText());
      assertEquals("application/json", response.getBody().getContentType().getType());
    });
  }
}

17 Resilience4j

Resilience4j 是一个为 Java 8 设计的轻量级容错库。它受到 Netflix Hystrix 的启发,后者处于 维护模式

resilience4j-ratpack 模块由 Resilience4j 项目维护。有关更多信息,请参阅他们的 入门指南

1.17 演示应用程序

Resilience4j 团队发布了一个 演示应用程序,它展示了在 Ratpack 中使用 Resilience4j 的方法。

18 配置

大多数应用程序都需要一定程度的配置。这可以用于指定要使用的正确外部资源(数据库、其他服务等)、调整性能或以其他方式根据给定环境的要求进行调整。Ratpack 提供了一个简单灵活的机制来访问 Ratpack 应用程序中的配置信息。

配置数据通过使用 Jackson 进行对象绑定来访问。Ratpack 提供的配置对象旨在开箱即用。配置数据可以从多个来源加载,例如 YAML 文件、JSON 文件、属性文件、环境变量和系统属性。

1.18 快速入门

入门

  1. RatpackServer 定义函数内,构建 ConfigData 实例(有关示例,请参阅类文档)
  2. 从配置数据中检索绑定的配置对象

2.18 配置源

ConfigDataBuilder 提供了轻松从最常见来源加载数据的方法。

可以通过 yamljsonprops 方法使用常用的文件格式。提供的签名可用于从本地文件(StringPath)、网络(URL)、类路径(使用 Resources.getResource(String) 获取 URL)或任何其他可以视为 ByteSource 的地方加载数据。此外,您可以从非文件来源(例如 Map/Properties 对象)加载数据(这对于默认值特别有用;请参阅 示例)、系统属性和环境变量。您还可以选择使用 object 方法从现有对象加载配置数据。如果需要额外的灵活性,您可以提供自己的 ConfigSource 实现。

1.2.18 扁平配置源

环境变量、PropertiesMap 是扁平的数据结构,而 Ratpack 使用的绑定模型是分层的。为了弥合这一差距,这些配置源实现应用约定以允许将扁平的键值对转换为有用的数据。

1.1.2.18 环境变量

默认环境变量配置源使用以下规则

如果需要自定义环境解析,您可以提供 EnvironmentParser 实现。

2.1.2.18 属性/映射

默认 Properties/Map 配置源使用以下规则

3.18 用法

1.3.18 排序

如果您有多个配置源,请从最不重要到最重要的顺序将其添加到构建器中。例如,如果您有一个配置文件,您希望能够通过系统属性覆盖它,那么您将首先添加配置文件源,然后添加系统属性源。同样,如果您有一些默认设置,您希望能够通过环境变量覆盖它们,那么您将首先添加默认设置源(可能通过 props),然后添加环境变量源。

2.3.18 错误处理

ConfigDataBuilder 文档 中所示,onError 可用于在从配置源加载数据时遇到错误时自定义行为。最常见的情况是,这用于通过忽略加载异常使配置源可选。

3.3.18 对象映射器

Ratpack 使用 Jackson 进行配置对象绑定。使用的默认 ObjectMapper 经过配置,预加载了常用的 Jackson 模块,并设置为允许无引号的字段名、允许单引号以及忽略未知字段名。这旨在使其易于使用,开箱即用。但是,有时您可能希望更改 Jackson 配置设置或添加额外的 Jackson 模块。如果是这样,这可以通过 ConfigData.of(...) 的各种签名或通过 ConfigDataBuilder.configureObjectMapper(...) 来实现。

4.3.18 绑定

构建完 ConfigData 实例后,您可以将数据绑定到配置对象。最简单的选择是定义一个类来表示应用程序配置的全部内容,并使用 ConfigData.get(Class) 一次性绑定到该类。或者,您可以使用 ConfigData.get(String, Class) 在数据中的指定路径处一次绑定一个对象。对于绑定到 ServerConfig 对象的常见情况,提供了 ConfigData.getServerConfig(...) 签名作为一种便利。

19 Spring Boot

ratpack-spring-boot 扩展提供了与 Spring Boot 的集成。该库有两个主要功能:一个允许从 Spring ApplicationContext 创建 Ratpack 服务器注册表,另一个允许将 Ratpack 本身嵌入 Spring Boot 应用程序(使 ApplicationContext 自动成为服务器注册表的一部分)。

1.19 Spring 便利类

在普通 Ratpack 应用程序中,您可以使用 Spring 便利类轻松创建注册表。这可以替代或补充 Guice 依赖注入,因为它以大致相同的方式工作,并且 Ratpack 允许将注册表方便地链接在一起。

以下是一个使用此 API 配置应用程序的主类的示例。

package my.app;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import ratpack.core.server.RatpackServer;

import static ratpack.spring.Spring.spring;

public class Main {
  public static void main(String... args) throws Exception {
    RatpackServer.start(server -> server
      .registry(spring(MyConfiguration.class))
      .handlers(chain -> chain
        .get(ctx -> ctx
          .render("Hello " + ctx.get(Service.class).message()))
        .get(":message", ctx -> ctx
          .render("Hello " + ctx.getPathTokens().get("message") + "!")
        )
      )
    );
  }
}

@Configuration
class MyConfiguration {
  @Bean
  public Service service() {
    return () -> "World!";
  }
}

interface Service {
  String message();
}

Spring.spring() 方法创建一个 ApplicationContext,并将其适配到 Ratpack Registry 接口。

注意:Spring ListableBeanFactory API 目前不支持查找带有参数化类型的 bean。因此,适配的 Registry 实例不支持此功能,因为存在此限制。有一个 功能请求 将泛型功能添加到 Spring ListableBeanFactory API 中。

2.19 将 Ratpack 嵌入 Spring Boot 应用程序

除了将 Spring(作为 Registry)嵌入 Ratpack 应用程序外,您还可以执行相反的操作:将 Ratpack 作为服务器嵌入 Spring Boot,提供一个不错的替代 Spring Boot 支持的 Servlet 容器。该功能集的核心是一个注解 @EnableRatpack,您可以在 Spring 配置类中添加该注解以启动 Ratpack。然后,您可以将处理程序声明为类型为 Action<Chain>@Beans,例如

package my.app;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;

import ratpack.func.Action;
import ratpack.core.handling.Chain;
import ratpack.spring.config.EnableRatpack;

@SpringBootApplication
@EnableRatpack
public class Main {

  @Bean
  public Action<Chain> home() {
    return chain -> chain
      .get(ctx -> ctx
        .render("Hello " + service().message())
      );
  }

  @Bean
  public Service service() {
    return () -> "World!";
  }

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

}

interface Service {
  String message();
}

提示:Ratpack 将自动为类路径下 “/public” 或 “/static” 中的静态内容注册处理程序(就像常规的 Spring Boot 应用程序一样)。

1.2.19 重用现有的 Guice 模块

如果 Ratpack 嵌入 Spring 应用程序,重用现有的 Guice 模块(例如,用于模板渲染)可能会有所帮助。为此,只需包含一个类型为 Module@Bean 即可。例如,使用 ThymeleafModule 来支持 Thymeleaf

package my.app;

import java.util.Collections;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;

import ratpack.func.Action;
import ratpack.core.handling.Chain;
import ratpack.thymeleaf.ThymeleafModule;
import ratpack.spring.config.EnableRatpack;

import static ratpack.thymeleaf3.Template.thymeleafTemplate;

@SpringBootApplication
@EnableRatpack
public class Main {

  @Bean
  public Action<Chain> home(Service service) {
    return chain -> chain.get(ctx -> ctx
      .render(thymeleafTemplate("myTemplate", 
        m -> m.put("key", "Hello " + service.message())))
    );
  }

  @Bean
  public ThymeleafModule thymeleafModule() {
    return new ThymeleafModule();
  }

  @Bean
  public Service service() {
    return () -> "World!";
  }

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

}

20 pac4j

pac4j 库是一个安全引擎,它抽象化了不同的身份验证协议,例如 OAuth、CAS、OpenID(Connect)、SAML、Google App Engine 和 HTTP(表单和基本身份验证),以及自定义身份验证机制(例如,数据库支持)。它还支持各种授权机制:角色/权限检查、CSRF 令牌、安全标头等。通过 pac4j/ratpack-pac4j(由 pac4j 社区维护)提供与 Ratpack 的集成。

Gradle 依赖项

implementation 'org.pac4j:ratpack-pac4j:3.0.0'

1.20 会话使用

如前所述,使用 ratpack-pac4j 需要通过 ratpack-session 支持会话。在身份验证后,用户的个人资料将存储在会话中。因此,终止会话将有效地注销用户。

2.20 演示应用程序

有关使用 Ratpack 与 pac4j 的完整应用程序示例,请参阅 ratpack-pac4j-demo 应用程序。

21 Retrofit 类型安全客户端

ratpack-retrofit2 扩展提供了与 retrofit2 库(v2.9.0)的声明式类型安全 HTTP 客户端集成。

retrofit 库允许使用类型安全接口表示 HTTP API。这使应用程序代码能够与底层 API 设计和实现无关,而专注于与 API 交互的行为方面。

使用 RatpackRetrofit 类生成的 retrofit 客户端由 Ratpack 的 HttpClient 支持,并且能够将 Ratpack 的 Promise 结构用作返回值类型。

通过使用 ratpack-retrofit2 集成,开发人员可以获得将 API 结构隔离为类和方法注释的好处,同时仍然利用 Netty 支持的 HttpClient 的非阻塞特性和 Ratpack 的执行模型。

1.21 用法

RatpackRetrofit.client(URI endpoint)RatpackRetrofit.client(String endpoint) 方法提供了创建 API 客户端的入口点。

提供的 URIString 指定了由 Builder 生成的所有客户端的基 URL。

可以使用 RatpackRetrofit.Builder.configure(Action<? super Retrofit.Builder> spec) 方法配置底层的 Retrofit.Builder

配置完成后,通过使用 api 接口调用 build(Class<T> api) 来构造客户端。此方法返回接口的生成实例,该实例将发出 HTTP 请求并将响应适配到配置的返回值类型。

import ratpack.exec.Promise;
import ratpack.retrofit.RatpackRetrofit;
import ratpack.test.embed.EmbeddedApp;
import retrofit2.http.GET;

import static org.junit.jupiter.api.Assertions.*;

public class Example {

  public static interface HelloApi {
    @GET("hi") Promise<String> hello();
  }

  public static void main(String... args) throws Exception {
    EmbeddedApp api = EmbeddedApp.of(s -> s
      .handlers(chain -> chain
        .get("hi", ctx -> ctx.render("hello"))
      )
    );
    EmbeddedApp.of(s -> s
      .registryOf(r -> 
        r.add(HelloApi.class,
          RatpackRetrofit
            .client(api.getAddress())
            .build(HelloApi.class))
      )
      .handlers(chain -> {
        chain.get(ctx -> {
          HelloApi helloApi = ctx.get(HelloApi.class);
            
          ctx.render(helloApi.hello());
        });
      })
    ).test(httpClient -> {
      assertEquals("hello", httpClient.getText());
      api.close();
    });
  }
}

2.21 在 Retrofit API 中使用 Ratpack Promises

ratpack-retrofit2 集成提供了对将 Ratpack 的 Promise 用作客户端接口返回值类型的支持。它支持在承诺值为以下类型时适应 Promise

以下示例显示了为同一端点配置的 3 种变体。

import ratpack.exec.Promise;
import ratpack.core.http.client.ReceivedResponse;
import retrofit2.Response;
import retrofit2.http.GET;

public interface Example {
  @GET("hi") Promise<String> hello();
  
  @GET("hi") Promise<Response<String>> helloResponse();
  
  @GET("hi") Promise<ReceivedResponse> helloRaw();
}

3.21 创建多个 API 实现

许多 API 可以使用不同的接口来表示不同的功能。要创建多个客户端,可以获取底层的 Retrofit 类。

import ratpack.exec.Promise;
import ratpack.retrofit.RatpackRetrofit;
import ratpack.test.embed.EmbeddedApp;
import retrofit2.http.GET;
import retrofit2.Retrofit;

import static org.junit.jupiter.api.Assertions.*;

  
public class Example {

  public static interface HelloApi {
    @GET("hi") Promise<String> hello();
  }
  
  public static interface GoodbyeApi {
    @GET("bye") Promise<String> bye();
  }

  public static void main(String... args) throws Exception {
    EmbeddedApp api = EmbeddedApp.of(s -> s
      .handlers(chain -> chain
        .get("hi", ctx -> ctx.render("hello"))
        .get("bye", ctx -> ctx.render("goodbye"))
      )
    );
    EmbeddedApp.of(s -> s
      .registryOf(r -> {
        Retrofit retrofit = RatpackRetrofit
          .client(api.getAddress())
          .retrofit();
        r.add(HelloApi.class, retrofit.create(HelloApi.class));
        r.add(GoodbyeApi.class, retrofit.create(GoodbyeApi.class));
      })
      .handlers(chain -> {
        chain.get(ctx -> {
            
          HelloApi hiApi = ctx.get(HelloApi.class);
          GoodbyeApi byeApi = ctx.get(GoodbyeApi.class);
            
          ctx.render(hiApi.hello().right(byeApi.bye()).map(p -> p.left() + " and " + p.right()));
        });
      })
    ).test(httpClient -> {
      assertEquals("hello and goodbye", httpClient.getText());
      api.close();
    });
  }
}

4.21 使用 Retrofit 转换器

默认情况下,ratpack-retrofit2 注册 ScalarsConverterFactory。这使得 API 响应能够转换为 Java String、原始类型及其包装类型。

如果远程 API 以 JSON 格式响应,则必须注册 JacksonConverterFactory

import com.fasterxml.jackson.databind.ObjectMapper;
import ratpack.exec.Promise;
import ratpack.retrofit.RatpackRetrofit;
import ratpack.exec.registry.Registry;
import ratpack.test.embed.EmbeddedApp;
import retrofit2.converter.jackson.JacksonConverterFactory;
import retrofit2.http.GET;
import retrofit2.Retrofit;

import java.util.List;

import static ratpack.core.jackson.Jackson.json;
import static org.junit.jupiter.api.Assertions.*;

  
public class Example {

  public static interface NameApi {
    @GET("names") Promise<List<String>> names();
  }

  public static void main(String... args) throws Exception {
    EmbeddedApp api = EmbeddedApp.of(s -> s
      .handlers(chain -> chain
        .get("names", ctx -> ctx.render(json(new String[]{"John", "Jane"})))
      )
    );
    EmbeddedApp.of(s -> s
      .registry(r -> 
        Registry.single(NameApi.class, 
          RatpackRetrofit
            .client(api.getAddress())
            .configure(b -> 
              b.addConverterFactory(
                JacksonConverterFactory.create(
                  r.get(ObjectMapper.class)
                )
              )
            )
            .build(NameApi.class))
      )
      .handlers(chain -> {
        chain.get(ctx -> {
          ctx.get(NameApi.class).names().then(nameList -> ctx.render(json(nameList)));  
        });
      })
    ).test(httpClient -> {
      assertEquals("[\"John\",\"Jane\"]", httpClient.getText());
      api.close();
    });
  }
}

22 Dropwizard 指标

ratpack-dropwizard-metrics jar 提供了与 Dropwizard 指标库 的集成。

Dropwizard 指标是 JVM 最好的指标库之一。它提供了一套指标类型和指标报告工具,可以深入了解应用程序的性能,无论是在开发阶段还是在生产环境中的实时阶段。它允许您轻松捕获诸如已服务的请求数量或响应时间等统计信息,以及更通用的信息,例如内部集合、队列的状态,或者代码的某些部分执行了多少次。通过衡量您的代码,您将确切地知道代码在运行时执行的操作,并能够做出明智的优化决策。

Ratpack 与 Dropwizard 指标的集成意味着您只需注册 Guice 模块,就可以为您捕获许多关键指标。如果您需要更深入的了解,Ratpack 还让您能够轻松地使用库的许多指标类型捕获其他指标,然后使用库的指标报告程序将所有这些指标报告到所需的输出。

有关详细的用法信息,请参阅 DropwizardMetricsModule

1.22 内置指标

Ratpack 为关键指标提供内置的指标收集器。当使用 DropwizardMetricsModule 在您的应用程序中启用指标时,内置的指标收集器也会自动启用。

Ratpack 为以下指标提供内置的指标收集器

Ratpack 还支持 Dropwizard 指标的 JVM 检测。有关用法信息,请参阅 DropwizardMetricsConfig.jvmMetrics(boolean)

2.22 自定义指标

Ratpack 允许您通过两种方式捕获您自己的应用程序指标

  1. 通过依赖项注入或上下文注册表查找获取 MetricRegistry,并向其注册您自己的指标。
  2. 向您注入的 Guice 类添加指标注释。

有关更多详细信息,请参阅 DropwizardMetricsModule

3.22 报告指标

Ratpack 支持以下输出的指标报告程序

有关如何使用 Websockets 使用实时指标的示例,请参阅 example-books 项目。

23 使用 Gradle 构建

使用 Ratpack 项目提供的 Gradle 插件,使用 Gradle 构建系统 是构建 Ratpack 应用程序的推荐方法。

Ratpack 纯粹是一个运行时工具包,而不是像 Ruby on RailsGrails 这样的开发时工具。这意味着您可以使用任何您喜欢的工具来构建 Ratpack 应用程序。提供的 Gradle 插件只是为了提供方便,并不是 Ratpack 开发的基础。

1.23 设置

第一个要求是将 Gradle 插件应用到您的 Gradle 项目...

buildscript {
  repositories {
    mavenCentral()
  }
  dependencies {
    classpath "io.ratpack:ratpack-gradle:2.0.0-rc-1"
  }
}

apply plugin: "io.ratpack.ratpack-java"

repositories {
  mavenCentral()
}

或者对于基于 Groovy 的 Ratpack 项目...

buildscript {
  repositories {
    mavenCentral()
  }
  dependencies {
    classpath "io.ratpack:ratpack-gradle:2.0.0-rc-1"
  }
}

apply plugin: "io.ratpack.ratpack-groovy"

repositories {
  mavenCentral()
}

'io.ratpack.ratpack-java' 插件应用核心 Gradle 'java' 插件'io.ratpack.ratpack-groovy' 插件应用核心 Gradle 'groovy' 插件。这意味着您可以像标准 Gradle 项目一样开始添加代码和依赖项到您的应用程序(例如,将源代码放在 src/main/[groovy|java] 中)。请注意,'io.ratpack.ratpack-groovy' 插件隐式应用 'io.ratpack.ratpack-java' 插件。

2.23 Ratpack 依赖项

要依赖 Ratpack 扩展库,只需将其添加为常规的实现依赖项...

buildscript {
  repositories {
    mavenCentral()
  }
  dependencies {
    classpath "io.ratpack:ratpack-gradle:2.0.0-rc-1"
  }
}

apply plugin: "io.ratpack.ratpack-groovy"

repositories {
  mavenCentral()
}

dependencies {
  implementation ratpack.dependency("dropwizard-metrics")
}

使用 ratpack.dependency("dropwizard-metrics") 等效于 "io.ratpack:ratpack-dropwizard-metrics:«ratpack-gradle 依赖项的版本»"。这是添加核心发行版中包含的依赖项的推荐方法。

'io.ratpack.ratpack-java' 插件添加以下隐式依赖项

'io.ratpack.ratpack-groovy' 插件添加以下隐式依赖项

可以在 search.maven.org 上浏览 可用的库。所有 Ratpack jar 都发布到 Maven Central

3.23 ‘application’ 插件

'ratpack-java''ratpack-groovy' 插件都应用核心 Gradle 'application' 插件。此插件提供了创建软件独立可执行分发的功能。这是 Ratpack 应用程序的首选部署格式。

'application' 插件要求指定应用程序的主类(即入口点)。您必须在 Gradle 构建文件中配置 'mainClassName' 属性,使其成为包含 'static void main(String[] args)' 方法的类的完全限定类名,该方法配置 Ratpack 服务器。'ratpack-groovy' 插件已将其预先配置为 GroovyRatpackMain。如果您希望使用自定义入口点,可以更改此设置(请参阅 'application' 插件文档)。

4.23 ‘shadow’ 插件

'ratpack-java''ratpack-groovy' 插件都附带了对第三方 'shadow' 插件 的集成支持。此插件提供了创建包含您的 ratpack 应用程序以及任何编译和运行时依赖项的自包含“fat-jar”的功能。

这些插件会对 'shadow' 插件的应用做出反应,并配置其他任务依赖项。它们不会应用 'shadow' 插件,并且出于兼容性原因,不会将 'shadow' 的版本作为依赖项提供。

要使用 'shadow' 集成,您需要在您的项目中包含依赖项并应用该插件。

buildscript {
  repositories {
    mavenCentral()
    gradlePluginPortal()
  }
  dependencies {
    classpath "io.ratpack:ratpack-gradle:2.0.0-rc-1"
    classpath 'gradle.plugin.com.github.johnrengelman:shadow:7.1.2'
  }
}

apply plugin: "io.ratpack.ratpack-java"
apply plugin: 'com.github.johnrengelman.shadow'

repositories {
  mavenCentral()
}

可以在项目的 Github 页面 上找到 'shadow' 插件的最新版本。

现在您可以通过运行以下命令让构建生成 fat-jar...

./gradlew shadowJar

5.23 基目录

基目录实际上是应用程序文件系统的根目录。在构建时,这实际上是应用程序的主要资源集(即 src/main/resources)。Ratpack 插件添加了一个补充的主要资源源 src/ratpack。您可以选择不使用此目录,而是使用 src/main/resources,或者通过 Gradle 构建中的 ratpack.baseDir 属性更改其位置。

buildscript {
  repositories {
    mavenCentral()
  }
  dependencies {
    classpath "io.ratpack:ratpack-gradle:2.0.0-rc-1"
  }
}

apply plugin: "io.ratpack.ratpack-groovy"

repositories {
  mavenCentral()
}

ratpack.baseDir = file('ratpack')

在打包为分发版时,插件将在分发版中创建一个名为 app 的目录,其中包含项目的所有主要资源。启动应用程序时,此目录将预先添加到类路径。这允许应用程序从基目录直接从磁盘读取文件,而不是从 JAR 中动态解压缩。这更高效。

有关更多信息,请参阅 启动

1.5.23 ‘ratpack.groovy’ 脚本

'ratpack-groovy' 插件希望主应用程序定义位于 基目录中ratpack.groovyRatpack.groovy 中。默认情况下,它将在 src/main/resourcessrc/ratpack 中进行查找。此文件不应放在 src/main/groovy 中,因为应用程序管理此文件的编译。因此,它需要以源代码形式(即作为 .groovy 文件)而不是已编译形式(即作为 .class 文件)在类路径中。

有关此文件内容的更多信息,请参阅 Groovy

6.23 运行应用程序

'application' 插件提供了用于启动 Ratpack 应用程序的 'run' 任务。这是一个核心 Gradle JavaExec 类型的任务。'ratpack-java' 插件将此 'run' 任务配置为使用系统属性 'ratpack.development' 设置为 true 启动。

如果您希望为开发时执行设置额外的系统属性,可以配置此任务…

buildscript {
  repositories {
    mavenCentral()
  }
  dependencies {
    classpath "io.ratpack:ratpack-gradle:2.0.0-rc-1"
  }
}

apply plugin: "io.ratpack.ratpack-java"

repositories {
  mavenCentral()
}

run {
  systemProperty "app.dbPassword", "secret"
}

1.6.23 开发时重新加载

Ratpack Gradle 插件与 Gradle 的持续构建功能 集成。要利用此功能,您可以使用 --continuous(或 -t)参数运行 run 任务。

对源代码或资源所做的任何更改都将被编译和处理,并因此导致应用程序重新加载

2.6.23 使用 'shadow' 插件运行

如果应用于项目,'shadow' 插件将提供 'runShadow' 任务,用于从 fat-jar 启动 Ratpack 应用程序。与 'run' 任务一样,这也是核心 Gradle JavaExec 类型的任务。'shadow' 插件将此 'runShadow' 任务配置为使用 java -jar <path/to/shadow-jar.jar> 命令启动进程。

由于应用程序是从打包的 jar 文件运行的,因此 'runShadow' 任务不支持类重新加载。

可以在此任务上配置额外的系统属性或 JVM 选项…

buildscript {
  repositories {
    mavenCentral()
    gradlePluginPortal()
  }
  dependencies {
    classpath "io.ratpack:ratpack-gradle:2.0.0-rc-1"
    classpath "gradle.plugin.com.github.johnrengelman:shadow:7.1.2"
  }
}

apply plugin: "io.ratpack.ratpack-java"
apply plugin: "com.github.johnrengelman.shadow"

repositories {
  mavenCentral()
}

runShadow {
  systemProperty "app.dbPassword", "secret"
}

24 部署到 Heroku

Heroku 是一个可扩展的多语言云应用程序平台。它允许您专注于使用您选择的语言编写应用程序,然后轻松地将它们部署到云端,而无需手动管理服务器、负载平衡、日志聚合等。Heroku 没有也不需要对 Ratpack 进行任何特殊的支持,除了它对 JVM 应用程序的一般支持。Heroku 是一个功能丰富的平台,拥有许多 元素,例如 Postgres、Redis、Memcache、RabbitMQ、New Relic 等。它是服务 Ratpack 应用程序的引人注目的选择。

部署到 Heroku 通常以源代码形式进行。部署就像在 CI 管道结束时执行 Git 推送一样简单。许多流行的云 CI 工具,例如 drone.ioTravis-CI(以及其他工具)对推送到 Heroku 具有方便的支持。

如果您不熟悉 Heroku,建议您阅读 Heroku 快速入门Buildpack 文档。本章的其余部分概述了将 Ratpack 应用程序部署到 Heroku 的要求和必要配置。

1.24 基于 Gradle 的构建

Ratpack 应用程序可以通过任何构建系统构建,但 Ratpack 团队推荐 Gradle。Heroku 通过 Gradle buildpack 对 Gradle 提供了原生支持,该 buildpack 与 Ratpack Gradle 插件配合良好。

所有 Gradle 项目都应使用 Gradle Wrapper。如果您的项目中存在 wrapper 脚本,Heroku 将检测到您的项目是用 Gradle 构建的。

1.1.24 构建

当 Gradle buildpack 检测到正在使用 Ratpack 时,它将默认调用 ./gradlew installDist -x testinstallDist 任务由 Ratpack Gradle 插件添加(在 Gradle 2.3 之前为 installApp),默认情况下应能正常工作。这将构建您的应用程序并将其安装到 build/install/«project name» 目录中。

如果您需要运行其他任务,可以将 stage 任务添加到您的 build.gradle 中。典型的 stage 任务可能如下所示

task stage {
  dependsOn clean, installDist
}

如果存在 stage 任务,Heroku 将运行此任务,而不是默认任务。

1.1.1.24 设置项目名称

默认情况下,Gradle 使用项目的目录名称作为项目的名称。在 Heroku(以及一些 CI 服务器)中,项目是在具有随机分配名称的临时目录中构建的。为了确保项目使用一致的名称,请在项目的根目录中向 settings.gradle 添加声明

rootProject.name = "«project name»"

这对于任何 Gradle 项目来说都是一个好习惯。

2.1.24 运行 (Procfile)

默认情况下,Heroku 将运行以下脚本以启动您的应用程序

build/install/«project name»/bin/«project name»

您可以通过在应用程序的根目录中创建 Procfile 并指定 Heroku 应用来启动应用程序的命令(以 web: 为前缀)来自定义此命令。

3.1.24 配置

有几种方法可以配置部署到 Heroku 的应用程序的环境。您可能希望使用这些机制来设置环境变量和/或 JVM 系统属性以配置您的应用程序。

使用 ratpackratpack-groovy Gradle 插件时使用的应用程序入口点支持使用 JVM 系统属性来为 ServerConfig 做出贡献(有关更多详细信息,请参阅 启动章节 章节)。Ratpack Gradle 插件创建的启动脚本支持标准 JAVA_OPTS 环境变量和特定于应用程序的 «PROJECT_NAME»_OPTS 环境变量。如果您的应用程序名称为 foo-Bar,则环境变量将命名为 FOO_BAR_OPTS

将所有这些整合在一起的一种方法是通过 env 启动您的应用程序

web: env "FOO_BAR_OPTS=-Dapp.dbPassword=secret" build/install/«project name»/bin/«project name»

通常最好不要使用 JAVA_OPTS,因为 Heroku 将其设置为 平台的实用默认值

另一种方法是使用 配置变量。通过 Procfile 设置环境的优点是,此信息位于您的版本控制的源代码树中。使用配置变量的优点是,它们仅对具有查看/更改它们与 Heroku 权限的人员可用。可以通过为应保密的的值(例如密码)设置配置变量并在您的 Procfile 中引用它们来结合这两种方法。

web: env "FOO_BAR_OPTS=-Dapp.dbPassword=$SECRET_DB_PASSWORD" build/install/«project name»/bin/«project name»

现在很容易查看源代码树中设置了哪些属性和环境变量,但敏感值只能通过 Heroku 管理工具查看。

2.24 其他构建工具和二进制部署

Ratpack 项目没有提供与其他构建工具的任何“官方”集成。但是,使用您喜欢的任何工具构建用于 Heroku 的 Ratpack 应用程序或以二进制形式部署都是可能的。

一旦您在 Heroku 环境中拥有了编译后的 Ratpack 应用程序(无论是通过使用其他构建工具构建还是通过二进制部署),您只需使用 java 直接启动应用程序即可。

web: java ratpack.groovy.GroovyRatpackMain

有关启动 Ratpack 应用程序的更多详细信息,请参阅 启动章节

3.24 一般注意事项

1.3.24 端口

Heroku 为每个应用程序分配一个临时的端口号,可以通过 PORT 环境变量获得。如果未设置 ratpack.port 系统属性,Ratpack 默认情况下会使用此环境变量。

25 日志记录

Ratpack 使用 SLF4J 进行日志记录,这使您可以在编译时轻松绑定您喜欢的日志记录库。

库选项包括

只需添加一个日志记录库作为依赖项,然后使用 SLF4J 语法进行日志记录。如果您目前正在使用其他日志记录库,SLF4J 提供了一个 迁移工具来自动进行转换。
Java 和 Groovy 的示例如下所示,有关更多详细信息,请参阅 SLF4J 手册

1.25 Java

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class LogExample {
  private final static Logger LOGGER = LoggerFactory.getLogger(LogExample.class);
    
  public void log() {
    LOGGER.info("Start logging");
    LOGGER.warn("Logging with a {} or {}", "parameter", "two");
    LOGGER.error("Log an exception", new Exception("Example exception"));
    LOGGER.info("Stop logging");
  }
}

2.25 Groovy

import groovy.util.logging.Slf4j

@Slf4j
class LogExample {
  void log() {
    log.info "Start logging"
    log.warn "Logging with a {} or {}", "parameter", "two"
    log.debug "Detailed information"
    log.info "Stop logging"
  }
}

3.25 请求日志记录

Ratpack 提供了一种机制来记录有关每个请求的信息,RequestLogger。请求记录器是一个处理程序。通过它的每个请求都将被记录,并在请求完成时记录。通常,它被放置在处理程序链的早期,并使用 Chain.all(Handler) 方法添加,以便记录所有请求。

Ratpack 提供了 RequestLogger.ncsa() 方法,该方法以 NCSA 通用日志格式 进行日志记录。此实现将日志记录到名为 ratpack.requests 的 slf4j 记录器(RequestLogger.ncsa(Logger) 方法允许指定备用记录器)。

import ratpack.core.handling.RequestLogger;
import ratpack.core.http.client.ReceivedResponse;
import ratpack.test.embed.EmbeddedApp;
import static org.junit.jupiter.api.Assertions.*;

public class Example {
  public static void main(String... args) throws Exception {
    EmbeddedApp.fromHandlers(c -> c
      .all(RequestLogger.ncsa())
      .all(ctx -> ctx.render("ok"))
    ).test(httpClient -> {
      ReceivedResponse response = httpClient.get();
      assertEquals("ok", response.getBody().getText());

      // Check log output: [ratpack-compute-1-1] INFO ratpack.requests - 127.0.0.1 - - [30/Jun/2015:11:01:18 -0500] "GET / HTTP/1.1" 200 2
    });
  }
}

有关创建具有替代格式的记录器的信息,请参阅 RequestLogger 的文档。

26 Java 9 支持

这是一个关于使用早期 Java 9+ 支持的已知问题、注意事项和解决方法的列表。虽然 Java 9 支持远未完成,但核心功能似乎只需要很少的警告和消息即可正常工作。

1.26 已知库注意事项

以下是已知版本冲突或 Ratpack 在 Java 9 上无缝运行的要求。

2.26 已知问题

以下是目前由 Java 9 和 Ratpack 的底层组件引起的问题。在每种情况下都将提供解决方法

3.26 已知 Java 9 错误/警告消息

以下是 Java 9+ 发出的预期消息。

警告:发生了非法反射访问操作
警告:com.google.inject.internal.cglib.core.$ReflectUtils$1 通过非法反射访问(文件:[用户 Gradle 缓存目录]/modules-2/files-2.1/com.google.inject/guice/[版本哈希]/[guice 版本 jar])
警告:请考虑向 com.google.inject.internal.cglib.core.$ReflectUtils$1 的维护人员报告此问题
警告:使用 –illegal-access=warn 可启用对后续非法反射访问操作的警告
警告:在未来版本中,所有非法访问操作都将被拒绝

11:04:44.477 [main] DEBUG i.n.u.i.PlatformDependent0 - -Dio.netty.noUnsafe: false
11:04:44.477 [main] DEBUG i.n.u.i.PlatformDependent0 - Java 版本:11
11:04:44.479 [main] DEBUG i.n.u.i.PlatformDependent0 - sun.misc.Unsafe.theUnsafe: 可用
11:04:44.479 [main] DEBUG i.n.u.i.PlatformDependent0 - sun.misc.Unsafe.copyMemory: 可用
11:04:44.480 [main] DEBUG i.n.u.i.PlatformDependent0 - java.nio.Buffer.address: 可用
11:04:44.483 [main] DEBUG i.n.u.i.PlatformDependent0 - 直接缓冲区构造函数:不可用
java.lang.UnsupportedOperationException: 反射 setAccessible(true) 已禁用
at io.netty.util.internal.ReflectionUtil.trySetAccessible(ReflectionUtil.java:31)
at io.netty.util.internal.PlatformDependent0$4.run(PlatformDependent0.java:224)
at java.base/java.security.AccessController.doPrivileged(Native Method)
at io.netty.util.internal.PlatformDependent0.(PlatformDependent0.java:218)
at io.netty.util.internal.PlatformDependent.isAndroid(PlatformDependent.java:212)
at io.netty.util.internal.PlatformDependent.(PlatformDependent.java:80)
at io.netty.util.ConstantPool.(ConstantPool.java:32)
at io.netty.util.AttributeKey$1.(AttributeKey.java:27)
at io.netty.util.AttributeKey.(AttributeKey.java:27)
at ratpack.core.server.internal.DefaultRatpackServer.(DefaultRatpackServer.java:69)
at ratpack.core.server.RatpackServer.of(RatpackServer.java:81)
at ratpack.core.server.RatpackServer.start(RatpackServer.java:92)
at ratpack.groovy.GroovyRatpackMain.main(GroovyRatpackMain.java:38)
11:04:44.484 [main] DEBUG i.n.u.i.PlatformDependent0 - java.nio.Bits.unaligned: 可用,true
11:04:44.485 [main] DEBUG i.n.u.i.PlatformDependent0 - jdk.internal.misc.Unsafe.allocateUninitializedArray(int): 不可用
java.lang.IllegalAccessException: 类 io.netty.util.internal.PlatformDependent0$6 无法访问类 jdk.internal.misc.Unsafe(在模块 java.base 中),因为模块 java.base 未将 jdk.internal.misc 导出到未命名模块 @366647c2
at java.base/jdk.internal.reflect.Reflection.newIllegalAccessException(Reflection.java:361)
at java.base/java.lang.reflect.AccessibleObject.checkAccess(AccessibleObject.java:591)
at java.base/java.lang.reflect.Method.invoke(Method.java:558)
at io.netty.util.internal.PlatformDependent0$6.run(PlatformDependent0.java:334)
at java.base/java.security.AccessController.doPrivileged(Native Method)
at io.netty.util.internal.PlatformDependent0.(PlatformDependent0.java:325)
at io.netty.util.internal.PlatformDependent.isAndroid(PlatformDependent.java:212)
at io.netty.util.internal.PlatformDependent.(PlatformDependent.java:80)
at io.netty.util.ConstantPool.(ConstantPool.java:32)
at io.netty.util.AttributeKey$1.(AttributeKey.java:27)
at io.netty.util.AttributeKey.(AttributeKey.java:27)
at ratpack.core.server.internal.DefaultRatpackServer.(DefaultRatpackServer.java:69)
at ratpack.core.server.RatpackServer.of(RatpackServer.java:81)
at ratpack.core.server.RatpackServer.start(RatpackServer.java:92)
at ratpack.groovy.GroovyRatpackMain.main(GroovyRatpackMain.java:38)
11:04:44.485 [main] DEBUG i.n.u.i.PlatformDependent0 - java.nio.DirectByteBuffer.(long, int): 不可用
11:04:44.485 [main] DEBUG i.n.u.internal.PlatformDependent - sun.misc.Unsafe: 可用

27 相关项目

这是一个与 Ratpack 相关的第三方项目的不完整列表。

1.27 示例应用程序

以下项目由 Ratpack 核心团队维护,作为简单示例。

2.27 其他语言实现

以下是使用除 Java 和 Groovy 之外的语言实现的 Ratpack 项目和示例。

3.27 第三方模块

以下第三方项目为 Ratpack 项目提供了额外的功能。

28 Ratpack 项目

1.28 鸣谢

Ratpack 得益于以下人员的贡献。

1.1.28 活跃项目成员

以下人员目前正在积极地将自己的时间贡献给 Ratpack。

2.1.28 贡献者

以下人员做出了重大贡献。

3.1.28 过去项目成员

以下人员在过去贡献了自己的时间来开发 Ratpack,但现在不再积极参与了。

2.28 关于本手册

1.2.28 资源

1.1.2.28

2.1.2.28 字体

Web 字体由 Google Web 字体 提供。

3.1.2.28 图像