От Hello World до Spring Web MVC и при чём тут сервлеты - 1

Вступление

Как мы знаем, успех к Java пришёл именно благодаря эволюции программного обеспечения, которое стремится в сеть. Поэтому, и мы возьмём за основу обычное консольное приложение "Hello World" и поймём, что ему нужно, чтобы стать из консольного приложения сетевым приложением. Итак, для начала нужно создать Java проект. Программисты люди ленивые. В доисторические времена, когда одни охотились на мамонтов, другие сидели и пытались не запутаться во всём многообразии Java библиотек, структурах каталогов. Чтобы разработчик мог управлять процессом создания приложения, чтобы он смог просто написать "хочу библиотеку такую-то версии 2" придумали специальные средства — системы сборки. Две самые известные из них: Maven и Gradle. Для данной статьи мы воспользуемся Gradle. Если раньше нам пришлось бы самим создать структуру каталогов, то сейчас Gradle при помощи плагина Gradle Init Plugin позволяет создать Java проект со структурой каталогов и базовым Main классом в одну команду: gradle init --type java-application Данная команда выполняет инициализацию (init) для нас Java-приложение (java-application) с консольным Hello World. После завершения в каталоге появится файл — build.gradle. Это наш build script — то есть некий сценарий создания приложения с описанием того, какие действия для этого нужно выполнять. Откроем его и добавим в него строку: jar.baseName = 'webproject' Gradle позволяет выполнять различные действия над проектом и эти действия называются tasks. При помощи выполнения команды (task'а) gradle build в каталоге /build/libs будет создан JAR файл. И, как Вы догадались, имя ему теперь будет webproject.jar. Но если мы выполним java -jar ./build/libs/webproject.jar, то получим ошибку: no main manifest attribute. Всё потому, что для java приложения нужно приложить некий манифест — это такое описание, как работать с приложением, как его воспринимать. Тогда JVM, которая и будет выполнять java приложение будет знать, какой класс является точкой входа в программу и другую информацию (например, classpath). Если внимательнее присмотреться к содержимому build скрипта, то мы увидим подключаемые плагины. Например: apply plugin: 'java' Если зайти на страницу плагина Gradle Java Plugin, то можем увидеть, что мы можем сконфигурировать манифест:

jar {
    manifest {
        attributes 'Main-Class': 'App'
    }
}
Главный класс, точку входа в программу, для нас сгенерировал Gradle Init Plugin. И она даже указана в параметре mainClassName. Но нам это не подошло, т.к. этот параметр относится к другому плагину, Gradle Application Plugin. Итак, у нас есть Java приложение, которые выводит Hello World на экран. Java приложение это упаковывается в JAR (Java ARchive). Оно простое, консольное, не актуальное. Как же превратить его в веб-приложение?
От Hello World до Spring Web MVC и при чём тут сервлеты - 2

Servlet API

Для того, чтобы Java смогла работать с сетью ещё в далёкие времена появилась спецификация, называемая Servlet API. Именно данная спецификация описывается клиент-серверное взаимодействие, получение сообщения от клиента (например, браузера) и отправку ответа (например, с текстом страницы). Естественно, с тех пор много что поменялось, но суть в том, что чтобы Java приложение стало веб-приложением используется Servlet API. Чтобы не рассуждать голословно, возьмём в руки ту самую спецификацию: JSR-000340 JavaTM Servlet 3.1. Прежде всего, нас интересует "Chapter 1: Overview". В ней описываются основные понятия, которые мы должны уяснить себе. Во-первых, что такое серврлет? В главе "1.1 What is a Servlet?" сказано, что Servlet — это Java компонент, который управляется контейнером и который генерирует динамическое содержание. Как и другие Java компоненты сервлет является Java классом, который скомпилирован в байт-код и который может быть загружен в веб-сервер, использующий технологию Java. Важно, что сервлеты взаимодействуют с веб-клиентом (например, браузером) в рамках парадигмы запрос/ответ (request/response), которую реализует Servlet Container. Получается, что Servlet'ы живут в каком-то Servlet Container'е. Что это? В главе "1.2 What is a Servlet Container?" сказано, что Servlet Container — это некоторая часть веб-сервера или сервера приложений, которая предоставляет сетевые сервисы через которые посылаются запросы и ответы посылаются. Этот самый Servlet Container управляет жизненным циклом сервлетов. Все Servlet Container'ы обязаны поддерживать протокол HTTP как минимум, но могут поддерживать и другие. Например, HTTPS. Так же важно, что именно Servlet Container может накладывать на окружение, в котором выполняются сервлеты, какие-либо связанные с безопасностью ограничения. Важно так же, что согласно "10.6 Web Application Archive File" веб-приложение должно быть упаковано в WAR (Web ARchive) файл. То есть теперь нам нужно удалить наши jar и application плагины на что-то другое. И это — Gradle WAR plugin. А вместо jar.baseName указать war.baseName Т.к. мы не используем больше jar плагин, то мы удалили и настройки manifest. Когда мы запускали JAR — виртуальной машине джава (JVM) нужно было через манифест подсказать, как работать с нашим приложением. Потому что JVM выполняла его. Веб-приложение, судя по всему, выполняем некий веб-сервер. Получается, ему надо как-то подсказать, как работать с нашим веб-приложением? И оказывается что да. У веб-приложений свой, особенный манифест. Называется он Deployment Descriptor. Ему отведен целый раздел: "14. Deployment Descriptor". Есть важный раздел: "Chapter 10: Web Applications". Он рассказывает о том, что из себя представляет веб-приложение с точки зрения Servlet API. Например, в главе "10.5 Directory Structure" указано, где должен быть Deployment Descriptor: /WEB-INF/web.xml. Куда разместить WEB-INF? Как сказано в Gradle WAR plugin, он добавляет новый layout: src/main/webapp . Поэтому создадим такой каталог, внутри создадим каталог WEB-INF, а внутри создадим файл web.xml. Важно, чтобы каталог назывался WEB-INF, а не META-INF ! Скопируем себе из "14.5.1 A Basic Example" пример XML:
От Hello World до Spring Web MVC и при чём тут сервлеты - 3
Как мы видим, для конфигурации используется XML документ. XML документ, чтобы считаться корректным (Valid) должен соответствовать какой-нибудь "схеме". Можно считать это своего рода интерфейсом для XML документа. В схеме указано, какие элементы могуть быть в XML документе, какого типа данные могут задавать элемент, порядок, обязательность и прочие аспекты. В скопированном из документации примере указана версия 2.5, а мы хотим использовать версию 3.1. Естественно, спецификация с изменением версий менялась, добавлялись новые возможности. Поэтому схему нужно использовать не ту, что использовали для версии 2.5 (web-app_2_5.xsd). Какую же схему использовать для версии 3.1? В этом нам поможет документация, глава "14.3 Deployment Descriptor", в которой указано specification is available at http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd То есть нам надо заменить везде ссылку на схему на указанную xsd, не забыв поменять version="2.5" на 3.1, а так же изменить везде namespace (xmlns и в xsi:schemaLocation). Они указывают, в рамках какого пространства имён мы будем работать (если очень просто, то какие имена элементов мы можем использовать). Если открыть файл схемы, то в targetNamespace будет указан тот самый namespace, который мы должны указывать:
От Hello World до Spring Web MVC и при чём тут сервлеты - 4
Как мы помним, в Manifest Jar-файла мы писали, какой класс мы хотим использовать. Что же делать тут? Тут нужно указать, какой класс сервлета мы хотим использовать, когда получим запрос от веб-клиента. Описание можно прочитать в главе "14.4 Deployment Descriptor Diagram". Выглядеть это будет следующим образом:
От Hello World до Spring Web MVC и при чём тут сервлеты - 5
Тут всё просто. Объявляется серверлет, а дальше производится его маппинг на некоторый шаблон. В данном случае, на /app. Когда сработает шаблон, на выполнение будет запущен метод сервлета. Для красоты класс App стоит перенести в пакет, не забыв поправить xml конфигурацию. Но и это ещё не всё. App должен быть сервлетом. Что это значит, быть серврлетом? Это значит, что мы должны наследоваться от HttpServlet'а. Пример можно подсмотреть в главе "8.1.1 @WebServlet". В соответствии с ним наш App класс станет выглядеть так:

import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class App extends HttpServlet {
    public String getGreeting() {
        return "Hello world.";
    }
	
	public void doGet(HttpServletRequest request, HttpServletResponse response) {
		response.setContentType("text/html");
		try {
			response.getWriter().println(getGreeting());
		} catch (IOException e) {
			throw new IllegalStateException(e);
		}
	}
}
Но наш проект ещё не готов. Потому что мы теперь зависим от Servlet API версии 3.1. А это значит, что в нашем build скрипте надо указать зависимость от Servlet API. JVM ведь надо знать, что то что Вы написали в коде правильно, как это использовать. Как мы помним, спецификация — это по своей сути только интерфейсы, описывающие, как это всё должно работать. А реализации лежат на стороне веб-сервера. Поэтому без Servlet API будет Находим нужную библиотеку на Maven Central: javax.servlet-api. И добавляем запись в блок dependencies. В Maven repository, как Вы видели, указано provided. Перед использованием зависимости необходимо указать scope. В Gradle нет scope с именем "provided", зато там есть scope "compile only". Поэтому укажем: providedCompile 'javax.servlet:javax.servlet-api:3.1.0' Уф, вроде всё? Gradle Build соберёт наш проект в WAR файл. И что же дальше с ним делать? Для начала, нам нужен Web Server. В гугле пишем "web server java list" и видим список веб-серверов. Выберем из этого списка, например, TomCat. Переходим на сайт Apache Tomcat, скачиваем последнюю версию (на текущий момент 9 версия) в виде zip архива (если для Windows). Распаковываем в какой-нибудь каталог. Ура, у нас есть веб-сервер. Из каталога веб-сервера в подгаталоге bin выполняем из командной строки catalina и видим доступные опции. Выполним: catalina start. У каждого веб-сервера есть каталог, за которым веб-сервер следит. Если там появляется файл веб-приложения, то веб-сервер начинает его у себя устанавливать. Такая установка называется развёртыванием или деплоем (deploy). Да да, именно поэтому "deployment descriptor". То есть как нужно правильно развернуть приложение. В Tomcat такой каталог — webapps. Скопируем туда war, который мы сделали при помощи gradle build. После этого в логе мы увидим что-то вроде: Deployment of web application archive [tomcat\webapps\webproject.war] has finished in [время] ms Чтобы понимать ещё лучше, в каталоге tomcat отредактируем файл \conf\tomcat-users.xml, добавив следующие строки:
От Hello World до Spring Web MVC и при чём тут сервлеты - 6
Теперь перезапускаем сервер (catalina stop, catalina start) и переходим по адресу http://127.0.0.1:8080/manager Тут мы увидим пути всех приложений. Нашему webproject скорей всего дали путь /webproject. Что это за путь? В спецификации в главе "10.1 Web Applications Within Web Servers" сказано, что веб-приложение ассоциируется с некоторым путём внутри приложения (в данном случае, это /webproject). Все запросы через этот путь будут ассоциированы с одним и тем же ServletContext'ом. Данный путь ещё называется contextRoot. А согласно "10.2 Relationship to ServletContext" сервлет контейнер соотносит веб-приложение и ServletContext один к одному. То есть у каждого веб-приложения свой ServletContext. Что же такое ServletContext? Как гласит спецификация, ServletContext — это некоторый объект, который предоставляет сервлетам некое представление о приложении, в котором они выполняются, "view of the application". Подробнее про Servlet Context рассказывается в главе 4 спецификации Servlet API. Удивительно, что Servlet API в версии 3.1 больше не требует обязательного присутствия web.xml. Например, можно задать серврлет при помощи аннотаций:

import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@WebServlet("/app2")
public class App2 extends HttpServlet {

    public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
        response.setContentType("text/html");
        response.getWriter().println("app2");
    }
}
По теме так же рекомендуются: "Собеседование по Java EE — JEE Servlet API (вопросы и ответы)". Итак, у нас есть Servlet — он отвечает за то, какой ответ выдать веб-клиенту. У нас есть ServletContainer, который получает запросы от пользователя, соотносит путь, к которму обратились, с путём к сервлету и если соответствие найдено - выполняет Servlet. Хорошо. Какое же место в этой картине мира занимает Spring?

