Skip to content

16. Вступ до Spring MVC

16.1. Роль Spring MVC у веб-додатку

Розглянемо роль Spring MVC у розробці веб-додатку. Найчастіше такий додаток будується на багатошаровій архітектурі, наприклад, такій:

  • шар [Web] — це шар, що взаємодіє з користувачем веб-додатка. Користувач взаємодіє з веб-додатком через веб-сторінки, що відображаються у браузері. Саме в цьому шарі, і лише в ньому, розташований Spring MVC;
  • шар [métier] реалізує правила управління додатком, такі як розрахунок заробітної плати або рахунку-фактури. Цей рівень використовує дані, що надходять від користувача через рівень [Web], а також дані з SGBD через рівень [DAO];
  • Рівень [DAO] (об’єкти доступу до даних), рівень [ORM] (об’єктно-реляційний мапер) та драйвер JDBC забезпечують доступ до даних SGBD. Рівень [ORM] забезпечує зв’язок між об’єктами, що обробляються рівнем [DAO], та рядками й стовпцями таблиць реляційної бази даних. Специфікація під назвою JPA (Java Persistence API) дозволяє абстрагуватися від використовуваного ORM, якщо він реалізує ці специфікації. Саме так буде в цьому посібнику, тому відтепер ми будемо називати шар ORM шаром JPA;
  • інтеграція шарів здійснюється фреймворком Spring;

16.2. Модель розробки Spring MVC

Spring MVC реалізує так звану архітектурну модель MVC (Модель – Представлення – Контролер) наступним чином:

Обробка запиту клієнта відбувається наступним чином:

  1. запит — запитувані URL мають вигляд http://machine:port/contexte/Action/param1/param2/....?p1=v1&p2=v2&.... [Front Controller] використовує файл конфігурації або анотації Java для «маршрутизації» запиту до відповідного контролера та відповідної дії в межах цього контролера. Для цього він використовує поле [Action] з URL. Решта URL [/param1/param2/...] складається з необов’язкових параметрів, які будуть передані до дії. Значенням параметра C у MVC тут є рядок [Front Controller, Contrôleur, Action]. Якщо жоден контролер не може обробити запитувану дію, веб-сервер відповість, що запитуваний URL не знайдено.
  1. Обробка
      • (продовження)
    • обрана дія може використовувати параметри parami, які їй передала [Front Controller]. Вони можуть походити з кількох джерел:
      • з шляху [/param1/param2/...] файлу URL,
      • з параметрів [p1=v1&p2=v2] від URL,
      • з параметрів, надісланих браузером разом із запитом;
    • під час обробки запиту користувача дії може знадобитися рівень [métier] [2b]. Після обробки запиту клієнта це може викликати різні відповіді. Класичним прикладом є:
      • сторінка з повідомленням про помилку, якщо запит не вдалося обробити належним чином
      • сторінка підтвердження в іншому випадку
    • дія вимагає відображення певного виду [3]. Цей вид відображатиме дані, які називаються моделлю виду. Це «M» у MVC. Дія створить цю модель M [2c] і вимагатиме відображення певного виду V [3];
  1. відповідь — обраний вигляд V використовує модель M, створену дією, для ініціалізації динамічних частин відповіді HTML, яку він повинен надіслати клієнту, а потім надсилає цю відповідь.

Для веб-сервісу / jSON попередня архітектура дещо змінена:

  • у [4a] модель, яка є класом Java, перетворюється на рядок jSON за допомогою бібліотеки jSON;
  • у [4b] цей рядок jSON надсилається до браузера;

Тепер уточнимо зв’язок між веб-архітектурою MVC та багаторівневою архітектурою. Залежно від того, як ми визначаємо модель, ці два поняття можуть бути пов’язані або ні. Розглянемо однорівневий веб-додаток Spring MVC:

Якщо ми реалізуємо шар [Web] за допомогою Spring MVC, ми отримаємо веб-архітектуру MVC, але не багатошарову архітектуру. У цьому випадку шар [web] буде відповідати за все: представлення, бізнес-логіку, доступ до даних. Цю роботу виконуватимуть дії.

