Стас Пасинков
26 уровень
Киев

Создание простого веб-приложения на сервлетах и jsp (часть 1)

Пост из группы Java Developer
1709 участников
Уровень знаний, необходимых для понимания статьи: вы уже более-менее разобрались с Java Core и хотели бы посмотреть на JavaEE-технологии и web-программирование. Логичнее всего, если вы сейчас изучаете квест Java Collections, где рассматриваются близкие статье темы.
Этот материал является логическим продолжением моей статьи Создание простейшего веб-проекта в IntelliJ Idea Enterprise. В ней я продемонстрировал, как создать работающий шаблон веб-проекта. В этот раз я покажу, как создать простое, но вполне симпатичное веб-приложение с помощью технологий Java Servlet API и JavaServer Pages API. У нашего приложения будет главная страница с двумя ссылками:
  • на страницу добавления пользователя;
  • на страницу просмотра списка пользователей.
Я по-прежнему буду использовать IntelliJ Idea Enterprise Edition, Apache Maven (просто подключим несколько зависимостей) и Apache Tomcat. В конце «украсим» наше приложение используя фреймворк W3.CSS. Будем считать, что на данный момент у вас уже есть пустой проект, который мы и будем тут развивать. Если же нету — пробегитесь по первой статье и сделайте его. Это займет всего лишь несколько минут :)

Немного о структуре будущего приложения

Главная страница (/) у нас будет самой обычной статической html-страничкой с шапкой и двумя ссылками/кнопками:
  • добавить нового пользователя (будет отправлять на адрес /add);
  • просмотреть список пользователей (отправляет на адрес /list).
Запросы по этим адресам Tomcat будет ловить и отправлять на один из двух сервлетов, которые мы сделаем (маппинг мы распишем в файле web.xml). А сервлеты, в свою очередь, будут обрабатывать запросы, подготавливать данные (или сохранять их в случае добавления пользователя), и передавать управление в соответствующие jsp-файлы, которые уже будут «отрисовывть» результат. Данные будем хранить в самом обычном списке (List).

Создадим статическую главную страницу

Если у вас в папке web лежит index.jsp — удаляйте его. Вместо него в этой папке создадим простой html-файл с именем index.html:
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>My super project!</title>
</head>
<body>
    <!-- header -->
    <div>
        <h1>Super app!<//h1>
    </div>

    <div>       <!-- content -->
        <div>    <!-- buttons holder -->
            <button onclick="location.href='/list'">List users<//button>
            <button onclick="location.href='/add'">Add user<//button>
        </div>
    </div>
</body>
</html>
Тут ничего сложного. В title указываем заголовок нашей страницы. В теле страницы у нас два основных дива: header (шапка) и content (контент). В контенте у нас холдер для наших кнопок, ну и собственно две кнопки, которые по нажатию отправляют на соответствующие адреса. Можете запустить проект, и посмотреть, как он сейчас выглядит. Если нажимать на кнопки, открываются страницы с ошибкой 404, потому что у нас их пока нет. Но это говорит о том, что кнопки работают. Замечу, это не самый универсальный вариант, поскольку если у вас вдруг отключён JavaScript, в браузере от этих кнопок толку никакого. Но будем считать, что никто JavaScript не отключал:). Ясно, что можно было бы обойтись простыми ссылками, но мне больше по душе кнопочки. Вы же делайте, как вам больше нравится. И не смотрите, что в моих примерах будет много div-ов. Потом мы их наполним стилями, и всё станет выглядеть красивее:).

Создаем jsp-файлы для отрисовки результата

В той же директории web создадим папку, куда будем складывать наши jsp-файлы. Я назвал её views, а вы, снова-таки, можете поимпровизировать. В этой папке создадим два jsp-файла:
  • add.jsp — страничка для добавления пользователей;
  • list.jsp — страничка для показа списка пользователей.
Проставим им соответствующие заголовки страницы. Что-нибудь вроде «Add new user» и «Users list», и пока так и оставим.

Создадим два сервлета

Сервлеты будут принимать и обрабатывать запросы, которые им будет передавать Tomcat. В папке src/main/java создадим пакет app, в котором будут лежать наши исходники. Там у нас будет ещё разных пакета. Поэтому, чтобы эти пакеты не создавались внутри друг друга, давайте создадим в пакете app какой-нибудь класс (потом удалим). Теперь создадим в пакете app три разных пакета:
  • entities — тут будут лежать наши сущности (сам класс, который будет описывать объекты пользователей);
  • model — тут будет наша модель (об этом чуть позже);
  • servlets — ну а тут будут наши сервлеты.