Spring Web MVC

Отлично, у нас есть веб-приложение. Теперь нам нужно подключить Spring. Как же нам это сделать. Во-первых, нужно разобраться с тем, как правильно подключить Spring к проекту. Оказывается, раньше можно было это делать в сооветствии с документацией проекта Spring platform , но теперь "The Platform will reach the end of its supported life on 9 April 2019", то есть не желательно её использовать, т.к. скоро она перестанет поддерживаться. Единственный выход — "Users of the Platform are encourage to start using Spring Boot's dependency management". Поэтому перейдём в документацию Spring Boot. Уточню, что мы не используем сам Spring Boot, а только Dependency Management от Spring Boot. То есть проект Spring Boot может предоставлять знание о том, какие версии библиотек нужно использовать (в том числе Spring MVC). Там найдём 3.2. Using Spring Boot’s dependency management in isolation. Согласно документации в build скрипт добавляем:

plugins {
    id 'org.springframework.boot' version '2.0.4.RELEASE' apply false
}
apply plugin: 'io.spring.dependency-management'
и

dependencyManagement {
    imports {
        mavenBom org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES
    }
}
Как видно, мы указали apply false, т.е. не применяем сам Spring Boot, но используем от туда управление зависимостями. Такое управление зависимостями так же называют BOM — "Bill Of Materials". Теперь мы готовы подключать сам проект Spring Web MVC. Spring Web MVC является частью проекта Spring Framework и нас интересует раздел "Web Servlet". Добавим зависимость в build script: compile 'org.springframework:spring-webmvc'. Как мы видим, мы выставили scope compile, т.к. веб-сервер не предоставляет нам Spring. Наш проект вынужден включать библиотеку Spring внутрь себя. Далее нам важно прочитать раздел "1.2. DispatcherServlet" , где сказано, что Spring MVC построен вокруг шаблона "Front controller", когда есть некий центральный сервлет, который предоставляет конфигурацию и делегирование другим компонентам. Dispatcher можно перевести как диспетчер. Итак, первым делом в web.xml объявляем:
От Hello World до Spring Web MVC и при чём тут сервлеты - 7
Как мы видим, это на самом деле обычный Listener, определённый в спецификации Servlet API. Если быть более точным, то это ServletContextListener, то есть он срабатывает на инициализацию Servlet Context'а для нашего веб приложения. Далее надо указать настройку, которая расскажет Spring, где лежит его особый xml конфиг с настройками:
От Hello World до Spring Web MVC и при чём тут сервлеты - 8
Как видно, это просто обычная настройка, которая хранится на уровне Servlet Context, но которая будет использована Spring'ом при инициализации Application Context. Теперь нужно объявить вместо всех сервлетов один единственный диспетчер, распределяющий все остальные запросы.
От Hello World до Spring Web MVC и при чём тут сервлеты - 9
И тут никакой магии. Если мы посмотрим — это HttpServlet, просто в котором Spring выполняет много всего, что делает его фрэймворком. Осталось соотнести (смапить) определённый шаблон url с сервлетом:
От Hello World до Spring Web MVC и при чём тут сервлеты - 10
Всё как мы делали ранее. Теперь давайте создадим что-нибудь, что должен показать наш веб-сервер. Например, создадим в нашем WEB-INF подкаталог pages, а там файл hello.jsp. Содержимое может быть самым примитивным. Например, внутри тегов html тег h1 с текстом "Hello World". И не забудем создать файл applicationContext.xml, который мы указали ранее. Возьмём пример из документации Spring: "1.10.3. Automatically detecting classes and registering bean definitions".
От Hello World до Spring Web MVC и при чём тут сервлеты - 11
Т.к. мы включаем таким образом автоопределение, то мы можем теперь создать 2 класса (они будут считаться Spring Bean'ами из-за использования специальных Spring аннотаций), которые Spring теперь сам создаст и выполнит с их помощью донастройку нашего приложения:
  1. Веб-конфигурация для примера конфигурирования в Java стиле:

    
    @Configuration
    @EnableWebMvc
    public class WebConfig implements WebMvcConfigurer {
        @Override
        public void configureViewResolvers(ViewResolverRegistry registry) {
            registry.jsp("/WEB-INF/pages/", ".jsp");
        }
        @Override
        public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
            configurer.enable();
        }
    }
    

    Данный пример описан в документации Spring Framework: "1.11. MVC Config".

    Здесь мы регистрируем ViewResolver, который будет помогать определить, как находиться jsp страницы. Второй метод обеспечивает включение "Default servlet".

    Подробнее можно прочитать про это тут: "В чем заключается необходимость и использование default-servlet-handler".

  2. Контроллер HelloController для описания маппинга запросов на определённый JSP

    
    @Controller
    public class HelloController {
        @GetMapping("/hello")
        public String handle(Model model) {
            return "hello";
        }
    }
    

    Здесь мы использовали аннотацию @Controller, описанную в документации в главе "1.4. Annotated Controllers".

Теперь, когда наше приложение будет развёрнуто, то когда мы отправим запрос /webproject/hello (где /webproject — это context root), то сначала будет отрабатывать DispatcherServlet. Он, как главный диспетчер, определит, что мы /* подходит под текущий запрос, значит это DispatcherServlet должен что-то делать. Дальше он пройдётся по всем маппингам, которые найдёт. Увидит, что есть HelloController с методом handle, который замаплен на /hello и выполнит его. Данный метод вернёт текст "hello". Данный текст получит ViewResolver, который расскажет серверу, где искать jsp файлы, которые необходимо отобразить клиенту. Таким образом в конечном итоге клиент получит ту самую заветную страницу.

Заключение

Надеюсь, из статьи будет понятно, что слово "контекст" — это не страшно. Что спецификации, оказываются, очень полезны. А документация — наш друг, а не враг. Надеюсь, будет понятно, на чём основана работа Spring, как он подключается и при чём тут Servlet API.