• ,

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

Продолжаю описывать процесс создания веб-приложения используя сервлеты, jsp, мавен и томкат. Начало статьи, если необходимо.
Создаем сущности.
Создадим в пакете entities класс User, в котором создадим две приватные строковые переменные name и password. Создадим конструкторы (по умолчанию и такой, который бы принимал оба значения), геттеры/сеттеры, переопределим метод toString() на всякий случай, а так же методы equals() и hashCode().

public class User {
    private String name;
    private String password;

    public User() {
    }

    public User(String name, String password) {
        this.name = name;
        this.password = password;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    @Override
    public String toString() {
        return "User{" +
                "name='" + name + '\'' +
                ", password='" + password + '\'' +
                '}';
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        User user = (User) o;

        if (name != null ? !name.equals(user.name) : user.name != null) return false;
        return password != null ? password.equals(user.password) : user.password == null;

    }

    @Override
    public int hashCode() {
        int result = name != null ? name.hashCode() : 0;
        result = 31 * result + (password != null ? password.hashCode() : 0);
        return result;
    }
}


Теперь можем приступить к созданию списка пользователей, куда мы будем наших пользователей добавлять, и откуда будем их забирать для отображения.
Но есть проблема. Объекты наших сервлетов мы не создаем, их создает за нас томкат. Методы, которые мы переопределяем в них — тоже за нас уже определены и добавить параметр мы не можем. Как же тогда создать общий список, который бы виден был в обоих наших сервлетах? Если мы просто в каждом сервлете создадим свой объект списка — то получится, что добавлять пользователей мы будем в один список, а выводить список пользователей сервлетом ListServlet — совершенно другой.
Получается, что нам нужен такой объект, который был бы общим для обоих сервлетов. Если говорить обобщенно, то нам нужен такой объект, который был бы общим для всех классов в нашей программе; единственный объект на всю программу.

Надеюсь, вы что-то да слышали про шаблоны проектирования. И возможно для кого-то это первая реальная необходимость использования шаблона Singleton в своей программе.
Можете поизвращаться и запилить какой-нибудь крутой синглтон, с двойными проверками и синхронизациями (да-да, у нас многопоточное приложение, так как сервлеты томкат запускает в разных потоках), но я буду использовать вариант с ранней инициализацией, так как в данном случае он вполне подходит.

Создание модели.
Создадим тогда класс (и реализуем в нем шаблон singleton) в пакете model, ну и назовем тоже вполне красочно Model.
Создадим в нем приватный объект списка пользователей, и сделаем два метода: один для того, чтоб можно было добавить пользователя, а второй — пусть просто возвращает список строк (имен пользователей). Поскольку наш объект пользователя состоит из имени и пароля — то пароли пользователей мы «светить» не хотели бы, поэтому и возвращать будем только список их имен.

public class Model {
    private static Model instance = new Model();

    private List<User> model;

    public static Model getInstance() {
        return instance;
    }

    private Model() {
        model = new ArrayList<>();
    }

    public void add(User user) {
        model.add(user);
    }

    public List<String> list() {
        return model.stream()
                .map(User::getName)
                .collect(Collectors.toList());
    }
}


Немного про mvc.
Раз уж вы слышали про singleton, значит возможно слышали и про другой шаблон проектирования — MVC (model-view-controller, или по-русски модель-представление-контроллер, или прям так как и на английском модель-вью-контроллер).
Его суть в том, чтобы отделять бизнес-логику от представления. То-есть, отделять код, который определяет что делать от кода, который определяет как отображать. View (представление или просто вьюхи) как-раз и отвечает за то, в каком виде представлять какие-то данные. В нашем случае вьюхи — это наши jsp странички. Именно поэтому я их и положил в папочку с названием views.
Модель — это собственно сами данные, с которыми работает программа. В нашем случае это пользователи (список пользователей). Ну а контроллеры — связующее звено между ними. Берут данные из модели и передают их во вьюхи, ну или получают от томката какие-то данные, обрабатывают их и передают модели. Бизнес-логика (то-есть что делать) должна быть описана в них, а не в модели или во вьюхе. Таким образом, каждый занимается своим делом:
  • модель хранит данные
  • вьюхи рисуют красивое представление данных
  • контроллеры занимаются обработкой данных
Это позволяет всем им быть достаточно простыми и поддерживаемыми. А не монструозной свалкой всего кода в одном классе.
MVC подходит не только для веб-программирования, но все-же в этой сфере он встречается очень часто (если не всегда).
В нашем случае в качестве контроллеров будут выступать сервлеты.
Да это очень поверхностное и даже грубое описание этого паттерна, но эта статья не о шаблонах проектирования, а о том, как сделать простенькое веб-приложение :) Кто хочет узнать больше — гугл знает все! :)

