URI 链接

本节描述了 TODAY Framework 中用于处理 URI 的各种选项。

UriComponents

UriComponentsBuilder 有助于从带有变量的 URI 模板构建 URI,如下例所示:

UriComponents uriComponents = UriComponentsBuilder
    .fromUriString("https://example.com/hotels/{hotel}") (1)
    .queryParam("q", "{q}") (2)
    .encode() (3)
    .build(); (4)

URI uri = uriComponents.expand("Westin", "123").toUri(); (5)
1 带 URI 模板的静态工厂方法。
2 添加或替换 URI 组件。
3 请求对 URI 模板和 URI 变量进行编码。
4 构建 UriComponents
5 展开变量并获取 URI

前面的示例可以合并为一个链,并使用 buildAndExpand 进行简化,如下例所示:

URI uri = UriComponentsBuilder
    .fromUriString("https://example.com/hotels/{hotel}")
    .queryParam("q", "{q}")
    .encode()
    .buildAndExpand("Westin", "123")
    .toUri();

您可以进一步简化,直接生成 URI(这意味着进行编码),如下例所示:

URI uri = UriComponentsBuilder
    .fromUriString("https://example.com/hotels/{hotel}")
    .queryParam("q", "{q}")
    .build("Westin", "123");

您还可以使用完整的 URI 模板进一步简化,如下例所示:

URI uri = UriComponentsBuilder
  .fromUriString("https://example.com/hotels/{hotel}?q={q}")
  .build("Westin", "123");

UriBuilder

UriComponentsBuilder 实现了 UriBuilder。您可以创建一个 UriBuilder, 进而使用 UriBuilderFactoryUriBuilderFactoryUriBuilder 一起提供了一个可插拔的机制, 用于根据共享配置(如基础 URL、编码偏好和其他详细信息)从 URI 模板构建 URI。

您可以配置 RestTemplateWebClient 使用 UriBuilderFactory 来自定义 URI 的准备。 DefaultUriBuilderFactoryUriBuilderFactory 的默认实现,它在内部使用 UriComponentsBuilder 并公开共享配置选项。

以下示例展示了如何配置 RestTemplate

// import infra.web.util.DefaultUriBuilderFactory.EncodingMode;

String baseUrl = "https://example.org";
DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory(baseUrl);
factory.setEncodingMode(EncodingMode.TEMPLATE_AND_VALUES);

RestTemplate restTemplate = new RestTemplate();
restTemplate.setUriTemplateHandler(factory);

下面的例子配置了一个 WebClient:

// import infra.web.util.DefaultUriBuilderFactory.EncodingMode;

String baseUrl = "https://example.org";
DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory(baseUrl);
factory.setEncodingMode(EncodingMode.TEMPLATE_AND_VALUES);

WebClient client = WebClient.builder().uriBuilderFactory(factory).build();

此外,您也可以直接使用 DefaultUriBuilderFactory。它类似于使用 UriComponentsBuilder, 但不是静态工厂方法,而是一个实际的实例,它持有配置和偏好,如下例所示:

String baseUrl = "https://example.com";
DefaultUriBuilderFactory uriBuilderFactory = new DefaultUriBuilderFactory(baseUrl);

URI uri = uriBuilderFactory.uriString("/hotels/{hotel}")
    .queryParam("q", "{q}")
    .build("Westin", "123");

URI Encoding

UriComponentsBuilder 在两个级别上公开编码选项:

两个选项都使用转义八位字节替换非 ASCII 和非法字符。然而,第一个选项还替换了 URI 变量中出现的具有保留含义的字符。

考虑 ";",它在路径中是合法的,但具有保留含义。第一个选项将 URI 变量中的 ";" 替换为 "%3B",但不替换 URI 模板中的 ";"。相比之下,第二个选项从不替换 ";",因为它在路径中是合法字符。

对于大多数情况,第一个选项可能会给出预期的结果,因为它将 URI 变量视为需要完全编码的不透明数据,而第二个选项在 URI 变量确实包含保留字符时很有用。如果根本不展开 URI 变量,第二个选项也很有用,因为那也会编码任何看起来像 URI 变量的内容。

以下示例使用了第一个选项:

URI uri = UriComponentsBuilder.fromPath("/hotel list/{city}")
    .queryParam("q", "{q}")
    .encode()
    .buildAndExpand("New York", "foo+bar")
    .toUri();

// Result is "/hotel%20list/New%20York?q=foo%2Bbar"

您可以进一步简化前面的示例,直接生成 URI(这意味着进行编码),如下例所示:

URI uri = UriComponentsBuilder.fromPath("/hotel list/{city}")
  .queryParam("q", "{q}")
  .build("New York", "foo+bar");

您还可以使用完整的 URI 模板进一步简化,如下例所示:

URI uri = UriComponentsBuilder.fromUriString("/hotel list/{city}?q={q}")
  .build("New York", "foo+bar");

WebClientRestTemplate 通过 UriBuilderFactory 策略在内部扩展和编码 URI 模板。 两者都可以配置自定义策略,如下例所示:

String baseUrl = "https://example.com";
DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory(baseUrl)
factory.setEncodingMode(EncodingMode.TEMPLATE_AND_VALUES);

