Опирается на правила:
R-HEX-BOOT-1…R-HEX-BOOT-3иR-HEX-BOOT-X1…R-HEX-BOOT-X2из Hexagonal Style Guide → раздел 7. Bootstrap / composition root.
Важно знать
bootstrap/— composition root:@SpringBootApplication+application.yml+@Configuration-классы +Dockerfile.bootstrap/build.gradle.ktsзависит от всех модулей:core/,persistence/, всех*-in-adapter/, всех*-out-adapter/.- Никто не зависит от
bootstrap/— это закрывающий узел.@SpringBootApplication(scanBasePackages = ...)покрывает все адаптеры либо через component scan, либо через explicit@Import.- Бизнес-логика и REST-контроллеры в
bootstrap/— запрещены. Только композиция и configs.@SpringBootApplicationвcore/или адаптере — запрещён. Только вbootstrap/, иначе невозможно собрать сервис из частей.
bootstrap/ — это маленький, но критически важный модуль. Все остальные — core/, адаптеры — это «кирпичи». bootstrap/ — это «дом», который собирается из этих кирпичей. Он содержит точку входа (main), конфигурацию Spring, application.yml, Dockerfile. И ничего больше. Никакой бизнес-логики, никаких контроллеров — всё это в соответствующих модулях. Раскрытие правил R-HEX-BOOT-* ниже.
Что лежит в bootstrap/
R-HEX-BOOT-1: точный состав модуля.
bootstrap/
├── src/main/java/<pkg>/bootstrap/
│ ├── <App>Application.java # @SpringBootApplication + main()
│ ├── config/ # @Configuration-классы для wiring
│ │ ├── ClockConfig.java
│ │ ├── ObjectMapperConfig.java
│ │ └── SecurityConfig.java # (если не разнесён по in-adapter)
│ └── ...
├── src/main/resources/
│ ├── application.yml # общий config
│ ├── application-local.yml # локальный профиль
│ ├── application-integration-test.yml
│ ├── application-production.yml
│ └── logback-spring.xml
├── Dockerfile
├── docker-compose.yml # для local-разработки
└── helm/ # Helm chart (если используется)
Минимальная точка входа:
@SpringBootApplication
public class OrderServiceApplication {
public static void main(String[] args) {
SpringApplication.run(OrderServiceApplication.class, args);
}
}
В @Configuration-классах живут только бины, которые требуют явного провода:
- Production-бины для interfaces
Clock,UuidProvider(см. Bootstrap design). ObjectMapperс custom-настройками (если стандартного Spring Boot mapper'а не хватает).- Custom
DefaultConfigurationCustomizerдля jOOQ (см. DSLContext). RestClient-bean'ы (если их wiring не делается per-system в out-adapter'ах).
Большинство бинов поднимется автоматически через component scan — @Component-аннотированные классы в адаптерах попадут в ApplicationContext без явного wiring.
bootstrap/ зависит от всех
R-HEX-BOOT-2: gradle-зависимости.
// bootstrap/build.gradle.kts
dependencies {
implementation(project(":core"))
implementation(project(":persistence"))
implementation(project(":user-api-in-adapter"))
implementation(project(":admin-api-in-adapter"))
implementation(project(":kafka-in-adapter"))
implementation(project(":sber-out-adapter"))
implementation(project(":sms-out-adapter"))
implementation(project(":kafka-out-adapter"))
implementation(project(":scheduler-out-adapter"))
// Spring Boot стартеры — только тут
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-actuator")
implementation("org.springframework.boot:spring-boot-starter-jooq")
implementation("org.springframework.boot:spring-boot-starter-security")
implementation("org.springframework.boot:spring-boot-starter-oauth2-resource-server")
implementation("org.springframework.kafka:spring-kafka")
// Liquibase
implementation("org.liquibase:liquibase-core")
runtimeOnly("org.postgresql:postgresql")
}
И никто не зависит от bootstrap/. В core/build.gradle.kts, persistence/build.gradle.kts, и любом другом модуле — нет project(":bootstrap"). Это критично:
bootstrap/— sink-модуль, в нём заканчиваются все стрелки зависимостей.- Если бы
core/зависел отbootstrap/, циклическая зависимость в gradle сломает сборку. - Если бы adapter зависел от bootstrap — нарушение стрелки
bootstrap → core ← adapters(см. Module structure →R-HEX-MOD-X2).
Component scan покрывает адаптеры
R-HEX-BOOT-3: два способа сделать бины из адаптеров видимыми для Spring.
Вариант 1 — default component scan по корневому пакету. @SpringBootApplication сканирует пакет своего класса и все суб-пакеты:
package ru.example.order; // ← корневой пакет приложения
@SpringBootApplication
public class OrderServiceApplication { /* ... */ }
Если все модули лежат в пакетах под ru.example.order.*:
core/.../ru/example/order/core/...
persistence/.../ru/example/order/persistence/...
user-api-in-adapter/.../ru/example/order/userapi/...
sber-out-adapter/.../ru/example/order/sberout/...
Spring увидит все @Component/@Service/@RestController-классы автоматически.
Вариант 2 — explicit scanBasePackages если структура не позволяет default:
@SpringBootApplication(scanBasePackages = {
"ru.example.order.core",
"ru.example.order.persistence",
"ru.example.order.userapi",
"ru.example.order.sberout",
// ...
})
public class OrderServiceApplication { /* ... */ }
Вариант 3 — @Import-классов конфигурации. Каждый адаптер экспортирует свой @Configuration-класс, bootstrap явно импортирует:
@SpringBootApplication
@Import({PersistenceConfig.class, SberOutAdapterConfig.class, UserApiInAdapterConfig.class})
public class OrderServiceApplication { /* ... */ }
Это чище в смысле «явного контракта между модулями», но требует больше boilerplate. На практике — variant 1 или 2.
Профили в application.yml
bootstrap/ владеет всеми профилями:
# application.yml — общий для всех профилей
spring:
application:
name: order-service
jpa:
open-in-view: false
# application-local.yml
spring:
datasource:
url: jdbc:postgresql://localhost:5432/orders
username: orders
password: orders
sber:
api-url: https://sber-sandbox.example.com
# application-production.yml
spring:
datasource:
url: ${DB_URL}
username: ${DB_USER}
password: ${DB_PASSWORD}
sber:
api-url: ${SBER_API_URL}
См. Spring Bootstrap Style Guide — детальные правила раскладки профилей.
Что запрещено
R-HEX-BOOT-X1: бизнес-логика или REST-контроллеры в bootstrap/.
// ПЛОХО
package ru.example.order.bootstrap;
@RestController // ← контроллер в bootstrap
public class OrderController {
@PostMapping("/orders")
public OrderJson createOrder(@RequestBody CreateOrderRequest req) {
// ...
}
}
Что не так:
- Размывается ответственность.
bootstrap/должен быть «сборка из частей», как только в нём появляется логика — он перестаёт быть тонким composition root. - Невозможно перенести. Логика в bootstrap не уезжает в
*-in-adapter/-модуль без рефакторинга — она впаяна в Spring-application. - Тестируется как полконтекста. Тест на controller — это
@SpringBootTest(потому что класс в bootstrap), а должен быть@WebMvcTestна in-adapter-модуле.
Controller — в user-api-in-adapter или admin-api-in-adapter. Логика — в core/. bootstrap/ — только composition.
R-HEX-BOOT-X2: @SpringBootApplication в core/ или адаптере.
// ПЛОХО
package ru.example.order.core;
@SpringBootApplication // ← в core?!
public class CoreApplication { /* ... */ }
Что не так:
- Невозможно собрать сервис из частей. Когда
@SpringBootApplicationвcore/, любой сервис на основе этого core тянет всю Spring-инфраструктуру. Идея «core можно использовать в Lambda» рушится. - Дублирование конфигурации. Один Application-класс в core, второй в bootstrap = два контекста, два main()-метода — что запускать?
@SpringBootApplicationнесёт component scan. Если он стартует из core-пакета, его scan может перекрыть scan из bootstrap — конфликт.
@SpringBootApplication строго в bootstrap/. Один. Везде.
Куда дальше
- Hexagonal Style Guide → раздел 7. Bootstrap / composition root — нормативные формулировки.
- Spring Bootstrap Style Guide — раскладка профилей,
application.yml, production-бины для clock/UUID. - Структура модулей — как
bootstrap/собирается из core и адаптеров. - Архитектурные тесты — где ArchUnit-тесты живут (обычно в
bootstrap/src/test/).