Вернемся к нашим вьюхам.

Создаем форму добавления пользователя.
Добавим в файл add.jsp форму, состоящую из двух текстовых инпутов (один обычный, другой типа пароль) и кнопки для отправки данных на сервер.
<form method="post">
    <label>Name:
        <input type="text" name="name"><br />
    </label>

    <label>Password:
        <input type="password" name="pass"><br />
    </label>
    <button type="submit">Submit</button>
</form>

Здесь у формы указан атрибут method со значением post. Это говорит о том, что данные из этой формы полетят на сервер в виде POST-запроса. Атрибут action не указан, значит запрос этот полетит по тому же адресу, по которому мы перешли на эту страничку (/add). Таким образом наш сервлет, который привязан к этому адресу, при получении GET-запроса возвращает эту jsp с формой добавления пользователей, а если получит POST-запрос — значит эта форма отправила туда свои данные (которые мы в методе doPost() вытащим из объекта запроса, обработаем и передадим в модель на сохранение).
Стоит обратить внимание, что у инпутов здесь указан параметр name (для поля с именем он имеет значение name, а для поля с паролем — pass). Это довольно важный момент. Так как чтобы получить из запроса (внутри сервлета уже) эти данные (имя и пароль которые будут введены) — мы будем использовать именно эти name и pass. Но об этом чуть позже.
Сама кнопка отправки данных у меня сделана снова же в виде button, а не инпутом, как это обычно принято. Не знаю насколько такой вариант универсален, но у меня в хроме работает :)

Обработка POST-запроса сервлетом.
Вернемся к сервлету AddServlet.
Мы уже знаем, что чтобы наш сервлет умел «ловить» GET-запросы — мы переопределили метод doGet() из класса HttpServlet.
Чтобы научить наш сервлет ловить еще и POST-звапросы — мы переопределяем еще и метод doPost().
Он получает аналогичные объекты запроса и ответа от томката, с которыми мы и будем работать.
Для начала вытащим из запроса параметры name и pass, которые отправила форма (если вы их в форме назвали по-другому — тогда именно те названия и пишете). После чего создадим объект нашего пользователя, используя полученные данные.
Потом получим объект модели и добавим созданного пользователя в модель.
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    String name = req.getParameter("name");
    String password = req.getParameter("pass");
    User user = new User(name, password);
    Model model = Model.getInstance();
    model.add(user);
}


Передача данных во вьюху.
Теперь перейдем к сервлету ListServlet.
У нас тут уже реализован метод doGet(), который просто передает управление во вьюху list.jsp. Если у вас этого еще нет — сделайте по аналогии с таким же методом из сервлета AddServlet.
Теперь было бы неплохо получить из модели список имен пользователей и передать их во вьюху, которая их там получит и красивенько отобразит. Для этого воспользуемся снова же объектом запроса, который мы получили от томката. К этому объекту мы можем добавить атрибут, дав ему какое-то имя ну и собственно сам объект, который бы мы хотели передать во вьюху.
Благодаря тому, что при передаче процесса выполнения из сервлета во вьюху мы передаем туда эти же объекты запроса и ответа, что получил сам сервлет, то и добавив наш список имен к объекту запроса мы потом из этого объекта запроса во вьюхе сможем наш список имен пользователей и получить.
С классом ListServlet мы закончили, поэтому привожу код всего класса
package app.servlets;

import app.model.Model;

import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;

public class ListServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        Model model = Model.getInstance();
        List<String> names = model.list();
        req.setAttribute("userNames", names);

        RequestDispatcher requestDispatcher = req.getRequestDispatcher("views/list.jsp");
        requestDispatcher.forward(req, resp);
    }
}


Выполнение java-кода в jsp файлах.
Теперь перейдем к файлу list.jsp.
Этот файл выполнится только когда ListServlet передаст сюда процесс выполнения. Кроме того, мы в том сервлете уже подготовили список имен пользователей из модели и передали сюда в объекте запроса.
Имея список имен мы можем пробежаться по нему циклом for и вывести каждое имя.
Как я уже говорил, jsp файлы могут выполнять java-кода (в принципе, этим и отличаются от статичных html страничек).
Для того, чтобы выполнить какой-то код — достаточно поставить в нужном нам месте конструкцию
<!-- html код -->
<%
    // java код
%>
<!-- html код -->

