|
对于最新的稳定版本,请使用 Spring Framework 7.0.6! |
URI链接
本节描述了Spring框架中处理URI的各种选项。
URI组件
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")
URI构建器
Spring MVC 和 Spring WebFlux
UriComponentsBuilder 实现 UriBuilder. 您可以创建一个
UriBuilder,进而使用 UriBuilderFactory. 一起,UriBuilderFactory 和 UriBuilder 提供了一个可插拔的机制来根据 URI 模板构建 URI,基于共享配置,例如基础 URL、编码偏好和其他细节。
您可以使用 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(): Pre-encodes the URI template first and then strictly encodes URI variables when expanded.
-
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()
The DefaultUriBuilderFactory implementation uses UriComponentsBuilder internally to
expand and encode URI templates. As a factory, it provides a single place to configure
the approach to encoding, based on one of the below encoding modes:
-
TEMPLATE_AND_VALUES: 使用UriComponentsBuilder#encode(),对应于前面列表中的第一个选项,用于预编码URI模板,并在扩展时严格编码URI变量。 -
VALUES_ONLY: 不对URI模板进行编码,而是通过UriUtils#encodeUriVariables在将URI变量展开到模板之前应用严格的编码。 -
URI_COMPONENT: 使用UriComponents#encode(),对应于前面列表中的第二个选项,用于在 URI 变量扩展后对 URI 组件值进行编码。 -
NONE: 未应用任何编码。
The RestTemplate is set to EncodingMode.URI_COMPONENT for historic
reasons and for backwards compatibility. The WebClient relies on the default value
in DefaultUriBuilderFactory, which was changed from EncodingMode.URI_COMPONENT in
5.0.x to EncodingMode.TEMPLATE_AND_VALUES in 5.1.
相对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()
Controller method signatures 在设计上有限制,当它们应该可用于使用 fromMethodCall 创建链接时。除了需要一个正确的参数签名外,还有一个技术限制在于返回类型(即,为链接构建器调用生成运行时代理),因此返回类型不能是 final。特别是,常见的用于视图名称的 String 返回类型在这里不起作用。您应该使用 ModelAndView 或者甚至是纯 Object(带有 String 返回值)。 |
早些时候的示例使用了MvcUriComponentsBuilder中的静态方法。内部,它们依赖于ServletUriComponentsBuilder来从当前请求的协议、主机、端口、上下文路径和Servlet路径准备一个基础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。