Введение

Если почитать историю языка Java, то мы увидим, что когда-то Java укрепил свои позиции благодаря тому, что выбрал приоритетным направлением веб-приложения. С первых дней Java пытался найти свой путь. Сначала Java предложил апплеты. Это предоставило много возможностей разработчикам по созданию динамического контента (содержимого) на статических HTML страницах. Однако, апплеты не оправдали ожиданий по многим причинам: безопасность, накладные расходы и другие. Тогда разработчики языка Java предложили альтернативу - Servlet API. И это оказалось правильным решением. Servlet API является спецификацией, на основе которой построено любое веб-приложение на Java, будь то приложение с веб-интерфейсом или веб-сервис, который возвращает информацию согласно запросу. Поэтому, путь к пониманию работы веб-приложений на Java начинается с понимания Servlet API.

Servlet API

Итак, Servlet API - это то, что предложили разработчики языка Java разработчикам. Servlet API - это спецификация, которую можно найти и почитать, которая должна отвечать на основные наши вопросы. Найти спецификацию можно тут: "JSR-000340 JavaTM Servlet 3.1 Final Release for Evaluation". В главе "1.1 What is a Servlet?" сказано, что сервлет - это веб-компонент, который создаёт динамический контент (то есть содержимое) и он основан на Java технологии. Основан на Java технологии означает, что сервлет представляет из себя Java класс, скомпилированный в байт-код. Сервлеты управляются контейнером сервлетов, который иногда называют Servlet Engine. Сервлет контейнер - это расширение веб-сервера, которое предоставляет функциональность сервлетов. В свою очередь сервлеты обеспечивают взаимодействие с клиентом в парадигме запрос/ответ, которая и реализуется сервлет-контейнером. В главе "1.2 What is a Servlet Container?" сказано, что сервлет контейнер - это часть веб-сервера или сервера приложений, который предоставляет сетевые сервисы, при помощи которых запросы и ответы посылаются, формируются и обрабатываются MIME-based запросы и ответы. Кроме того, сервлет контейнереы управляют жизненным циклом сервлетов (т.е. решают, когда их создавать, удалять и т.п.). Все сервлет контейнеры должны поддерживать протокол HTTP для получения запросов и отправления ответов. Тут хочется добавить, что MIME - это такой стандарт, спецификация, которая говорит, как надо кодировать информацию и форматировать сообщения, чтобы их можно было пересылать по интернету.

Web-server

Веб-сервер - это сервер, который принимает HTTP-запросы от клиентов и выдающий им HTTP ответы (как правило, вместе с HTML страницей, изображением, файлом или другими данными). Запрашиваемые ресурсы обознаются URL-адресами. Одним из самых популярных веб-серверов с поддержкой Servlet API является Apache Tomcat. Большинство веб-серверов - сложные механизмы, которые состоят из различных компонентов, каждый из которых выполняет свои функции. Например:

Connectors

— На входе у нас есть Connectors (т.е. коннекторы), которые принимают входящие запросы от клиентов. HTTP коннектор в Tomcat реализован при помощи компонента "Coyote". Коннекторы принимают данные от клиента и передают их дальше в Tomcat Engine. Servlet Container - Tomcat Engine в свою очередь обрабатывает полученный от клиента request при помощи компонента "Catalina", который является сервлет контейнером. Подробнее см. документацию Tomcat : "Architecture Overview". Сущестуют и другие веб-серверы, поддерживающие спецификацию Servlet API. Например, "Jetty" или "Undertow". Они все имеют схожу архитектуру и понимая работу с одним сервлет контейнером можно перестроится на работу с другим.

Веб-приложение (Web Application)

