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
带有URI模板的静态工厂方法。
添加或替换URI组件。
请求对URI模板和URI变量进行编码。
构建一个
UriComponents对象。扩展变量并获取
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
带有URI模板的静态工厂方法。
添加或替换URI组件。
请求对URI模板和URI变量进行编码。
- \ [#4] 构建一个
UriComponents对象。 扩展变量并获取
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。
你可以使用UriBuilderFactory来配置RestTemplate和WebClient,以便自定义URL的生成方式。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类似,但不同于后者的是,DefaultUriBuilderFactory是一个实际的实例,该实例负责保存配置和偏好设置,如下例所示:
- 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生活标准中的URL解析算法。它对各种意外的输入情况都能采取较为宽容的处理方式。浏览器实现这种解析器是为了更宽容地处理用户输入的URL。更多详情,请参阅URL生活标准和URL解析测试用例。
默认情况下,RestClient、WebClient 和 RestTemplate 使用 RFC 解析器类型,并期望应用程序提供符合 RFC 语法的 URL 模板。要更改这一点,你可以自定义这些客户端中的任意一个的 UriBuilderFactory。
应用程序和框架可能进一步依赖UriComponentsBuilder来满足自身的需求,以便解析用户提供的URL,从而检查并可能验证URI的各个组成部分,如协议(scheme)、主机(host)、端口(port)、路径(path)和查询参数(query)。这些组件可以选择使用WhatWG提供的解析器类型,以便更宽松地处理URL,并与浏览器解析URL的方式保持一致。这在发生重定向到输入URL时,或者当URL作为对浏览器的响应的一部分时尤为重要。
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_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")
你可以创建相对于上下文路径(context path)的 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的参数设置为空值。一般来说,只有@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标签库(即META-INF/spring.tld)中声明的mvcUrl函数,但是很容易定义你自己的函数,或者为其他模板技术准备类似的函数。
其工作原理如下:在应用程序启动时,每个@RequestMapping都会通过HandlerMethodMappingNamingStrategy被赋予一个默认名称,该策略的默认实现会使用类的首字母和方法名来生成名称(例如,ThingController中的getThing方法会被命名为“TC#getThing”)。如果存在名称冲突,你可以使用@RequestMapping(name="..")来指定一个明确的名称,或者自己实现一个HandlerMethodMappingNamingStrategy。