117 lines
5.6 KiB
Markdown
117 lines
5.6 KiB
Markdown
---
|
|
title: error / spring-jpa-flyway-circular-dependency-2026-06-23
|
|
source_type: error-note
|
|
status: raw
|
|
branch: feature-build-release-supply-chain-contract
|
|
related_projects: [ca-skeleton]
|
|
tags: [error, spring-boot, circular-dependency, flyway, jpa]
|
|
created: 2026-06-23
|
|
updated: 2026-06-23
|
|
---
|
|
|
|
# Spring JPA-Flyway Circular Dependency during Context Initialization
|
|
|
|
## Parent
|
|
- Parent branch note: [[raw/branch-notes/feature-build-release-supply-chain-contract]]
|
|
|
|
## Symptoms
|
|
|
|
During application startup using `bootRun`, the application context failed to initialize with a circular dependency error:
|
|
|
|
```text
|
|
Description:
|
|
|
|
The dependencies of some of the beans in the application context form a cycle:
|
|
|
|
entityManagerFactory defined in class path resource [org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaConfiguration.class]
|
|
┌─────┐
|
|
| flyway defined in class path resource [org/springframework/boot/autoconfigure/flyway/FlywayAutoConfiguration$FlywayConfiguration.class]
|
|
↑ ↓
|
|
| postgreSqlPersistenceConfig
|
|
└─────┘
|
|
```
|
|
|
|
When running without a local database connection, this circularity prevented the application from failing fast with a connection error and instead produced secondary errors such as `NoSuchBeanDefinitionException` during context shutdown (e.g. `No bean named 'org.springframework.context.annotation.ConfigurationClassPostProcessor.importRegistry' available`).
|
|
|
|
## Root Cause
|
|
|
|
1. The configuration class `PostgreSqlPersistenceConfig` uses `@PersistenceContext` to inject the JPA `EntityManager`:
|
|
```java
|
|
@PersistenceContext
|
|
private EntityManager entityManager;
|
|
```
|
|
2. Creating `PostgreSqlPersistenceConfig` therefore requires the `EntityManager` (and consequently `EntityManagerFactory`) to be initialized and available.
|
|
3. In `PostgreSqlPersistenceConfig`, a customizer bean was declared as a non-static `@Bean`:
|
|
```java
|
|
@Bean
|
|
public FlywayConfigurationCustomizer postgreSqlFlywayLocationCustomizer() {
|
|
return configuration -> configuration.locations("classpath:db/migration/postgresql");
|
|
}
|
|
```
|
|
4. Because it is a non-static `@Bean` method, Spring requires instantiating `PostgreSqlPersistenceConfig` *before* it can call the method to register the customizer.
|
|
5. However:
|
|
- `EntityManagerFactory` depends on `flywayInitializer` (to ensure migrations run first).
|
|
- `flywayInitializer` depends on the `Flyway` bean.
|
|
- The `Flyway` bean depends on all registered `FlywayConfigurationCustomizer` beans.
|
|
- Spring tries to resolve `FlywayConfigurationCustomizer` -> instantiates `PostgreSqlPersistenceConfig` -> injects `EntityManager` -> creates `EntityManagerFactory` -> waits for `flywayInitializer` -> waits for `Flyway` -> waits for `FlywayConfigurationCustomizer`.
|
|
- This forms a cycle: `EntityManagerFactory` -> `flywayInitializer` -> `Flyway` -> `PostgreSqlPersistenceConfig` -> `EntityManagerFactory`.
|
|
|
|
## Solution
|
|
|
|
### 1. Make the Flyway customizer static
|
|
Change the `FlywayConfigurationCustomizer` bean declaration to a `static @Bean` method in both `PostgreSqlPersistenceConfig.java` and `SamplePostgreSqlPersistenceConfig.java`:
|
|
|
|
```java
|
|
@Bean
|
|
public static FlywayConfigurationCustomizer postgreSqlFlywayLocationCustomizer() {
|
|
return configuration -> configuration.locations("classpath:db/migration/postgresql");
|
|
}
|
|
```
|
|
|
|
This allows Spring to invoke the customizer registration without instantiating the enclosing configuration class, breaking the immediate `EntityManagerFactory` dependency cycle.
|
|
|
|
### 2. Refactor `@PersistenceContext` Field Injection to Parameter Injection
|
|
To prevent the configuration classes from triggering early instantiation of the JPA infrastructure (which causes `NoSuchBeanDefinitionException: No bean named 'org.springframework.context.annotation.ConfigurationClassPostProcessor.importRegistry' available`), eliminate the class-level `EntityManager` field injection and instead inject `EntityManager` as a parameter to the factory `@Bean` methods:
|
|
|
|
**Before:**
|
|
```java
|
|
@Configuration
|
|
public class PostgreSqlPersistenceConfig {
|
|
@PersistenceContext
|
|
private EntityManager entityManager;
|
|
|
|
@Bean
|
|
public OutboxClaimRepository outboxClaimRepository() {
|
|
return new PostgreSqlOutboxClaimRepository(entityManager);
|
|
}
|
|
}
|
|
```
|
|
|
|
**After:**
|
|
```java
|
|
@Configuration
|
|
public class PostgreSqlPersistenceConfig {
|
|
@Bean
|
|
public OutboxClaimRepository outboxClaimRepository(EntityManager entityManager) {
|
|
return new PostgreSqlOutboxClaimRepository(entityManager);
|
|
}
|
|
}
|
|
```
|
|
|
|
### Why it works
|
|
- **Static customizer**: Decouples customizer registration from the instantiation of the enclosing `@Configuration` class.
|
|
- **Parameter injection**: Prevents Spring's configuration class post-processor from resolving the `EntityManager` bean prematurely during class creation, postponing its resolution until the specific factory method is executed. This completely avoids the early bootstrap lifecycle cycle.
|
|
|
|
## Verification & Outcomes
|
|
|
|
### Local Verification
|
|
1. Ensured the local database container `ca-pg` is running on port `5432`.
|
|
2. Cleaned and recreated the database using `psql` to clear any checksum mismatch issues.
|
|
3. Ran `./gradlew :app-bootstrap:bootRun`.
|
|
4. The application initialized the connection pool, ran Flyway migrations, and successfully booted:
|
|
```text
|
|
2026-06-23 14:32:00.619 INFO [main] o.f.core.internal.command.DbMigrate - Successfully applied 3 migrations to schema "public", now at version v4 (execution time 00:00.085s)
|
|
...
|
|
2026-06-23 14:32:03.278 INFO [main] d.c.bootstrap.CaSkeletonApplication - Started CaSkeletonApplication in 4.87 seconds (process running for 5.034)
|
|
```
|