Сокращение ручной работы на примере Spring Boot-проекта: OpenAPI generator, QueryDsl, OpenAI

Меня зовут Александр, я Java-разработчик в GlowByte. Работаю в практике управления рисками и комплаенс (Risk & Compliance). Хочу поделиться своим опытом и в целом рассказать о том, чем мы здесь занимаемся. А занимаемся мы автоматизацией систем управления рисками — от AML (противодействие легализации доходов) и операционных рисков до коллекшна (взыскание просроченной задолженности) и систем принятия решений.
Моё направление — как раз последнее: системы принятия решений (СПР). Если коротко, мы автоматизируем стратегии, где нужно в реальном времени перерабатывать кучу входных параметров, учитывать множество факторов и выдавать сложные, комплексные решения. Типичные примеры: оценка риска, предстраховые проверки, системы мониторинга, расчет резервов, расчет комиссионных вознаграждений страховым агентам и многое другое.

Чем это интересно? На выходе — не просто «да/нет», а полноценное управляемое бизнес-правило, которое можно быстро менять без переписывания кода.

В этой статье хочу поделиться опытом разработки backend API на Spring Boot-проекте, где нам пришлось много работать с OpenAPI-спецификацией, динамическими запросами и тестированием бизнес-логики.

Это не статья про «серебряную пулю» и не история о том, как AI заменил разработчика. Скорее это практический рассказ о трёх инструментах, которые помогли нам уменьшить объём ручной работы:

  • OpenAPI Generator — для генерации API-слоя и DTO из спецификации;
  • QueryDSL — для динамических типобезопасных запросов;
  • LLM/GPT — для ускорения покрытия бизнес-логики unit и интеграционными тестами.
Что было на входе
Проект был backend API-приложением на Java/Spring Boot. На входе у нас было примерно следующее:

  • OpenAPI-спецификация примерно на 100 подробно описанных REST endpoint’ов;
  • несколько сотен описанных request/response DTO;
  • модель данных из 50+ таблиц;
  • около десятка интеграций: аутентификация и авторизация, VCS, обмен с БД другой структуры через Apache NiFi, микросервис для тяжёлых вычислений;
  • разветвлённая бизнес-логика;
  • асинхронные процессы;
  • ролевая модель;
  • сессионность взаимодействия;
  • необходимость покрыть тестами бизнес-логику, включая repository-методы со сложными запросами.

При этом важными особенностями были модель данных и бизнес-требования. Часть решений приходилось корректировать уже в процессе — по мере уточнения нюансов логики работы системы, ограничений инфраструктуры и требований ИБ. Массовое покрытие нестабильных участков unit-тестами на ранней стадии имело низкий ROI: сигнатуры, модель данных и бизнес-правила ещё активно менялись.

В таких условиях меньше всего хотелось тратить время на ручное написание однотипного кода. Lombok и JPA Buddy помогали, но полностью проблему boilerplate не решали.
OpenAPI Generator: контракт отдельно, реализация отдельно
Первым инструментом стал OpenAPI Generator. У нас уже была подробная OpenAPI-спецификация. Было бы странно вручную переносить из неё все endpoint’ы, DTO, enum’ы и request/response-модели в Java-код. Поэтому мы использовали генерацию.

В результате из спецификации генерировались:

  • DTO-классы;
  • enum’ы;
  • интерфейсы API;
  • методы контроллеров с заданными сигнатурами и документацией;
  • описание request/response-структур.

Упрощённо это выглядело так:
```yaml
paths:
  /tasks/{taskId}:
    get:
      operationId: getTaskById
      tags:
        - task
      parameters:
        - name: taskId
          in: path
          required: true
          schema:
            type: integer
            format: int64
      responses:
        '200':
          description: Task found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TaskResponse'
```
После генерации мы получали Java-абстракцию, которую оставалось реализовать:
```java
@Override
public ResponseEntity<TaskResponseDto> getTaskById(Long taskId) {
    return ResponseEntity.ok(taskService.getById(taskId));
}
```
В реальном коде всё, конечно, было объёмнее: валидация, обработка ролей, маппинг, сервисный слой. Но основной плюс был в другом: контракт API жил в одном месте.

Если в OpenAPI-спецификации менялся endpoint, поле или тип, это становилось видно на этапе генерации или компиляции. Не нужно было вручную синхронизировать Swagger-документацию и Java-классы.

OpenAPI Generator хорошо снимает механическую работу:

  • не нужно руками писать десятки DTO, и по сути готов скелет для всех контроллеров с сигнатурами и документацией;
  • изменения контракта быстрее доходят до компилятора;
  • проще держать единый формат API.

И самый главный плюс: проще перекладывать ответственность за косяки на аналитика, который описывал спецификацию. Шутка, конечно, хотя…))
QueryDSL: динамические запросы вместо Criteria API
Вторым инструментом, который сильно помог, стал QueryDSL. В проекте было много сложных запросов с параметризацией. Писать такие запросы через строковый JPQL неудобно и небезопасно. Писать через Criteria API можно, но код быстро становится тяжёлым для чтения. QueryDSL в таких местах оказался наиболее удобным компромиссом.

