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()
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, реалізація якої за замовчуванням використовує великі літери класу та ім'я методу (наприклад, метод get ThingController стає "TC#getThing"). Якщо імена не збігаються, можна використовувати @RequestMapping(name="..") для призначення явного імені або реалізувати власну стратегію іменування HandlerMethodMappingNamingStrategy.