Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 20 additions & 6 deletions documentation-website/Writerside/topics/Spring-Boot-integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,11 @@ spring.exposed.generate-ddl=true
```

When enabled, the starter detects all classes extending `org.jetbrains.exposed.v1.core.Table` and creates the schema
during application startup.
during the bean initialization phase, before any bean that depends on the schema is initialized. Beans that need to
access the database during their own initialization (for example, in `@PostConstruct` or
`InitializingBean.afterPropertiesSet()`) can declare this requirement with Spring Boot's
[`@DependsOnDatabaseInitialization`](https://docs.spring.io/spring-boot/api/kotlin/spring-boot-project/spring-boot/org.springframework.boot.sql.init.dependency/-depends-on-database-initialization/index.html)
annotation. Beans that use built-in detection points such as `JdbcOperations` are ordered automatically.

### Exclude packages

Expand Down Expand Up @@ -256,18 +260,28 @@ When you build a native image, Spring Boot applies AOT processing. AOT restricts
In particular, beans declared with `@ConditionalOnProperty` cannot change their behavior at runtime. As a result,
setting `spring.exposed.generate-ddl=true` does not enable automatic schema creation in a native image.

Instead, create the database schema programmatically. For example:
Instead, create the database schema programmatically. Run it during the bean initialization phase via
`InitializingBean` so that downstream beans can rely on the schema being ready when their own initialization runs:

```kotlin
@Component
@Transactional
class SchemaInitialize : ApplicationRunner {
override fun run(args: ApplicationArguments) {
SchemaUtils.create(MessageEntity)
class SchemaInitialize(
private val transactionManager: PlatformTransactionManager
) : InitializingBean {
override fun afterPropertiesSet() {
TransactionTemplate(transactionManager).execute {
SchemaUtils.create(MessageEntity)
}
}
}
```

> Use programmatic transaction management with `TransactionTemplate` here rather than `@Transactional`.
> The AOP proxy that powers `@Transactional` is not yet active during `afterPropertiesSet()`, so the
> annotation would be silently ignored.
>
{style="note"}

### Resolve `KotlinReflectionInternalError: Unresolved class`

If the native image build or runtime fails with `KotlinReflectionInternalError: Unresolved class` the application likely
Expand Down
12 changes: 9 additions & 3 deletions exposed-spring-boot-starter/api/exposed-spring-boot-starter.api
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
public class org/jetbrains/exposed/v1/spring/boot/DatabaseInitializer : org/springframework/boot/ApplicationRunner, org/springframework/core/Ordered {
public class org/jetbrains/exposed/v1/spring/boot/DatabaseInitializer : org/springframework/beans/factory/InitializingBean, org/springframework/core/Ordered {
public static final field Companion Lorg/jetbrains/exposed/v1/spring/boot/DatabaseInitializer$Companion;
public static final field DATABASE_INITIALIZER_ORDER I
public fun <init> (Lorg/springframework/context/ApplicationContext;Ljava/util/List;)V
public fun <init> (Lorg/springframework/context/ApplicationContext;Ljava/util/List;Lorg/springframework/transaction/PlatformTransactionManager;)V
public fun afterPropertiesSet ()V
public fun getOrder ()I
public fun run (Lorg/springframework/boot/ApplicationArguments;)V
}
Expand All @@ -21,8 +22,13 @@ public final class org/jetbrains/exposed/v1/spring/boot/ExposedAotContribution :
public class org/jetbrains/exposed/v1/spring/boot/autoconfigure/ExposedAutoConfiguration {
public fun <init> (Lorg/springframework/context/ApplicationContext;)V
public fun databaseConfig ()Lorg/jetbrains/exposed/v1/core/DatabaseConfig;
public fun databaseInitializer ()Lorg/jetbrains/exposed/v1/spring/boot/DatabaseInitializer;
public fun databaseInitializer (Lorg/jetbrains/exposed/v1/spring/transaction/SpringTransactionManager;)Lorg/jetbrains/exposed/v1/spring/boot/DatabaseInitializer;
public fun exposedSpringTransactionAttributeSource ()Lorg/jetbrains/exposed/v1/spring/transaction/ExposedSpringTransactionAttributeSource;
public fun springTransactionManager (Ljavax/sql/DataSource;Lorg/jetbrains/exposed/v1/core/DatabaseConfig;)Lorg/jetbrains/exposed/v1/spring/transaction/SpringTransactionManager;
}

public final class org/jetbrains/exposed/v1/spring/boot/autoconfigure/ExposedDatabaseInitializerDetector : org/springframework/boot/sql/init/dependency/DatabaseInitializerDetector {
public fun <init> ()V
public fun detect (Lorg/springframework/beans/factory/config/ConfigurableListableBeanFactory;)Ljava/util/Set;
}

Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,29 @@ package org.jetbrains.exposed.v1.spring.boot
import org.jetbrains.exposed.v1.core.Table
import org.jetbrains.exposed.v1.jdbc.SchemaUtils
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.InitializingBean
import org.springframework.boot.ApplicationArguments
import org.springframework.boot.ApplicationRunner
import org.springframework.boot.autoconfigure.AutoConfigurationPackages
import org.springframework.context.ApplicationContext
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider
import org.springframework.core.Ordered
import org.springframework.core.type.filter.AssignableTypeFilter
import org.springframework.core.type.filter.RegexPatternTypeFilter
import org.springframework.transaction.annotation.Transactional
import org.springframework.transaction.PlatformTransactionManager
import org.springframework.transaction.support.TransactionTemplate
import java.util.regex.Pattern

/**
* Base class responsible for the automatic creation of a database schema, using the results of [discoverExposedTables].
* Creates the database schema using discovered [Table][org.jetbrains.exposed.v1.core.Table] objects
* during the bean initialization phase.
*
* If more than just table creation is required, a derived class can be implemented to override the transactional
* function, [run], so that other schema operations can be performed when initialized.
*
* @property applicationContext The Spring ApplicationContext container responsible for managing beans.
* @property excludedPackages List of packages to exclude, so that their contained tables are not auto-created.
* A derived class can override [run] to perform additional schema operations.
*/
open class DatabaseInitializer(private val applicationContext: ApplicationContext, private val excludedPackages: List<String>) :
ApplicationRunner, Ordered {
open class DatabaseInitializer(
private val applicationContext: ApplicationContext,
private val excludedPackages: List<String>,
private val transactionManager: PlatformTransactionManager
) : InitializingBean, Ordered {
override fun getOrder(): Int = DATABASE_INITIALIZER_ORDER

companion object {
Expand All @@ -33,8 +34,16 @@ open class DatabaseInitializer(private val applicationContext: ApplicationContex

private val logger = LoggerFactory.getLogger(javaClass)

@Transactional
override fun run(args: ApplicationArguments?) {
override fun afterPropertiesSet() {
TransactionTemplate(transactionManager).execute {
run(null)
}
}

/**
* Discovers and creates database tables. Subclasses can override this to add custom schema operations.
*/
open fun run(args: ApplicationArguments?) {
val exposedTables = discoverExposedTables(applicationContext, excludedPackages)
logger.info("Schema generation for tables '{}'", exposedTables.map { it.tableName })

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,6 @@ open class ExposedAutoConfiguration(private val applicationContext: ApplicationC

/**
* Returns a [SpringTransactionManager] instance using the specified [datasource] and [databaseConfig].
*
* To enable logging of all transaction queries by the SpringTransactionManager instance, set the property
* `spring.exposed.show-sql` to `true` in the application.properties file.
*/
@Bean
open fun springTransactionManager(datasource: DataSource, databaseConfig: DatabaseConfig): SpringTransactionManager {
Expand All @@ -65,10 +62,15 @@ open class ExposedAutoConfiguration(private val applicationContext: ApplicationC
*
* The property `spring.exposed.excluded-packages` can be used to ensure that tables in specified packages are
* not auto-created.
*
* DDL runs during the bean initialization phase via `InitializingBean.afterPropertiesSet`, before downstream
* beans annotated with `@DependsOnDatabaseInitialization` are initialized (ordering is enforced automatically
* via [ExposedDatabaseInitializerDetector]).
*/
@Bean
@ConditionalOnProperty("spring.exposed.generate-ddl", havingValue = "true", matchIfMissing = false)
open fun databaseInitializer() = DatabaseInitializer(applicationContext, excludedPackages)
open fun databaseInitializer(springTransactionManager: SpringTransactionManager) =
DatabaseInitializer(applicationContext, excludedPackages, springTransactionManager)

/**
* Returns an [ExposedSpringTransactionAttributeSource] instance.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package org.jetbrains.exposed.v1.spring.boot.autoconfigure

import org.jetbrains.exposed.v1.spring.boot.DatabaseInitializer
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory
import org.springframework.boot.sql.init.dependency.DatabaseInitializerDetector

/**
* Registers Exposed's [DatabaseInitializer] as a database initializer bean for Spring Boot's automatic
* dependency ordering, so that beans annotated with
* [org.springframework.boot.sql.init.dependency.DependsOnDatabaseInitialization] are initialized after the
* schema has been created by [DatabaseInitializer.afterPropertiesSet].
*/
class ExposedDatabaseInitializerDetector : DatabaseInitializerDetector {

override fun detect(beanFactory: ConfigurableListableBeanFactory): Set<String> {
return beanFactory.getBeanNamesForType(
DatabaseInitializer::class.java, true, false
).toSet()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
org.springframework.boot.sql.init.dependency.DatabaseInitializerDetector=\
org.jetbrains.exposed.v1.spring.boot.autoconfigure.ExposedDatabaseInitializerDetector
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.context.ApplicationContext
import org.springframework.jdbc.datasource.DataSourceTransactionManager
import org.springframework.jdbc.datasource.SimpleDriverDataSource

@SpringBootTest(
classes = [Application::class],
Expand All @@ -25,10 +27,14 @@ open class DatabaseInitializerTest {
fun `should create schema for TestTable and not for IgnoreTable`() {
Assertions.assertThrows(ExposedSQLException::class.java) {
Database.connect("jdbc:h2:mem:test-spring", user = "sa", driver = "org.h2.Driver")
val dataSource = SimpleDriverDataSource(org.h2.Driver(), "jdbc:h2:mem:test-spring", "sa", "")
val txManager = DataSourceTransactionManager(dataSource)
transaction {
DatabaseInitializer(applicationContext, listOf("org.jetbrains.exposed.v1.spring.boot.tables.ignore")).run(
null
)
DatabaseInitializer(
applicationContext,
listOf("org.jetbrains.exposed.v1.spring.boot.tables.ignore"),
txManager
).run(null)
Assertions.assertEquals(0L, TestTable.selectAll().count())
IgnoreTable.selectAll().count()
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package org.jetbrains.exposed.v1.spring.boot.autoconfigure

import org.jetbrains.exposed.v1.jdbc.selectAll
import org.jetbrains.exposed.v1.spring.boot.Application
import org.jetbrains.exposed.v1.spring.boot.DatabaseInitializer
import org.jetbrains.exposed.v1.spring.boot.tables.TestTable
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.InitializingBean
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
import org.springframework.boot.sql.init.dependency.DependsOnDatabaseInitialization
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.boot.test.context.TestConfiguration
import org.springframework.context.annotation.Bean
import org.springframework.transaction.PlatformTransactionManager
import org.springframework.transaction.support.TransactionTemplate

/**
* @author zbqmgldjfh@gmail.com
* Verifies that [DatabaseInitializer] is registered as a database initializer via Spring Boot's
* [DatabaseInitializerDetector][org.springframework.boot.sql.init.dependency.DatabaseInitializerDetector] SPI,
* so that beans annotated with [DependsOnDatabaseInitialization] automatically wait for DDL to complete
* without requiring explicit `@DependsOn("databaseInitializer")`.
*/
@SpringBootTest(
classes = [Application::class, DatabaseInitializerEarlyInitTest.EarlyInitConfig::class],
properties = [
"spring.datasource.url=jdbc:h2:mem:test-early-init;DB_CLOSE_DELAY=-1",
"spring.datasource.driver-class-name=org.h2.Driver",
"spring.exposed.generate-ddl=true"
]
)
open class DatabaseInitializerEarlyInitTest {

@TestConfiguration
open class EarlyInitConfig {
@Bean
@DependsOnDatabaseInitialization
@ConditionalOnProperty("spring.exposed.generate-ddl", havingValue = "true")
open fun schemaVerifier(transactionManager: PlatformTransactionManager): SchemaVerifierBean =
SchemaVerifierBean(transactionManager)
}

/**
* A bean that queries the TestTable during its own initialization.
* Uses [DependsOnDatabaseInitialization] (Spring Boot standard) instead of
* `@DependsOn("databaseInitializer")` to verify SPI-based automatic ordering.
*/
class SchemaVerifierBean(
private val transactionManager: PlatformTransactionManager
) : InitializingBean {
var tableRowCount: Long = -1

override fun afterPropertiesSet() {
TransactionTemplate(transactionManager).execute {
tableRowCount = TestTable.selectAll().count()
}
}
}

@Autowired
private lateinit var schemaVerifier: SchemaVerifierBean

@Test
fun `schema should be available during bean initialization via SPI`() {
assertEquals(
0L,
schemaVerifier.tableRowCount,
"TestTable should be queryable during afterPropertiesSet with @DependsOnDatabaseInitialization"
)
}

@Test
fun `DatabaseInitializer should be an InitializingBean`() {
assertTrue(
InitializingBean::class.java.isAssignableFrom(DatabaseInitializer::class.java),
"DatabaseInitializer should implement InitializingBean"
)
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
public class org/jetbrains/exposed/v1/spring/boot4/DatabaseInitializer : org/springframework/boot/ApplicationRunner, org/springframework/core/Ordered {
public class org/jetbrains/exposed/v1/spring/boot4/DatabaseInitializer : org/springframework/beans/factory/InitializingBean, org/springframework/core/Ordered {
public static final field Companion Lorg/jetbrains/exposed/v1/spring/boot4/DatabaseInitializer$Companion;
public static final field DATABASE_INITIALIZER_ORDER I
public fun <init> (Lorg/springframework/context/ApplicationContext;Ljava/util/List;)V
public fun <init> (Lorg/springframework/context/ApplicationContext;Ljava/util/List;Lorg/springframework/transaction/PlatformTransactionManager;)V
public fun afterPropertiesSet ()V
public fun getOrder ()I
public fun run (Lorg/springframework/boot/ApplicationArguments;)V
}
Expand All @@ -21,8 +22,13 @@ public final class org/jetbrains/exposed/v1/spring/boot4/ExposedAotContribution
public class org/jetbrains/exposed/v1/spring/boot4/autoconfigure/ExposedAutoConfiguration {
public fun <init> (Lorg/springframework/context/ApplicationContext;)V
public fun databaseConfig ()Lorg/jetbrains/exposed/v1/core/DatabaseConfig;
public fun databaseInitializer ()Lorg/jetbrains/exposed/v1/spring/boot4/DatabaseInitializer;
public fun databaseInitializer (Lorg/jetbrains/exposed/v1/spring7/transaction/SpringTransactionManager;)Lorg/jetbrains/exposed/v1/spring/boot4/DatabaseInitializer;
public fun exposedSpringTransactionAttributeSource ()Lorg/jetbrains/exposed/v1/spring7/transaction/ExposedSpringTransactionAttributeSource;
public fun springTransactionManager (Ljavax/sql/DataSource;Lorg/jetbrains/exposed/v1/core/DatabaseConfig;)Lorg/jetbrains/exposed/v1/spring7/transaction/SpringTransactionManager;
}

public final class org/jetbrains/exposed/v1/spring/boot4/autoconfigure/ExposedDatabaseInitializerDetector : org/springframework/boot/sql/init/dependency/DatabaseInitializerDetector {
public fun <init> ()V
public fun detect (Lorg/springframework/beans/factory/config/ConfigurableListableBeanFactory;)Ljava/util/Set;
}

Original file line number Diff line number Diff line change
Expand Up @@ -3,30 +3,29 @@ package org.jetbrains.exposed.v1.spring.boot4
import org.jetbrains.exposed.v1.core.Table
import org.jetbrains.exposed.v1.jdbc.SchemaUtils
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.InitializingBean
import org.springframework.boot.ApplicationArguments
import org.springframework.boot.ApplicationRunner
import org.springframework.boot.autoconfigure.AutoConfigurationPackages
import org.springframework.context.ApplicationContext
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider
import org.springframework.core.Ordered
import org.springframework.core.type.filter.AssignableTypeFilter
import org.springframework.core.type.filter.RegexPatternTypeFilter
import org.springframework.transaction.annotation.Transactional
import org.springframework.transaction.PlatformTransactionManager
import org.springframework.transaction.support.TransactionTemplate
import java.util.regex.Pattern

/**
* Base class responsible for the automatic creation of a database schema, using the results of [discoverExposedTables].
* Creates the database schema using discovered [Table][org.jetbrains.exposed.v1.core.Table] objects
* during the bean initialization phase.
*
* If more than just table creation is required, a derived class can be implemented to override the transactional
* function, [run], so that other schema operations can be performed when initialized.
*
* @property applicationContext The Spring ApplicationContext container responsible for managing beans.
* @property excludedPackages List of packages to exclude, so that their contained tables are not auto-created.
* A derived class can override [run] to perform additional schema operations.
*/
open class DatabaseInitializer(
private val applicationContext: ApplicationContext,
private val excludedPackages: List<String>
) : ApplicationRunner, Ordered {
private val excludedPackages: List<String>,
private val transactionManager: PlatformTransactionManager
) : InitializingBean, Ordered {
override fun getOrder(): Int = DATABASE_INITIALIZER_ORDER

companion object {
Expand All @@ -35,8 +34,16 @@ open class DatabaseInitializer(

private val logger = LoggerFactory.getLogger(javaClass)

@Transactional
override fun run(args: ApplicationArguments) {
override fun afterPropertiesSet() {
TransactionTemplate(transactionManager).execute {

@zbqmgldjfh Jiwoo Kim (zbqmgldjfh) Jul 9, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moving to TransactionTemplate is mandatory, not optional.

The AOP proxy isn't active during afterPropertiesSet(), so Transactional gets silently ignored.
If this was overlooked, the DDL likely executed without a transaction or failed.

run(null)
}
}

/**
* Discovers and creates database tables. Subclasses can override this to add custom schema operations.
*/
open fun run(args: ApplicationArguments?) {
val exposedTables = discoverExposedTables(applicationContext, excludedPackages)
logger.info("Schema generation for tables '{}'", exposedTables.map { it.tableName })

Expand Down
Loading
Loading