# 1.4 URI链接

本部分介绍了Spring框架中可用于URI的各种选项。

## 1.4.1 UriComponents

UriComponentsBuilder有助于从具有变量的URI模板中构建URI，如以下示例所示：

```java
UriComponents uriComponents = UriComponentsBuilder
        .fromUriString("https://example.com/hotels/{hotel}")  
        .queryParam("q", "{q}")  
        .encode() 
        .build(); 

URI uri = uriComponents.expand("Westin", "123").toUri();
```

可以将前面的示例合并为一个链，并通过buildAndExpand进行缩短，如以下示例所示：

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

您可以通过直接转到URI（这意味着编码）来进一步缩短它，如以下示例所示：

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

您可以使用完整的URI模板进一步缩短它，如以下示例所示：

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

## 1.4.2 UriBuilder

UriComponentsBuilder实现UriBuilder。 您可以依次使用UriBuilderFactory创建UriBuilder。 UriBuilderFactory和UriBuilder一起提供了一种可插入的机制，可以基于共享配置（例如基本URL，编码首选项和其他详细信息）从URI模板构建URI。

您可以使用UriBuilderFactory配置RestTemplate和WebClient以自定义URI的准备。 DefaultUriBuilderFactory是UriBuilderFactory的默认实现，该实现在内部使用UriComponentsBuilder并公开共享的配置选项。

以下示例显示如何配置RestTemplate：

```java
// import org.springframework.web.util.DefaultUriBuilderFactory.EncodingMode;

String baseUrl = "https://example.org";
DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory(baseUrl);
factory.setEncodingMode(EncodingMode.TEMPLATE_AND_VARIABLES);

RestTemplate restTemplate = new RestTemplate();
restTemplate.setUriTemplateHandler(factory);
```

下面的例子配置了WebClient：

```java
// import org.springframework.web.util.DefaultUriBuilderFactory.EncodingMode;

String baseUrl = "https://example.org";
DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory(baseUrl);
factory.setEncodingMode(EncodingMode.TEMPLATE_AND_VARIABLES);

WebClient client = WebClient.builder().uriBuilderFactory(factory).build();
```

此外，您也可以直接使用DefaultUriBuilderFactory。 它类似于使用UriComponentsBuilder，但不是静态工厂方法，它是一个包含配置和首选项的实际实例，如以下示例所示：

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

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

## 1.4.3 URI Encoding

UriComponentsBuilder在两个级别公开了编码选项：

* UriComponentsBuilder＃encode（）：首先对URI模板进行预编码，然后在扩展时严格对URI变量进行编码。
* UriComponents＃encode（）：扩展URI变量后，对URI组件进行编码。

这两个选项都用转义的八位字节替换非ASCII和非法字符。 但是，第一个选项还会替换出现在URI变量中的具有保留含义的字符。

> 考虑“;”，这在路径上是合法的，但具有保留的含义。 第一个选项代替“;” URI变量中带有“％3B”，但URI模板中没有。 相比之下，第二个选项永远不会替换“;”，因为它是路径中的合法字符。

在大多数情况下，第一个选项可能会产生预期的结果，因为它将URI变量视为要完全编码的不透明数据，而选项2仅在URI变量有意包含保留字符的情况下才有用。

以下示例使用第一个选项：

```java
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（这意味着编码）来缩短前面的示例，如以下示例所示：

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

您可以使用完整的URI模板进一步缩短它，如以下示例所示：

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

WebClient和RestTemplate通过UriBuilderFactory策略在内部扩展和编码URI模板。 两者都可以使用自定义策略进行配置。 如下例所示：

```java
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＃encodeUriUriVariables对URI变量进行严格编码。
* URI\_COMPONENTS：在扩展URI变量后，使用UriComponents＃encode（）（对应于先前列表中的第二个选项）对URI组件值进行编码。
* NONE：未应用编码。

由于历史原因和向后兼容性，将RestTemplate设置为EncodingMode.URI\_COMPONENTS。 WebClient依赖于DefaultUriBuilderFactory中的默认值，该默认值已从5.0.x中的EncodingMode.URI\_COMPONENTS更改为5.1中的EncodingMode.TEMPLATE\_AND\_VALUES。

