URI Links

This section describes various options available in the TODAY Framework to work with URI’s.

UriComponents

UriComponentsBuilder 有助于从带有变量的 URI 模板构建 URI,如下例所示:

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

前面的示例可以合并为一个链,并使用 buildAndExpand 进行简化,如下例所示:

URI uri = UriComponentsBuilder
    .fromUriString("https://example.com/hotels/{hotel}")
    .queryParam("q", "{q}")
    .encode()
    .buildAndExpand("Westin", "123")
    .toUri();

您可以进一步简化,直接生成 URI(这意味着进行编码),如下例所示:

URI uri = UriComponentsBuilder
    .fromUriString("https://example.com/hotels/{hotel}")
    .queryParam("q", "{q}")
    .build("Westin", "123");

您还可以使用完整的 URI 模板进一步简化,如下例所示:

URI uri = UriComponentsBuilder
  .fromUriString("https://example.com/hotels/{hotel}?q={q}")
  .build("Westin", "123");

UriBuilder

UriComponentsBuilder 实现了 UriBuilder。您可以创建一个 UriBuilder, 进而使用 UriBuilderFactoryUriBuilderFactoryUriBuilder 一起提供了一个可插拔的机制, 用于根据共享配置(如基础 URL、编码偏好和其他详细信息)从 URI 模板构建 URI。

您可以配置 RestTemplateWebClient 使用 UriBuilderFactory 来自定义 URI 的准备。 DefaultUriBuilderFactoryUriBuilderFactory 的默认实现,它在内部使用 UriComponentsBuilder 并公开共享配置选项。

以下示例展示了如何配置 RestTemplate

// import cn.taketoday.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);

下面的例子配置了一个 WebClient:

// import cn.taketoday.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();

此外,您也可以直接使用 DefaultUriBuilderFactory。它类似于使用 UriComponentsBuilder, 但不是静态工厂方法,而是一个实际的实例,它持有配置和偏好,如下例所示:

String baseUrl = "https://example.com";
DefaultUriBuilderFactory uriBuilderFactory = new DefaultUriBuilderFactory(baseUrl);

URI uri = uriBuilderFactory.uriString("/hotels/{hotel}")
    .queryParam("q", "{q}")
    .build("Westin", "123");

URI Encoding

UriComponentsBuilder 在两个级别上公开编码选项:

两个选项都使用转义八位字节替换非 ASCII 和非法字符。然而,第一个选项还替换了 URI 变量中出现的具有保留含义的字符。

考虑 ";",它在路径中是合法的,但具有保留含义。第一个选项将 URI 变量中的 ";" 替换为 "%3B",但不替换 URI 模板中的 ";"。相比之下,第二个选项从不替换 ";",因为它在路径中是合法字符。

对于大多数情况,第一个选项可能会给出预期的结果,因为它将 URI 变量视为需要完全编码的不透明数据,而第二个选项在 URI 变量确实包含保留字符时很有用。如果根本不展开 URI 变量,第二个选项也很有用,因为那也会编码任何看起来像 URI 变量的内容。

以下示例使用了第一个选项:

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"

您可以进一步简化前面的示例,直接生成 URI(这意味着进行编码),如下例所示:

URI uri = UriComponentsBuilder.fromPath("/hotel list/{city}")
  .queryParam("q", "{q}")
  .build("New York", "foo+bar");

您还可以使用完整的 URI 模板进一步简化,如下例所示:

URI uri = UriComponentsBuilder.fromUriString("/hotel list/{city}?q={q}")
  .build("New York", "foo+bar");

WebClientRestTemplate 通过 UriBuilderFactory 策略在内部扩展和编码 URI 模板。 两者都可以配置自定义策略,如下例所示:

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

DefaultUriBuilderFactory 的实现内部使用 UriComponentsBuilder 来展开和编码 URI 模板。 作为一个工厂,它提供了一个地方来配置编码方法,基于以下编码模式之一:

  • TEMPLATE_AND_VALUES:使用 UriComponentsBuilder#encode(),对应于前面列表中的第一个选项,预先编码 URI 模板,并在展开时严格编码 URI 变量。

  • VALUES_ONLY:不编码 URI 模板,而是通过 UriUtils#encodeUriVariables 在将它们展开到模板之前,对 URI 变量应用严格编码。

  • URI_COMPONENT:使用 UriComponents#encode(),对应于前面列表中的第二个选项,在 URI 变量展开后对 URI 组件值进行编码。

  • NONE:不应用编码。

