Аннотации и ArchUnit-правила для гексагональной архитектуры в Java-проектах.
hexagonal-architecture-core— аннотации (zero dependencies)hexagonal-architecture-test— готовые ArchUnit-правила
dependencies {
implementation("ru.vikulinva:hexagonal-architecture-core:1.0.0")
testImplementation("ru.vikulinva:hexagonal-architecture-test:1.0.0")
}| Аннотация | Назначение | Где применять |
|---|---|---|
@InboundAdapter |
REST-контроллеры, gRPC, Kafka-консьюмеры | Классы в adapter-слое |
@OutboundAdapter |
JPA-репозитории, HTTP-клиенты, Kafka-продюсеры | Классы в adapter-слое |
@InboundPort |
Use case интерфейсы | Интерфейсы/классы в application-слое |
@OutboundPort |
Интерфейсы для внешних систем | Только интерфейсы в application-слое |
// Domain — чистая бизнес-логика
public class Order extends AggregateRoot<OrderId> { ... }
// Application layer — порты
@InboundPort
public class PlaceOrderUseCase {
private final OrderPersistencePort persistencePort;
public void execute(String orderId) {
Order order = persistencePort.findById(orderId).orElseThrow();
order.place();
persistencePort.save(order);
}
}
@OutboundPort
public interface OrderPersistencePort {
Optional<Order> findById(String id);
Order save(Order order);
}
// Adapter layer — реализации
@InboundAdapter("REST controller for orders")
@RestController
public class OrderController {
private final PlaceOrderUseCase placeOrder;
@PostMapping("/orders/{id}/place")
public void placeOrder(@PathVariable String id) {
placeOrder.execute(id);
}
}
@OutboundAdapter("jOOQ-based order persistence")
@Repository
public class JooqOrderRepository implements OrderPersistencePort {
private final DSLContext dsl;
@Override
public Optional<Order> findById(String id) {
return dsl.selectFrom(ORDERS)
.where(ORDERS.ID.eq(id))
.fetchOptional(this::toOrder);
}
@Override
public Order save(Order order) { ... }
}Один тест для проверки всех правил:
import org.junit.jupiter.api.Test;
import ru.vikulinva.hexagonal.test.HexagonalArchitectureRules;
class ArchitectureTest {
@Test
void hexagonalArchitecture() {
HexagonalArchitectureRules.verify(
"com.example.myservice",
"com.example.myservice.domain",
"com.example.myservice.app",
"com.example.myservice.adapter"
);
}
}- Домен не зависит от фреймворков — Spring, JPA, jOOQ, Kafka, Hibernate, gRPC, MongoDB, Redis, Elasticsearch, AWS SDK
- Домен не зависит от адаптеров
- Домен не зависит от application layer
- Application layer не зависит от адаптеров — направление: adapter → application → domain
- Inbound-адаптеры не зависят от outbound — и наоборот
@OutboundPortтолько на интерфейсах@InboundAdapter/@OutboundAdapterне в domain-пакете
@Test
void domainIsPure() {
JavaClasses classes = new ClassFileImporter()
.importPackages("com.example.myservice");
HexagonalArchitectureRules
.domainMustNotDependOnFrameworks("com.example.myservice.domain")
.check(classes);
}HexagonalArchitectureRules.domainMustNotDependOnFrameworks(domainPkg)
HexagonalArchitectureRules.domainMustNotDependOnAdapters(domainPkg, adapterPkg)
HexagonalArchitectureRules.domainMustNotDependOnApplication(domainPkg, appPkg)
HexagonalArchitectureRules.applicationMustNotDependOnAdapters(appPkg, adapterPkg)
HexagonalArchitectureRules.inboundAdaptersMustNotDependOnOutboundAdapters(adapterPkg)
HexagonalArchitectureRules.outboundAdaptersMustNotDependOnInboundAdapters(adapterPkg)
HexagonalArchitectureRules.outboundPortMustBeInterface()
HexagonalArchitectureRules.adapterAnnotationsMustNotBeInDomain(domainPkg)com.example.myservice/
├── domain/ # Entity, ValueObject, AggregateRoot, DomainEvent
│ ├── model/
│ └── event/
├── app/ # Use cases, порты
│ ├── port/in/ # @InboundPort
│ └── port/out/ # @OutboundPort
└── adapter/ # Инфраструктура
├── in/ # @InboundAdapter — REST, gRPC, Kafka consumers
│ ├── rest/
│ └── grpc/
└── out/ # @OutboundAdapter — JPA, HTTP clients, Kafka producers
├── persistence/
└── client/
- Java 21+
- ArchUnit 1.4+ (для test-модуля)
- Совместим с
ddd-building-blocksиusecase-pattern