|
对于最新稳定版本,请使用 Spring Framework 7.0.6! |
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 创建一个
UriBuilder。UriBuilderFactory 和
UriBuilder 共同提供了一种可插拔的机制,用于基于共享配置(如基础 URL、编码偏好和其他细节)从 URI 模板构建 URI。
您可以使用 RestTemplate 来配置 WebClient 和 UriBuilderFactory,以自定义 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 在两个层级上提供了编码选项:
-
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 模板进行编码,而是在将 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()
在前面的示例中,我们提供了实际的方法参数值(在此例中为 long 类型的值: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,根据当前请求的协议(scheme)、主机(host)、端口(port)、上下文路径(context path)和 Servlet 路径(servlet path)来构建基础 URL。这在大多数情况下都能很好地工作。
然而,有时这种方式可能不够用。例如,你可能处于请求上下文之外(比如一个用于预先生成链接的批处理任务),或者你可能需要插入一个路径前缀(例如某个已被从请求路径中移除的语言环境前缀,现在需要重新插入到生成的链接中)。
对于此类情况,您可以使用接受 fromXxx 参数的静态 UriComponentsBuilder 重载方法来指定一个基础 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 会忽略来自 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 被分配一个默认名称,该策略的默认实现使用类名和方法名中的大写字母(例如,getThing 类中的 ThingController 方法会变成 "TC#getThing")。如果出现名称冲突,你可以使用 @RequestMapping(name="..") 来显式指定一个名称,或者实现你自己的 HandlerMethodMappingNamingStrategy。