Внутри такой конструкции мы получаем доступ к нескольким переменным:
request — наш объект запроса, который мы передали из сервлета, где он назывался просто req
responce — объект ответа, в сервлете назывался resp
out — объект типа JspWriter (наследуется от обычного Writer), при помощи которого можем «писать» что-либо прямо в саму html страничку. Запись out.println(«Hello world!») очень похожа на запись System.out.println(«Hello world!»), но не стоит их путать! out.println() «пишет» в html страничку, а System.out.println — в системный вывод. Если вызвать внутри раздела с джава кодом jsp метод System.out.println() — то результаты увидите в консоли томката, а не на страничке, как возможно хотелось бы :)
Про другие доступные объекты внутри jsp можно поискать тут.
Используя объект request мы можем получить список имен, который передавали из сервлета (мы прикрепили соответствующий атрибут к этому объекту), а используя объект out — можем вывести эти имена.
Сделаем это пока просто в виде html списка:
<ul>
    <%
        List<String> names = (List<String>) request.getAttribute("userNames");

        if (names != null && !names.isEmpty()) {
            for (String s : names) {
                out.println("<li>" + s + "</li>");
            }
        }
    %>
</ul>


Если же хотим выводить список только в том случае, когда есть пользователи, а иначе выводить предупреждение, что пользователей пока нет — можем немного переписать этот участок:
<%
    List<String> names = (List<String>) request.getAttribute("userNames");

    if (names != null && !names.isEmpty()) {
        out.println("<ui>");
        for (String s : names) {
            out.println("<li>" + s + "</li>");
        }
        out.println("</ui>");
    } else out.println("<p>There are no users yet!</p>");
%>


Теперь, когда мы умеем передавать данные из сервлетов во вьюхи — можем немного улучшить наш AddServlet, чтобы выводилось уведомление об успешном добавлении пользователя. Для этого в методе doPost() после того, как добавили нового пользователя в модель — можем добавить имя этого пользователя в атрибуты объекта req и передать управление обратно во вьюху add.jsp. А в ней уже сделать участок с джава кодом, где проверять есть ли такой атрибут в запросе, и если да — то выводить сообщение, что пользователь успешно добавлен.

После этих изменений полный код сервлета AddServlet будет выглядеть примерно так:
package app.servlets;

import app.entities.User;
import app.model.Model;

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

public class AddServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        RequestDispatcher requestDispatcher = req.getRequestDispatcher("views/add.jsp");
        requestDispatcher.forward(req, resp);
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String name = req.getParameter("name");
        String password = req.getParameter("pass");
        User user = new User(name, password);
        Model model = Model.getInstance();
        model.add(user);

        req.setAttribute("userName", name);
        doGet(req, resp);
    }
}

Тут в конце метода doPost() мы устанавливаем атрибут с именем добавленного в модель пользователя, после чего вызываем метод doGet(), в который передаем текущие запрос и ответ. А метод doGet() уже передает управление во вьюху, куда и отправляет объект запроса с прикрепленным именем добавленного пользователя в качестве атрибута.

Осталось подправить саму add.jsp чтобы она выводила такое уведомление, если присутствует такой атрибут.
Конечный вариант add.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
    <head>
        <title>Add new user</title>
    </head>

    <body>
        <div>
            <h1>Super app!</h1>
        </div>

        <div>
            <%
                if (request.getAttribute("userName") != null) {
                    out.println("<p>User '" + request.getAttribute("userName") + "' added!</p>");
                }
            %>
            <div>
                <div>
                    <h2>Add user</h2>
                </div>

                <form method="post">
                    <label>Name:
                        <input type="text" name="name"><br />
                    </label>
                    <label>Password:
                        <input type="password" name="pass"><br />
                    </label>
                    <button type="submit">Submit</button>
                </form>
            </div>
        </div>

        <div>
            <button onclick="location.href='/'">Back to main</button>
        </div>
    </body>
</html>

Тело страницы состоит из div-a с шапкой, после чего div-контейнер для контента, в нем проверка существует ли атрибут с именем пользователя, потом div с формой добавления пользователей, ну и в конце футер с кнопкой возврата на главную страницу.
Может показаться, что слишком много div-ов, но мы их потом используем, когда добавим стилей :)

