URI 链接

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

UriComponents

Spring MVC 和 Spring WebFlux

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

  • Java

  • Kotlin

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
val uriComponents = UriComponentsBuilder
		.fromUriString("https://example.com/hotels/{hotel}") (1)
		.queryParam("q", "{q}") (2)
		.encode() (3)
		.build() (4)

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

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

  • Java

  • Kotlin

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

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

  • Java

  • Kotlin

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

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

  • Java

  • Kotlin

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

UriBuilder

Spring MVC 和 Spring WebFlux

UriComponentsBuilder 实现了 UriBuilder。您可以使用 UriBuilderFactory 来创建 UriBuilderUriBuilderFactoryUriBuilder 共同提供了一种可插拔机制,可以基于共享配置(如基本 URL、编码偏好等细节)从 URI 模板构建 URI。

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

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

  • Java

  • Kotlin

// 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);
// import org.springframework.web.util.DefaultUriBuilderFactory.EncodingMode

val baseUrl = "https://example.org"
val factory = DefaultUriBuilderFactory(baseUrl)
factory.encodingMode = EncodingMode.TEMPLATE_AND_VALUES

val restTemplate = RestTemplate()
restTemplate.uriTemplateHandler = factory

下面示例配置了一个 WebClient

  • Java

  • Kotlin

// 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();
// import org.springframework.web.util.DefaultUriBuilderFactory.EncodingMode

val baseUrl = "https://example.org"
val factory = DefaultUriBuilderFactory(baseUrl)
factory.encodingMode = EncodingMode.TEMPLATE_AND_VALUES

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

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

  • Java

  • Kotlin

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

URI uri = uriBuilderFactory.uriString("/hotels/{hotel}")
		.queryParam("q", "{q}")
		.build("Westin", "123");
val baseUrl = "https://example.com"
val uriBuilderFactory = DefaultUriBuilderFactory(baseUrl)

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

URI 解析

Spring MVC 和 Spring WebFlux

UriComponentsBuilder 支持两种 URI 解析器类型

  1. RFC 解析器 — 此解析器类型要求 URI 字符串符合 RFC 3986 语法,并将语法偏差视为非法。

  2. WhatWG 解析器 — 此解析器基于 WhatWG URL 活标准中的 URL 解析算法。它对各种意外输入提供宽松处理。浏览器实现此算法是为了宽松处理用户输入的 URL。有关更多详细信息,请参见 URL 活标准和 URL 解析测试用例

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

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

URI 编码

Spring MVC 和 Spring WebFlux

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

这两种选项都会将非 ASCII 和非法字符替换为转义八位字节。但是,第一种选项也会替换出现在 URI 变量中具有保留含义的字符。

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

在大多数情况下,第一种选项可能会得到预期的结果,因为它将 URI 变量视为不透明数据进行完全编码,而如果 URI 变量有意包含保留字符,则第二种选项会很有用。当完全不展开 URI 变量时,第二种选项也很有用,因为它也会对任何偶然看起来像 URI 变量的内容进行编码。

下面示例使用第一种选项

  • Java

  • Kotlin

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"
val 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 (这意味着编码) 进一步缩短前面的示例,如下例所示

  • Java

  • Kotlin

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

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

  • Java

  • Kotlin

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

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

  • Java

  • Kotlin

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();
val baseUrl = "https://example.com"
val factory = DefaultUriBuilderFactory(baseUrl).apply {
	encodingMode = EncodingMode.TEMPLATE_AND_VALUES
}

// Customize the RestTemplate..
val restTemplate = RestTemplate().apply {
	uriTemplateHandler = factory
}

// Customize the WebClient..
val 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: 不应用编码。

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

相对 Servlet 请求

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

  • Java

  • Kotlin

HttpServletRequest request = ...

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

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

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

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

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

  • Java

  • Kotlin

HttpServletRequest request = ...

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

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

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

val uri = ServletUriComponentsBuilder.fromContextPath(request)
		.path("/accounts")
		.build()
		.toUri()

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

  • Java

  • Kotlin

HttpServletRequest request = ...

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

URI uri = ServletUriComponentsBuilder.fromServletMapping(request)
		.path("/accounts")
		.build()
		.toUri();
val request: HttpServletRequest = ...

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

val uri = ServletUriComponentsBuilder.fromServletMapping(request)
		.path("/accounts")
		.build()
		.toUri()
从 5.1 版本开始,ServletUriComponentsBuilder 忽略来自 ForwardedX-Forwarded-* 头信息,这些头信息指定了客户端原始地址。考虑使用 ForwardedHeaderFilter 来提取并使用或丢弃此类头信息。

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

  • Java

  • Kotlin

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

	@GetMapping("/bookings/{booking}")
	public ModelAndView getBooking(@PathVariable Long booking) {
		// ...
	}
}
@Controller
@RequestMapping("/hotels/{hotel}")
class BookingController {

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

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

  • Java

  • Kotlin

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

URI uri = uriComponents.encode().toUri();
val uriComponents = MvcUriComponentsBuilder
	.fromMethodName(BookingController::class.java, "getBooking", 21).buildAndExpand(42)

val uri = uriComponents.encode().toUri()

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

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

  • Java

  • Kotlin

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

URI uri = uriComponents.encode().toUri();
val uriComponents = MvcUriComponentsBuilder
	.fromMethodCall(on(BookingController::class.java).getBooking(21)).buildAndExpand(42)

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

前面的示例使用了 MvcUriComponentsBuilder 中的静态方法。在内部,它们依赖于 ServletUriComponentsBuilder 从当前请求的 scheme、host、port、context path 和 servlet path 准备一个基本 URL。这在大多数情况下都工作得很好。但是,有时它可能不够。例如,您可能在请求上下文之外(例如准备链接的批处理进程),或者您可能需要插入路径前缀(例如从请求路径中删除的 locale 前缀需要重新插入到链接中)。

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

  • Java

  • Kotlin

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();
val base = ServletUriComponentsBuilder.fromCurrentContextPath().path("/en")
val builder = MvcUriComponentsBuilder.relativeTo(base)
builder.withMethodCall(on(BookingController::class.java).getBooking(21)).buildAndExpand(42)

val uri = uriComponents.encode().toUri()
从 5.1 版本开始,MvcUriComponentsBuilder 忽略来自 ForwardedX-Forwarded-* 头信息,这些头信息指定了客户端原始地址。考虑使用 ForwardedHeaderFilter 来提取并使用或丢弃此类头信息。

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

考虑以下示例

  • Java

  • Kotlin

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

	@RequestMapping("/{country}")
	public HttpEntity<PersonAddress> getAddress(@PathVariable String country) { ... }
}
@RequestMapping("/people/{id}/addresses")
class PersonAddressController {

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

给定前面的控制器,您可以从 JSP 准备链接,如下所示

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

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

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