Упрощённый пример:
```java
BooleanBuilder predicate = new BooleanBuilder();

if (filter.getStatus() != null) {
    predicate.and(task.status.eq(filter.getStatus()));
}

if (filter.getCreatedFrom() != null) {
    predicate.and(task.createdAt.goe(filter.getCreatedFrom()));
}

if (filter.getCreatedTo() != null) {
    predicate.and(task.createdAt.loe(filter.getCreatedTo()));
}

if (filter.getAssigneeId() != null) {
    predicate.and(task.assignee.id.eq(filter.getAssigneeId()));
}

return queryFactory
        .select(Projections.constructor(TaskShortView.class,
                task.id,
                task.name,
                task.status,
                task.createdAt,
                user.displayName))
        .from(task)
        .leftJoin(task.assignee, user)
        .where(predicate)
        .orderBy(task.createdAt.desc())
        .fetch();
```
С QueryDSL удобно то, что запрос остаётся типобезопасным. Если поле переименовали или изменили тип, это заметно при компиляции. Плюс хорошо читается логика root. get динамических фильтров. Плюс удобство маппинга в проекции. Хотя важно заметить, что при использовании Projections. constructor типы и порядок параметров DTO-проекции надо контролировать.

Вместо не самого приятного Criteria API:
```java
criteriaBuilder.and(
    criteriaBuilder.equal(root.get("status"), status),
    criteriaBuilder.greaterThanOrEqualTo(root.get("createdAt"), createdFrom)
);
```
получается более декларативный код:
```java
predicate.and(task.status.eq(status));
predicate.and(task.createdAt.goe(createdFrom));
```
Тестирование при помощи LLM (OpenAI ChatGPT 5.2, 5.4)
Ввиду нестабильности ТЗ и принятия ряда решений на ходу (думаю, для многих знакомая ситуация), было принято решение покрыть код тестами после стабилизации основных пользовательских сценариев и ручной проверки критичных edge cases. Благо, хорошая декомпозиция проекта сводила к минимуму регрессии, но, естественно, не отменяла необходимости тестирования.

С появлением ChatGPT 5.2 использование его как помощника в написании тестов стало целесообразно. Подчеркну, что именно как помощника, когда мы говорим об объёмном контексте, а не покрытии класса, например, со строковыми утилитами — с этим справлялись и его предшественники. В процессе написания перешёл на свежий 5.4, но особой разницы не почувствовал.

Для меня рабочим сценарием создания промпта для unit-тестов стал следующий:

  1. Даю модели код тестируемого класса, начиная промпт с нижних уровней абстракции. Стоит добавить, что все методы имели javaDoc-описание, поэтому промптинг становился существенно проще.
  2. Добавляю DTO, enum’ы и зависимости, которые, очевидно, следует добавить.
  3. Спрашиваю у модели, что нужно ещё, и читаю описание её понимания происходящего.
  4. Пробегаюсь по результату, смотрю, что тестируем, и не является ли это откровенной синтетикой. Если надо, добавляю запрос на непроверенные edge cases.
  5. Запускаю тесты, правлю ошибки компиляции, если есть, далее разбираюсь с не прошедшими (их было примерно 5−7%).
  6. Перехожу на уровень выше и по горизонтали обхожу компоненты.
  7. В определённый момент (примерно на 30% от лимита токенов) модель начинала периодически сбоить, но не сильно — в основном забывала код, который уже был в контексте, и надо было его ей продублировать.

За interface-per-implementation anti-pattern замолвите словечко…

Требования к проекту включали использование интерфейса, описывающего поведение компонента, даже если этот компонент являлся его единственной реализацией. По моим субъективным наблюдениям, многим разработчикам такой подход претит, и я не исключение. Тем не менее написание в таком стиле позволило сделать промпты на большом контексте существенно лаконичней, потому что был готовый задокументированный код абстракций, которые инжектились в тестируемый компонент, а иногда это в совокупности могло составлять 1000+ строк кода, если бы мы добавляли реализации. Таким образом, я достаточно быстро набил руку в составлении промптов, учитывая, что я знал проект очень хорошо.
Интеграционные тесты
В ряде сложных запросов встречались ситуации с логическими ветвлениями, что тоже считалось бизнес-логикой. Здесь в целом работала логика с unit-тестами, просто в контекст добавлялись @Entity, задействованные в запросе. Модель безошибочно справлялась с созданием db fixtures.
Результат
За пару недель мне удалось при помощи OpenAI сформировать 1008 тестов. Для этой модели было свойственно писать порой объёмные тесты, проверяющие сразу несколько кейсов. Я не стал их дробить, в большинстве они вполне читаемы. Я не сильно углублялся в формальные данные code coverage, а старался делать акцент на понимании того, что вообще тестируем и будет ли это способствовать стабильности приложения, но LLM обычно предлагала большинство очевидных edge cases.
Заключение
Все три инструмента ощутимо ускорили разработку, но важно помнить, что ни один из них не заменяет экспертизу разработчика.

Спецификация требует контроля. Query Dsl не отменяет понимания SQL и модели данных, особенно в местах, где необходимо переходить на native query. LLM, в свою очередь, является отличным помощником, но, подчеркну, именно помощником, избавляя разработчика, обладающего должной экспертизой, от части рутинных задач, подбрасывающая порой интересные идеи, но при этом требующая постоянного ревью своих решений. Кстати, заметил, что это ревью кода от LLM — штука достаточно трудозатратная, и работа со сложными промптами с последующим ревью результатов, зачастую с несколькими итерациями правок, выматывает намного сильнее, чем просто написание кода. Но опять же всё зависит от проекта — MVP чего-то простого современные агенты генерируют на ура.

Ссылки на библиотеки:
https://github.com/querydsl/querydsl
https://github.com/springdoc/springdoc-openapi