# Kickoff Java Project — Core Maven Modules Core Maven modules scaffolded for every project. See `conventions.md` for JSpecify, starter renames, and known pitfalls. Reference: Janus module POMs and source classes. --- ## common module (`-common`) Shared utilities, constants, annotations. ### POM dependencies ```xml org.jspecifyjspecify org.springframework.bootspring-boot-starter-logging io.github.jaspeenulid-java jakarta.annotationjakarta.annotation-api jakarta.validationjakarta.validation-api org.springframework.bootspring-boot-starter-testtest ``` ### Source structure - `src/main/java//common/package-info.java` — `@NullMarked` - `src/test/java//common/` — at least one sample unit test --- ## model module (`-model`) Domain entities, repositories, Liquibase migrations. ### POM dependencies ```xml ${project.groupId}-common org.springframework.bootspring-boot-starter-jooq org.springframework.bootspring-boot-starter-jpa org.springframework.bootspring-boot-starter-validation org.springframework.dataspring-data-commons org.postgresqlpostgresqlruntime software.amazon.jdbcaws-advanced-jdbc-wrapper software.amazon.awssdkrds software.amazon.awssdksso software.amazon.awssdkssooidc org.springframework.bootspring-boot-starter-liquibase io.github.jaspeenulid-java org.springframework.bootspring-boot-starter-testtest org.testcontainerstestcontainers-postgresqltest ``` ### jOOQ codegen plugin (if jOOQ) ```xml org.codehaus.mojo build-helper-maven-plugin add-generated-sources add-source generate-sources target/generated-sources/jooq org.testcontainers testcontainers-jooq-codegen-maven-plugin org.testcontainerstestcontainers${testcontainers.version} org.testcontainerstestcontainers-jdbc${testcontainers.version} org.testcontainerstestcontainers-postgresql${testcontainers.version} org.postgresqlpostgresql${postgresql.version} generate-jooq-sources generate generate-sources ${skip.jooq.codegen} POSTGRES postgres:17.5 db/changelog/db.changelog-master.yaml src/main/resources public false truetrue falsefalse falsetrue co.humand..jooq target/generated-sources/jooq ``` ### Liquibase - `src/main/resources/db/changelog/db.changelog-master.yaml` with `includeAll: path: changes/` - `src/main/resources/db/changelog/changes/` — one initial migration (placeholder SQL) ### Entity convention ```java @Data @Builder @NoArgsConstructor @AllArgsConstructor public class SampleEntity { private long id; private long instanceId; private String name; @Nullable private String description; private Instant createdAt; private Instant updatedAt; } ``` ### Repository pattern (jOOQ) ```java public interface SampleEntityRepository { Optional findById(long id); } ``` ```java @Repository @RequiredArgsConstructor public class SampleEntityRepositoryImpl implements SampleEntityRepository { private final DSLContext dslContext; @Override public Optional findById(long id) { // jOOQ DSL query } } ``` ### Source structure - `src/main/java//model/package-info.java` — `@NullMarked` (top-level only) - `src/test/java/` — at least one sample unit test --- ## core module (`-core`) Business logic services, exceptions, caching. ### POM dependencies ```xml ${project.groupId}-common ${project.groupId}-model org.springframework.bootspring-boot-starter org.springframework.bootspring-boot-starter-validation org.springframework.bootspring-boot-starter-cache com.datadoghqdd-trace-api org.springframework.bootspring-boot-starter-testtest ``` ### Service pattern ```java @Service @RequiredArgsConstructor public class SampleService { private final SampleEntityRepository sampleEntityRepository; @Transactional(readOnly = true) public SampleEntity findById(long id) { return sampleEntityRepository.findById(id) .orElseThrow(() -> new SampleNotFoundException("Sample not found: " + id)); } } ``` If **Redis = Yes**: add `@Cacheable(value = CacheNames.X, key = "#id")` on read methods, `@CacheEvict` on writes. If **Trace = Yes**: add `@Trace` on service methods. ### Exception pattern ```java public class SampleNotFoundException extends RuntimeException { public SampleNotFoundException(String message) { super(message); } } ``` ```java public enum ExceptionCodes { ENTITY_NOT_FOUND, VALIDATION_ERROR } ``` ### Source structure - `src/main/java//core/package-info.java` — `@NullMarked` - `src/test/java/` — at least one sample unit test --- ## webapp module (`-webapp`) Spring Boot application entry point, controllers, security config, test infrastructure. ### POM dependencies ```xml ${project.groupId}-common ${project.groupId}-model ${project.groupId}-core ${project.groupId}-security ${project.groupId}-kafka org.springframework.bootspring-boot-starter-webmvc org.springframework.bootspring-boot-starter-actuator org.springframework.bootspring-boot-starter-security org.springframework.bootspring-boot-starter-security-oauth2-resource-server org.springframework.bootspring-boot-starter-validation org.springframework.bootspring-boot-starter-data-redis org.springframework.bootspring-boot-starter-cache io.micrometermicrometer-registry-statsd org.springframework.bootspring-boot-starter-logging co.humand.commonslogging co.humand.commons.securityauthentication org.springdocspringdoc-openapi-starter-webmvc-ui commons-iocommons-io org.springframework.bootspring-boot-starter-testtest org.springframework.bootspring-boot-restclienttest org.springframework.bootspring-boot-resttestclienttest org.springframework.bootspring-boot-testcontainerstest org.springframework.bootspring-boot-starter-security-testtest org.testcontainerstestcontainers-junit-jupitertest org.testcontainerstestcontainers-postgresqltest org.testcontainerstestcontainers-kafkatest org.springframework.bootspring-boot-starter-kafka-testtest org.awaitilityawaitilitytest ``` ### Build plugins **spring-boot-maven-plugin:** ```xml org.springframework.boot spring-boot-maven-plugin co.humand..webapp.Application repackage ``` **maven-failsafe-plugin** (integration tests): ```xml org.apache.maven.plugins maven-failsafe-plugin **/*IntegrationTest.java ${project.build.outputDirectory} @{argLine} --enable-preview --enable-native-access=ALL-UNNAMED integration-testverify ``` **build-helper-maven-plugin** (test-integration source): ```xml org.codehaus.mojo build-helper-maven-plugin add-integration-test-source add-test-source generate-test-sources src/test-integration/java add-integration-test-resource add-test-resource generate-test-resources src/test-integration/resources ``` **JaCoCo** (merged coverage for unit + integration): ```xml org.jacoco jacoco-maven-plugin **/co/humand//jooq/** **/co/humand//webapp/config/cache/** default-prepare-agentprepare-agent prepare-agent-integration prepare-agent-integration ${project.build.directory}/jacoco-it.exec merge-resultsmergeverify ${project.build.directory}*.exec ${project.build.directory}/jacoco-merged.exec report-mergedreportverify ${project.build.directory}/jacoco-merged.exec ${project.reporting.outputDirectory}/jacoco-merged check-coveragecheckverify ${project.build.directory}/jacoco-merged.exec BUNDLE INSTRUCTIONCOVEREDRATIO0.00 BRANCHCOVEREDRATIO0.00 ``` Initial thresholds: `0.00` (0%). Increase as tests are added. ### Application.java ```java @SpringBootApplication(scanBasePackages = "co.humand.") @Import(SecurityConfiguration.class) public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } } ``` ### WebSecurityConfiguration Copy from Janus `WebSecurityConfiguration`. Key structure: - `@Configuration @EnableWebSecurity @EnableMethodSecurity` - `SecurityFilterChain` bean: CSRF disabled, CORS with defaults, OAuth2 resource server with JWT, permit actuator/swagger/OPTIONS, authenticated for rest, `BearerTokenAuthenticationFilter`, stateless sessions - `JwtDecoder` bean using `SecurityConfigurationProperties` - `CorsConfigurationSource` bean from `SecurityConfigurationProperties.Cors` ### Security classes (in webapp) - **AuthenticatedUser** — record with JWT claims (`instanceId`, `userId`, `scope`, etc.) - **JwtTokenConverter** — converts JWT to `JanusAuthenticationToken` with `AuthenticatedUser` as principal. Null-check all claims. - **CustomAuthenticationEntryPoint** — returns 401 JSON error - **JanusAuthenticationToken** (or `AuthenticationToken`) — extends `AbstractAuthenticationToken` ### HttpLoggingConfiguration ```java @Configuration class HttpLoggingConfiguration { @Bean PrincipalInfoExtractor principalInfoExtractor() { return authentication -> { if (authentication != null && authentication.getPrincipal() instanceof AuthenticatedUser user) { return new PrincipalInfo(user.userId(), user.instanceId()); } return PrincipalInfo.ANONYMOUS; }; } @Bean FilterRegistrationBean httpLoggingFilterRegistration( HttpIncomingLoggingFilter httpIncomingLoggingFilter) { FilterRegistrationBean registration = new FilterRegistrationBean<>(httpIncomingLoggingFilter); registration.setEnabled(false); return registration; } } ``` ### OpenApiConfig (if Swagger=Yes) ```java @Configuration public class OpenApiConfig { @Bean public OpenAPI openAPI(@Value("${server.servlet.context-path}") String contextPath) { return new OpenAPI() .servers(List.of(new Server().url(contextPath).description("Server URL"))) .info(new Info().title(" Service").version("v1.0.0")) .components(new Components() .addSecuritySchemes("JWT", new SecurityScheme() .type(SecurityScheme.Type.HTTP).scheme("bearer") .bearerFormat("JWT").in(SecurityScheme.In.HEADER).name("Authorization"))) .addSecurityItem(new SecurityRequirement().addList("JWT")); } } ``` ### GlobalExceptionHandler ```java @ControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(SampleNotFoundException.class) public ResponseEntity> handle(SampleNotFoundException ex) { return ResponseEntity.status(HttpStatus.NOT_FOUND) .body(Map.of("code", ExceptionCodes.ENTITY_NOT_FOUND.name(), "message", ex.getMessage())); } } ``` ### Controller pattern **If Swagger=Yes** — API interface + controller: ```java @Tag(name = "Sample") public interface SampleApi { @Operation(summary = "Get sample by ID") @GetMapping("/{id}") @PreAuthorize("isAuthenticated()") ResponseEntity getById(@PathVariable long id, @AuthenticationPrincipal AuthenticatedUser user); } ``` ```java @RestController @RequestMapping("/sample") @RequiredArgsConstructor public class SampleController implements SampleApi { private final SampleService sampleService; @Override public ResponseEntity getById(long id, AuthenticatedUser user) { return ResponseEntity.ok(SampleResponse.from(sampleService.findById(id))); } } ``` **If Swagger=No** — annotations directly on controller. ### Response DTO ```java public record SampleResponse(long id, String name) { public static SampleResponse from(SampleEntity entity) { return new SampleResponse(entity.getId(), entity.getName()); } } ``` ### application.yml ```yaml humand: : db: endpoint: -db port: 5432 database: username: jooq # or hibernate liquibase: username: liquibase security: jwt: secret: "" jwks-rsa-url: "" logging: http: tracing-header: x-request-id log-body: false max-body-length: 10000 server: port: 8080 servlet: context-path: /api/v1/ shutdown: graceful spring: application.name: -webapp lifecycle: timeout-per-shutdown-phase: 15s datasource: type: org.springframework.jdbc.datasource.SimpleDriverDataSource url: "jdbc:aws-wrapper:postgresql://${humand..db.endpoint}:${humand..db.port}/${humand..db.database}" username: "${humand..db.username}" driver-class-name: software.amazon.jdbc.Driver jooq: # if jOOQ sql-dialect: POSTGRES # jpa: # if Hibernate # hibernate.ddl-auto: validate # open-in-view: false liquibase: user: "${humand..db.liquibase.username}" management: server: port: 8081 endpoints: web: exposure: include: "health,loggers,metrics" endpoint: health: show-components: always loggers: access: unrestricted logging: level: root: info co.humand.: debug ``` ### Source structure - `src/main/java//webapp/package-info.java` — `@NullMarked` (top-level only, subpackages inherit) - `src/test/java/` — at least one sample unit test - `src/test-integration/java/` — BaseIntegrationTest + ActuatorIntegrationTest - **Do NOT add `package-info.java` in `src/test-integration/java/` for the same package as `src/test/java/`** — compiler treats both as one and will error --- ## Test Infrastructure (webapp) ### BaseIntegrationTest Copy from Janus. Key elements: ```java @SpringBootTest(classes = Application.class, webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT) @AutoConfigureTestRestTemplate @ActiveProfiles("test") @Import({BaseTestConfiguration.class}) public abstract class BaseIntegrationTest { @LocalServerPort protected int port; @Value("${server.servlet.context-path}") protected String contextPath; @Autowired protected TestRestTemplate restTemplate; public record TestUser(long instanceId, long userId) {} protected String getBaseUrl() { ... } protected String generateJwt(TestUser user, String scope) { ... } protected String generateJwtWithClaims(JWTClaimsSet claims) { ... } protected RequestBuilder get/post/put/delete(String endpoint) { ... } protected class RequestBuilder { public RequestBuilder withUser(TestUser user) { ... } public RequestBuilder withBody(B body) { ... } public RequestBuilder withHeader(String name, String value) { ... } public ResponseEntity exchange(Class responseType) { ... } public ResponseEntity exchange(ParameterizedTypeReference responseType) { ... } } } ``` **JWT generation**: RS256 with `signing-key.pem`, kid `"test-key"`. Expiration: `Instant.now().plus(Duration.ofMinutes(1))`. **Imports**: `org.springframework.boot.resttestclient.TestRestTemplate`, `org.springframework.boot.resttestclient.autoconfigure.AutoConfigureTestRestTemplate` ### BaseTestConfiguration ```java @Import({SecurityConfiguration.class, TestContainersConfiguration.class}) // If Kafka=Yes: also import KafkaDLTTestListenerConfig.class public class BaseTestConfiguration { // Add @Bean @Primary mocks for external dependencies as needed } ``` ### TestContainersConfiguration ```java @TestConfiguration(proxyBeanMethods = false) public class TestContainersConfiguration { @Bean @ServiceConnection @ConditionalOnProperty(name = "testcontainers.postgres.enabled", havingValue = "true", matchIfMissing = true) PostgreSQLContainer postgresContainer() { return new PostgreSQLContainer(DockerImageName.parse("postgres:17.7")); } // If Kafka=Yes: @Bean @ServiceConnection(name = "kafka") @ConditionalOnProperty(name = "testcontainers.kafka.enabled", havingValue = "true") ConfluentKafkaContainer kafkaContainer() { return new ConfluentKafkaContainer(DockerImageName.parse("confluentinc/cp-kafka:7.8.0")); } @Bean @ConditionalOnProperty(name = "testcontainers.kafka.enabled", havingValue = "true") DynamicPropertyRegistrar kafkaPropertiesRegistrar(final ConfluentKafkaContainer kafkaContainer) { return registry -> registry.add("spring.kafka.bootstrap-servers", kafkaContainer::getBootstrapServers); } } ``` ### TestJwksController ```java @RestController @Profile("test") public class TestJwksController { @GetMapping(path = "/.well-known/jwks.json", produces = MediaType.APPLICATION_JSON_VALUE) public Resource getJwks() { return new ClassPathResource("static/jwks.json"); } } ``` Copy `signing-key.pem` and `static/jwks.json` from Janus `src/test-integration/resources/`. ### ActuatorIntegrationTest (mandatory) ```java class ActuatorIntegrationTest extends BaseIntegrationTest { @Value("${local.management.port}") private int managementPort; @Test void actuatorHealthReturnsUp() throws Exception { HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("http://localhost:" + managementPort + "/actuator/health")) .GET() .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); assertThat(response.statusCode()).isEqualTo(200); assertThat(response.body()).contains("UP"); } } ``` Uses `java.net.http.HttpClient`, NOT `TestRestTemplate`, because management port is separate. ### application-test.yml ```yaml humand: : security: cors: allowed-origins: ['*'] allowed-headers: ['*'] jwt: secret: test-secret jwks-rsa-url: http://localhost:${server.port}/api/v1//.well-known/jwks.json cache: enabled: false # if Redis=Yes db: username: jooq liquibase: username: liquibase spring: application.name: -webapp-test autoconfigure: exclude: # if Redis=Yes - org.springframework.boot.data.redis.autoconfigure.DataRedisAutoConfiguration - org.springframework.boot.data.redis.autoconfigure.DataRedisRepositoriesAutoConfiguration datasource: url: jdbc:tc:postgresql:17.5:/// driver-class-name: org.testcontainers.jdbc.ContainerDatabaseDriver liquibase: drop-first: true enabled: true change-log: classpath:db/changelog/db.changelog-master.yaml jooq: sql-dialect: POSTGRES server: port: 8999 shutdown: immediate management: server: port: 0 endpoints: web: exposure: include: health testcontainers: postgres: enabled: true kafka: # if Kafka=Yes enabled: true ``` --- ## jacoco-aggregate module (`-jacoco-aggregate`) Aggregates JaCoCo coverage across all modules. ### POM ```xml pom ${project.groupId}-common ${project.groupId}-model ${project.groupId}-core ${project.groupId}-security ${project.groupId}-kafka ${project.groupId}-webapp org.jacoco jacoco-maven-plugin default-report report report-aggregate report-aggregate prepare-package target/*.exec false ```