Тепер розглянемо багаторівневу веб-архітектуру:

Рівень [Web] можна реалізувати без фреймворку та без дотримання моделі MVC. У цьому випадку ми дійсно маємо багаторівневу архітектуру, але веб-рівень не реалізує модель MVC.

Наприклад, у середовищі .NET цей веб-шар [Web]може бути реалізований за допомогою ASP.NET та MVC, і тоді ми отримуємо багатошарову архітектуру з шаром [Web] типу MVC. Зробивши це, можна замінити цей шар ASP.NET MVC на класичний шар ASP.NET (WebForms), зберігаючи решту (бізнес-логіка, DAO, ORM) без змін. У результаті ми отримуємо багаторівневу архітектуру з рівнем [Web], який більше не є типом MVC.

У MVC ми зазначили, що модель M відповідає представленню V, c.a.d, — сукупності даних, що відображаються представленням V. Наводимо ще одне визначення моделі M для MVC:

Багато авторів вважають, що те, що знаходиться праворуч від шару [Web], утворює модель M для MVC. Щоб уникнути двозначностей, можна говорити про:

  • моделі домену, коли мається на увазі все, що знаходиться праворуч від шару [Web]
  • про модель подання, коли йдеться про дані, що відображаються поданням V

Надалі термін «модель M» позначатиме виключно модель подання V.

16.3. Веб-проект / jSON із Spring MVC

