Как упростить создание сайтов с помощью фреймворка Javalin: разбираем на примере

Читать в полной версии →

Зачем использовать простой фреймворк Javalin на языке Java для обработки запросов и формирования ответов, и как работает шаблонизация.

Статью подготовил Сергей Чепурнов, наставник на Хекслете, Java-разработчик с опытом работы более 7 лет в распределенных командах. Профиль Сергея на GitHub

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

Все веб-приложения работают по одному принципу. Во-первых, они состоят из двух частей:

Во-вторых, в каждом приложении клиент и сервер обмениваются информацией таким образом:

Такая последовательность из трех шагов называется циклом «запрос-обработка-ответ». На схеме она выглядит так:

Обработка запросов и формирование ответов — это рутинная задача в каждом веб-приложении.

При программировании на языке Java есть два способа работы с циклом «запрос-обработка-ответ»:

  1. Использование Servlet API — это стандартный пакет классов и интерфейсов, то есть встроенный механизм построения клиент-серверного приложения по схеме «запрос-ответ». Главный минус — придется писать много однотипного кода, создавать множество классов и т. п.
  2. Использование фреймворка для автоматизации повторяющихся задач. Фреймворк — это каркас веб-приложения, определяющий структуру программы. Код веб-приложения в таком случае легче читается, поддерживается и тестируется. Благодаря фреймворкам вы можете сосредоточиться на логике сайта, не отвлекаясь на продумывание базовой архитектуры или кодирование вспомогательных инструментов.

Рассмотрим фреймворк Javalin: он настолько прост, что работающее веб-приложение можно написать буквально в две строчки кода:

class App {

   public static void main(String[] args) {
       Javalin app = Javalin.create().start(7070);
       app.get("/", ctx -> ctx.result("Hello World"));
   }

}

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

Для примера мы будем использовать систему сборки Gradle. В этом случае запуск приложения выполняется командой run в корне директории проекта:

./gradlew run

Откроем в браузере страницу с адресом localhost:7070 и посмотрим, запустилось ли наше двухстрочное приложение.

Все прошло успешно:

Запрос /  и ответ Hello World связаны между собой. Клиент делает запрос по адресу localhost:7070. При этом сервер понимает, что к нему обратились с запросом «/», и в качестве ответа возвращает строку Hello World.

Чтобы разобраться подробнее, вернемся к двум строкам выше. Посмотрим, что в них написано:

class App {

   public static void main(String[] args) {
       Javalin app = Javalin.create().start(7070);
       app.get("/", ctx -> ctx.result("Hello World"));
   }

}

Первая строка создает объект типа Javalin и устанавливает номер порта, на котором будет работать приложение (порт 7070 в данном случае). Вторая строка добавляет обработчик для запроса /.  Обработчик записывается в виде лямбда-выражения и формирует ответ в виде строки.

Клиент получает ответ от сервера в виде http-ответа, в теле которого содержится строка:

HTTP/1.1 200 OK

Date: Mon, 01 Jun 2022 08:32:33 GMT

Content-Type: text/plain

Content-Length: 11

Hello World

Как работает маршрутизация в Javalin

Поступающие запросы нужно маршрутизировать — то есть обработать на сервере.  Когда сервер получает запрос от клиента, он начинает маршрутизацию: обрабатывает запрос и определяет, какой ответ нужно отправить клиенту.

Как это происходит:

  1. Сервер считывает /about в конце URL и отличает этот запрос от всех остальных
  2. Затем сервер формирует подходящий ответ — отправляет html-страницу «О блоге».

Рассмотрим пример. Создадим веб-приложение в виде блога: https://java-javalin-blog.hexlet.app. Пользователь переходит по ссылкам внутри блога и получает разную информацию. Например, на https://java-javalin-blog.hexlet.app/articles — увидит список всех статей, а на странице https://java-javalin-blog.hexlet.app/about — узнает больше о блоге и авторах.

Посмотрим, как маршрутизация работает в приложении:

private static void addRoutes(Javalin app) {

   app.get("/", ctx -> {
       ctx.html("Привет от Хекслета!");
   });
   app.get("/about", ctx -> {
       ctx.html("О блоге. Эксперименты с Javalin на Хекслете");
   });
   app.get("/articles", ctx -> {
       ctx.html("Список статей");
   });
}

