UriComponents

Spring MVC и Spring WebFlux

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();
  1. Статический фабричный метод с URI-шаблоном.
  2. Добавляем или заменяем URI-компоненты.
  3. Запрос на кодирование URI-шаблона и URI-переменных.
  4. Собираем UriComponents.
  5. Расширяем переменные и получаем URI.
Kotlin
val uriComponents = UriComponentsBuilder
        .fromUriString("https://example.com/hotels/{hotel}")
        .queryParam("q", "{q}")
        .encode()
        .build()
val uri = uriComponents.expand("Westin", "123").toUri()
  1. Статический фабричный метод с URI-шаблоном.
  2. Добавляем или заменяем URI-компоненты.
  3. Запрос на кодирование URI-шаблона и URI-переменных.
  4. Собираем UriComponents.
  5. Расширяем переменные и получаем URI.

Код из предыдущего примера можно объединить в одну цепочку и сократить с помощью buildAndExpand, как показано в следующем примере:

Java
URI uri = UriComponentsBuilder
        .fromUriString("https://example.com/hotels/{hotel}")
        .queryParam("q", "{q}")
        .encode()
        .buildAndExpand("Westin", "123")
        .toUri();
Kotlin
val 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");
Kotlin
val 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");
Kotlin
val uri = UriComponentsBuilder
        .fromUriString("https://example.com/hotels/{hotel}?q={q}")
        .build("Westin", "123")

UriBuilder

Spring MVC и Spring WebFlux

UriComponentsBuilder реализует UriBuilder. В свою очередь, можно создать UriBuilder с помощью UriBuilderFactory. Вместе UriBuilderFactory и UriBuilder предоставляют подключаемый механизм для создания URI-идентификаторов из URI-шаблонов на основе общей конфигурации, такой как базовый URL-адрес, параметры кодировки и другие детали.

Можно сконфигурировать RestTemplate и WebClient с помощью UriBuilderFactory, чтобы настроить подготовку URI-идентификаторов. DefaultUriBuilderFactory – это реализация UriBuilderFactory по умолчанию, которая использует UriComponentsBuilder на внутреннем уровне и открывает общие параметры конфигурации.

В следующем примере показано, как сконфигурировать такой бин:

Java
// импортируем 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);
Kotlin
// импортируем 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
// импортируем 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();
Kotlin
// импортируем 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
String baseUrl = "https://example.com";
DefaultUriBuilderFactory uriBuilderFactory = new DefaultUriBuilderFactory(baseUrl);
URI uri = uriBuilderFactory.uriString("/hotels/{hotel}")
        .queryParam("q", "{q}")
        .build("Westin", "123");
Kotlin
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 открывает опции кодирования на двух уровнях:

  • UriComponentsBuilder#encode(): Сначала предварительно кодирует URI-шаблон, а затем строго кодирует переменные URI-идентификаторов при расширении.

  • UriComponents#encode(): Кодирует URI-компоненты после расширения переменных URI-идентификаторов.

Обе опции заменяют символы, не относящиеся к стандарту ASCII, и недопустимые символы на экранированные октеты. Однако первый вариант также заменяет символы с зарезервированным значением, которые появляются в переменных URI-идентификаторов.

Рассмотрим ";", который является допустимым в пути, но имеет зарезервированное значение. Первый вариант заменяет ";" на "%3B" в переменных URI-идентификаторов, но не в URI-шаблоне. В отличие от этого, второй вариант никогда не заменяет ";", поскольку он является допустимым символом в пути.

В большинстве случаев первый вариант, скорее всего, даст ожидаемый результат, поскольку он учитывает переменные URI-идентификаторов как непрозрачные данные, которые должны быть полностью закодированы, в то время как второй вариант полезен, если переменные URI-идентификаторов намеренно содержат зарезервированные символы. Второй вариант также работает, если не расширять переменные URI-идентификаторов вообще, поскольку в этом случае кодируется все, что случайным образом будет похоже на URI-переменную.

