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