Итак, чтобы веб-приложение мы смогли запустить, нам нужен веб-сервер, который поддерживает Servlet API (т.е. в котором есть компонент-расширение, которое для веб-сервера реализует поддержку Servlet API). Хорошо. А что же такое вообще веб-приложение? Согласно главе "10 Web Applications" спецификации Servlet API, Web application - это набор сервлетов, HTML страниц, классов и других ресурсов, которые составляют законечнное приложение на веб-сервере. Согласно главе "10.6 Web Application Archive File", веб-приложение может быть запаковано в Web ARchive (в архив с расширением WAR). Как сказано на странице "Glossary-219":
То есть WAR сделано вместо JAR, чтобы показать, что это веб-приложение. Следующим важным для понимания фактом является то, что у нас должна быть определённая структура каталогов в нашем WAR архиве. В спецификации Servlet API в главе "10.5 Directory Structure". В этой главе сказано, что есть особый каталог, который называется "WEB-INF". Данный каталог особен тем, что он не виден клиенту и не может быть напрямую ему показан, но доступен для кода сервлетов. Так же сказано, что может содержать каталог WEB-INF:
Из всего этого списка нам теперь неизвестен и непонятен пункт про какой-то файл web.xml, называемый deployment descriptor (дескриптор развёртывания). Что же это такое? Деплоймент дескриптору отведена глава "14. Deployment Descriptor". Если кратко, то деплоймент дескриптор - это такой xml файл, который описывает, как нужно разворачивать (то есть запускать) на веб-сервере наше веб-приложение. Например, в деплоймент дескрипторе указывается по каким URL надо обращаться к нашему приложению, указываются настройки безопасности, которые относятся к нашему приложению и т.д. В главе "14.2 Rules for Processing the Deployment" сказано, что web.xml прежде чем наше приложение будет настроено и запущено будет провалидирован по схеме (то есть будет выполнена проверка, что содержимое web.xml написано правильно согласно схеме). А в главе "14.3 Deployment Descriptor" указано, что схема лежит здесь: http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd Если посмотреть на содержимое файла, то мы можем увидеть:
Для чего используется схема для XML файлов? В схемах указано, как правильно заполнять XML документ: какие элементы можно использовать, какого типа данные в элементах могут быть указаны, в каком порядке элементы должны идти, какие элементы обязательны и т.д. Можно сравнить схему XML документа с интерфейсом в Java, ведь схема в Java тоже указывает, каким образом должны быть написаны классы, которые удовлятворяют данному интерфейсу (т.е. которые реализуют данный интерфейс). Итак, мы вооружены тайными знаниями и готовы создавать своё первое веб-приложение!

Создание веб-приложения

Работы с современным Java приложением уже трудно представить без использования систем автоматической сборки проектов. Одними из самых популярных систем являются Maven и Gradle. Воспользуемся для данного обзора Gradle. Установка Gradle описано на официальном сайте. Для создании нового приложения нам понадобится встроенный в Gradle плагин: "Build Init Plugin". Для выполнения создания java приложения необходимо выполнить следующую команду: gradle init --type java-application
После создания проекта нам понадобится отредактировать файл build.gradle. Это так называемый Build Script (подробнее см. документацию Gradle: "Writing Build Scripts"). В данном файле описывается то, каким образом необходимо собирать проект и другие аспекты работы с Java проектом. В блоке plugins описывается, какие "Gradle плагины" необходимо использовать для текущего Gradle проекта. Плагины расширяют возможности нашего проекта. Например, по умолчанию исопльзуется плагин "java". Данный плагин всегда используется, если нам нужна поддержка Java. Но вот плагин "application" нам не нужен, т.к. в его описании указано, что он служит для обеспечения создания "executable JVM application", т.е. выполняемых JVM приложений. Нам же нужно создание Web приложения в виде WAR архива. И если мы в документации Gradle поищем слово WAR, то мы найдём и "War Plugin". Следовательно, укажем следующие плагины:
plugins {
    id 'java'
    id 'war'
}
Так же в "War Plugin Default Settings" сказано, что каталог со всем содержимым веб-приложения должен быть "src/main/webapp", там должен лежать тот самый каталог WEB-INF, в котором должен лежать web.xml. Создадим такой файл. Заполнять же его будем несколько позднее, т.к. мы ещё не имеем достаточно информации для этого. В блоке "dependencies" указываем зависимости нашего проекта, то есть те библиотеки/фрэймворки, без которых не может работать наше приложение. В данном случае, мы пишем веб-приложение, а значит мы не можем работать без Servlet API:
dependencies {
    providedCompile 'javax.servlet:javax.servlet-api:3.1.0'
    testCompile 'junit:junit:4.12'
}
providedCompile говорит о том, что зависимость не нужно включать в наш WAR архив веб-приложения, она нужна только для компиляции. А при выполнении данная зависимость будет предоставлена кем-то другим (то есть веб-сервером). Ну и оставляем в build script информацию о том, какой репозиторий зависимостей мы хотим использовать (из него будут скачаны все указанные dependencies):
repositories {
    jcenter()
}
Всё остальное из файла build script'а убираем. Теперь отредактируем класс src\main\java\App.java. Сделаем из него сервлет. В спецификации Servlet API в главе "CHAPTER 2. The Servlet Interface" сказано, что Servlet Interface имеет базовую реализацию HttpServlet, которой должно быть достаточно в большинстве случаев и разработчикам достаточно унаследоваться от неё. А в главе "2.1.1 HTTP Specific Request Handling Methods" указаны основные методы, которые обрабатывают входящие запросы. Таким образом, перепишем класс App.java:
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.PrintWriter;
import java.io.IOException;

public class App extends HttpServlet {
    public String getGreeting() {
        return "Hello world.";
    }

