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 來建立 UriBuilderUriBuilderFactoryUriBuilder 共同提供了一種可插拔機制,可以基於共享配置(如基本 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 支援兩種 URI 解析器型別

  1. RFC 解析器 — 此解析器型別要求 URI 字串符合 RFC 3986 語法,並將語法偏差視為非法。

  2. WhatWG 解析器 — 此解析器基於 WhatWG URL 活標準中的 URL 解析演算法。它對各種意外輸入提供寬鬆處理。瀏覽器實現此演算法是為了寬鬆處理使用者輸入的 URL。有關更多詳細資訊,請參見 URL 活標準和 URL 解析測試用例

預設情況下,RestClientWebClientRestTemplate 使用 RFC 解析器型別,並期望應用程式提供符合 RFC 語法的 URL 模板。要更改這一點,您可以在任何客戶端上定製 UriBuilderFactory

應用程式和框架可能會進一步依賴 UriComponentsBuilder 來解析使用者提供的 URL,以檢查和可能驗證 URI 元件,如 scheme、host、port、path 和 query。這些元件可以決定使用 WhatWG 解析器型別,以便更寬鬆地處理 URL,並在重定向到輸入 URL 或響應包含該 URL 時,與瀏覽器解析 URI 的方式保持一致。

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();

// 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")

WebClientRestTemplate 透過 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_COMPONENTWebClient 依賴 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 (例如,/main/*) 的 URI,如下例所示

  • 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 忽略來自 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()

在前面的示例中,我們提供了實際的方法引數值(在本例中是 long 值:21)作為路徑變數並插入到 URL 中。此外,我們提供了值 42 來填充任何剩餘的 URI 變數,例如從型別級別請求對映繼承的 hotel 變數。如果方法有更多引數,對於 URL 不需要使用的引數,我們可以提供 null。一般來說,只有 @PathVariable@RequestParam 引數與構造 URL 相關。

還有其他使用 MvcUriComponentsBuilder 的方法。例如,您可以使用類似於透過代理進行 mock 測試的技術來避免透過方法名稱引用控制器方法,如下例所示(該示例假設靜態匯入了 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 path 準備一個基本 URL。這在大多數情況下都工作得很好。但是,有時它可能不夠。例如,您可能在請求上下文之外(例如準備連結的批處理程序),或者您可能需要插入路徑字首(例如從請求路徑中刪除的 locale 字首需要重新插入到連結中)。

對於這種情況,您可以使用接受 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')}">Get Address</a>

前面的示例依賴於 Spring 標籤庫(即 META-INF/spring.tld)中宣告的 mvcUrl 函式,但是很容易定義自己的函式或為其他模板技術準備類似的函式。

工作原理如下。在啟動時,每個 @RequestMapping 都會透過 HandlerMethodMappingNamingStrategy 分配一個預設名稱,其預設實現使用類名和方法名的首字母大寫(例如,ThingController 中的 getThing 方法變為 "TC#getThing")。如果存在名稱衝突,您可以使用 @RequestMapping(name="..") 指定顯式名稱,或者實現自己的 HandlerMethodMappingNamingStrategy