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,
进而使用 UriBuilderFactory。UriBuilderFactory 和 UriBuilder 一起提供了一个可插拔的机制,
用于根据共享配置(如基础 URL、编码偏好和其他详细信息)从 URI 模板构建 URI。
您可以配置 RestTemplate 和 WebClient 使用 UriBuilderFactory 来自定义 URI 的准备。
DefaultUriBuilderFactory 是 UriBuilderFactory 的默认实现,它在内部使用 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 在两个级别上公开编码选项:
-
UriComponentsBuilder#encode(): 首先对 URI 模板进行预编码,然后在展开 URI 变量时严格编码。
-
UriComponents#encode(): 在展开 URI 变量后对 URI 组件进行编码。
两个选项都使用转义八位字节替换非 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");
WebClient 和 RestTemplate 通过 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();