Сайт [http://spring.io/guides] пропонує навчальні посібники для початківців, що допоможуть ознайомитися з екосистемою Spring. Ми скористаємося одним із них, щоб дізнатися про конфігурацію Maven, необхідну для проекту Spring MVC.

16.3.1. Демонстраційний проект

  • у [1] ми імпортуємо один із посібників Spring;
  • у [2] ми обираємо приклад [Rest Service];
  • у [3] вибираємо проект Maven;
  • у [4] вибираємо остаточну версію посібника;
  • у [5] підтверджуємо;
  • у [6] — імпортований проєкт;

Веб-сервіси, доступні через стандартні URL і які надають текст jSON, часто називають сервісами REST (REpresentational State Transfer). Сервіс вважається Restful, якщо він дотримується певних правил.

Тепер розглянемо імпортований проєкт, спочатку його конфігурацію Maven.

16.3.2. Конфігурація Maven

Файл [pom.xml] має такий вигляд:


<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.springframework</groupId>
    <artifactId>gs-rest-service</artifactId>
    <version>0.1.0</version>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.2.2.RELEASE</version>
    </parent>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    </dependencies>

    <properties>
        <start-class>hello.Application</start-class>
    </properties>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

    <repositories>
        <repository>
            <id>spring-releases</id>
            <url>https://repo.spring.io/libs-release</url>
        </repository>
    </repositories>
    <pluginRepositories>
        <pluginRepository>
            <id>spring-releases</id>
            <url>https://repo.spring.io/libs-release</url>
        </pluginRepository>
    </pluginRepositories>
</project>
  • рядки 6–8: властивості проекту Maven. Відсутній тег [<packaging>], що вказує тип файлу, який створюється під час компіляції Maven. За його відсутності використовується тип [jar]. Отже, додаток є консольним виконуваним файлом, а не веб-додатком, для якого тип упаковки був би [war];
  • рядки 10–14: проект Maven має батьківський проект [spring-boot-starter-parent]. Саме він визначає основну частину залежностей проекту. Їх може бути достатньо, і в цьому випадку додаткові залежності не додаються, або ж їх може не вистачати, і тоді додаються відсутні залежності;
  • рядки 17–20: артефакт [spring-boot-starter-web] містить бібліотеки, необхідні для проекту Spring MVC типу веб-сервісу, у якому не генеруються візуальні елементи. Цей артефакт містить величезну кількість бібліотек, зокрема бібліотеки вбудованого сервера Tomcat. Саме на цьому сервері буде виконуватися додаток;

Бібліотек, що входять до складу цієї конфігурації, дуже багато:

Вище показано три архіви сервера Tomcat.

16.3.3. Архітектура сервісу Spring [web / jSON]

Для веб-сервісу / jSON Spring MVC реалізує модель MVC наступним чином:

  • у [4a] модель, яка є класом Java, перетворюється на рядок jSON за допомогою бібліотеки jSON;
  • у [4b] цей рядок jSON надсилається до браузера;

16.3.4. Контролер C

  

Імпортований додаток має такий контролер:


package hello;

import java.util.concurrent.atomic.AtomicLong;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class GreetingController {

    private static final String template = "Hello, %s!";
    private final AtomicLong counter = new AtomicLong();

    @RequestMapping("/greeting")
    public Greeting greeting(@RequestParam(value = "name", defaultValue = "World") String name) {
        return new Greeting(counter.incrementAndGet(), String.format(template, name));
    }
}
  • рядок 9: анотація [@RestController] перетворює клас [GreetingController] на контролер Spring, тобто його методи реєструються для обробки URL. Ми вже бачили подібну анотацію [@Controller]. Результатом методів цього контролера був тип [String], який являв собою ім’я поданого виду. Тут ситуація інша. Методи контролера типу [@RestController] повертають об’єкти, які серіалізуються для відправки до браузера. Тип серіалізації залежить від конфігурації Spring MVC. У цьому випадку вони будуть серіалізовані у формат jSON. Саме наявність бібліотеки jSON у залежностях проєкту змушує Spring Boot шляхом автоконфігурації налаштувати проєкт саме таким чином;
  • рядок 14: анотація [@RequestMapping] вказує на URL, який обробляє метод, у даному випадку URL [/greeting];
  • рядок 15: ми вже пояснювали анотацію [@RequestParam]. Результатом, що повертається методом, є об’єкт типу [Greeting].
  • рядок 12: ціле число типу «long» атомарного типу. Це означає, що воно підтримує паралельний доступ. Кілька потоків можуть одночасно намагатися збільшити значення змінної [counter]. Це відбуватиметься коректно. Потік може зчитати значення лічильника лише тоді, коли потік, який його змінює, завершив свою операцію.

16.3.5. Модель M

Модель M, створена за допомогою попереднього методу, є таким об’єктом [Greeting]:

  

package hello;

public class Greeting {

    private final long id;
    private final String content;

    public Greeting(long id, String content) {
        this.id = id;
        this.content = content;
    }

    public long getId() {
        return id;
    }

    public String getContent() {
        return content;
    }
}

Перетворення jSON цього об’єкта створить рядок {"id":n,"content":"текст"}. У підсумку рядок jSON, створений методом контролера, матиме такий вигляд:

{"id":2,"content":"Hello, World!"}

або

{"id":2,"content":"Hello, John!"}

16.3.6. Виконання

  

Клас [Application.java] є виконуваним класом проекту. Його код такий:


package hello;

import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.SpringApplication;
import org.springframework.context.annotation.ComponentScan;

@ComponentScan
@EnableAutoConfiguration
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }

}

Ми вже зустрічали та пояснювали цей код у попередньому прикладі. Запустимо проект:

 

Ми отримуємо такі записи консолі:

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v1.1.9.RELEASE)

