Создаём веб-приложение с Java Servlets

java

Одной из самых приятных особенностей Java является её многогранная природа. Конечно, создание традиционных десктопных и даже мобильных приложений — это здорово. Но что, если вы хотите уйти с проторенных дорожек и зайти на территорию разработки web приложений на Java? Для вас есть хорошая новость: в комплекте с языком идёт полноценный Servlet API, который позволяет вам создавать надёжные веб-приложения без особых хлопот.

Это статья для продолжающих. Если вы только начинаете знакомство с Java, советуем пройти быстрый старт, посмотреть видеокурс и выбрать одну из книг для новичков.

Создание приложений на Java с помощью Servlets

Встречайте сервлеты, особый тип Java-программ, выполняемый в пределах веб-контейнера (также называемый контейнером сервлетов, как, например, Tomcat и Jelly), которые позволяют обрабатывать запросы клиентов и ответы сервера просто и эффективно. Сейчас не время и не место дотошно объяснять вам, что из себя представляет сервлет. Достаточно сказать, что сервлеты создаются и уничтожаются их контейнерами, а не разработчиком, и действуют как промежуточный уровень между клиентами (как правило, веб-браузерами) и другими приложениями, запущенными на сервере (например, базами данных).

Сервлет — классная штука, которая помимо всего прочего может принимать данные от клиента, как правило через GET и POST-запросы, работать с cookie и параметрами сеанса. А ещё она обрабатывает данные через дополнительные уровни приложений и отправляет выходные данные клиенту как в текстовом, так и в бинарном форматах (HTML, XML, PDF, JPG, GIF и т.д.), во многих случаях используя Java Server Pages (JSP) файлы.

Лучше всего начинать работу с сервлетами через конкретный пример. Мы создадим простое веб-приложение, которое позволит клиентам регистрироваться с помощью простой HTML-формы. Данные, предоставленные клиентами, будут собираться сервлетом и проверяться валидаторами.

Разбираемся с конфигурационными файлами

Чтобы вы понимали структуру нашего будущего приложения, вот как она будет выглядеть:

Первым шагом к созданию приложения является определение так называемого дескриптора развёртывания. Он указывает, как приложение должно быть развёрнуто в определенной среде. Когда дело касается веб-приложений, дескриптор развёртывания представляет собой простой XML-файл, называемый web.xml и является частью стандартной спецификации Java EE. В нашем случае он будет выглядеть так:

<?xml version="1.0" encoding="UTF-8"?>

<web-app version="3.1"
             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"
             metadata-complete="false">

</web-app>

Как вы видите, web.xml только определяет версию Java Servlet Specification (3.1), которую мы будем использовать в приложении. Разумеется, в нём может быть гораздо больше содержимого, включая директивы сопоставления сервлетов, параметры инициализации, список приветственных файлов и несколько дополнительных настроек. Но чтобы не усложнять процесс разработки, давайте оставим его таким как есть.

Так как наше приложение будет использовать Servlet API и Java Servlet Pages (JSP) для отображения данных клиента, нам нужно загрузить все необходимые JAR. Maven сделает за нас трудную работу, вот как будет выглядеть файл POM:

<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/maven-v4_0_0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.electricalweb.customerapplication</groupId>
    <artifactId>customerapplication</artifactId>
    <packaging>war</packaging>
    <version>1.0-SNAPSHOT</version>
    <name>Customer Application</name>
    <url>http://maven.apache.org</url>

    <dependencies>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <version>3.1.0</version>
        </dependency>

        <dependency>
            <groupId>jstl<groupId>
            <artifactId>jstl</artifactId>
            <version>1.2</version>
        </dependency>

        <dependency>
            <groupId>javax.el</groupId>
            <artifactId>el-api</artifactId>
            <version>2.2</version>
        </dependency>
    </dependencies>

    <build>
        <finalName>Customer Application</finalName>
    </build>
</project>

Если говорить просто, pom.xml определяет все зависимости, необходимые для работы с сервлетами: JSP, Java Standard Tag Library (JSTL) и Java Expression Language (JEL).

Итак, мы уже создали конфигурационные файлы приложения. Однако в текущем состоянии оно буквально ничего не делает. Мы хотим, чтобы клиенты могли регистрироваться с помощью HTML-формы, поэтому следующее, что нам нужно сделать, — это создать JSP-файлы, которые будут отображать вышеупомянутую форму и данные клиента после успешного завершения регистрации. Этим мы сейчас и займёмся.

Работаем над внешним видом