Обратите внимание, что каждому URL соответствует определенный ответ — именно он возвращается в виде html-страницы. Таким образом, пользователь увидит в браузере такую страницу:

Как работают динамически сформированные запросы

До этого мы рассматривали статические адреса такого вида:

Кроме статических адресов, в веб-приложениях используются и динамически сформированные адреса. Например:

Такие URL запрашивают статьи по их уникальному идентификатору (id). По id сервер различает запросы и возвращает соответствующую статью в виде html-страницы.

Если на сайте есть список статей, то его можно выводить на странице в виде списка названий, а ссылки формировать динамически и с разными идентификаторами. Обычно шаблон ссылки выглядит следующим образом: “/articles/{id}”, где вместо "{id}” программным путем подставляется конкретный идентификатор статьи.

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

Рассмотрим, как выглядит обработка запроса https://java-javalin-blog.hexlet.app/articles/1 в коде приложения.

Во-первых, в маршрутизации появляется отсылка к методу контроллера:

app.routes(() -> {
   path("articles", () -> {
       get(ArticleController.listArticles);
       path("{id}", () -> {
           get(ArticleController.showArticle);
       });
   });
});

Эта настройка маршрутизации сообщает о двух важных аспектах:

  1. GET-запрос по адресу /articles попадает на обработчик ArticleController.listArticles.
  2. GET-запрос по адресу articles/{id} с любым целочисленным параметром id попадает на обработчик ArticleController.showArticle

Во-вторых, обработчик запроса вида articles/{id} — метод контроллера, который извлекает параметр id из пути запроса, затем извлекает статью из хранилища с этим идентификатором и возвращает html-страницу с текстом найденной статьи:

public final class ArticleController {

public static Handler showArticle = ctx -> {
   int id = ctx.pathParamAsClass("id",
                 Integer.class).getOrDefault(null);

   Article article = getArticleById(id);

   ctx.attribute("article", article);
   ctx.render("articles/show.html");
};

//...
}

Извлекать параметры запроса (path param), в данном случае id, можно с помощью фреймворка Javalin:

int id = ctx.pathParamAsClass("id",Integer.class).getOrDefault(null);

Какими бывают запросы: GET, DELETE, POST, PUT

Выше мы рассмотрели, как получить список статей и извлечь одну конкретную статью, —  для этого используется метод GET HTTP. Но обычно в блоге можно еще и добавлять и удалять статьи — с этими задачами справляются другие методы HTTP.

Обычно веб-приложение использует четыре метода:

  1. GET – получение данных
  2. DELETE – удаление данных
  3. POST – создание данных
  4. PUT – обновление данных.

Для примера попробуем добавить статью в блог. Для этого используем метод POST HTTP:

app.routes(() -> {
   path("articles", () -> {
       get(ArticleController.listArticles);
       post(ArticleController.createArticle);
       path("{id}", () -> {
           get(ArticleController.showArticle);
       });
   });
});

Теперь у нас есть форма создания статьи:

А так выглядит разметка этой формы:

<html>
   <form action="/articles" method="post">

     <label for="name">Наименование</label>
     <input type="text" id="name" name="name">

     <label for="description" >Описание</label>
     <textarea id="description", name="description"></textarea>

     <button type="submit">Создать</button>

   </form>
</html>

Сосредоточимся на отправке данных. Сначала пользователь вводит название и текст статьи, а потом нажимает кнопку «Создать». Так формируется запрос POST HTTP:

POST /articles HTTP/1.1
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9
Cache-Control: max-age=0
Connection: keep-alive
Content-Length: 42
Content-Type: application/x-www-form-urlencoded
Cookie: JSESSIONID=node03qhzyddchy2h1mzcr9nwqfazr0.node0
Host: java-javalin-blog.hexlet.app/
Origin: https://java-javalin-blog.hexlet.app/
Referer: https://java-javalin-blog.hexlet.app/articles/new

name=Example+name&description=Example+text

А теперь посмотрим, как обработчик POST запроса выглядит в коде:

public final class ArticleController {

   public static Handler createArticle = ctx -> {
       String name = ctx.formParam("name");
       String description = ctx.formParam("description");

       Article article = new Article(name, description);

       save(article);

       ctx.redirect("/articles");
   };