    public void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
 		// https://www.oracle.com/technetwork/java/servlet-142430.html
 		PrintWriter out = resp.getWriter();
 		out.println(this.getGreeting());
 		out.close();
 	}
}
Итак, у нас вроде всё готово. Осталось правильно написать дескриптор развёртывания. Из схемы скопируем в web.xml следующий текст:
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:schemaLocation="..."
      version="3.1">
      ...
</web-app>
А так же путь к схеме, который так же указан в самой схеме: http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd Теперь посмотрим пример того, как должен выглядеть web.xml в спецификации Servlet API. Данный пример указан в главе "14.5.1 A Basic Example". Совместим то, что указано в схеме с тем примером, который указан в спецификации. Получим следующее:
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
      http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
      version="3.1">
      <display-name>A Simple Web Application</display-name>
      <servlet>
		<servlet-name>app</servlet-name>
		<servlet-class>App</servlet-class>
	  </servlet>
	  <servlet-mapping>
		<servlet-name>app</servlet-name>
		<url-pattern>/app</url-pattern>
	  </servlet-mapping>
</web-app>
Как видно, мы использовали схему и schemaLocation, которые были указаны ранее. А описание самих элементов взяли из примера из главы 14.5.1. Если мы сделали всё правильно, то выполним gradle задачу gradle war без ошибок:

Запуск веб-приложения

Как же происходит запуск веб-приложения? Давайте разберёмся сначала с более сложным вариантом. Мы ранее уже говорили, что есть Apache Tomcat веб-сервер, который поддерживает Servlet API. А это значит, что наш собранный war архив мы сможем развернуть (ещё говорят "задеплоить") на этом сервере. На странице "Download Tomcat" скачаем из раздела "Binary Distributions" тип поставки "Core" в формате zip. И распакуем скачанный архив в какой-нибудь каталог, например в C:\apache-tomcat-9.0.14. Прежде чем запускать сервер откроем на редактирование файл conf\tomcat-users.xml и добавим в него следующую строку: <user username="tomcat" password="tomcat" roles="tomcat,manager-gui,admin-gui"/> Теперь в командной строке переходим в каталог bin и выполняем catalina.bat start. По умолчанию консоль сервера будет доступна по адресу http://localhost:8080/manager. Логин и пароль те самые, которые мы указали в tomcat-users.xml. У Tomcat'а есть каталог "webapps", в котором лежат веб-приложения. Если мы хотим развернуть своё - мы должны скопировать туда свой war архив. Когда мы ранее выполнили команду gradle war, то в каталоге \build\libs\ создался war архив. Вот его то нам и надо скопировать. После копирования обновим страницу http://localhost:8080/manager и увидим:
Выполнив http://localhost:8080/javaweb/app мы обратимся к нашему сервлету, т.к. обращение /app ранее мы "замапили" (то есть сопоставили) на сервлет App. Существует более быстрый способ проверить, как работает приложение. И в этом нам снова помогает система сборки. В build script нашего Gradle проекта мы можем добавить в секцию plugins новый плагин "Gretty": id "org.gretty" version "2.3.1" И теперь мы можем выполнять gradle task для запуска нашего приложения: gradle appRun
Подробнее см. "Add the gretty plugin and run the app".

Spring и Servlet API

Сервлеты — основа всего. И даже популярный сейчас Spring Framework ничто иное, как надстройка над Servlet API. Для начала, Spring Framework - это новая зависимость для нашего проекта. Поэтому, добавим её в build script в блок dependencies: compile 'org.springframework:spring-webmvc:5.1.3.RELEASE' В документации Spring Framework есть глава "1.1. DispatcherServlet". В ней сказано, что Spring Framework построен по шаблону "front controller" - это когда есть центральный сервлет, который называется "DispatcherServlet". Все запросы поступают на этот сервлет, а он делегирует уже вызовы нужным компонентам. Видите, даже тут сервлеты. В деплоймент дескриптор необходимо добавить listener (слушателя):
<listener>
	&ltlistener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
Данный слушатель является слушателем событий сервлет контекста. То есть когда стартует Servlet Context, то стартует и контекст Spring'а (WebApplicationContext). Что такое Servlet Context? Servlet Context описан в спецификации Servle API в главе "CHAPTER 4. Servlet Context". Сервлет контекст - это "взгляд" сервлетов на веб-приложение, внутри которого сервлеты запущены. У каждого веб-приложения свой Servlet Context. Дальше для включения Spring Framework необходимо указать context-param - параметр инициализации сервлет контекста.
<context-param>
	<param-name>contextConfigLocation</param-name>
	<param-value>/WEB-INF/app-context.xml</param-value>
</context-param>
И завершает конфигурацию определение DispatcherServlet:
<servlet>
	<servlet-name>app</servlet-name>
	<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
	<init-param>
		<param-name>contextConfigLocation</param-name>
 		<param-value></param-value>
	</init-param>
	<load-on-startup>1</load-on-startup>
