UriComponents
Spring MVC и Spring WebFlux
UriComponentsBuilder
помогает создавать URI-идентификаторы из URI-шаблонов с переменными, как показано в следующем примере:
UriComponents uriComponents = UriComponentsBuilder
.fromUriString("https://example.com/hotels/{hotel}")
.queryParam("q", "{q}")
.encode()
.build();
URI uri = uriComponents.expand("Westin", "123").toUri();
- Статический фабричный метод с URI-шаблоном.
- Добавляем или заменяем URI-компоненты.
- Запрос на кодирование URI-шаблона и URI-переменных.
- Собираем
UriComponents
. - Расширяем переменные и получаем
URI
.
val uriComponents = UriComponentsBuilder
.fromUriString("https://example.com/hotels/{hotel}")
.queryParam("q", "{q}")
.encode()
.build()
val uri = uriComponents.expand("Westin", "123").toUri()
- Статический фабричный метод с URI-шаблоном.
- Добавляем или заменяем URI-компоненты.
- Запрос на кодирование URI-шаблона и URI-переменных.
- Собираем
UriComponents
. - Расширяем переменные и получаем
URI
.
Код из предыдущего примера можно объединить в одну цепочку и сократить с помощью buildAndExpand
, как показано в следующем примере:
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-идентификатору (что подразумевает кодировку), как показано в следующем примере:
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-шаблона, как показано в следующем примере:
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
. В свою очередь, можно создать UriBuilder
с помощью UriBuilderFactory
. Вместе UriBuilderFactory
и UriBuilder
предоставляют подключаемый механизм для создания URI-идентификаторов из URI-шаблонов на основе общей конфигурации, такой как базовый URL-адрес, параметры кодировки и другие детали.
Можно сконфигурировать RestTemplate
и WebClient
с помощью UriBuilderFactory
, чтобы настроить подготовку URI-идентификаторов. DefaultUriBuilderFactory
– это реализация UriBuilderFactory
по умолчанию, которая использует UriComponentsBuilder
на внутреннем уровне и открывает общие параметры конфигурации.
В следующем примере показано, как сконфигурировать такой бин:
// импортируем 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);
// импортируем 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
:
// импортируем 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();
// импортируем 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
, но вместо статических фабричных методов, это реальный экземпляр, который хранит конфигурацию и параметры, как показано в следующем примере:
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
открывает опции кодирования на двух уровнях:
-
UriComponentsBuilder#encode(): Сначала предварительно кодирует URI-шаблон, а затем строго кодирует переменные URI-идентификаторов при расширении.
-
UriComponents#encode(): Кодирует URI-компоненты после расширения переменных URI-идентификаторов.
Обе опции заменяют символы, не относящиеся к стандарту ASCII, и недопустимые символы на экранированные октеты. Однако первый вариант также заменяет символы с зарезервированным значением, которые появляются в переменных URI-идентификаторов.
В большинстве случаев первый вариант, скорее всего, даст ожидаемый результат, поскольку он учитывает переменные URI-идентификаторов как непрозрачные данные, которые должны быть полностью закодированы, в то время как второй вариант полезен, если переменные URI-идентификаторов намеренно содержат зарезервированные символы. Второй вариант также работает, если не расширять переменные URI-идентификаторов вообще, поскольку в этом случае кодируется все, что случайным образом будет похоже на URI-переменную.
В следующем примере используется первый вариант:
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-идентификатору (что подразумевает кодировку), как показано в следующем примере:
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-шаблона, как показано в следующем примере:
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")
WebClient
и RestTemplate
расширяют и кодируют URI-шаблоны на внутреннем уровне с помощью стратегии UriBuilderFactory
. Оба варианта можно сконфигурировать с помощью кастомной стратегии, как показано в следующем примере:
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-шаблон и, вместо этого, применяет строгое кодирование к переменным 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-идентификаторов относительно текущего запроса, как показано в следующем примере:
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-идентификаторы относительно контекстного пути, как показано в следующем примере:
HttpServletRequest request = ...
// Повторное использование схемы, хоста, порта и контекстного пути...
URI uri = ServletUriComponentsBuilder.fromContextPath(request)
.path("/accounts")
.build()
.toUri();
val request: HttpServletRequest = ...
// Повторное использование схемы, хоста, порта и контекстного пути...
val uri = ServletUriComponentsBuilder.fromContextPath(request)
.path("/accounts")
.build()
.toUri()
Можно создавать URI-идентификаторы относительно сервлета (например, /main/*
), как показано в следующем примере:
HttpServletRequest request = ...
// Повторно использует схему, хост, порт, контекстный путь и префикс отображения сервлетов...
URI uri = ServletUriComponentsBuilder.fromServletMapping(request)
.path("/accounts")
.build()
.toUri();
val request: HttpServletRequest = ...
// Повторно использует схему, хост, порт, контекстный путь и префикс отображения сервлетов...
val uri = ServletUriComponentsBuilder.fromServletMapping(request)
.path("/accounts")
.build()
.toUri()
ServletUriComponentsBuilder
игнорирует информацию из заголовков Forwarded
и X-Forwarded-*
, которые указывают адрес со стороны клиента. Рассмотрите возможность использования ForwardedHeaderFilter
для извлечения и использования или отбрасывания таких заголовков.Ссылки на контроллеры
Spring MVC содержит механизм для подготовки ссылок на методы контроллера. Например, следующий контроллер MVC позволяет создавать ссылки:
@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 {
// ...
}
}
Можно подготовить ссылку, обратившись к методу по имени, как показано в следующем примере:
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
, унаследованная от отображения запроса на уровне типов. Если бы метод имел больше аргументов, мы могли бы поставить null для аргументов, не нужных для URL-адреса. В общем, только аргументы с аннотациями @PathVariable
и @RequestParam
имеют значение для построения URL-адреса.
Существуют дополнительные способы использования MvcUriComponentsBuilder
. Например, чтобы избежать обращения к методу контроллера по имени, как показано в следующем примере (пример предполагает статический импорт MvcUriComponentsBuilder.on
), можно использовать технику, схожую с тестированием с использованием фиктивных объектов через прокси:
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
для подготовки базового URL-адреса из схемы, хоста, порта, пути контекста и пути сервлета текущего запроса. Это отлично работает в большинстве случаев. Однако иногда этого может оказаться недостаточно. К примеру, вы можете находиться вне контекста запроса (например, при пакетном процессе, который подготавливает ссылки) или, возможно, вам нужно вставить префикс пути (например, префикс региональных настроек, который был удален из пути запроса и должен быть снова вставлен в ссылки).
Для таких случаев можно использовать статические перегруженные методы fromXxx
, которые принимают UriComponentsBuilder
для использования базового URL-адреса. Кроме того, можно создать экземпляр MvcUriComponentsBuilder
с базовым URL-адресом, а затем использовать методы withXxx
, основанные на экземпляре. Например, в следующем листинге используется withMethodCall
:
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()
MvcUriComponentsBuilder
игнорирует информацию из заголовков Forwarded
и X-Forwarded-*
, в которых указывается адрес со стороны клиента. Рассмотрите возможность использования ForwardedHeaderFilter для извлечения и использования или отбрасывания таких заголовков.Ссылки в представлениях
В таких представлениях, как Thymeleaf, FreeMarker или JSP, можно формировать ссылки на аннотированные контроллеры, ссылаясь на неявно или явно присвоенное имя для каждого отображения запроса.
Рассмотрим следующий пример:
@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>
В предыдущем примере мы полагаемся на функцию mvcUrl
, объявленную в библиотеке тегов Spring (то есть META-INF/spring.tld), но определить свою собственную функцию или подготовить аналогичную для других технологий шаблонизации тоже достаточно просто.
Вот как это работает. При запуске каждой аннотации @RequestMapping
присваивается имя по умолчанию через стартегию HandlerMethodMappingNamingStrategy
, реализация которой по умолчанию использует заглавные буквы класса и имя метода (например, метод getThing
в ThingController
становится "TC#getThing"). Если имена не совпадают, то можно использовать @RequestMapping(name="..")
для назначения явного имени или реализовать собственную стратегию именования HandlerMethodMappingNamingStrategy
.