3. Дії: відповідь
Розглянемо архітектуру додатка Spring MVC:
![]() |
У цьому розділі ми розглянемо процес, завдяки якому запит [1] надходить до контролера та дії [2a], які його оброблять — цей механізм називається маршрутизацією. Крім того, ми розглянемо різні відповіді [3], які дія може надати браузеру. Це може бути не лише представлення V [4b].
3.1. Новий проєкт
Ми створюємо новий проект Spring MVC:
![]() |
- у [1-2] ми створюємо новий проєкт на базі Spring Boot;
![]() |
- у [3] — назва проекту Maven;
- у [4] — група Maven, у яку буде розміщено результат компіляції проєкту;
- в [5] — ім'я, присвоєне результату компіляції;
- у [6] — опис проекту;
- [7] — пакет, у який буде розміщено виконуваний клас проекту;
- у [8] — тип проекту. Це веб-проект із Thymeleaf-шаблонами. Тут можна побачити всі готові до використання залежності Maven, що надаються проектом Spring Boot;
- у полі [9] вказується, що продукт, отриманий у результаті збірки Maven, буде упакований у архів jar, а не war. Проект використовуватиме вбудований сервер Tomcat, який буде знаходитися серед його залежностей;
- у [10] ми переходимо до наступного кроку майстра;
- у [11] вказується папка проекту;
![]() |
- у [12] — згенерований проект;
- у [14-15] перейменуйте пакет [istia.st.springmvc];
![]() |
- в [16] — нову назву пакета;
- у [17] — новий проєкт;
Тепер створюємо новий клас;
![]() |
- у [1-3] створюємо новий клас;
![]() |
- у [5] ми надаємо йому ім'я, а в [4] вказуємо його пакет;
- у [6] — новий проєкт;
Наразі клас має такий вигляд:
package istia.st.springmvc;
public class ActionsController {
}
Ми змінюємо цей код наступним чином:
package istia.st.springmvc;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class ActionsController {
}
- рядок 6: анотація [@RestController] вказує на дві речі:
- що клас [ActionsController], який має таку анотацію, є контролером Spring MVC, а отже, містить дії, що обробляють запити URL від клієнтів;
- що результат цих дій надсилається клієнту;
Інша анотація [@Controller], з якою ми стикалися, відрізняється: дії контролера, позначеного цією анотацією, повертають ім’я подання, яке має бути відображене. Саме поєднання цього подання та моделі, побудованої дією для цього подання, і формує відповідь, що надсилається клієнту.
Зміна структури нашого проєкту призводить до зміни його конфігурації:
![]() |
Клас [Application] змінюється наступним чином:
package istia.st.springmvc.main;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
@Configuration
@ComponentScan({"istia.st.springmvc.controllers"})
@EnableAutoConfiguration
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
- рядок 9: анотація [ComponentScan] приймає як параметр масив імен пакетів, у яких Spring Boot має шукати компоненти Spring. Тут ми додаємо до цього масиву пакет [istia.st.springmvc.controllers], щоб контролер, анотований [@RestController], був знайдений;
Ми створимо в контролері різні дії, щоб проілюструвати їхні основні характеристики. Спочатку ми розглянемо різні типи відповідей, які може надавати дія в додатку без представлень.
3.2. [/a01, /a02] — Hello world
Наша перша дія буде такою:
@RestController
public class ActionsController {
// ----------------------- hello world ------------------------
@RequestMapping(value = "/a01", method = RequestMethod.GET)
public String a01() {
return "Greetings from Spring Boot!";
}
}
- рядок 4: анотація [RequestMapping] визначає запит, що обробляється анотованою дією:
- атрибут [value] — це оброблювана URL,
- атрибут [method] визначає прийнятий метод;
Таким чином, метод [a01] обробляє запит HTTP [GET /a01].
- рядок 5: метод [a01] повертає тип [String], який буде надіслано клієнту без змін;
- рядок 6: повернений рядок;
Запустимо додаток, як ми це вже робили кілька разів, а потім із клієнтом [Advanced Rest Client] запитуємо URL [/a01] із GET [1-2]:
![]() |
- у [3] — відповідь сервера;
- на [4] — заголовки HTTP відповіді. Бачимо, що використовується кодування [ISO-8859-1]. Можна віддати перевагу кодуванню UTF-8. Це можна налаштувати;
- у [5] ми запитуємо те саме URL за допомогою браузера Chrome;
Ми додаємо наступну дію [/a02] у контролер [ActionsController] (таким чином, іноді можна сплутати URL та метод, що її обробляє, під назвою дії):
// ----------------------- символи з діакритичними знаками - UTF8 ------------------------
@RequestMapping(value = "/a02", method = RequestMethod.GET, produces="text/plain;charset=UTF-8")
public String a02() {
return "caractères accentués : éèàôûî";
}
- рядок 2: атрибут [produces="text/plain;charset=UTF-8"] вказує, що дія надсилає текстовий потік із символами, закодованими у форматі [UTF-8]. Цей формат, зокрема, дозволяє використовувати символи з діакритичними знаками;
Щоб врахувати цю нову дію, нам потрібно перезапустити додаток:
![]() |
Результат такий:
![]() |
- у форматі [1] видно тип документа, надісланого сервером;
- у [2-3] присутні символи з діакритичними знаками;
3.3. [/a03]: перетворити потік XML
Ми додаємо таку дію [/a03]:
// ----------------------- text/xml ------------------------
@RequestMapping(value = "/a03", method = RequestMethod.GET, produces = "text/xml;charset=UTF-8")
public String a03() {
String greeting = "<greetings><greeting>Greetings from Spring Boot!</greeting></greetings>";
return greeting;
}
- рядок 2: атрибут [produces="text/xml;charset=UTF-8"] вказує, що дія надсилає потік XML із символами, закодованими у форматі [UTF-8];
В результаті виконання отримуємо наступне:
![]() |
- у [1] заголовок HTTP вказує, що надісланий документ має формат HTML;
- у [2] браузер Chrome використовує цю інформацію для форматування отриманого тексту XML;
Нагадаємо, що в Chrome можна переглянути обмін даними HTTP між клієнтом і сервером у вікні розробника (Ctrl-Shift-I):

