URI 链接

本节介绍 Spring 框架中可用于处理 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 组件,例如方案、主机、端口、路径和查询。此类组件可以决定使用 WhatWG 解析器类型来更宽松地处理 URL,并与浏览器解析 URI 的方式保持一致,以防重定向到输入 URL 或将其包含在对浏览器的响应中。

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 的 URI(例如,/main/*),如下例所示

  • 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()

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

还有其他方法可以使用 MvcUriComponentsBuilder。例如,您可以使用类似于通过代理进行模拟测试的技术来避免按名称引用控制器方法,如下例所示(该示例假设静态导入 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 从当前请求的方案、主机、端口、上下文路径和 Servlet 路径准备基本 URL。这在大多数情况下都能很好地工作。但是,有时这可能不够。例如,您可能处于请求上下文之外(例如准备链接的批处理过程),或者您可能需要插入路径前缀(例如从请求路径中删除并需要重新插入链接的语言环境前缀)。

对于这种情况,您可以使用接受 UriComponentsBuilder 的静态 fromXxx 重载方法来使用基本 URL。或者,您可以使用基本 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