Ну и конечный вариант list.jsp
<%@ page import="java.util.List" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
    <head>
        <title>Users</title>
    </head>

    <body>
        <div>
            <h1>Super app!</h1>
        </div>

        <div>
            <div>
                <div>
                    <h2>Users</h2>
                </div>
                <%
                    List<String> names = (List<String>) request.getAttribute("userNames");

                    if (names != null && !names.isEmpty()) {
                        out.println("<ui>");
                        for (String s : names) {
                            out.println("<li>" + s + "</li>");
                        }
                        out.println("</ui>");
                    } else out.println("<p>There are no users yet!</p>");
                %>
            </div>
        </div>

        <div>
            <button onclick="location.href='/'">Back to main</button>
        </div>
    </body>
</html>


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

Осталось лишь приукрасить… :)

Добавление стилей. Используем фреймворк W3.CSS.
В данный момент наше приложение рабочее, но абсолютно вырвиглазное :)
Нужно добавить фон, цвета текста и кнопок, стилизировать списки, сделать выравнивание, добавить отступов, в общем много чего. Если писать стили вручную — это может занять много времени и нервов. Поэтому я предлагаю воспользоваться CSS фреймворком W3.CSS. В нем уже есть готовые классы со стилями, осталось только расставить в нужных местах те css классы, которые мы хотим в этих местах применить.
Для того, чтобы добавить их к себе на страницы во-первых нам надо подключить файл со стилями. Это можно сделать двумя способами:
1. пройтись по нашим страницам и в разделе head вставить прямую ссылку на файл со стилями
<link rel="stylesheet" href="https://www.w3schools.com/w3css/4/w3.css">

Такой вариант подходит, если у вас постоянное подключение к интернету. Тогда при открытии ваших страниц на локальном сервере — стили подтянутся из интернета.
2. Если же вы хотите иметь все стили у себя локально и не быть зависимым от интернет-соединения — можете просто скачать файл со стилями и поместить его где-нибудь внутри папочки web (например web/styles/w3.css), после чего пройтись по всем нашим страничкам (index.html, add.jsp, list.jsp) и вписать внутри раздела head ссылку на этот файл со стилями
<link rel="stylesheet" href="styles/w3.css">


После этого просто пройтись по тегам и подописывать им те стили, которые вам понравятся. Я не буду останавливаться на этом подробно, а сразу дам свои готовые варианты трех моих файлов с раставленными классами стилей.

index.html
<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Super app!</title>
        <link rel="stylesheet" href="https://www.w3schools.com/w3css/4/w3.css">
    </head>

    <body class="w3-light-grey">
        <div class="w3-container w3-blue-grey w3-opacity w3-right-align">
            <h1>Super app!</h1>
        </div>

        <div class="w3-container w3-center">
            <div class="w3-bar w3-padding-large w3-padding-24">
                <button class="w3-btn w3-hover-light-blue w3-round-large" onclick="location.href='/list'">List users</button>
                <button class="w3-btn w3-hover-green w3-round-large" onclick="location.href='/add'">Add user</button>
            </div>
        </div>
    </body>
</html>


add.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
    <head>
        <title>Add new user</title>
        <link rel="stylesheet" href="https://www.w3schools.com/w3css/4/w3.css">
    </head>

    <body class="w3-light-grey">
        <div class="w3-container w3-blue-grey w3-opacity w3-right-align">
            <h1>Super app!</h1>
        </div>

        <div class="w3-container w3-padding">
            <%
                if (request.getAttribute("userName") != null) {
                    out.println("<div class=\"w3-panel w3-green w3-display-container w3-card-4 w3-round\">\n" +
                            "   <span onclick=\"this.parentElement.style.display='none'\"\n" +
                            "   class=\"w3-button w3-margin-right w3-display-right w3-round-large w3-hover-green w3-border w3-border-green w3-hover-border-grey\">×</span>\n" +
                            "   <h5>User '" + request.getAttribute("userName") + "' added!</h5>\n" +
                            "</div>");
                }
            %>
            <div class="w3-card-4">
                <div class="w3-container w3-center w3-green">
                    <h2>Add user</h2>
                </div>
                <form method="post" class="w3-selection w3-light-grey w3-padding">
                    <label>Name:
                        <input type="text" name="name" class="w3-input w3-animate-input w3-border w3-round-large" style="width: 30%"><br />
                    </label>
                    <label>Password:
                        <input type="password" name="pass" class="w3-input w3-animate-input w3-border w3-round-large" style="width: 30%"><br />
                    </label>
                    <button type="submit" class="w3-btn w3-green w3-round-large w3-margin-bottom">Submit</button>
                </form>
            </div>
        </div>

        <div class="w3-container w3-grey w3-opacity w3-right-align w3-padding">
            <button class="w3-btn w3-round-large" onclick="location.href='/'">Back to main</button>
        </div>
    </body>
</html>


