Корпоративные серверные приложения
Что это такое
Большинство серверных приложений на Java повторяют одну форму: фреймворк запускает процесс, контейнер инверсии управления создаёт объекты и связывает их, входящие запросы направляются в аннотированные методы, а доступ к данным идёт через слой репозиториев поверх реляционной базы. Конвенции здесь сильные и одинаковые от проекта к проекту, поэтому опытный разработчик на Java быстро осваивается в незнакомом коде.
Почему это важно
Конвенции содержат решения задач, с которыми сталкивается любое серверное приложение: жизненный цикл, связывание зависимостей, границы транзакций, обработка запросов. Их принятие означает, что эти задачи не решаются заново, а единообразие дорого стоит, когда команда растёт или появляется подрядчик. Однако цена в том, что конвенции поглощают решения, которые должно принимать само приложение — охват транзакции, отображение ошибок, место валидации. Тогда проект может начать следовать фреймворку вместо проектирования системы.
Ключевые идеи
Контейнер создаёт граф объектов
Вместо того чтобы каждый класс создавал своих помощников сам, фреймворк строит их один раз при запуске из объявленных компонентов и внедряет туда, где они нужны. Поскольку создание происходит централизованно, жизненный цикл, области видимости и конфигурация решаются в одном месте; следствие, за которым нужно следить, — настолько всеядный контейнер, что граф зависимостей становится невидимым, и лишь явное создание с неглубоким графом сохраняет его читаемым.
контейнер -> создаёт OrderService(Database db, PaymentGateway gateway)
-> внедряет его в обработчик запроса
Запрос обрабатывается в определённой области видимости
Фреймворк обычно предлагает один экземпляр на запрос, один на приложение и внедряет объект уровня запроса в то, что этот запрос обслуживает. Именно знание области видимости останавливает классическую ошибку: состояние запроса хранят в объекте уровня приложения, и данные одного пользователя попадают в запрос другого.
Границы транзакций — проектное решение, к которому фреймворк подталкивает небрежно
Аннотация может открыть и зафиксировать транзакцию вокруг метода: это удобно и определяет, что атомарно. Слишком низкая граница делает связанные записи неатомарными; слишком высокая держит блокировки всё время несвязанной работы. Где проходит граница — решение о корректности, а не деталь конфигурации.
Слой репозиториев изолирует хранение, и его протечки становятся интерфейсом
Репозиторий переводит между объектами домена и строками или таблицами базы. Пока он тонкий, абстракция держится. Когда наружу протекают запросы и ленивая загрузка, код начинает зависеть от механизма хранения. Честная позиция такова: репозиторий служит интерфейсом над отображением, и любое поведение, которое зависит от того, как достаются строки, должно быть явным, а не случайным.
Аннотации фреймворка делают поведение невидимым для читателя
Транзакционное поведение метода, его безопасность и кэширование могут быть объявлены аннотацией, о которой тело метода не упоминает. Это удобно и означает, что чтение метода в отрыве от остального может вводить в заблуждение. Небольшая аннотированная поверхность и аннотации ближе к началу пути вызова делают поведение понятным тому, кто прослеживает запрос.
Конвенция вместо изобретения — это ценность и одновременно риск
Способ делать вещи, предложенный фреймворком, почти всегда разумное значение по умолчанию, поэтому он и распространился. Риск в том, чтобы перенять его, не решив, что на самом деле нужно приложению: валидация в контроллере, когда её место в домене, сущности, отданные наружу через API, и набор тестов ради одной ветки, который поднимает весь контейнер. Если относиться к конвенциям как к отправной точке, а не как к архитектуре, проект остаётся видимым.
Практика
Проследите запрос по слоям
Возьмите реальное приложение и проследите один его эндпоинт от диспетчера до базы, называя область видимости и границу транзакции на каждом шаге.
Готово, когда: вы можете сказать, где транзакция начинается и фиксируется и какие объекты разделяются между запросами.
Перенесите правило валидации на нужный слой
Найдите правило валидации, которое проверяется в обработчике запроса, решите, принадлежит ли оно домену, затем перенесите его и поправьте тесты.
Готово, когда: правило соблюдается независимо от того, через какую точку входа пришёл запрос, а тест покрывает поведение домена напрямую.
Частые ошибки
- Состояние запроса в объекте уровня приложения — данные одного пользователя появляются в запросе другого; исправить: держите состояние запроса в объекте уровня запроса.
- Граница транзакции там, где оказалась удобная аннотация — связанные записи неатомарны или блокировки держатся слишком долго; исправить: размещайте границу осознанно.
- Сущности хранения, отданные наружу через API — схема становится публичным контрактом; исправить: отображайте в отдельное представление на границе.
- Опора на аннотации, которых не видно в методе — поведение остаётся невидимым и удивляет того, кто сопровождает код; исправить: держите аннотированную поверхность небольшой и прослеживаемой.
- Подъём всего контейнера ради одной ветки — тесты медленные, а отказы неоднозначные; исправить: тестируйте логику домена напрямую, а интеграционные тесты оставьте для связывания.
Что читать дальше
- Сборка и зависимости (05-backend-data/04-java/03-builds-and-dependencies) — как фреймворк и его зависимости попадают в артефакт.
- Java (05-backend-data/04-java) — язык, на котором написаны эти серверные приложения.
- Микросервисы (05-backend-data/12-microservices) — альтернатива одному приложению, содержащему всё это, и когда такая форма уместна.