Введение
Когда-то 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>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
Это слушатель событий сервлет контекста. То есть, когда стартует Servlet Context, стартует и контекст Spring'а (WebApplicationContext).
Что такое 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
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