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 创建 UriBuilder。UriBuilderFactory 和 UriBuilder 共同提供了一种可插拔的机制,用于基于共享配置(例如基本 URL、编码首选项和其他详细信息)从 URI 模板构建 URI。
您可以使用 UriBuilderFactory 配置 RestTemplate 和 WebClient,以自定义 URI 的准备。DefaultUriBuilderFactory 是 UriBuilderFactory 的默认实现,它在内部使用 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 解析器类型
-
RFC 解析器——此解析器类型要求 URI 字符串符合 RFC 3986 语法,并将与该语法的偏差视为非法。
-
WhatWG 解析器——此解析器基于 WhatWG URL Living 标准中的 URL 解析算法。它对各种意外输入情况提供宽松处理。浏览器实现此功能是为了宽松处理用户输入的 URL。有关更多详细信息,请参阅 URL Living 标准和 URL 解析测试用例。
默认情况下,RestClient、WebClient 和 RestTemplate 使用 RFC 解析器类型,并期望应用程序提供符合 RFC 语法的 URL 模板。要更改此设置,您可以自定义任何客户端上的 UriBuilderFactory。
应用程序和框架可以进一步依赖 UriComponentsBuilder 来解析用户提供的 URL,以检查并可能验证 URI 组件,例如方案、主机、端口、路径和查询。此类组件可以决定使用 WhatWG 解析器类型,以便更宽松地处理 URL,并在重定向到输入 URL 或将其包含在对浏览器的响应中时与浏览器解析 URI 的方式保持一致。
URI 编码
Spring MVC 和 Spring WebFlux
UriComponentsBuilder 在两个级别上公开编码选项
-
UriComponentsBuilder#encode():首先预编码 URI 模板,然后在扩展时严格编码 URI 变量。
-
UriComponents#encode():在 URI 变量扩展后编码 URI 组件。
这两种选项都将非 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")
WebClient 和 RestTemplate 通过 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_COMPONENT。WebClient 依赖于 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 忽略来自 Forwarded 和 X-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 以使用基本 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 忽略来自 Forwarded 和 X-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 标签库中声明的 mvcUrl 函数(即 META-INF/spring.tld),但很容易定义自己的函数或为其他模板技术准备一个类似的函数。
其工作原理如下。在启动时,每个 @RequestMapping 都通过 HandlerMethodMappingNamingStrategy 分配一个默认名称,其默认实现使用类名和方法名的大写字母(例如,ThingController 中的 getThing 方法变为“TC#getThing”)。如果存在名称冲突,您可以使用 @RequestMapping(name="..") 分配显式名称或实现自己的 HandlerMethodMappingNamingStrategy。