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

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

基于 Jackson,开箱即用地支持处理 JSON 请求主体。有关示例,请参阅 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。您还必须记住注册您的 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) 发送静态资源(例如文件)。

TODO 介绍 sendFile 方法(指向使用 render(file(«path»))) 代替)。

TODO 介绍 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

您可以设置要与响应一起发送的 Cookie Response#cookie(String, String)。要检索要与响应一起设置的一组 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 内容协商

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

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,并且将在响应中使用 Add-Cookie 标头将 Cookie 发送到客户端。因此,您需要确保向客户端发送响应,否则会话将丢失。连续的客户端请求在 Cookie 标头中包含 Cookie,因此 Ratpack 可以确定客户端的会话。

以下是一些深入了解 Ratpack 会话处理的有用链接: - ratpack-session 模块测试 中有关会话工作原理的更多示例 - ratpack javadoc 包含许多示例和信息 - 可以自定义 Cookie 行为(例如,更改 Cookie 的名称,使用自定义 ID/过期日期)方法是向会话模块提供自定义的 SessionCookieConfig

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

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