跳到主要内容

URI 链接

ChatGPT-4o 中英对照 URI Links

本节介绍了 Spring 框架中可用于处理 URI 的各种选项。

UriComponents

Spring MVC 和 Spring WebFlux

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
java
  • 使用 URI 模板的静态工厂方法。

  • 添加或替换 URI 组件。

  • 请求对 URI 模板和 URI 变量进行编码。

  • 构建一个 UriComponents

  • 展开变量并获取 URI

前面的示例可以通过 buildAndExpand 合并为一个链并缩短,如下例所示:

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

您可以通过直接使用 URI(这意味着编码)来进一步缩短它,如以下示例所示:

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

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

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

UriBuilder

Spring MVC 和 Spring WebFlux

UriComponentsBuilder 实现了 UriBuilder。你可以通过 UriBuilderFactory 创建一个 UriBuilderUriBuilderFactoryUriBuilder 一起提供了一种可插拔的机制,用于根据共享配置(例如基本 URL、编码偏好和其他细节)从 URI 模板构建 URI。

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

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

// import org.springframework.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);
java

以下示例配置了一个 WebClient

// import org.springframework.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();
java

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

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

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

URI 解析

Spring MVC 和 Spring WebFlux

UriComponentsBuilder 支持两种 URI 解析器类型:

  1. RFC 解析器 — 这种解析器类型要求 URI 字符串符合 RFC 3986 语法,并将偏离该语法的情况视为非法。

  2. WhatWG 解析器 — 这种解析器基于 URL 解析算法,该算法在 WhatWG URL Living 标准中定义。它对各种意外输入情况提供宽松的处理。浏览器实现此功能以宽松地处理用户输入的 URL。有关更多详细信息,请参阅 URL Living 标准和 URL 解析测试用例

默认情况下,RestClientWebClientRestTemplate 使用 RFC 解析器类型,并期望应用程序提供符合 RFC 语法的 URL 模板。要更改这一点,你可以在任何一个客户端上自定义 UriBuilderFactory

应用程序和框架可能会进一步依赖 UriComponentsBuilder 来解析用户提供的 URL,以便检查和可能验证 URI 组件,例如 scheme、host、port、path 和 query。这样的组件可以选择使用 WhatWG 解析器类型,以更宽松地处理 URL,并与浏览器解析 URI 的方式保持一致,以防重定向到输入 URL 或在响应中包含给浏览器。

URI 编码

Spring MVC 和 Spring WebFlux

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"
java

您可以通过直接访问 URI(这意味着编码)来缩短前面的示例,如以下示例所示:

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

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

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

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();
java

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

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

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

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

  • NONE:不进行编码。

RestTemplate 被设置为 EncodingMode.URI_COMPONENT 是出于历史原因和向后兼容性考虑。WebClient 依赖于 DefaultUriBuilderFactory 中的默认值,该默认值在 5.0.x 中从 EncodingMode.URI_COMPONENT 更改为 5.1 中的 EncodingMode.TEMPLATE_AND_VALUES

相对 Servlet 请求

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

HttpServletRequest request = ...

// Re-uses scheme, host, port, path, and query string...

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

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

HttpServletRequest request = ...

// Re-uses scheme, host, port, and context path...

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

您可以创建相对于 Servlet 的 URI(例如,/main/*),如下例所示:

HttpServletRequest request = ...

// Re-uses scheme, host, port, context path, and Servlet mapping prefix...

URI uri = ServletUriComponentsBuilder.fromServletMapping(request)
.path("/accounts")
.build()
.toUri();
java
备注

从 5.1 开始,ServletUriComponentsBuilder 会忽略来自 ForwardedX-Forwarded-* 头的信息,这些头指定了客户端发起的地址。请考虑使用 ForwardedHeaderFilter 来提取和使用或丢弃这些头。

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

@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();
java

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

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

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

URI uri = uriComponents.encode().toUri();
java
备注

当控制器方法签名设计用于通过 fromMethodCall 创建链接时,其设计受到限制。除了需要一个合适的参数签名外,对返回类型也有技术限制(即为链接构建器调用生成运行时代理),因此返回类型不能是 final。特别是,常见的用于视图名称的 String 返回类型在这里不起作用。你应该使用 ModelAndView 或者甚至是普通的 Object(带有 String 返回值)来代替。

前面的示例中使用了 MvcUriComponentsBuilder 的静态方法。在内部,它们依赖于 ServletUriComponentsBuilder 来从当前请求的方案、主机、端口、上下文路径和 servlet 路径准备一个基本 URL。这在大多数情况下都能很好地工作。然而,有时这可能是不够的。例如,您可能处于请求的上下文之外(例如准备链接的批处理过程),或者您可能需要插入一个路径前缀(例如从请求路径中移除的区域设置前缀,需要重新插入到链接中)。

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

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

URI uri = uriComponents.encode().toUri();
java
备注

从 5.1 版本开始,MvcUriComponentsBuilder 会忽略来自 ForwardedX-Forwarded-* 头的信息,这些头指定了客户端发起的地址。可以考虑使用 ForwardedHeaderFilter 来提取和使用或丢弃这些头。

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

考虑以下示例:

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

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

根据前面的控制器,你可以从 JSP 准备一个链接,如下所示:

<%@ taglib uri="http://www.springframework.org/tags" prefix="s" %>
...
<a href="${s:mvcUrl('PAC#getAddress').arg(0,'US').buildAndExpand('123')}">Get Address</a>
jsp

前面的示例依赖于在 Spring 标签库(即 META-INF/spring.tld)中声明的 mvcUrl 函数,但定义您自己的函数或为其他模板技术准备一个类似的函数是很容易的。

以下是其工作原理。在启动时,每个 @RequestMapping 都通过 HandlerMethodMappingNamingStrategy 被分配一个默认名称,其默认实现使用类名和方法名的首字母(例如,ThingController 中的 getThing 方法变为 "TC#getThing")。如果存在名称冲突,你可以使用 @RequestMapping(name="..") 来分配一个显式名称,或者实现你自己的 HandlerMethodMappingNamingStrategy