list.jsp
<%@ page import="java.util.List" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
    <head>
        <title>Users list</title>
        <link rel="stylesheet" href="https://www.w3schools.com/w3css/4/w3.css">
    </head>

    <body class="w3-light-grey">
        <div class="w3-container w3-blue-grey w3-opacity w3-right-align">
            <h1>Super app!</h1>
        </div>

        <div class="w3-container w3-center w3-margin-bottom w3-padding">
            <div class="w3-card-4">
                <div class="w3-container w3-light-blue">
                    <h2>Users</h2>
                </div>
                <%
                    List<String> names = (List<String>) request.getAttribute("userNames");

                    if (names != null && !names.isEmpty()) {
                        out.println("<ul class=\"w3-ul\">");
                        for (String s : names) {
                            out.println("<li class=\"w3-hover-sand\">" + s + "</li>");
                        }
                        out.println("</ul>");

                    } else out.println("<div class=\"w3-panel w3-red w3-display-container w3-card-4 w3-round\">\n"
+
                            "   <span onclick=\"this.parentElement.style.display='none'\"\n" +
                            "   class=\"w3-button w3-margin-right w3-display-right w3-round-large w3-hover-red w3-border w3-border-red w3-hover-border-grey\">×</span>\n" +
                            "   <h5>There are no users yet!</h5>\n" +
                            "</div>");
                %>
            </div>
        </div>

        <div class="w3-container w3-grey w3-opacity w3-right-align w3-padding">
            <button class="w3-btn w3-round-large" onclick="location.href='/'">Back to main</button>
        </div>
    </body>
</html>


Вот и все :)
Если у вас остались какие-то вопросы или есть какие-то замечания, или же наоборот что-то не получается — оставьте комментарий.

Ну и парочку скриншотов приложу что из этого всего получилось.
Главная страница приложения

Окно добавления пользователя

Список пользователей

И напоследок.
Если будет желание попрактиковаться с этим проектом — можете попробовать:
  • сделать сервлет и jsp для удаления пользователя и еще пару для изменения/редактирования существующего пользователя. Получится настоящее CrUD веб приложение :) на сервлетах))
  • заменить список (List) на работу с базой данных, чтоб добавленные пользователи не пропадали после перезапуска сервера :)
  • Удачи!

5 комментариев

wolfio
  • wolfio
  • 0
  • Комментарий отредактирован 2017-10-30 13:22:53 пользователем wolfio
расписано довольно детально) правда пока не пробовал все это воспроизвести
однако, лично мое предположение: web-сервис — это в моем понимании все же клиент-сервер.
Я конечно понимаю, что браузер условно и есть клиент, но тогда это скорее не сервис, а веб-сайт.
Не хватает клиентской части, для обращения к базе имен (ну чтобы мимо браузера)

Как долго ты разбирался со всем этим направлением?
Какие основные направления JS нужно знать, для таких вот относительно простых задач?)
fatfaggy
Не хватает клиентской части, для обращения к базе имен (ну чтобы мимо браузера)
ты имеешь ввиду получать список имен пользователей, например, в андроид-приложение не вызывая браузер? типа, открыл приложение, отправился запрос, принялся ответ, а приложение тебе отрисовало список имен пользователей? ну это лучше делать через REST, о котором я тут не писал и не планировал :) хотел написать именно про веб вариант, с браузером :)

Как долго ты разбирался со всем этим направлением?
нуу, я почитал Head First Servlets&JSP немного, потом сидел экспериментировал (большую часть времени боролся с ошибками, что томкат выдавал, чаще всего из-за кривой настройки артефактов). не знаю даже сколько времени, но не меньше нескольких недель :) но это было давно

Какие основные направления JS нужно знать, для таких вот относительно простых задач?)
ооо, javascript — это свой отдельный мир)) конкретно в этом примере из js там только закрытие окошек уведомлений
this.parentElement.style.display='none'

который я честно скопипастил из туториала W3.CSS :)
аа, ну и
location.href='/'
чтоб добавить кнопкам адрес перехода, который загуглился за пару секунд :)
а вообще по javascript могу предложить вот этот курс. сначала может показаться слишком просто, потому что синтаксис во многом похож на синтаксис java, но как дойдешь до замыканий — тогда начнется))
saudabaew
Если бы статьи были написаны на месяц раньше, то можно было бы съэкономить кучу времени при решении задачи на стажировку. Описано кратко и по делу. Спасибо!
realcorwin
Делайте и дальше подобные тексты. У вас явно есть к сему талант и желание. А остальным будет польза :).
fatfaggy
спасибо :) постараюсь
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.