В следующем примере используется первый вариант:

Java
URI uri = UriComponentsBuilder.fromPath("/hotel list/{city}")
        .queryParam("q", "{q}")
        .encode()
        .buildAndExpand("New York", "foo+bar")
        .toUri();
// Результат "/hotel%20list/New%20York?q=foo%2Bbar"
Kotlin
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
URI uri = UriComponentsBuilder.fromPath("/hotel list/{city}")
        .queryParam("q", "{q}")
        .build("New York", "foo+bar");
Kotlin
val uri = UriComponentsBuilder.fromPath("/hotel list/{city}")
        .queryParam("q", "{q}")
        .build("New York", "foo+bar")

Можно сократить его даже еще больше с помощью полного URI-шаблона, как показано в следующем примере:

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

WebClient и RestTemplate расширяют и кодируют URI-шаблоны на внутреннем уровне с помощью стратегии UriBuilderFactory. Оба варианта можно сконфигурировать с помощью кастомной стратегии, как показано в следующем примере:

Java
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();
Kotlin
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-шаблон и, вместо этого, применяет строгое кодирование к переменным URI-идентификаторов через UriUtils#encodeUriVariables перед тем, как расширить их в шаблон.

  • URI_COMPONENT: Использует UriComponents#encode(), соответствующий второму варианту в предыдущем списке, для кодирования значения компонента URI-идентификатора после расширения URI-переменных.

  • NONE: Кодирование не применяется.

RestTemplate установлен в EncodingMode.URI_COMPONENT по историческим причинам и для обратной совместимости. WebClient обращается к значению по умолчанию в DefaultUriBuilderFactory, которое было изменено с EncodingMode.URI_COMPONENT в 5.0.x на EncodingMode.TEMPLATE_AND_VALUES в 5.1.

Относительные запросы сервлетов

Вы можете использовать ServletUriComponentsBuilder для создания URI-идентификаторов относительно текущего запроса, как показано в следующем примере:

Java
HttpServletRequest request = ...
// Повторно использует схему, хост, порт, путь и строку запроса...
URI uri = ServletUriComponentsBuilder.fromRequest(request)
        .replaceQueryParam("accountId", "{id}")
        .build("123");
Kotlin
val request: HttpServletRequest = ...
// Повторно использует схему, хост, порт, путь и строку запроса...
val uri = ServletUriComponentsBuilder.fromRequest(request)
        .replaceQueryParam("accountId", "{id}")
        .build("123")

Можно создавать URI-идентификаторы относительно контекстного пути, как показано в следующем примере:

Java
HttpServletRequest request = ...
// Повторное использование схемы, хоста, порта и контекстного пути...
URI uri = ServletUriComponentsBuilder.fromContextPath(request)
        .path("/accounts")
        .build()
        .toUri();
Kotlin
val request: HttpServletRequest = ...
// Повторное использование схемы, хоста, порта и контекстного пути...
val uri = ServletUriComponentsBuilder.fromContextPath(request)
        .path("/accounts")
        .build()
        .toUri()

