Java Web App Architecture In Takes Framework

The following text is a partial translation of the original English article, performed by ChatGPT (gpt-3.5-turbo) and this Jekyll plugin:

Я использовал Servlets, JSP, JAX-RS, Spring Framework, Play Framework, JSF с Facelets и немного Spark Framework. Все эти решения, по моему скромному мнению, очень далеки от объектно-ориентированного и элегантного подхода. Они все полны статических методов, не тестируемых структур данных и грязных хаков. Поэтому примерно месяц назад я решил создать свой собственный веб-фреймворк на Java. Я положил несколько основных принципов в его основу: 1) Нет NULL-значений, 2) нет публичных статических методов, 3) нет изменяемых классов и 4) нет приведения типов классов, отражения и операторов instanceof. Эти четыре основных принципа должны гарантировать чистый код и прозрачную архитектуру. Так родился фреймворк Takes. Давайте посмотрим, что было создано и как это работает.

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

Во-первых, чтобы создать веб-сервер, нам необходимо создать новый сетевой сокет, который принимает соединения на определенном TCP-порту. Обычно это 80, но я буду использовать 8080 для тестирования. Это делается на языке Java с помощью класса ServerSocket:

Достаточно, чтобы запустить веб-сервер. Теперь сокет готов и слушает порт 8080. Когда кто-то открывает http://localhost:8080 в своем браузере, устанавливается соединение и браузер будет вращать свое ожидающее колесо бесконечно. Скомпилируйте этот отрывок и попробуйте. Мы только что создали простой веб-сервер без использования каких-либо фреймворков. Пока мы ничего не делаем с входящими соединениями, но мы их также не отклоняем. Все они накапливаются внутри этого объекта “server”. Это происходит в фоновом потоке, поэтому мы должны поместить while(true) после этого. Без этой бесконечной паузы приложение завершит свое выполнение немедленно, и серверный сокет закроется.

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

Метод блокирует свою нить и ожидает появления нового подключения. Как только это происходит, он возвращает экземпляр Socket. Чтобы принять следующее подключение, мы должны снова вызвать accept(). Таким образом, наш веб-сервер должен работать следующим образом:

Это бесконечный цикл, который принимает новое соединение, понимает его, создает ответ, возвращает ответ и снова принимает новое соединение. Протокол HTTP является безсостоятельным, что означает, что сервер не должен помнить, что произошло в предыдущем соединении. Его единственным интересом является входящий HTTP-запрос в этом конкретном соединении.

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

Вы увидите что-то вроде этого:

Клиент (например, браузер Google Chrome) передает этот текст через установленное соединение. Он подключается к порту 8080 на localhostи сразу же отправляет этот текст в него, затем ожидает ответа.

Наша задача - создать HTTP-ответ, используя информацию, полученную в запросе. Если наш сервер очень простой, мы можем в основном игнорировать всю информацию в запросе и просто возвращать “Hello, world!” для всех запросов (для простоты я использую IOUtils).

Вот и всё. Сервер готов. Попробуйте скомпилировать и запустить его. Откройте браузер и перейдите по адресу http://localhost:8080, и вы увидите Hello, world!.

Вот и все, что вам нужно для создания веб-сервера. Теперь давайте обсудим, как сделать его объектно-ориентированным и композиционным. Давайте попробуем посмотреть, как был создан фреймворк Takes.

Маршрутизация/диспетчеризация объединены с печатью ответа в Takes. Вам всего лишь нужно создать один класс, который реализует интерфейс Take, чтобы создать работающее веб-приложение.

А теперь пришло время запустить сервер:

Этот класс FtBasic выполняет точно те же манипуляции с сокетом, описанные выше. Он запускает серверный сокет на порту 8080 и направляет все входящие соединения через экземпляр TkFoo, который мы передаем в его конструктор. Он выполняет это направление в бесконечном цикле, проверяя каждую секунду, не пора ли остановиться с помощью экземпляра Exit. Очевидно, Exit.NEVER всегда отвечает: “Пожалуйста, не останавливайтесь.”

Теперь давайте посмотрим, что находится в HTTP-запросе, приходящем в TkFoo, и что мы можем получить из него. Вот как определен интерфейс Request в Takes:

Запрос делится на две части: заголовок и тело. Заголовок содержит все строки, которые идут перед пустой строкой, начинающей тело, согласно спецификации HTTP в RFC 2616. В фреймворке есть множество полезных декораторов для Request. Например, RqMethod поможет вам получить имя метода из первой строки заголовка.