</servlet>

<servlet-mapping>
	<servlet-name>app</servlet-name>
	<url-pattern>/</url-pattern>
</servlet-mapping>
И теперь нам осталось заполнить файл, указанный в contextConfigLocation. То, как это сделать указано в документации Spring Framework в главе "1.3.1. Declaration":
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:p="http://www.springframework.org/schema/p"
    xmlns:mvc="http://www.springframework.org/schema/mvc"
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context.xsd
        http://www.springframework.org/schema/mvc
        http://www.springframework.org/schema/mvc/spring-mvc.xsd">

    <context:component-scan base-package="ru.javarush.javaweb"/>
    <mvc:annotation-driven/>
</beans>
Тут важно не только указать, какой пакет сканировать, но и указать, что мы хотим annotation-driven, то есть управлять аннотациями тем, как будет работать Spring. Остаётся только создать пакет ru.javarush.javaweb и разместить в нём класс Spring контроллера:
package ru.javarush.javaweb;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class SpringController {

    @GetMapping("/app")
    @ResponseBody
    public String getGreeting() {
        return "Hello world.";
    }
}
Запустив теперь gradle appRun и перейдя по адресу http://127.0.0.1:8080/javaweb/app мы получим всё тот же Hello World. Как видно, Spring Framework тесно переплетается с Servlet API и использует его, чтобы работать поверх него.

Аннотации

Как мы видели, аннотации - это удобно. И не мы одни так подумали. Поэтому в спецификации Servlet API начиная с версии 3.0 появилась глава "CHAPTER 8 Annotations and pluggability", которая указывает, что сервлет контейнеры должны поддерживать возможность указывать то, что раньше указывалось в Deployment Descriptor'е через аннотации. Таким образом, web.xml можно совсем удалить из проекта, а над классом сервлета указывать аннотацию @WebServlet и указывать, на какой путь "мапить" сервлет. Тут вроде всё понятно. Но что делать, если мы к проекту подключили Spring, который требует более сложных настроек? Тут всё чуть-чуть сложнее. Во-первых, в документации Spring сказано, что для настройки Spring без web.xml нужно использовать свой класс, который будет реализовывать WebApplicationInitializer. Подробнее см. главу "1.1. DispatcherServlet". Получается, что это класс Spring'а. Как же тогда используется Servlet API тут? На самом деле, в Servlet API 3.0 был добавлен ServletContainerInitializer. Используя особый механизм в Java (называется SPI), Spring указывает свой инициализатор сервлет контейнера, который называется SpringServletContainerInitializer. В свою очередь он уже ищет реализации WebApplicationInitializer и вызывает нужные методы и выполняет нужные настройки. Подробнее см. "How servlet container finds WebApplicationInitializer implementations". Таким образом, выше указанные настройки можно выполнить следующим образом:
package ru.javarush.javaweb.config;

import org.springframework.web.WebApplicationInitializer;
import org.springframework.web.context.ContextLoaderListener;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
import org.springframework.web.servlet.DispatcherServlet;

import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRegistration;

public class AppInitializer implements WebApplicationInitializer {
    @Override
    public void onStartup(ServletContext servletContext) throws ServletException {
        AnnotationConfigWebApplicationContext ctx = new AnnotationConfigWebApplicationContext();
        // регистрируем конфигурацию созданую высше
        ctx.register(AppConfig.class);
        // добавляем в контекст слушателя с нашей конфигурацией
        servletContext.addListener(new ContextLoaderListener(ctx));

        ctx.setServletContext(servletContext);

        // настраиваем маппинг Dispatcher Servlet-а
        ServletRegistration.Dynamic servlet =
                servletContext.addServlet("dispatcher", new DispatcherServlet(ctx));
        servlet.addMapping("/");
        servlet.setLoadOnStartup(1);
    }
}
Теперь при помощи "Java-based configuration" укажем, какой пакет сканировать + включим аннотации:
package ru.javarush.javaweb.config;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;

@Configuration
@EnableWebMvc
@ComponentScan(basePackages = "ru.javarush.javaweb.controllers")
public class AppConfig {
}
А сам SpringController перенесён был в ru.javarush.javaweb.controllers, чтобы при сканировании конфигурация не находила сама себя, а искала только контроллеры.

Подведение итогов

Надеюсь, данный краткий обзор пролил свет на то, как в Java работают веб-приложения. Это лишь верхушка айсберга, но непонимая основы трудно понимать, как работают технологии, основанные на этой основе. Servlet API - центральная часть любых веб-приложений на Java и мы разобрались, как в это встраиваются другие фрэймворки. А для продолжения можно посмотреть следующие материалы: #Viacheslav