How to Create a Java Web Framework from Scratch, the Right Object-Oriented Way

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

Как разработать веб-приложение на Java? Вы устанавливаете Spring, читаете руководство, создаете контроллеры, создаете несколько представлений, добавляете некоторые аннотации и все работает. Что бы вы делали, если бы не было Spring (и нет Ruby on Rails на Ruby, и нет Symphony на PHP, и нет… и т.д.)? Давайте попробуем создать веб-приложение с нуля, начиная с чистого Java SDK и заканчивая полнофункциональным веб-приложением, покрытым модульными тестами. Я записал вебинар №42 об этом всего несколько недель назад, но этот статья должна объяснить все еще более подробно.

Прежде всего, нам нужно создать HTTP-сервер, который будет открывать серверное сокетное соединение, слушать входящие подключения, читать все, что они должны сказать (HTTP-запросы) и возвращать информацию, которую хотел бы любой веб-браузер (HTTP-ответы). Вы знаете, как работает HTTP, верно? Если нет, вот небольшое напоминание:

Веб-браузер отправляет запрос на сервер и запрос выглядит так (это обычный текстовый фрагмент данных):

Сервер должен прочитать этот текст, подготовить ответ (который должен быть HTML-страницей, читаемой браузером) и возвратить его таким образом:

Вот и все. Это очень простой и, я бы сказал, примитивный протокол. Реализация веб-сервера на Java тоже не такая сложная. Вот она, в очень упрощенной форме:

Попробуйте запустить его, он должен работать. Вы должны иметь возможность открыть страницу http://localhost:8080 в своем браузере и увидеть текст “Привет, мир!”.

Это пока не веб-приложение, а просто каркас, который выполняет простую обработку HTTP-запросов в HTTP-ответы. Однако здесь нет серьезного ООП. Здесь преобладает процедурный стиль, но это работает. Теперь мы должны сосредоточиться на более важном вопросе: как добавить больше функций в веб-приложение и сделать его способным обрабатывать разные страницы, отображать больший контент и обрабатывать ошибки? Переменную request в коде выше нужно как-то преобразовать в response.

Самый простой способ состоит в том, чтобы 1) преобразовать запрос в DTO со всеми необходимыми данными, затем 2) отправить его в “контроллер”, который знает, что делать с данными из DTO, и затем 3) получить DTO-ответ от контроллера, извлечь данные и отобразить ответ. Так делается в Spring и большинстве других фреймворков. Однако мы не пойдем по этому пути, мы попытаемся сделать это без DTO и чисто объектно-ориентированно.

Я должен сказать, что существует несколько вариантов проектирования в стиле ООП. Сейчас я покажу вам только один из этих вариантов. Вы, несомненно, знакомы с нашим фреймворком Takes, который появился несколько лет назад - он имеет свой собственный дизайн, также ориентированный на объекты. Но то, что я предлагаю сейчас, кажется лучше. Возможно, вы придумаете что-то еще, поэтому не стесняйтесь высказывать свои идеи в комментариях ниже или даже создавать репозиторий GitHub и делиться своими мыслями прямо там.

Я предлагаю ввести два интерфейса: Resource и Output. Resource - это сущность на стороне сервера, которая изменяется в зависимости от параметров запроса, поступающих на вход. Например, когда все, что мы знаем о запросе, это GET /, это один ресурс. Но если мы также знаем, что запрос содержит, например, Accept: text/plain, мы можем изменить запрос и создать новый, который передаст обычный текст. Вот интерфейс:

Вот как мы создаем его и изменяем:

Обратите внимание: каждый вызов .refine() возвращает новый экземпляр интерфейса Resource. Все они являются неизменяемыми, так же, как и объекты. Благодаря такому подходу мы не разделяем данные от их обработчика. Ресурс - это данные и обработчик. Каждый ресурс знает, что делать с данными и получает только те данные, которые ему предназначены. Технически говоря, мы просто реализуем диспетчеризацию запросов, но в объектно-ориентированном стиле.

Затем нам нужно преобразовать ресурс в ответ. Мы даем ресурсу возможность отрисовывать себя в ответе. Мы не хотим, чтобы данные в виде какого-то DTO покидали ресурс. Мы хотим, чтобы ресурс печатал ответ. Как насчет добавления дополнительного метода print() в ресурс:

А затем интерфейс “Output” выглядит так:

Вот простая реализация Output:

Для создания HTTP-ответа мы можем сделать следующее:

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

Сначала мы разбираем запрос, разделяя его заголовок на строки и игнорируя тело запроса. Вы можете изменить код, чтобы разобрать тело и передать его в метод refine() с использованием ключа X-Body. На данный момент код выше этого не делает. Но вы понимаете идею. Часть разбора фрагмента подготавливает найденные в запросе пары и передает их по одной в инкапсулированный ресурс, изменяя его, пока он не достигнет конечной формы. Простой ресурс, который всегда возвращает текст, может выглядеть так:

Ресурс, который обращает внимание на строку запроса и отправляет запрос другим ресурсам в зависимости от пути в запросе, может выглядеть так:

Я надеюсь, что вы поняли идею. Код выше довольно набросочный, и большинство вариантов использования не реализованы, но вы можете сделать это сами, если вас это интересует. Код находится в репозитории yegor256/jpages. Не стесняйтесь внести свой вклад с помощью запроса на включение изменений и сделать этот небольшой фреймворк реальным.

Translated by ChatGPT gpt-3.5-turbo/42 on 2023-12-27 at 13:42

sixnines availability badge   GitHub stars