RqHref will help extract the query part and parse it. For example, this is the request:

GET /user?id=123 HTTP/1.1
Host: www.example.com

Этот код извлечет 123:

RqPrint can get the entire request or its body printed as a String:

final String body = new RqPrint(request).printBody();

Идея здесь заключается в том, чтобы сохранить простоту интерфейса Request и предоставить эту функциональность разбора запроса его декораторам. Такой подход помогает фреймворку сохранять классы небольшими и связанными. Каждый декоратор очень маленький и надежный, делая ровно одну вещь. Все эти декораторы находятся в пакете org.takes.rq. Как вы, возможно, уже понимаете, префикс Rq означает Request.

Давайте создадим наше первое реальное веб-приложение, которое будет делать что-то полезное. Я бы порекомендовал начать с класса Entry, который требуется для запуска приложения на Java из командной строки.

Этот класс содержит только один статический метод main(), который будет вызываться JVM при запуске приложения из командной строки. Как видите, он создает экземпляр FtCli, передавая ему экземпляр класса TkApp и аргументы командной строки. Мы создадим класс TkApp сейчас. FtCli (переводится как “пользовательский интерфейс командной строки”) создает экземпляр того же FtBasic, обертывает его несколькими полезными декораторами и настраивает его в соответствии с аргументами командной строки. Например, --port=8080 будет преобразован в номер порта 8080 и передан вторым аргументом конструктору FtBasic.

Само веб-приложение называется TkApp и расширяет TsWrap:

Мы обсудим этот класс TkFork через минуту.

Если вы используете Maven, это pom.xml, с которого вам следует начать:

Выполнение команды mvn clean package должно создать файл foo.jar в папке target и собрать все зависимости JAR в папку target/deps. Теперь вы можете запустить приложение из командной строки.

Приложение готово, и вы можете его развернуть, скажем, на Heroku. Просто создайте файл Procfile в корне репозитория и отправьте репозиторий на Heroku. Вот как должен выглядеть файл Procfile:

TkFork

Этот класс TkFork кажется одним из основных элементов фреймворка. Он помогает направлять входящий HTTP-запрос на правильный take. Его логика очень проста, и внутри него всего несколько строк кода. Он инкапсулирует коллекцию “вилок”, которые являются экземплярами интерфейса Fork.

Его единственный метод route() либо возвращает пустой итератор, либо итератор с одним Response. TkFork проходит через все вилки, вызывая их методы route(), пока один из них не вернет ответ. Когда это происходит, TkFork возвращает этот ответ вызывающей стороне, которой является FtBasic.

Давайте теперь создадим простую вилку сами. Например, мы хотим показать статус приложения, когда запрашивается URL /status. Вот код:

Я считаю, что логика здесь ясна. Мы либо возвращаем пустой итератор, либо итератор с экземпляром TkStatus внутри. Если возвращается пустой итератор, TkFork попытается найти другой форк в коллекции, который действительно получает экземпляр Response. Кстати, если ничего не найдено и все форки возвращают пустые итераторы, TkFork выбросит исключение “Страница не найдена”.

Точно такая же логика реализуется готовым форком под названием FkRegex, который пытается сопоставить путь запроса URI с предоставленным регулярным выражением.

Мы можем создать многоуровневую структуру классов TkFork; например:

Опять же, я считаю, что это очевидно. Экземпляр FkRegex попросит инкапсулированный экземпляр TkFork вернуть ответ и попытается получить его из того, что инкапсулировано в FkParams. Если HTTP-запрос равен /status?f=xml, будет возвращен экземпляр TkStatusXML.

Теперь давайте обсудим структуру HTTP-ответа и его объектно-ориентированную абстракцию - Response. Вот как выглядит интерфейс:

Кажется, очень похоже на Request, не так ли? Ну, оно практически идентично, в основном потому, что структура HTTP-запроса и ответа практически идентична. Единственное отличие - это первая строка.

Есть набор полезных декораторов, которые помогают в построении ответа. Они являются компонуемыми, что делает их очень удобными. Например, если вы хотите построить ответ, содержащий HTML-страницу, вы комбинируете их таким образом:

В этом примере декоратор RsWithBody создает ответ с телом, но без заголовков вообще. Затем RsWithType добавляет заголовок Content-Type: text/html к нему. Затем RsWithStatus гарантирует, что первая строка ответа содержит HTTP/1.1 200 OK.

Вы можете создать свои собственные декораторы, которые могут использовать существующие. Взгляните, как это сделано в RsPage на rultor.com.