   //...
}

Всего одной строчкой кода можно получить доступ к данным формы (form param), которые передаются в теле POST-запроса.

Чтобы получить две переменные из запроса, требуется две строчки кода в методе- обработчике:


String name = ctx.formParam("name");
String description = ctx.formParam("description");

В примерах выше мы рассмотрели:

Перейдем к извлечению переменных запроса (query param) из URL. Для этого рассмотрим пример постраничного вывода статей.

В этом случае GET-запрос для запроса 1-ой страницы со статьями выглядит следующим образом: https://java-javalin-blog.hexlet.app/articles?page=1

В результате данного запроса в браузере можно наблюдать первую страницу со статьями:

Настроить маршрутизацию для такого запроса можно так:

app.routes(() -> {
   //...
   path("articles", () -> {
       get(ArticleController.listArticles);
   });
});

В коде обработчика доступ к переменной запроса (query param) в URL происходит также в одну строчку:

public static Handler listArticles = ctx -> {

   int page = ctx.queryParamAsClass("page",                     
                                    Integer.class).getOrDefault(1);

   int rowsPerPage = 10;
   List<Article> articles = getArticlesFromDB(page, rowsPerPage);
   //...

   ctx.attribute("articles", articles);
   ctx.render("articles/index.html");
};

Как работает шаблонизация

Представим, что клиент запрашивает страницу с первой статьей https://java-javalin-blog.hexlet.app/articles/1. Тогда сервер получает GET-запрос и присылает в ответ статью в виде html-страницы. Эта страница формируется программным путем с динамическим контентом.

Для этого используется шаблонизатор — инструмент, который позволяет упрощать генерацию конечных html-страниц за счет использования шаблонов. Здесь для примера мы используем Thymeleaf.

В нашем примере код обработчика будет выглядеть так:

public final class ArticleController {

public static Handler showArticle = ctx -> {
   int id = ctx.pathParamAsClass("id",
                 Integer.class).getOrDefault(null);

   Article article = getArticleById(id);

   ctx.attribute("article", article);
   ctx.render("articles/show.html");
};

//...
}

Посмотрим еще раз на две последние строки кода. Здесь вызываются методы контекста приложения:

ctx.attribute("article", article);
ctx.render("articles/show.html");

Первый метод — .attribute(). Он записывает объект article как атрибут, к которому можно получить доступ в шаблоне.

Второй метод — .render(). Он производит рендеринг шаблона — то есть записывает динамический контент в html-странице.

Посмотрим на пример шаблона show.html для вывода одной статьи:

<!DOCTYPE html>
<html xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
     layout:decorate="~{layouts/application.html}">

 <section layout:fragment="header">
   Просмотр статьи
 </section>

 <section layout:fragment="content" th:object="${article}">
   <div class="card">
     <div class="card-header bg-secondary text-white">
       <h2 th:text="*{getName()}"></h2>
     </div>
     <div class="card-body bg-light">
       <p class="lead" th:text="*{getDescription()}"></p>
     </div>
 </section>

</html>

В этом шаблоне программным путем устанавливаются название статьи и текст самой статьи. Это происходит за счет того, что в шаблоне есть доступ к объекту article, у которого вызываются методы getName() и getDescription().

В итоге генерируется такая html-страница:

<html>
<!-- ... -->
<main class="flex-grow-1">

 <div class="container mt-4">
   <h1 class="my-4">
     <section>
       Просмотр статьи
     </section>
   </h1>
   <section>
     <div class="card">
       <div class="card-header bg-secondary text-white">
         <h2>Моя первая статья</h2>
       </div>
       <div class="card-body bg-light">
         <p class="lead">Какой-то очень интересный контент. Все читаем!</p>
       </div>
   </section>
 </div>
</main>
<!-- ... -->
</html>

Фреймворк Javalin прост в использовании и при этом помогает автоматизировать рутинные задачи при написании веб-приложения. Он идеально подходит для обучения, потому что позволяет сосредоточиться на логике сайта, а не на продумывании базовой архитектуры или кодировании вспомогательных инструментов.

Дополнительные материалы

Читайте также: Как выбрать свой первый опен-сорс проект: инструкция от Хекслета