Відтепер ми не будемо систематично робити знімки екрана обміну даними HTTP між клієнтом і сервером. Іноді ми обмежимося лише наведенням тексту цих обмінів.
3.4. [/a04, /a05]: відтворити потік jSON
Ми додаємо таку дію [/a04]:
// ----------------------- генерувати jSON ------------------------
@RequestMapping(value = "/a04", method = RequestMethod.GET)
public Map<String, Object> a04() {
Map<String, Object> map = new HashMap<String, Object>();
map.put("1", "un");
map.put("2", new int[] { 4, 5 });
return map;
}
- рядок 3: дія повертає тип [Map] — словник. Нагадаємо, що з контролером типу [@RestController] результатом дії є відповідь, що надсилається клієнту. Оскільки протокол HTTP є протоколом обміну текстовими рядками, відповідь клієнта має бути серіалізована у рядок символів. Для цього Spring MVC використовує різні конвертери [Objet <---> chaîne de caractères]. Прив’язка конкретного об’єкта до конвертера здійснюється шляхом конфігурації. У цьому випадку автоконфігурація Spring Boot перевірить залежності проєкту:
![]() |
Вищезазначені залежності Jackson — це бібліотеки для серіалізації/десеріалізації об’єктів у рядки jSON. Spring Boot використовуватиме ці бібліотеки для серіалізації/десеріалізації об’єктів, що повертаються діями. Приклад коду Java для серіалізації/десеріалізації об’єктів Java у jSON наведено в розділі 9.7.
Зверніть увагу, що в рядку 2 ми не вказали тип відправленої відповіді. Давайте подивимося, який тип за замовчуванням буде відправлено.
У Chrome [1-3] отримано такі результати:
![]() |
Тепер додамо таку дію: [/a05]:
// ----------------------- генерувати jSON - 2 ------------------------
@RequestMapping(value = "/a05", method = RequestMethod.GET)
public Personne a05() {
return new Personne(1,"carole",45);
}
Клас [Personne] має такий вигляд:
![]() |
package istia.st.sprinmvc.models;
public class Personne {
// ідентифікатор
private Integer id;
// ім'я
private String nom;
// вік
private int age;
// розробники
public Personne() {
}
public Personne(String nom, int age) {
this.nom = nom;
this.age = age;
}
public Personne(Integer id, String nom, int age) {
this(nom, age);
this.id = id;
}
@Override
public String toString() {
return String.format("[id=%s, nom=%s, age=%d]", id, nom, age);
}
// методи getter та setter
...
}
Виконання дає такі результати:
![]() |
- у [1] сервер вказує, що документ, який він надсилає, має номер jSON;
- у [2] — отримано документ jSON;
3.5. [/a06]: повернути порожній потік
Ми додаємо таку дію [/a06]:
// ----------------------- повернути порожній потік ------------------------
@RequestMapping(value = "/a06")
public void a06() {
}
- у рядку 3 дія [/a06] нічого не повертає. Тоді Spring MVC згенерує порожню відповідь для клієнта;
Виконання дає такі результати:
![]() |
Вище атрибут HTTP [Content-Length] у відповіді вказує, що сервер надсилає порожній документ.
3.6. [/a07, /a08, /a09]: тип потоку з [Content-Type]
Ми додаємо таку дію [/a07]:
// ----------------------- text/html ------------------------
@RequestMapping(value = "/a07", method = RequestMethod.GET, produces = "text/html;charset=UTF-8")
public String a07() {
String greeting = "<h1>Greetings from Spring Boot!</h1>";
return greeting;
}
- у рядку 2 дія [/a07] повертає потік HTML [text/html];
- рядок 4: рядок HTML;
Виконання дає такі результати:
![]() |
- у [1] видно, що Chrome інтерпретував тег HTML <h1>, який відображає його вміст великими літерами;
Тепер зробимо те саме з наступною дією [/a08]:
// ----------------------- результат HTML у форматі text/plain ------------------------
@RequestMapping(value = "/a08", method = RequestMethod.GET, produces = "text/plain;charset=UTF-8")
public String a08() {
String greeting = "<h1>Greetings from Spring Boot!</h1>";
return greeting;
}
- рядок 2: відповідь дії має тип [text/plain];
Результати такі:
![]() |
- у випадку з [1] Chrome не розпізнав тег HTML <h1>, оскільки сервер повідомив йому, що надсилає потік [text/plain] [2];
Давайте повторимо щось подібне з наступною дією [/a09]:
// ----------------------- результат HTML у форматі text/xml ------------------------
@RequestMapping(value = "/a09", method = RequestMethod.GET, produces = "text/xml;charset=UTF-8")
public String a09() {
String greeting = "<h1>Greetings from Spring Boot!</h1>";
return greeting;
}
- рядок 2: надсилаємо потік типу [text/xml];
Результати такі:
![]() |
- у випадку з [1] Chrome не розпізнав тег HTML <h1>, оскільки сервер повідомив йому, що надсилає потік [text/xml] [2]. Тоді він обробив тег <h1> як тег XML;
З цих прикладів слід винести урок про важливість заголовка HTTP [Content-Type] у відповіді сервера. Браузер використовує цей заголовок, щоб зрозуміти, як інтерпретувати документ, який він отримує;
3.7. [/a10, /a11, /a12]: перенаправлення клієнта
Ми створюємо новий контролер [RedirectController]:
![]() |
Код [RedirectCntroller] наразі буде таким:
package istia.st.springmvc.controllers;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
@Controller
public class RedirectController {
}
- рядок 7: використовуємо анотацію [@Controller], завдяки чому відтепер за замовчуванням тип [String] результату дій позначає назву дії або подання;
Ми створюємо таку дію [/a10]:
// ------------ перенаправлення на дію третьої сторони -----------------------
@RequestMapping(value = "/a10", method = RequestMethod.GET)
public String a10() {
return "a01";
}
- рядок 4: як результат повертаємо «a01», що є назвою дії. Саме вона надсилатиме відповідь клієнту;
Ось приклад:
![]() |
- у [2] ми отримали потік від дії [/a01];
- у [3] браузер відображає URL дії [/a10];
Тепер створимо таку дію [/a11]:
// ------------ тимчасове перенаправлення 302 на сторонню дію -----------------------
@RequestMapping(value = "/a11", method = RequestMethod.GET)
public String a11() {
return "redirect:/a01";
}
Отримуємо такі результати:
![]() |
- у логах Chrome [1-2] ми бачимо два запити: один до [/a11], інший до [/a01];
- у записі [3] сервер відповідає кодом [302], який вимагає від клієнтського браузера перенаправитися наURL, вказаний у заголовку HTTP [Location:] [4]. Код [302] є кодом тимчасового перенаправлення;
Після цього браузер надсилає другий запит до сервера перенаправлення URL:
![]() |
- на [5] — другий запит клієнта;
- на [6], клієнтський браузер відображає URL із запиту на перенаправлення;
Може знадобитися вказати постійне перенаправлення, і в цьому випадку клієнту потрібно надіслати такий заголовок: HTTP:
що означає, що перенаправлення є постійним. Ця відмінність між тимчасовим (302) та постійним (301) перенаправленням враховується деякими пошуковими системами.
Ми записуємо дію [/a12], яка здійснить це постійне перенаправлення:
// ------------ постійне перенаправлення 301 на дію стороннього ресурсу----------------
@RequestMapping(value = "/a12", method = RequestMethod.GET)
public void a12(HttpServletResponse response) {
response.setStatus(301);
response.addHeader("Location", "/a01");
}
- рядок 3: ми просимо Spring MVC ввести об’єкт [HttpServletResponse], який інкапсулює відповідь, що надсилається клієнту;
- рядок 4: встановлюємо [status] для відповіді, [301] для заголовка HTTP:
- рядок 5: вручну створюється наступний заголовок HTTP:
який є заголовком перенаправлення URL.
Виконання дає такі результати:
![]() | ![]() |
З цього прикладу слід запам’ятати, як:
- генерувати статус відповіді HTTP;
- включити заголовок HTTP у відповідь;
3.8. [/a13]: генерувати повну відповідь
Можна повністю контролювати відповідь, як показано в наступній дії класу [ResponsesController]:
![]() |
// ----------------------- повне формування відповіді ------------------------
@RequestMapping(value = "/a13")
public void a13(HttpServletResponse response) throws IOException {
response.setStatus(666);
response.addHeader("header1", "qq chose");
response.addHeader("Content-Type", "text/html;charset=UTF-8");
String greeting = "<h1>Greetings from Spring Boot!</h1>";
response.getWriter().write(greeting);
}
- рядок 3: результатом дії є [void]. У цьому випадку, щоб надіслати клієнту непорожню відповідь, потрібно використати об’єкт [HttpServletResponse response], наданий Spring MVC;
- рядок 4: відповіді присвоюється статус, який не буде розпізнаний клієнтом;
- рядок 5: додаємо заголовок HTTP, який не буде розпізнаний клієнтом;
- рядок 6: додаємо заголовок HTTP [Content-Type], щоб вказати тип потоку, який буде надіслано, у даному випадку HTML;
- рядки 7–8: документ, який йтиме після заголовків HTTP у відповіді;
Результати такі:
![]() |
- у [1] можна впізнати елементи нашої відповіді;
- у [2-3] видно, що Chrome проігнорував той факт, що:
- статус HTTP у відповіді не був розпізнаним статусом HTTP,
- заголовок [header1] не був визнаним заголовком HTTP;
Якщо клієнт — це не браузер, а програмний клієнт, можна вільно використовувати будь-які статуси та заголовки.



