После этого тот класс из пакета app можно спокойно удалять (если вы его, конечно, создавали). В пакете servlets создадим два класса:
  • AddServlet — будет обрабатывать запросы, поступившие по адресу /add;
  • ListServlet — будет обрабатывать запросы, поступившие по адресу /list.

Подключение зависимостей в Maven

Tomcat версии 9.* реализует спецификации Servlet версии 4.0 и JavaServer Pages версии 2.3. Об этом написано в официальной документации 9-го Tomcat в первом же абзаце во второй строке. Это значит, что если вы, как и я, используете эту версию Tomcat, то наш код, который мы напишем и отправим выполняться, будет использовать именно указанные версии. Но нам бы хотелось иметь эти спецификации и в нашем проекте, чтобы наш код, который их использует, хотя бы успешно компилировался. А для этого нам надо их подгрузить к себе в проект. Вот тут-то и приходит на помощь Maven.

Общее правило следующее: если вам надо подключить что-то к вашему проекту, используя Maven:

  • идете на сайт репозитория от Maven;
  • ищите там нужную вам библиотеку нужной версии;
  • получаете код зависимости, который надо вставить в ваш pom.xml;
  • вставляете! :)
Итак, начинаем. Для начала, подготовим pom-файл. Где-то после записи /version, но до /project вставляете следующее:
<dependencies>

</dependencies>
Тем самым мы указали, что внутри этих тегов мы перечислим нужные нам зависимости. Теперь заходите на mvnrepository.com, там вверху будет поле поиска. Для начала вбиваете в поиск servlet. Первый же результат, где более семи тысяч использований, нам и подходит. Помним, что нам нужна версия 4.0 (для 9-го Tomcat, для других версий, возможно, подойдут и более старые реализации). Это довольно свежая версия, поэтому использований не так уж и много, но нам нужна именно она. Откроется страница, откуда можно взять код этой зависимости для разнообразных менеджеров пакетов и даже можно просто скачать. Но поскольку мы хотим подключить её с помощью Maven, то и код выбираем на вкладке Maven. Копируем и вставляем в наш pom-файл внутрь раздела с зависимостями. Если в правом нижнем углу IDEA вылезет уведомление, где спросят, хотим ли мы включить автоимпорт, соглашаемся. Если случайно отказались, заходите в «Настройки» и включите автоимпорт вручную: Settings (Ctrl + Alt + S) -> Build, Execution, Deployment -> Maven -> Importing Это позволит держать pom-файл и файлы настройки IDEA для этого проекта синхронизированными. Теперь по тому же принципу найдем и подключим JavaServer Pages версии 2.3 (в поиске вбиваете jsp). И раз мы уже взялись за Maven, давайте сразу укажем ему, что наши исходники соответствуют синтаксису Java 8, и что компилировать их нужно в байткод той же версии. После всех этих манипуляций наш 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>ru.javarush.info.fatfaggy</groupId>
    <artifactId>my-super-project</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compile.source>1.8</maven.compile.source>
        <maven.compile.target>1.8</maven.compile.target>
    </properties>

    <dependencies>
        <!-- Servlet API 4.0 for tomcat 9 -->
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <version>4.0.0</version>
            <scope>provided</scope>
        </dependency>

        <!-- JavaServer Pages API 2.3 for tomcat 9 -->
        <dependency>
            <groupId>javax.servlet.jsp</groupId>
            <artifactId>javax.servlet.jsp-api</artifactId>
            <version>2.3.1</version>
            <scope>provided</scope>
        </dependency>
    </dependencies>

</project>

Делаем из наших сервлетов настоящие сервлеты

В данный момент пара созданных нами сервлетов на деле являются обычными классами. У них нет никакой функциональности. Но теперь-то мы подключили к нашему проекту Servlet API, а, раз так, мы можем использовать классы оттуда. Чтоб сделать наши сервлеты «настоящими» сервлетами, достаточно просто унаследовать их от класса HttpServlet.

Маппинг или разметка

Теперь было бы неплохо как-то рассказать Tomcat, чтобы запросы с адреса /add обрабатывались нашим сервлетом AddServlet, и, соответственно, запросы по адресу /list обрабатывались сервлетом ListServlet. Именно этот процесс и называется маппингом(разметкой). Делается это в файле web.xml по такому принципу:
  • сначала описываем сервлет (даем какое-то имя и указываем путь к самому классу);
  • потом привязываем этот сервлет к конкретному адресу (указываем имя сервлета, которое мы ему только-что дали и указываем адрес, запросы с которого стоит отправлять на этот сервлет).