Relative Servlet Requests

You can use ServletUriComponentsBuilder to create URIs relative to the current request, as the following example shows:

HttpServletRequest request = ...

// Re-uses scheme, host, port, path, and query string...

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

You can create URIs relative to the context path, as the following example shows:

HttpServletRequest request = ...

// Re-uses scheme, host, port, and context path...

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

You can create URIs relative to a Servlet (for example, /main/*), as the following example shows:

  • Java

HttpServletRequest request = ...

// Re-uses scheme, host, port, context path, and Servlet mapping prefix...

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

Web MVC provides a mechanism to prepare links to controller methods. For example, the following MVC controller allows for link creation:

  • Java

@Controller
@RequestMapping("/hotels/{hotel}")
public class BookingController {

  @GetMapping("/bookings/{booking}")
  public ModelAndView getBooking(@PathVariable Long booking) {
    // ...
  }
}

You can prepare a link by referring to the method by name, as the following example shows:

  • Java

UriComponents uriComponents = MvcUriComponentsBuilder
  .fromMethodName(BookingController.class, "getBooking", 21).buildAndExpand(42);

URI uri = uriComponents.encode().toUri();

In the preceding example, we provide actual method argument values (in this case, the long value: 21) to be used as a path variable and inserted into the URL. Furthermore, we provide the value, 42, to fill in any remaining URI variables, such as the hotel variable inherited from the type-level request mapping. If the method had more arguments, we could supply null for arguments not needed for the URL. In general, only @PathVariable and @RequestParam arguments are relevant for constructing the URL.

There are additional ways to use MvcUriComponentsBuilder. For example, you can use a technique akin to mock testing through proxies to avoid referring to the controller method by name, as the following example shows (the example assumes static import of MvcUriComponentsBuilder.on):

  • Java

UriComponents uriComponents = MvcUriComponentsBuilder
  .fromMethodCall(on(BookingController.class).getBooking(21)).buildAndExpand(42);

URI uri = uriComponents.encode().toUri();
Controller method signatures are limited in their design when they are supposed to be usable for link creation with fromMethodCall. Aside from needing a proper parameter signature, there is a technical limitation on the return type (namely, generating a runtime proxy for link builder invocations), so the return type must not be final. In particular, the common String return type for view names does not work here. You should use ModelAndView or even plain Object (with a String return value) instead.

The earlier examples use static methods in MvcUriComponentsBuilder. Internally, they rely on ServletUriComponentsBuilder to prepare a base URL from the scheme, host, port, context path, and mockApi path of the current request. This works well in most cases. However, sometimes, it can be insufficient. For example, you may be outside the context of a request (such as a batch process that prepares links) or perhaps you need to insert a path prefix (such as a locale prefix that was removed from the request path and needs to be re-inserted into links).

For such cases, you can use the static fromXxx overloaded methods that accept a UriComponentsBuilder to use a base URL. Alternatively, you can create an instance of MvcUriComponentsBuilder with a base URL and then use the instance-based withXxx methods. For example, the following listing uses withMethodCall:

  • Java

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

In views such as Thymeleaf, FreeMarker, or JSP, you can build links to annotated controllers by referring to the implicitly or explicitly assigned name for each request mapping.

Consider the following example:

  • Java

@RequestMapping("/people/{id}/addresses")
public class PersonAddressController {

  @RequestMapping("/{country}")
  public HttpEntity<PersonAddress> getAddress(@PathVariable String country) { ... }
}

Given the preceding controller, you can prepare a link from a JSP, as follows:

<%@ taglib uri="http://www.springframework.org/tags" prefix="s" %>
...
<a href="${s:mvcUrl('PAC#getAddress').arg(0,'US').buildAndExpand('123')}">Get Address</a>

The preceding example relies on the mvcUrl function declared in the Infra tag library (that is, META-INF/spring.tld), but it is easy to define your own function or prepare a similar one for other templating technologies.

Here is how this works. On startup, every @RequestMapping is assigned a default name through HandlerMethodMappingNamingStrategy, whose default implementation uses the capital letters of the class and the method name (for example, the getThing method in ThingController becomes "TC#getThing"). If there is a name clash, you can use @RequestMapping(name="..") to assign an explicit name or implement your own HandlerMethodMappingNamingStrategy.