Можно создавать URI-идентификаторы относительно сервлета (например, /main/*), как показано в следующем примере:

Java
HttpServletRequest request = ...
// Повторно использует схему, хост, порт, контекстный путь и префикс отображения сервлетов...
URI uri = ServletUriComponentsBuilder.fromServletMapping(request)
        .path("/accounts")
        .build()
        .toUri();
Kotlin
val request: HttpServletRequest = ...
// Повторно использует схему, хост, порт, контекстный путь и префикс отображения сервлетов...
val uri = ServletUriComponentsBuilder.fromServletMapping(request)
        .path("/accounts")
        .build()
        .toUri()
Начиная с версии 5.1, ServletUriComponentsBuilder игнорирует информацию из заголовков Forwarded и X-Forwarded-*, которые указывают адрес со стороны клиента. Рассмотрите возможность использования ForwardedHeaderFilter для извлечения и использования или отбрасывания таких заголовков.

Ссылки на контроллеры

Spring MVC содержит механизм для подготовки ссылок на методы контроллера. Например, следующий контроллер MVC позволяет создавать ссылки:

Java
@Controller
@RequestMapping("/hotels/{hotel}")
public class BookingController {
    @GetMapping("/bookings/{booking}")
    public ModelAndView getBooking(@PathVariable Long booking) {
        // ...
    }
}
Kotlin
@Controller
@RequestMapping("/hotels/{hotel}")
class BookingController {
    @GetMapping("/bookings/{booking}")
    fun getBooking(@PathVariable booking: Long): ModelAndView {
        // ...
    }
}

Можно подготовить ссылку, обратившись к методу по имени, как показано в следующем примере:

Java
UriComponents uriComponents = MvcUriComponentsBuilder
    .fromMethodName(BookingController.class, "getBooking", 21).buildAndExpand(42);
URI uri = uriComponents.encode().toUri();
Kotlin
val uriComponents = MvcUriComponentsBuilder
    .fromMethodName(BookingController::class.java, "getBooking", 21).buildAndExpand(42)
val uri = uriComponents.encode().toUri()

В предыдущем примере мы указываем фактические значения аргументов метода (в данном случае длинное значение: 21) для использования в качестве переменной пути и вставки в URL-адрес. Кроме того, мы указываем значение 42 для заполнения любых оставшихся переменных URI-идентификаторов, таких как переменная hotel, унаследованная от отображения запроса на уровне типов. Если бы метод имел больше аргументов, мы могли бы поставить null для аргументов, не нужных для URL-адреса. В общем, только аргументы с аннотациями @PathVariable и @RequestParam имеют значение для построения URL-адреса.

Существуют дополнительные способы использования MvcUriComponentsBuilder. Например, чтобы избежать обращения к методу контроллера по имени, как показано в следующем примере (пример предполагает статический импорт MvcUriComponentsBuilder.on), можно использовать технику, схожую с тестированием с использованием фиктивных объектов через прокси:

Java
UriComponents uriComponents = MvcUriComponentsBuilder
    .fromMethodCall(on(BookingController.class).getBooking(21)).buildAndExpand(42);
URI uri = uriComponents.encode().toUri();
Kotlin
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 для подготовки базового URL-адреса из схемы, хоста, порта, пути контекста и пути сервлета текущего запроса. Это отлично работает в большинстве случаев. Однако иногда этого может оказаться недостаточно. К примеру, вы можете находиться вне контекста запроса (например, при пакетном процессе, который подготавливает ссылки) или, возможно, вам нужно вставить префикс пути (например, префикс региональных настроек, который был удален из пути запроса и должен быть снова вставлен в ссылки).

Для таких случаев можно использовать статические перегруженные методы fromXxx, которые принимают UriComponentsBuilder для использования базового URL-адреса. Кроме того, можно создать экземпляр MvcUriComponentsBuilder с базовым URL-адресом, а затем использовать методы 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();
Kotlin
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
@RequestMapping("/people/{id}/addresses")
public class PersonAddressController {
    @RequestMapping("/{country}")
    public HttpEntity<PersonAddress> getAddress(@PathVariable String country) { ... }
}
Kotlin
@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>

В предыдущем примере мы полагаемся на функцию mvcUrl, объявленную в библиотеке тегов Spring (то есть META-INF/spring.tld), но определить свою собственную функцию или подготовить аналогичную для других технологий шаблонизации тоже достаточно просто.

Вот как это работает. При запуске каждой аннотации @RequestMapping присваивается имя по умолчанию через стартегию HandlerMethodMappingNamingStrategy, реализация которой по умолчанию использует заглавные буквы класса и имя метода (например, метод getThing в ThingController становится "TC#getThing"). Если имена не совпадают, то можно использовать @RequestMapping(name="..") для назначения явного имени или реализовать собственную стратегию именования HandlerMethodMappingNamingStrategy.