Описываем сервлет:
<servlet>
    <servlet-name>add</servlet-name>
    <servlet-class>app.servlets.AddServlet</servlet-class>
</servlet>
Теперь привязываем его к адресу:
<servlet-mapping>
    <servlet-name>add</servlet-name>
    <url-pattern>/add</url-pattern>
</servlet-mapping>
Как видно, servlet-name в обоих случаях одинаковое. Благодаря этому Tomcat знает, что если пришел запрос на адрес /add, его нужно передать в сервлет app.servlets.AddServlet. Со вторым сервлетом проделываем то же самое. В итоге, наш web.xml имеет примерно следующее содержание:
<?xml version="1.0" encoding="UTF-8"?>
<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">

    <!-- add servlet -->
    <servlet>
        <servlet-name>add</servlet-name>
        <servlet-class>app.servlets.AddServlet</servlet-class>
    </servlet>

    <servlet-mapping>
        <servlet-name>add</servlet-name>
        <url-pattern>/add</url-pattern>
    </servlet-mapping>

    <!-- list servlet -->
    <servlet>
        <servlet-name>list</servlet-name>
        <servlet-class>app.servlets.ListServlet</servlet-class>
    </servlet>

    <servlet-mapping>
        <servlet-name>list</servlet-name>
        <url-pattern>/list</url-pattern>
    </servlet-mapping>
</web-app>
Кстати, мы тут не создали разметку для главной страницы (по адресу /). Дело в том, что в данном случае он нам и не нужен. Наша главная страница — простой html-файл, который просто отображает две кнопки. Там нет динамического контента, поэтому нам нет смысла заводить под него отдельный сервлет, на который будут передаваться запросы с адреса /, и который не будет ничего делать, кроме передачи выполнения на какую-нибудь jsp (которую тоже пришлось бы завести), которая и рисовала бы нам две кнопки. Нам этого не нужно, нас устраивает статичный ресурс. Tomcat когда получит запрос, проверит, что нет ни одного сервлета, который смог бы обработать запрос по такому адресу, а потом увидит что по этому адресу собственно лежит уже готовый html-файл, который он успешно и отдаст. Можем запустить наше приложение снова (перезапустить сервер или задеплоить повторно, как вам больше хочется) и убедиться, что главная страничка отрисовывается, ничего не сломалось, когда нажимаем на кнопки — то переходы происходят, но пока тоже пишется ошибка. Кстати, если до этого у нас была ошибка 404, то теперь 405. Значит маппинг сработал, сервлеты нашлись, да вот только не нашлось в них подходящих методов чтоб обработать запрос.

Краткое лирическое отступление: что происходит «под капотом»?