Внешний вид приложения будет определяться двумя JSP-файлами — в контексте MVC они называются представлениями. Первый будет отвечать за отображение формы регистрации и возможных ошибок, вызванных после проверки введённых данных. Второй будет обычной страницей приветствия, в которой будут показаны данные, введённые клиентом, после успешного завершения процесса регистрации.

Вот первый JSP-файл:

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Регистрация</title>
</head>
<body>
    <h1>Регистрация</h1>

    <c:if test="${violations != null}">
        <c:forEach items="${violations}" var="violation">
            <p>${violation}.</p>
        </c:forEach>
    </c:if>

    <form action="${pageContext.request.contextPath}/processcustomer" method="post">
        <label for="firstname">Имя : </label>
        <input type="text" name="firstname" id="firstname" value="${firstname}">
        <label for="lastname">Фамилия:
        <input type="text" name="lastname" id="lastname" value="${lastname}">
        <label for="email">Email: </label>
        <input type="text" name="email" id="email" value="${email}">
        <input type="submit" name="signup" value="Sign Up">
    </form>
</body>
</html>

Файл содержит простой HTML с парочкой дополнений. Вот она, прелесть JSP в сочетании с JSTL и JEL. Обратите внимание на то, как легко проверить наличие ошибок валидации, используя такие стандартные теги, как <с:if> и <c:forEach>.

Атрибут формы регистрации action указывает на следующий URL: ${pageContext.request.contextPath}/processcustomer. Это значит, что каждый раз, когда клиент пытается зарегистрироваться, данные будут отправляться в processcustomer независимо от URL, по которому доступна форма. Это достигается за счёт функциональности объектов, доступных из JSP-файла, таких как request.

Скоро мы увидим, как сервлет связывается с URL processcustomer и как он взаимодействует с введёнными данными. А пока давайте посмотрим на JSP-файл, который отвечает за страницу приветствия:

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Данные клиента</title>
</head>
<body>
    <h1>Спасибо за регистрацию!</h1>
    <h2>Ваши введённые данные:</h2>
    <p><strong>Имя:</strong> ${firstname}</p>
    <p><strong>Фамилия:</strong> ${lastname}</p>
    <p><strong>Email: </strong>${email}</p>
</body>
</html>

Теперь, когда мы разобрались с отображением страниц, следующим шагом будет создание сервлета, ответственного за сбор данных клиента из POST-запросов и подтверждение данных простым способом.

Пишем контроллер

Написать сервлет, способный получить данные из формы регистрации, проще простого. Всё, что нам нужно сделать, — это написать подкласс для класса HttpServlet и реализовать его методы doGet() или doPost() (или оба, если надо). В данном случае сервлет будет взаимодействовать с данными, поступающими из POST-запросов.

Вот как он выглядит:

@WebServlet(name = "CustomerController", urlPatterns = "/processcustomer")
public class CustomerController extends HttpServlet {