2014-11-28 15:22:55.005  INFO 3152 --- [           main] hello.Application                        : Starting Application on Gportpers3 with PID 3152 (started by ST in D:\data\istia-1415\spring mvc\dvp-final\gs-rest-service)
2014-11-28 15:22:55.046  INFO 3152 --- [           main] ationConfigEmbeddedWebApplicationContext : Refreshing org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@62e136d3: startup date [Fri Nov 28 15:22:55 CET 2014]; root of context hierarchy
2014-11-28 15:22:55.762  INFO 3152 --- [           main] o.s.b.f.s.DefaultListableBeanFactory     : Overriding bean definition for bean 'beanNameViewResolver': replacing [Root bean: class [null]; scope=; abstract=false; lazyInit=false; autowireMode=3; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=org.springframework.boot.autoconfigure.web.ErrorMvcAutoConfiguration$WhitelabelErrorViewConfiguration; factoryMethodName=beanNameViewResolver; initMethodName=null; destroyMethodName=(inferred); defined in class path resource [org/springframework/boot/autoconfigure/web/ErrorMvcAutoConfiguration$WhitelabelErrorViewConfiguration.class]] with [Root bean: class [null]; scope=; abstract=false; lazyInit=false; autowireMode=3; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration$WebMvcAutoConfigurationAdapter; factoryMethodName=beanNameViewResolver; initMethodName=null; destroyMethodName=(inferred); defined in class path resource [org/springframework/boot/autoconfigure/web/WebMvcAutoConfiguration$WebMvcAutoConfigurationAdapter.class]]
2014-11-28 15:22:56.567  INFO 3152 --- [           main] .t.TomcatEmbeddedServletContainerFactory : Server initialized with port: 8080
2014-11-28 15:22:56.738  INFO 3152 --- [           main] o.apache.catalina.core.StandardService   : Starting service Tomcat
2014-11-28 15:22:56.740  INFO 3152 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet Engine: Apache Tomcat/7.0.56
2014-11-28 15:22:56.869  INFO 3152 --- [ost-startStop-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2014-11-28 15:22:56.870  INFO 3152 --- [ost-startStop-1] o.s.web.context.ContextLoader            : Root WebApplicationContext: initialization completed in 1827 ms
2014-11-28 15:22:57.478  INFO 3152 --- [ost-startStop-1] o.s.b.c.e.ServletRegistrationBean        : Mapping servlet: 'dispatcherServlet' to [/]
2014-11-28 15:22:57.481  INFO 3152 --- [ost-startStop-1] o.s.b.c.embedded.FilterRegistrationBean  : Mapping filter: 'hiddenHttpMethodFilter' to: [/*]
2014-11-28 15:22:57.685  INFO 3152 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/**/favicon.ico] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2014-11-28 15:22:57.879  INFO 3152 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/greeting],methods=[],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public hello.Greeting hello.GreetingController.greeting(java.lang.String)
2014-11-28 15:22:57.884  INFO 3152 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error],methods=[],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public org.springframework.http.ResponseEntity<java.util.Map<java.lang.String, java.lang.Object>> org.springframework.boot.autoconfigure.web.BasicErrorController.error(javax.servlet.http.HttpServletRequest)
2014-11-28 15:22:57.885  INFO 3152 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error],methods=[],params=[],headers=[],consumes=[],produces=[text/html],custom=[]}" onto public org.springframework.web.servlet.ModelAndView org.springframework.boot.autoconfigure.web.BasicErrorController.errorHtml(javax.servlet.http.HttpServletRequest)
2014-11-28 15:22:57.906  INFO 3152 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/webjars/**] на обробник типу [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2014-11-28 15:22:57.907  INFO 3152 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/**] обробник типу [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2014-11-28 15:22:58.231  INFO 3152 --- [           main] o.s.j.e.a.AnnotationMBeanExporter        : Registering beans for JMX exposure on startup
2014-11-28 15:22:58.318  INFO 3152 --- [           main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8080/http
2014-11-28 15:22:58.319  INFO 3152 --- [           main] hello.Application                        : Started Application in 3.788 seconds (JVM running for 4.424)
  • рядок 13: сервер Tomcat запускається на порту 8080 (рядок 12);
  • рядок 17: сервлет [DispatcherServlet] присутній;
  • рядок 20: виявлено метод [GreetingController.greeting];

Щоб протестувати веб-додаток, надсилаємо запит на URL [http://localhost:8080/greeting]:

 

Ми успішно отримуємо очікуваний рядок jSON. Може бути цікаво подивитися заголовки HTTP, надіслані сервером. Для цього ми скористаємося розширенням для Chrome під назвою [Advanced Rest Client] (Chrome / Ctrl-T / Меню [Applications] / [Advanced Rest Client] — див. Додатки, параграф 23.11):

  • у [1] — запитуване URL;
  • у [2] використовується метод GET;
  • у [3] — відповідь jSON;
  • у [4] сервер повідомив, що надсилає відповідь у форматі jSON;
  • у [5] запитується той самий URL, але цього разу з POST;
  • у [7] інформація надсилається на сервер у формі [urlencoded];
  • у [6] — параметр name із його значенням;
  • у [8] браузер повідомляє серверу, що надсилає йому інформацію [urlencoded];
  • у [9] — відповідь jSON від сервера;

16.3.7. Створення виконуваного архіву

Тепер ми створюємо виконуваний архів:

  • у [1]: виконується ціль Maven;
  • у [2]: є дві цілі (goals): [clean] для видалення папки [target] з проекту Maven, [package] для її повторного створення;
  • у [3]: згенерована папка [target] буде розміщена в цій папці;
  • у [4]: генерується ціль;

У журналах, що з’являються в консолі, важливо побачити плагін [spring-boot-maven-plugin]. Саме він генерує виконуваний архів (див. [pom.xml] нижче):


    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
</build>
[INFO] --- spring-boot-maven-plugin:1.1.0.RELEASE:repackage (default) @ gs-rest-service ---

У консолі перейдіть до створеної папки:


D:\Temp\wksSTS\gs-rest-service\target>dir
 ...
11/06/2014  15:30    <DIR>          classes
11/06/2014  15:30    <DIR>          generated-sources
11/06/2014  15:30        11 073 572 gs-rest-service-0.1.0.jar
11/06/2014  15:30             3 690 gs-rest-service-0.1.0.jar.original
11/06/2014  15:30    <DIR>          maven-archiver
11/06/2014  15:30    <DIR>          maven-status
...
  • рядок 5: створений архів;

Цей архів запускається наступним чином:


D:\Temp\wksSTS\gs-rest-service-complete\target>java -jar gs-rest-service-0.1.0.jar

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v1.1.0.RELEASE)

2014-06-11 15:32:47.088  INFO 4972 --- [           main] hello.Application
                  : Starting Application on Gportpers3 with PID 4972 (D:\Temp\wk
sSTS\gs-rest-service-complete\target\gs-rest-service-0.1.0.jar started by ST in
D:\Temp\wksSTS\gs-rest-service-complete\target)
...

Тепер, коли веб-додаток запущено, його можна відкрити у браузері:

 

16.3.8. Розгортання додатка на сервері Tomcat

Як і в попередньому проєкті, ми змінюємо файл [pom.xml] наступним чином:


<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.springframework</groupId>
    <artifactId>gs-rest-service</artifactId>
    <version>0.1.0</version>
    <packaging>war</packaging>

    ...
</project>
  • рядок 9: потрібно вказати, що буде створено архів war (Web ARchive);

Крім того, потрібно налаштувати веб-додаток. За відсутності файлу [web.xml] це робиться за допомогою класу, що успадковує [SpringBootServletInitializer]:

  

Клас [ApplicationInitializer] має такий вигляд:


package hello;

import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.context.web.SpringBootServletInitializer;

public class ApplicationInitializer extends SpringBootServletInitializer {

    @Override
    protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
        return application.sources(Application.class);
    }

}
  • рядок 6: клас [ApplicationInitializer] успадковує клас [SpringBootServletInitializer];
  • рядок 9: метод [configure] перевизначено (рядок 8);
  • рядок 10: вказано клас, який налаштовує проект;

Щоб виконати проект, можна вчинити так:

  • у [1-2] запускаємо проект на одному із серверів, зареєстрованих у IDE Eclipse;

Після цього можна завантажити URL [http://localhost:8080/gs-rest-service/greeting/?name=Mitchell] у браузері:

 

16.4. Conclusion

Ми запровадили тип проектів Spring MVC, у яких веб-додаток надсилає потік jSON до браузера. Тепер ми розробимо веб-додаток / jSON для оприлюднення в Інтернеті бази даних [dbproduitscategories], яку ми розглядали в попередніх розділах.