Возвращение простых страниц “Hello, world” не является большой проблемой, как мы видим. Но что насчет более сложного вывода, такого как HTML-страницы, XML-документы, наборы данных JSON и т. д.? Существует несколько удобных декораторов Response, которые позволяют делать все это. Давайте начнем с Velocity, простого шаблонизатора. Хорошо, он не такой простой. Он довольно мощный, но я бы рекомендовал использовать его только в простых ситуациях. Вот как это работает:

Конструктор RsVelocity принимает единственный аргумент, который должен быть шаблоном Velocity. Затем вы вызываете метод with(), внедряя данные в контекст Velocity. Когда приходит время рендеринга HTTP-ответа, RsVelocity “оценивает” шаблон по настроенному контексту. Опять же, я рекомендую использовать этот подход к шаблонизации только для простых выводов.

Для более сложных HTML-документов я рекомендую использовать XML/XSLT в сочетании с Xembly. Я объяснил эту идею в нескольких предыдущих постах: XML+XSLT в браузере и RESTful API и веб-сайт в одном URL. Это просто и мощно - Java генерирует XML-вывод, а XSLT-процессор превращает его в HTML-документы. Вот как мы отделяем представление от данных. XSL-стиль является “видом”, а TkIndex - “контроллером” в терминах MVC.

Я скоро напишу отдельную статью о шаблонизации с помощью Xembly и XSL.

Тем временем мы создадим декораторы для рендеринга JSF/Facelets и JSP в Takes. Если вас интересует помощь, пожалуйста, форкните фреймворк и отправьте свои запросы на объединение изменений.

Теперь возникает вопрос, что делать с постоянными сущностями, такими как базы данных, структуры в памяти, сетевые соединения и т. д. Мое предложение - инициализировать их внутри класса Entry и передавать их как аргументы в конструктор TkApp. Затем TkApp будет передавать их в конструкторы пользовательских takes.

Например, у нас есть база данных PostgreSQL, в которой содержатся некоторые данные таблицы, которые нам необходимо отобразить. Вот как я бы инициализировал соединение с ней в классе Entry (я использую пул соединений BoneCP):

Теперь конструктор TkApp должен принимать единственный аргумент типа java.sql.Source.

Класс TkIndex также принимает единственный аргумент класса Source. Я верю, что вы знаете, что с ним делать внутри TkIndex, чтобы получить данные таблицы SQL и преобразовать их в HTML. Суть в том, что зависимость должна быть внедрена в приложение (экземпляр класса TkApp) в момент его создания. Это чистый и прозрачный механизм внедрения зависимостей, который абсолютно не требует контейнера. Подробнее об этом читайте в статье Dependency Injection Containers Are Code Polluters.

Поскольку каждый класс неизменяемый, а все зависимости внедряются только через конструкторы, модульное тестирование очень просто. Допустим, мы хотим протестировать TkStatus, который должен возвращать HTML-ответ (я использую JUnit 4 и Hamcrest):

Также мы можем запустить всё приложение или любое отдельное действие на тестовом HTTP-сервере и проверить его поведение через реальное TCP-соединение; например (я использую jcabi-http, чтобы выполнить HTTP-запрос и проверить вывод).

FtRemote запускает тестовый веб-сервер на случайном TCP-порту и вызывает метод exec() в предоставленном экземпляре FtRemote.Script. Первый аргумент этого метода является URI только что запущенной домашней страницы веб-сервера.

Архитектура фреймворка Takes очень модульная и компонуемая. Любой отдельный take может быть протестирован как самостоятельный компонент, абсолютно независимый от фреймворка и других takes.

Это вопрос, который я часто слышу. Идея проста и происходит из киноиндустрии. Когда делается фильм, съемочная группа делает много тейков, чтобы запечатлеть реальность и записать ее на пленку. Каждый снимок называется тейком.

Другими словами, тейк - это как снимок реальности.

То же самое относится к этой рамке. Каждый экземпляр Take представляет собой реальность в определенный момент времени. Затем эта реальность отправляется пользователю в виде Response.

P.S. Есть несколько слов о аутентификации: Как работает аутентификация на основе куки в рамках Takes.

P.P.S. Есть несколько реальных веб-систем, которые вам может быть интересно посмотреть. Они все используют рамку Takes, и их код открыт: rultor.com, jare.io, wring.io.

Translated by ChatGPT gpt-3.5-turbo/42 on 2023-11-17 at 15:02

sixnines availability badge   GitHub stars