// Customize the RestTemplate..
RestTemplate restTemplate = new RestTemplate();
restTemplate.setUriTemplateHandler(factory);

// Customize the WebClient..
WebClient client = WebClient.builder().uriBuilderFactory(factory).build();

DefaultUriBuilderFactory 的实现内部使用 UriComponentsBuilder 来展开和编码 URI 模板。 作为一个工厂,它提供了一个地方来配置编码方法,基于以下编码模式之一:

  • TEMPLATE_AND_VALUES:使用 UriComponentsBuilder#encode(),对应于前面列表中的第一个选项,预先编码 URI 模板,并在展开时严格编码 URI 变量。

  • VALUES_ONLY:不编码 URI 模板,而是通过 UriUtils#encodeUriVariables 在将它们展开到模板之前,对 URI 变量应用严格编码。

  • URI_COMPONENT:使用 UriComponents#encode(),对应于前面列表中的第二个选项,在 URI 变量展开后对 URI 组件值进行编码。

  • NONE:不应用编码。

相对 Web 请求

您可以使用 UriComponentsBuilder 创建相对于当前请求的 URI,如下例所示:

HttpRequest request = ...

// 重用 scheme, host, port, path, 和 query string...

URI uri = UriComponentsBuilder.fromRequest(request)
    .replaceQueryParam("accountId", "{id}")
    .build("123");

您可以创建相对于上下文路径(context path)的 URI,如下例所示:

HttpRequest request = ...

// 重用 scheme, host, port, 和 context path...

URI uri = UriComponentsBuilder.fromContextPath(request)
    .path("/accounts")
    .build()
    .toUri();

Web MVC 提供了一种机制来准备指向控制器方法的链接。例如,下面的 MVC 控制器允许创建链接:

  • Java

@Controller
@RequestMapping("/hotels/{hotel}")
public class BookingController {

  @GetMapping("/bookings/{booking}")
  public ModelAndView getBooking(@PathVariable Long booking) {
    // ...
  }
}

您可以通过名称引用方法来准备链接,如下例所示:

  • Java

UriComponents uriComponents = MvcUriComponentsBuilder
  .fromMethodName(BookingController.class, "getBooking", 21).buildAndExpand(42);

URI uri = uriComponents.encode().toUri();

在前面的示例中,我们提供了实际的方法参数值(在本例中为 long 值:21)作为路径变量并插入到 URL 中。 此外,我们提供了值 42 来填充任何剩余的 URI 变量,例如从类型级请求映射继承的 hotel 变量。 如果方法有更多参数,我们可以为不需要用于 URL 的参数提供 null。通常,只有 @PathVariable@RequestParam 参数与构建 URL 相关。

还有其他使用 MvcUriComponentsBuilder 的方法。例如,您可以使用类似于通过代理进行 mock 测试的技术, 以避免通过名称引用控制器方法,如下例所示(该示例假设静态导入了 MvcUriComponentsBuilder.on):

  • Java

UriComponents uriComponents = MvcUriComponentsBuilder
  .fromMethodCall(on(BookingController.class).getBooking(21)).buildAndExpand(42);

URI uri = uriComponents.encode().toUri();
控制器方法签名在设计用于通过 fromMethodCall 创建链接时受到限制。除了需要适当的参数签名外, 返回类型还有一个技术限制(即为链接构建器调用生成运行时代理),因此返回类型不能是 final。 特别是,用于视图名称的常见 String 返回类型在这里不起作用。您应该改用 ModelAndView 甚至普通的 Object(带有 String 返回值)。

前面的示例使用了 MvcUriComponentsBuilder 中的静态方法。在内部,它们依赖 UriComponentsBuilder 从当前请求的 scheme、host、port 准备基本 URL。这在大多数情况下效果很好。 但是,有时这可能还不够。例如,您可能处于请求上下文之外(例如准备链接的批处理过程),或者您可能需要插入路径前缀 (例如从请求路径中删除并需要重新插入到链接中的区域设置前缀)。

对于这种情况,您可以使用接受 UriComponentsBuilder 以使用基本 URL 的静态 fromXxx 重载方法。 或者,您可以创建一个带有基本 URL 的 MvcUriComponentsBuilder 实例,然后使用基于实例的 withXxx 方法。 例如,下面的清单使用了 withMethodCall

  • Java

UriComponentsBuilder base = UriComponentsBuilder.forHttpRequest(req).path("/en");
MvcUriComponentsBuilder builder = MvcUriComponentsBuilder.relativeTo(base);
builder.withMethodCall(on(BookingController.class).getBooking(21)).buildAndExpand(42);

URI uri = uriComponents.encode().toUri();

在 Thymeleaf、FreeMarker 或 JSP 等视图中,您可以通过引用每个请求映射的隐式或显式分配的名称, 来构建指向带注解控制器的链接。

考虑以下示例:

  • Java

@RequestMapping("/people/{id}/addresses")
public class PersonAddressController {

  @RequestMapping("/{country}")
  public HttpEntity<PersonAddress> getAddress(@PathVariable String country) { ... }
}