Skip to content

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):

Image

Відтепер ми не будемо систематично робити знімки екрана обміну даними 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:

HTTP/1.1 301 Moved Permanently 

що означає, що перенаправлення є постійним. Ця відмінність між тимчасовим (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:
HTTP/1.1 301 Moved Permanently
  • рядок 5: вручну створюється наступний заголовок HTTP:
Location: /a01 

який є заголовком перенаправлення 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;

Якщо клієнт — це не браузер, а програмний клієнт, можна вільно використовувати будь-які статуси та заголовки.