Зачем использовать простой фреймворк Javalin на языке Java для обработки запросов и формирования ответов, и как работает шаблонизация.
Статью подготовил Сергей Чепурнов, наставник на Хекслете, Java-разработчик с опытом работы более 7 лет в распределенных командах. Профиль Сергея на GitHub
Все веб-приложения работают по одному принципу. Во-первых, они состоят из двух частей:
Во-вторых, в каждом приложении клиент и сервер обмениваются информацией таким образом:
Такая последовательность из трех шагов называется циклом «запрос-обработка-ответ». На схеме она выглядит так:
Обработка запросов и формирование ответов — это рутинная задача в каждом веб-приложении.
При программировании на языке Java есть два способа работы с циклом «запрос-обработка-ответ»:
Рассмотрим фреймворк 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
Поступающие запросы нужно маршрутизировать — то есть обработать на сервере. Когда сервер получает запрос от клиента, он начинает маршрутизацию: обрабатывает запрос и определяет, какой ответ нужно отправить клиенту.
Как это происходит:
Рассмотрим пример. Создадим веб-приложение в виде блога: 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);
});
});
});
Эта настройка маршрутизации сообщает о двух важных аспектах:
/articles
попадает на обработчик ArticleController.listArticles
.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 HTTP. Но обычно в блоге можно еще и добавлять и удалять статьи — с этими задачами справляются другие методы HTTP.
Обычно веб-приложение использует четыре метода:
Для примера попробуем добавить статью в блог. Для этого используем метод 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 прост в использовании и при этом помогает автоматизировать рутинные задачи при написании веб-приложения. Он идеально подходит для обучения, потому что позволяет сосредоточиться на логике сайта, а не на продумывании базовой архитектуры или кодировании вспомогательных инструментов.
Работающая версия блога: https://java-javalin-blog.hexlet.app
Исходный код блога: https://github.com/hexlet-components/java-javalin-blog
Читайте также: Как выбрать свой первый опен-сорс проект: инструкция от Хекслета