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。一起,UriBuilderFactoryUriBuilder提供了一个可插拔的机制,根据共享配置(如基本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 在两个级别上公开编码选项:

这两个选项都会用转义的八进制替换非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();

// 结果是 "/hotel%20list/New%20York?q=foo%2Bbar"
val uri = UriComponentsBuilder.fromPath("/hotel list/{city}")
		.queryParam("q", "{q}")
		.encode()
		.buildAndExpand("New York", "foo+bar")
		.toUri()

// 结果是 "/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);

// 自定义 RestTemplate..
RestTemplate restTemplate = new RestTemplate();
restTemplate.setUriTemplateHandler(factory);

// 自定义 WebClient..
WebClient client = WebClient.builder().uriBuilderFactory(factory).build();
val baseUrl = "https://example.com"
val factory = DefaultUriBuilderFactory(baseUrl).apply {
	encodingMode = EncodingMode.TEMPLATE_AND_VALUES
}

// 自定义 RestTemplate..
val restTemplate = RestTemplate().apply {
	uriTemplateHandler = factory
}

// 自定义 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 = ...

// 重用方案、主机、端口、路径和查询字符串...

URI uri = ServletUriComponentsBuilder.fromRequest(request)
		.replaceQueryParam("accountId", "{id}")
		.build("123");
val request: HttpServletRequest = ...

// 重用方案、主机、端口、路径和查询字符串...

val uri = ServletUriComponentsBuilder.fromRequest(request)
		.replaceQueryParam("accountId", "{id}")
		.build("123")

您可以创建相对于上下文路径的URI,如下例所示:

  • Java

  • Kotlin

HttpServletRequest request = ...

// 重用方案、主机、端口和上下文路径...

URI uri = ServletUriComponentsBuilder.fromContextPath(request)
		.path("/accounts")
		.build()
		.toUri();
val request: HttpServletRequest = ...

// 重用方案、主机、端口和上下文路径...

val uri = ServletUriComponentsBuilder.fromContextPath(request)
		.path("/accounts")
		.build()
		.toUri()

您可以创建相对于Servlet(例如,/main/*)的URI,如下例所示:

  • Java

  • Kotlin

HttpServletRequest request = ...

// 重用方案、主机、端口、上下文路径和Servlet映射前缀...

URI uri = ServletUriComponentsBuilder.fromServletMapping(request)
		.path("/accounts")
		.build()
		.toUri();
val request: HttpServletRequest = ...

// 重用方案、主机、端口、上下文路径和Servlet映射前缀...

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()
当控制器方法签名被设计为可用于链接创建时,它们在设计上是有限制的,需要一个正确的参数签名,还有一个关于返回类型的技术限制(即生成运行时代理以进行链接构建调用),因此返回类型不能是final。特别是,用于视图名称的常见String返回类型在这里不起作用。您应该使用ModelAndView甚至纯粹的Object(带有String返回值)。

前面的示例使用了MvcUriComponentsBuilder中的静态方法。在内部,它们依赖于ServletUriComponentsBuilder,从当前请求的scheme、host、port、context path和servlet path准备基本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忽略了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')}">获取地址</a>

上面的示例依赖于Spring标签库中声明的 函数,但很容易为其他模板技术定义自己的函数或准备类似的函数。

这是它的工作原理。在启动时,每个@RequestMapping通过HandlerMethodMappingNamingStrategy分配一个默认名称,其默认实现使用类和方法名称的大写字母(例如,在ThingController中的getThing方法变为"TC#getThing")。如果存在名称冲突,您可以使用@RequestMapping(name="..")分配一个显式名称或实现自己的HandlerMethodMappingNamingStrategy。