- Add BaseAuditPO with audit fields (createUserId, updateUserId, createTime, updateTime, deleted, version) - Implement MapStruct for Order ↔ OrderPO mapping with @BeanMapping(ignoreByDefault=true) - Create Dockerfile with multi-stage build and runtime-only mode support - Add .dockerignore for optimized build context - Create init.sh script for project scaffolding with macOS/Linux compatibility - Update OrderPO to extend BaseAuditPO - Update OrderDao to use MapStruct mapper - Add MapStruct processor dependency to infrastructure module - Follow M2 v5 frozen contracts for audit trail and persistence layer Co-authored-by: Qwen-Coder <[email protected]>
12 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Project Overview
Java 21 通用项目模板 using Spring Boot 3.4+ with Clean Architecture (Hexagonal/Ports & Adapters). Multi-tenant SaaS service template implementing strict layer isolation and compile-time architecture enforcement via ArchUnit.
Group ID: com.ljwx.tmpl
Java Version: 21 (Virtual Threads enabled by default)
Build Tool: Maven (multi-module)
Common Commands
Build & Test
# Run all tests (unit + integration)
mvn test
# Run tests in a specific module
mvn test -pl tmpl-domain
mvn test -pl tmpl-application
mvn test -pl tmpl-infrastructure
mvn test -pl tmpl-api
# Run a single test class
mvn test -Dtest=TenantContextTest
mvn test -pl tmpl-application -Dtest=TenantContextTest
# Run specific test method
mvn test -Dtest=ErrorCodeTest#shouldReturnCorrectHttpStatus
Code Quality
# Apply code formatting (Google Java Format, AOSP style)
mvn spotless:apply
# Check formatting without fixing
mvn spotless:check
# Run full quality gate (format + static analysis + tests + coverage)
mvn verify
# Enable quality gates (normally skipped for fast local builds)
mvn verify -Djacoco.skip=false -Dspotbugs.skip=false -Ddependency-check.skip=false
Running the Application
# Start application (dev profile active by default)
mvn clean spring-boot:run
# From tmpl-api module
cd tmpl-api
mvn spring-boot:run
# With specific profile
mvn spring-boot:run -Dspring-boot.run.profiles=prod
# Access endpoints
# - Health check: http://localhost:8080/actuator/health
# - Swagger UI: http://localhost:8080/swagger-ui.html
# - Actuator: http://localhost:8080/actuator
Database
# Flyway migrations run automatically on startup
# Migration files: tmpl-api/src/main/resources/db/migration/V*.sql
# Start local PostgreSQL (if docker-compose.yml exists)
docker-compose up -d postgres
# Stop
docker-compose down
Architecture
Four-Layer Structure
Dependency direction: outer → inner (strictly enforced by ArchUnit)
tmpl-api (REST Layer)
↓ depends on
tmpl-infrastructure (Adapters) + tmpl-application (Use Cases)
↓ depends on
tmpl-domain (Pure Business Logic)
tmpl-domain
- Location:
/tmpl-domain - Purpose: Pure business logic, framework-agnostic
- Contains: Domain entities, value objects, repository interfaces,
DomainException,ErrorCodeenum - Constraints:
- Zero Spring/JPA/Hibernate dependencies
- Only Java stdlib +
jakarta.validation-api(annotations only, no runtime validator injection) - All business errors thrown as
DomainException(ErrorCode, optionalDetail)
tmpl-application
- Location:
/tmpl-application - Purpose: Use case orchestration, transaction boundaries, tenant context management
- Contains: UseCase interfaces, command/query DTOs,
TenantContext(ThreadLocal),TenantScope,@Transactionaldeclarations - Constraints:
- ONLY location for
@Transactional- never in infrastructure/api layers - No Web (
@RestController), Persistence (@Entity), Security, HTTP Client dependencies - Depends on: domain + spring-context + spring-tx only
- ONLY location for
tmpl-infrastructure
- Location:
/tmpl-infrastructure - Purpose: Technical implementation of ports (persistence, external services, events)
- Contains: JPA entities (PO), repositories, adapters, MapStruct mappers
- Constraints:
- Implements application layer Port interfaces
- No
@Transactionalhere (transactions controlled by application layer) - Repository pattern:
internal/OrderJpaRepository(package-private) →internal/OrderDao(public, tenant-aware) →adapter/OrderPersistenceAdapter(implements Port)
tmpl-api
- Location:
/tmpl-api - Purpose: HTTP boundary, Spring Boot entry point, filters, exception handling
- Contains:
TmplApplication.java, controllers, filters (TraceIdFilter,TenantContextFilter),GlobalExceptionHandler, Flyway migrations - Constraints:
- Controllers only inject UseCase interfaces (never infrastructure classes directly)
- Constructor injection only (no
@Autowiredon fields) - Request DTOs must NOT contain
tenantId/deptId/userId(comes from headers) - Security rule: Never read
tenantId/deptId/userIdfrom request body - only from gateway-injected headers viaTenantContext
Multi-Tenant Context
TenantContext (tmpl-application/context/TenantContext.java):
- ThreadLocal holder for
TenantScope(tenantId, deptId, userId, roles) - Set by
TenantContextFilterfrom HTTP headers (X-Tenant-Id,X-Dept-Id,X-User-Id,X-User-Roles) - Usage:
TenantScope scope = TenantContext.require(); // throws if not set Optional<TenantScope> scope = TenantContext.get(); // safe access String tenantId = TenantContext.getTenantId(); Long userId = TenantContext.getUserIdOrDefault(); // returns 0L if empty - CRITICAL: Always call
TenantContext.clear()in finally block to prevent ThreadLocal leaks
deptId = "0" semantic: Means cross-department access (requires SCOPE_ALL_DEPT role). Never store "0" in database.
Error Handling
ErrorCode enum (tmpl-domain/exception/ErrorCode.java):
- Frozen contract mapping error scenarios to HTTP status codes + default messages
- Example:
ORDER_NOT_FOUND(404, "订单不存在"), ORDER_AMOUNT_INVALID(422, "订单金额不合法")
DomainException (tmpl-domain/exception/DomainException.java):
// Use default message
throw new DomainException(ErrorCode.ORDER_NOT_FOUND);
// Override with custom detail
throw new DomainException(ErrorCode.ORDER_AMOUNT_INVALID, "金额不能为负: -100");
GlobalExceptionHandler (in tmpl-api):
- Catches
DomainException, readsErrorCodeto determine HTTP status - Returns structured JSON:
{code, message, details, traceId}
Auditing
BaseAuditPO (inherited by all JPA entities):
- Fields:
createUserId,updateUserId,createTime,updateTime,deleted,version - Auto-populated via
@CreatedBy,@LastModifiedBy,@CreatedDate,@LastModifiedDate - AuditorAwareImpl returns
TenantContext.getUserIdOrDefault()for audit fields
Transaction Boundaries
CRITICAL RULE: @Transactional ONLY in application layer (UseCase service implementations).
// ✅ Correct - in tmpl-application
@Service
public class CreateOrderService implements CreateOrderUseCase {
@Transactional
@Override
public OrderDetail execute(CreateOrderCommand cmd, TenantScope scope) {
// ...
}
}
// ❌ Wrong - infrastructure adapters must NOT declare @Transactional
Key Constraints (ArchUnit Enforced)
A-Class Rules (CI-blocking):
domainmodule: No imports fromspring.*,jakarta.persistence.*,org.hibernate.*applicationmodule: No Web/Persistence/Security/HTTP Client dependencies@Transactionalonly in application layer- Controllers: No direct imports of infrastructure implementation classes
- All JPA
@Query(native or JPQL) must includetenant_id/tenantIdchecks - Adapters: Only access
OrderDao, never rawJpaRepository
B-Class Rules (strong recommendation):
- UseCase methods named
execute(...) - Request DTOs must NOT contain
tenantId/deptId/userIdfields
C-Class Rules (optional):
@Asyncmethods must not callTenantContext.current()@RequestBodyparameters should have@Valid
Adding New Features
Add Domain Entity
- Create entity in
tmpl-domain/src/main/java/com/ljwx/tmpl/domain/{context}/entity/- Use Java 21
recordfor immutable entities/value objects - Use regular class if mutable state needed
- Include business validation logic (not just getters/setters)
- Implement business methods (avoid anemic domain model)
- Use Java 21
- Create value objects in
domain/{context}/valueobject/(preferrecord) - Add
ErrorCodeentries inErrorCode.javafor domain exceptions - Create repository interface in
domain/{context}/repository/- Method signatures reflect business intent
- Return domain objects (not POJOs)
- Create domain events in
domain/{context}/event/if needed - Write unit tests
- Verify constraints:
- ✅ No Spring annotations (
@Component,@Service, etc.) - ✅ No JPA annotations (
@Entity,@Table, etc.) - ✅ No framework dependencies
- ✅ No Spring annotations (
Add Use Case
- Create command/query DTO in
tmpl-application/src/main/java/com/ljwx/tmpl/application/{context}/port/in/- Use Java 21
recordfor immutable DTOs
- Use Java 21
- Create UseCase interface with
execute(...)method (naming convention enforced) - Implement service in
tmpl-application/.../service/- Add
@Serviceannotation - Add
@Transactionalfor write operations - Transaction rules: No external HTTP calls or MQ sends inside transactions
- Add
- Define output port interface in
port/out/if repository/external service access needed
Add Persistence
- Create JPA entity (PO) in
tmpl-infrastructure/persistence/{context}/internal/ - Create
JpaRepository(package-private) ininternal/ - Create
Daoclass (public, tenant-aware methods) ininternal/ - Create adapter implementing application port in
adapter/ - Create MapStruct mapper for domain ↔ PO conversion
- Create Flyway migration in
tmpl-api/src/main/resources/db/migration/V{version}__{description}.sql
Add Controller
- Create controller in
tmpl-api/src/main/java/com/ljwx/tmpl/api/controller/ - Constructor-inject UseCase interface (not implementation)
- Map HTTP request → Command/Query DTO
- Never include
tenantId/deptId/userIdin request body (read fromTenantContext) - Add OpenAPI annotations for documentation
Testing
Unit Tests
- Domain: Pure business logic tests, no mocks needed
- Application: Mock ports (repositories), test use case logic
- Run:
mvn test -pl tmpl-{module}
Integration Tests
- Located in
tmpl-api/src/test/java/ - Use
@SpringBootTest+ Testcontainers (PostgreSQL, Redis) - Test full request → response flow with real database
ArchUnit Tests
- Located in
tmpl-api/src/test/java/.../ArchitectureTest.java - Runs automatically with
mvn test - Enforces layer dependencies, naming conventions, annotations
Key Files
- Architecture spec:
/docs/architecture-spec.md- 31 core decisions, frozen contracts - Security context:
/docs/contracts/security-context.md- Multi-tenancy rules - Error handling:
/docs/contracts/errors.md- ErrorCode design - Transactions:
/docs/contracts/transactions.md- Transaction boundaries - POM:
/pom.xml- Parent POM with all dependency versions locked
Important Design Decisions
- No field injection: Always use constructor injection (no
@Autowiredon fields) - ErrorCode is frozen: Only add new entries or
@Deprecated, never change existingname/httpStatus - Transactions only in application layer: Infrastructure never declares
@Transactional - Tenant isolation everywhere: All queries filter by
tenantId, all writes includetenantId - ThreadLocal cleanup: Always
TenantContext.clear()in finally block - No layers skipped: Controller → UseCase → Domain (never Controller → Repository)
- MapStruct for mapping: Compile-time code generation, no runtime reflection
- Virtual Threads ready: Java 21 Virtual Threads enabled, ThreadLocal usage compatible
- Security context trust:
tenantId/deptId/userIdonly from gateway headers, never from request body - Rich domain model: Domain entities contain business logic, not just data (avoid anemic domain model)
- Prefer Java 21 records: Use
recordfor immutable value objects and entities when possible - UseCase naming: All UseCase entry methods named
execute(...)
CI/CD
Two-job pipeline:
- quality-gate (all branches): Compile, test, format check, SpotBugs, OWASP, JaCoCo ≥70%
- build-and-deploy (main/release only): Docker build, push to Harbor, update deploy repo
Quality gates skipped by default for fast local builds:
jacoco.skip=truespotbugs.skip=truedependency-check.skip=true
CI explicitly enables with -D{check}.skip=false.
Implementation Batches
Current: Batch 2 complete (Domain exceptions + Tenant context)
Remaining:
- Batch 3: Filters (TraceIdFilter, TenantContextFilter)
- Batch 4: ArchUnit tests
- Batch 5: Persistence layer + Flyway
- Batch 6: Docker + Kubernetes deployment