    @Override
    protected void doPost(
            HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {

        RequestCustomer customer = RequestCustomer.fromRequestParameters(request);
        customer.setAsRequestAttributes(request);
        List violations = customer.validate();

        if (!violations.isEmpty()) {
            request.setAttribute("violations", violations);
        }

        String url = determineUrl(violations);
        request.getRequestDispatcher(url).forward(request, response);
    }

    private String determineUrl(List violations) {
        if (!violations.isEmpty()) {
            return "/";
        } else {
            return "/WEB-INF/views/customerinfo.jsp";
        }
    }

    private static class RequestCustomer {

        private final String firstName;
        private final String lastName;
        private final String email;

        private RequestCustomer(String firstName, String lastName, String email) {
            this.firstName = firstName;
            this.lastName = lastName;
            this.email = email;
        }

        public static RequestCustomer fromRequestParameters(
            HttpServletRequest request) {
            return new RequestCustomer(
                    request.getParameter("firstname"),
                    request.getParameter("lastname"),
                    request.getParameter("email"));
        }

        public void setAsRequestAttributes(HttpServletRequest request) {
            request.setAttribute("firstname", firstName);
            request.setAttribute("lastname", lastName);
            request.setAttribute("email", email);
        }

        public List validate() {
            List violations = new ArrayList<>();
            if (!StringValidator.validate(firstName)) {
                    violations.add("Имя является обязательным полем");
            }
            if (!StringValidator.validate(lastName)) {
                    violations.add("Фамилия является обязательным полем");
            }
            if (!EmailValidator.validate(email)) {
                    violations.add("Email должен быть правильно сформирован");
            }
            return violations;
        }

    }

}

Первое, на что здесь стоит обратить внимание, — использование аннотации @WebServlet(name ="CustomerController", urlPatterns = "/processcustomer"). Она говорит контейнеру сервлета использовать класс CustomerController для обработки HTTP-запросов по адресу /processcustomer. Того же эффекта можно достичь путём добавления директив сопоставления сервлетов в web.xml, как здесь, но так как мы используем Servlet Specification 3.1 нам нет необходимости прибегать к такому способу.

Здесь мы назвали сервлет CustomerController, так как считается хорошей практикой использовать имя класса сервлета в качестве значения атрибута name аннотации @WebServlet. В противном случае некоторые контейнеры не смогут выполнить сопоставление, что приведёт к ошибке 404.

Сам класс CustomerController выполняет несколько простых задач. Во-первых, он собирает введённые в форму данные, используя реализацию интерфейса HttpServletRequest, который содержит значения, соответствующие полям firstname, lastname и email формы. Затем он устанавливает эти значения в качестве атрибутов запроса, поэтому их можно повторно отобразить либо в форме, либо на странице с результатами. Наконец, валидаторы проверяют правильность введённых данных.

Валидаторы — это простые классы, которые проверяют определённые свойства, например, является ли строка пустой и выглядит ли email как email. На GitLab автора можно посмотреть на их реализацию.

Результат валидации влияет на дальнейший ход событий: если данные не валидны, клиент перенаправляется через объект RequestDispatcher на страницу регистрации, где отображаются соответствующие ошибки. Если всё в порядке, то отображается страница приветствия.

Итак, мы создали целое веб-приложение на Java, которое позволяет зарегистрировать клиентов с помощью HTML-формы, базового сервлета и нескольких JSP-файлов. Пора его запустить.

Запускаем приложение

Для запуска приложения нужно проделать следующие шаги:

  1. Для начала нам понадобится Git (убедитесь, что скачиваете подходящую версию), Maven и контейнер сервлета (например, Apache TomcatJetty, или JBoss Wildfly). Если что-то из этого уже включено в вашу IDE, то вы можете использовать встроенный вариант.
  2. Используйте Git, чтобы клонировать репозиторий приложения, и импортируйте его в вашу IDE, желательно как проект Maven.
  3. Разверните проект в контейнере сервлета и запустите его. Развёртывание подразумевает создание WAR-файла или exploded WAR и его помещение в папку развёртывания контейнера по умолчанию. Зачастую IDE способна сделать развёртывание за вас, поэтому не перегружайте себя лишней работой и посмотрите документацию вашей IDE (документация для IntelliJ IDEA). Когда вы развернёте проект и запустите его, должен запуститься бразуер по умолчанию с окном регистрации.
  4. Попробуйте заполнить не все поля в форме или вовсе не заполнить их и вы увидите, как поверх соответствующих полей отобразятся ошибки. Введите всё как положено, и вас перенаправит на страницу приветствия.

Заключение

Итак, вы приобрели все навыки, необходимые для создания собственного веб-приложения на Java без необходимости прибегать к сложным фреймворкам. Всё, что вам нужно, — Servlet API, технология вроде JSP для отображения и встроенные средства Java. Здорово, правда?

Стоит отметить, что реализация класса CustomerController подчеркивает достоинства и недостатки сервлетов: с одной стороны, он вкратце показывает, насколько легко обрабатывать параметры запроса и отправлять ответы клиенту в разных форматах. Но эта функциональность имеет свою цену: обе реализации интерфейсов HttpServletResponse и HttpServletResponse являются обычными локаторами служб. Нельзя сказать, что это плохо, поскольку локаторы просто содержат данные. Однако нужно помнить, что эти реализации будут всегда привязаны к сервлету.

Перевод статьи «Building a Web App with Java Servlets»

Вакансии в тему:

Лого компании «Finch»
Android-разработчик
Android-разработчик
Finch, Москва, от 80 000 до 150 000 ₽ (до налогов)
Лого компании «Додо Пицца»
Старший .NET разработчик
Старший .NET разработчик
Додо Пицца, Москва, от 100 000 до 250 000 ₽
Лого компании «CSBI»
Full-stack разработчик ASP.NET
Full-stack разработчик ASP.NET
CSBI, Санкт-Петербург, от 80 000 до 120 000 ₽
Лого компании «Банк Точка»
Android developer
Android developer
Банк Точка, Екатеринбург, 130 000 ₽