## 1.4.4 Relative Servlet Requests

您可以使用ServletUriComponentsBuilder创建相对于当前请求的URI，如以下示例所示：

```java
HttpServletRequest request = ...

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

ServletUriComponentsBuilder ucb = ServletUriComponentsBuilder.fromRequest(request)
        .replaceQueryParam("accountId", "{id}").build()
        .expand("123")
        .encode();
```

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

```java
// Re-uses host, port and context path...

ServletUriComponentsBuilder ucb = ServletUriComponentsBuilder.fromContextPath(request)
        .path("/accounts").build()
```

您可以创建相对于Servlet的URI（例如/ main / \*），如以下示例所示：

```java
// Re-uses host, port, context path, and Servlet prefix...

ServletUriComponentsBuilder ucb = ServletUriComponentsBuilder.fromServletMapping(request)
        .path("/accounts").build()
```

> 从5.1开始，ServletUriComponentsBuilder会忽略来自Forwarded和X-Forwarded- \*标头的信息，这些标头指定了客户端起源的地址。 考虑使用ForwardedHeaderFilter提取和使用或丢弃此类标头。

## 1.4.5 Links to Controllers

Spring MVC提供了一种准备到控制器方法的链接的机制。 例如，以下MVC控制器允许创建链接：

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

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

您可以通过按名称引用方法来准备链接，如以下示例所示：

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

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

在前面的示例中，我们提供了实际的方法参数值（在本例中为long值：21），用作路径变量并插入到URL中。 此外，我们提供值42来填充所有剩余的URI变量，例如从类型级别请求映射继承的hotel变量。 如果该方法具有更多参数，则可以为URL不需要的参数提供null。 通常，只有@PathVariable和@RequestParam参数与构造URL有关。

还有其他使用MvcUriComponentsBuilder的方法。 例如，您可以使用类似于代理的模拟测试技术来避免按名称引用控制器方法，如以下示例所示（该示例假定静态导入MvcUriComponentsBuilder.on）：

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

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

> 当控制器方法签名可用于fromMethodCall的链接创建时，其设计受到限制。 除了需要适当的参数签名外，返回类型还存在技术限制（即，为链接生成器调用生成运行时代理），因此返回类型不得为最终值。 特别是，视图名称的通用String返回类型在这里不起作用。 您应该改用ModelAndView甚至普通对象（具有String返回值）。

较早的示例在MvcUriComponentsBuilder中使用静态方法。 在内部，它们依靠ServletUriComponentsBuilder从当前请求的方案，主机，端口，上下文路径和Servlet路径准备基本URL。 在大多数情况下，此方法效果很好。 但是，有时可能不足。 例如，您可能不在请求的上下文之内（例如，准备链接的批处理过程），或者您可能需要插入路径前缀（例如，从请求路径中删除且需要重新设置的语言环境前缀）。 插入链接）。

在这种情况下，可以使用静态的fromXxx重载方法，这些方法接受UriComponentsBuilder以使用基本URL。 或者，您可以使用基本URL创建MvcUriComponentsBuilder的实例，然后使用基于实例的withXxx方法。 例如，以下清单使用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();
```

从5.1开始，MvcUriComponentsBuilder将忽略来自Forwarded和X-Forwarded- \*标头的信息，这些标头指定了客户端起源的地址。 考虑使用ForwardedHeaderFilter提取和使用或丢弃此类标头。

## 1.4.6 Links in Views

在Thymeleaf，FreeMarker或JSP之类的视图中，您可以通过引用每个请求映射的隐式或显式分配的名称来构建到带注解的控制器的链接。

考虑以下示例：

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

    @RequestMapping("/{country}")
    public HttpEntity getAddress(@PathVariable String country) { ... }
}
```

给定前面的控制器，您可以按照以下步骤准备来自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分配一个默认名称，该默认名称的实现使用类的大写字母和方法名称（例如，ThingController中的getThing方法变为“ TC＃getThing”）。 如果名称冲突，则可以使用@RequestMapping（name =“ ..”）来分配一个明确的名称，或实现自己的HandlerMethodMappingNamingStrategy。