Вы, наверное, уже успели задуматься, каким образом наше приложение работает в Tomcat? Что там вообще происходит? И где метод main()? Как только вы вбиваете в браузере localhost:8080 и переходите по этому адресу, браузер отправляет на этот адрес запрос по протоколу http. Надеюсь, вы уже в курсе, что запросы могут быть разных «типов», самый популярные — GET и POST. На каждый запрос должен быть ответ. GET-запрос ожидает, что в ответ ему отдадут готовый html-код, который вернется в браузер, а браузер уже этот код красиво заменит на всякие буковки, кнопочки, формочки. POST-запрос немного интереснее, так как он с собой еще несет некую информацию. Например, в форме регистрации или авторизации пользователя вы ввели свои данные и нажали «отправить». В этот момент на сервер отправился POST-запрос с вашей личной информацией внутри. Сервер эту информацию принял, обработал и вернул какой-нибудь ответ (например, html-страничку с вашим профилем). Принципиальное отличие между ними в том, что GET-запросы предназначены только для получения данных с сервера, а POST-запросы несут с собой какую-то информацию, и данные на сервере могут измениться (например, когда вы заливаете свою фотку на сервер, она полетит в POST-запросе и сервер добавит её в базу данных, то есть произойдет какое-то изменение. Теперь вернемся к Tomcat. Когда он получает от клиента какой-то запрос, он смотрит на адрес. Ищет по своим данным, есть ли подходящий сервлет, который бы обрабатывал запросы по такому адресу (ну или готовый ресурс, который можно прям сразу и вернуть). Если он не нашел что вернуть, он кидает в ответ не html-страничку, а 404-ответ. Если же он нашел подходящий сервлет, который «сидит» на этом адресе, он смотрит какой тип запроса он получил (GET, POST, или какой-то другой), а потом спрашивает у сервлета, есть ли у него метод, который умел бы обрабатывать такой тип запросов. Если сервлет говорит, что не умеет обрабатывать такой тип, Tomcat кидает клиенту в ответ код 405. Что и произошло только-что у нас. Но если же нашелся и подходящий сервлет, и у него есть подходящий метод, Tomcat создает объект этого сервлета, запускает его в новом треде (thread), что позволяет сервлету работать в отдельном потоке, а Tomcat продолжает работать и дальше в своем, принимать и отправлять запросы. Кроме того, Tomcat создает ещё два объекта: один типа HttpServletRequest (коротко я его буду называть дальше запросом), а второй — типа HttpServletResponse (буду называть ответом). В первый объект она помещает все данные, что ему пришли в запросе от клиента, таким образом из этого объекта все те данные можно будет вытащить. Ну и после всего этого передает два эти объекта в подходящий метод того сервлета, который запущен в отдельном потоке. Как только сервлет закончит работу и у него будет готов ответ, который надо отправить клиенту, он поднимает флаг Tomcat’у, мол, «я закончил, все готово». Tomcat принимает ответ и отправляет его клиенту. Это позволяет Tomcat не отвлекаясь принимать запросы и отправлять ответы, а всю работу делают сервлеты, которые крутятся в отдельных потоках. Соответственно, когда мы пишем код сервлета, мы и определяем ту работу, которая будет выполняться. И да, можете считать, что метод main() находится в самом Tomcat (да, он написан на Java), и когда мы «запускаем» Tomcat, запускается метод main().

Ловим сервлетами GET-методы и отправляем простейшие ответы

В данный момент, в наших сервлетах нет подходящих методов (GET), поэтому Tomcat нам возвращает ошибку 405. Сделаем их! В классе HttpServlet, от которого мы унаследовали наши сервлеты, определены разные методы. Для того, чтобы задать какой-то код для методов, мы их просто переопредеяем. В данном случае нам надо переопределить метод doGet() в обоих сервлетах.
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

}
Как видим, этот метод принимает два обьекта: req (запрос) и resp (ответ). Это те самые объекты, которые создаст и наполнит нам Tomcat, когда вызовет соответствующий метод в этом сервлете. Для начала давайте сделаем простейшие ответы. Для этого возьмем объект resp и получим из него объект PrintWriter-а, которым можно составлять ответы. Ну и при помощи него выведем какую-нибудь простую строку.
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    PrintWriter writer = resp.getWriter();
    writer.println("Method GET from AddServlet");
}
Что-то подобное сделаем и в сервлете ListServlet, после чего запустим наш сервер снова. Как видим, все работает! При нажатии на кнопки открываются странички с тем текстом, который мы «записали» PrintWriter-ом. Вот только те наши jsp, которые мы подготовили для формирования страничек с ответами никак не используются. Это потому, что выполнение до них просто не доходит. Сервелет сам у нас сейчас формирует ответ и заканчивает работу, сигнализируя Tomcat, что у него готов ответ клиенту. Tomcat же просто берет этот ответ и отправляет его назад клиенту. Передаем управление из сервлетов в jsp Изменим код наших методов таким образом:
  • получаем из объекта запроса объект диспетчера запросов, куда передаем адрес jsp странички, которой мы хотим передать управление;
  • используя полученный объект — передаем управление в указанную jsp страницу, и не забываем вложить туда те объекты запроса и ответа, которые мы получили от Tomcat.
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    RequestDispatcher requestDispatcher = req.getRequestDispatcher("views/add.jsp");
    requestDispatcher.forward(req, resp);
}
В теле jsp-страниц (внутри тега body) можно что-то написать, чтобы мы могли четко видеть, какая из страниц отображается. После этого перезапускаем сервер и проверяем. Кнопочки на главной странице нажимаются, странички открываются, а, значит, запросы в сервлеты передаются, после чего управление передается в jsp страницы, которые уже и отрисовываются. На этом всё. В следующей части статьи мы займемся функциональностью нашего приложения.

Что еще почитать:

Создание простейшего веб-проекта в IntelliJ Idea Enterprise. Пошагово, с картинками

Мой чат