Description
I am facing a persistent Oracle JDBC issue when running a Spring Boot 4.0.1 Kotlin application compiled as a native image (GraalVM 25 / Java 25).
When the application tries to obtain a JDBC connection and executes even the simplest query like:
select 1 from dual
the call fails with:
java.sql.SQLException: ORA-00604: Error occurred at recursive SQL level 1. Check subsequent errors.
ORA-01756: quoted string not properly terminated
This happens before any of my queries actually run, meaning the failure occurs during Oracle session logon / recursive SQL execution.
The problem occurs:
- using JdbcClient
- with Oracle UCP (PoolDataSourceImpl) and also with HikariCP
- with a totally clean Oracle Free 23c database
- with a simple schema
- without any triggers or policies in the application schema
The exact same code works fine when running the JVM JAR normally.
The error only occurs in native image.
Minimal Kotlin Sample
PostDemoSpringApplication.kt
package santannaf.demo
import org.slf4j.LoggerFactory
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.core.ParameterizedTypeReference
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate
import org.springframework.jdbc.core.simple.JdbcClient
import org.springframework.stereotype.Service
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
import org.springframework.web.client.RestClient
@SpringBootApplication
class PostDemoSpringApplication
fun main(args: Array<String>) {
runApplication<PostDemoSpringApplication>(*args)
}
@RestController
@RequestMapping(path = ["/posts"])
class PostsController(
private val service: PostsService
) {
private val log = LoggerFactory.getLogger(javaClass)
@GetMapping
fun getPosts(): List<Post> {
log.info("get all posts")
val posts = service.getPosts()
log.info("fetch posts successfully: ${posts.size}")
return posts
}
@GetMapping(path = ["/user/{userId}"])
fun getPostsByUserId(@PathVariable userId: Long): Any {
log.info("get all posts by user id: $userId")
return service.ping()
// val posts = service.getPostsByUserId(userId)
// log.info("fetch posts successfully: ${posts.size} - for user id: $userId")
// return posts
}
}
interface PostsService {
fun getPosts(): List<Post>
fun getPostsByUserId(userId: Long): List<Post>
fun ping(): Int
}
@Service
class PostsServiceImpl(
restClient: RestClient.Builder,
val jdbcClient: JdbcClient,
private val namedParameterJdbcTemplate: NamedParameterJdbcTemplate
) : PostsService {
private val customHttp = restClient.baseUrl("https://jsonplaceholder.typicode.com").build()
private val insertSql = """
INSERT INTO POSTS (ID, TITLE, USER_ID, BODY)
VALUES (:id, :title, :userId, :body)
""".trimIndent()
private fun insert(post: Post): Int {
return jdbcClient.sql(insertSql)
.param("id", post.id)
.param("title", post.title)
.param("userId", post.userId)
.param("body", post.body)
.update()
}
fun insertOneByOne(posts: List<Post>) {
posts.forEach { post ->
jdbcClient.sql(insertSql)
.param("id", post.id)
.param("title", post.title)
.param("userId", post.userId)
.param("body", post.body)
.update()
}
}
private fun insertBatch(posts: List<Post>): IntArray {
if (posts.isEmpty()) return intArrayOf()
val batchValues: Array<Map<String, Any>> = posts.map { post ->
mapOf(
"id" to post.id,
"title" to post.title,
"userId" to post.userId,
"body" to post.body
)
}.toTypedArray()
return namedParameterJdbcTemplate.batchUpdate(insertSql, batchValues)
}
override fun getPosts(): List<Post> {
val posts = customHttp.get()
.uri("/posts")
.retrieve()
.body(object : ParameterizedTypeReference<List<Post>>() {}) ?: emptyList()
insertBatch(posts)
return posts
}
override fun ping(): Int {
return jdbcClient.sql("select 1 from dual")
.query(Int::class.java)
.single()
}
override fun getPostsByUserId(userId: Long): List<Post> {
val query = """
select id, title, user_id as userId, body from posts where user_id = :1
""".trimIndent()
return jdbcClient.sql(query)
.param(1, userId)
.query(Post::class.java)
.list()
.filterNotNull()
}
}
data class Post(val id: Long, val title: String, val userId: Long, val body: String)
application.properties
### Spring Config
spring.application.name=post-demo-spring
spring.threads.virtual.enabled=true
### Server Config
server.port=30001
### Management Config
management.endpoint.health.probes.enabled=true
management.endpoint.health.group.liveness.include=ping
management.endpoint.health.group.readiness.include=ping
management.endpoint.health.show-details=always
management.endpoints.web.exposure.include=metrics,info,health,prometheus
management.health.livenessstate.enabled=true
management.health.readinessstate.enabled=true
management.metrics.distribution.percentiles-histogram.http.server.requests=true
management.metrics.tags.instance.name=${INSTANCE_NAME:my-instance-name}
management.metrics.tags.instance.ip=${INSTANCE_IP:my-instance-ip}
management.metrics.tags.env=${ENV:local}
management.metrics.tags.server=${SERVER:onPremise}
management.metrics.tags.team=my_team
management.metrics.tags.service.name=${spring.application.name}
management.metrics.distribution.slo.http.server.requests=125ms,500ms,2s,8s
management.metrics.distribution.percentiles.http.server.requests=0.5,0.95,0.99
management.opentelemetry.resource-attributes.service.name=${spring.application.name}
management.opentelemetry.resource-attributes.cluster=local
management.opentelemetry.resource-attributes.deployment.environment=${ENV:local}
management.otlp.metrics.export.url=http://localhost:4318/v1/metrics
management.otlp.metrics.export.aggregation-temporality=cumulative
management.otlp.metrics.export.step=15s
management.otlp.metrics.export.base-time-unit=milliseconds
management.opentelemetry.tracing.export.otlp.transport=http
management.opentelemetry.tracing.export.otlp.endpoint=http://localhost:4318/v1/traces
management.tracing.sampling.probability=1.0
### Database Config
spring.datasource.url=jdbc:oracle:thin:@(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST=localhost)(PORT=1521))(CONNECT_DATA=(SERVICE_NAME=FREEPDB1)))
spring.datasource.username=${DB_USER:TEMPLATEBANKING_ADM}
spring.datasource.password=${DB_PASSWORD:adm}
spring.datasource.oracleucp.user=templatebanking_adm
spring.datasource.oracleucp.max-pool-size=10
spring.data.jdbc.dialect=oracle
spring.datasource.type=oracle.ucp.jdbc.PoolDataSourceImpl
spring.datasource.oracleucp.connection-factory-class-name=oracle.jdbc.pool.OracleDataSource
spring.datasource.oracleucp.connection-pool-name=oracle_pool
logging.level.com.zaxxer.hikari=INFO
logging.level.org.springframework.jdbc.core=DEBUG
logging.level.oracle.jdbc=DEBUG
build.gradle.kts
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins {
kotlin("jvm") version "2.3.0"
kotlin("plugin.spring") version "2.3.0"
id("org.springframework.boot") version "4.0.1"
id("io.spring.dependency-management") version "1.1.7"
id("org.graalvm.buildtools.native") version "0.11.3"
}
group = "santannaf.demo.brc.rinha.backend"
version = "0.0.1"
java {
toolchain {
languageVersion = JavaLanguageVersion.of(25)
}
}
repositories {
mavenCentral()
}
dependencyManagement {
imports {
mavenBom("io.opentelemetry.instrumentation:opentelemetry-instrumentation-bom:2.23.0")
}
}
dependencies {
// Spring Boot
implementation("org.springframework.boot:spring-boot-starter-webmvc")
implementation("org.springframework.boot:spring-boot-starter-restclient")
implementation("org.springframework.boot:spring-boot-starter-data-jdbc")
runtimeOnly("com.oracle.database.jdbc:ojdbc17")
runtimeOnly("com.oracle.database.jdbc:ucp17")
// Traces and Metrics
implementation("org.springframework.boot:spring-boot-starter-actuator")
implementation("org.springframework.boot:spring-boot-starter-opentelemetry")
implementation("io.opentelemetry.instrumentation:opentelemetry-spring-boot-starter")
// Kotlin
implementation("tools.jackson.module:jackson-module-kotlin")
implementation("org.jetbrains.kotlin:kotlin-reflect")
// Unit Tests
testImplementation("org.springframework.boot:spring-boot-starter-actuator-test")
testImplementation("org.springframework.boot:spring-boot-starter-opentelemetry-test")
testImplementation("org.springframework.boot:spring-boot-starter-restclient-test")
testImplementation("org.springframework.boot:spring-boot-starter-webmvc-test")
testImplementation("org.springframework.boot:spring-boot-starter-data-jdbc-test")
testImplementation("org.jetbrains.kotlin:kotlin-test-junit5")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}
kotlin {
compilerOptions {
freeCompilerArgs.addAll("-Xjsr305=strict", "-Xannotation-default-target=param-property")
jvmTarget.set(JvmTarget.JVM_25)
}
}
tasks.withType<Test> {
useJUnitPlatform()
}
tasks.bootJar {
archiveFileName.set("app.jar")
}
// I added this custom task to see if generating the hints would change anything, but // I'm having the same problem...
tasks.register<Exec>("runCustomJar") {
group = "application"
description = "Run custom jar"
dependsOn("bootJar")
val appName = "app.jar"
val addressesBuild = "./build/libs/$appName"
commandLine(
"java",
"-agentlib:native-image-agent=config-merge-dir=./src/main/resources/META-INF/native-image/",
"-jar",
addressesBuild
)
}
// idem above
graalvmNative {
binaries {
named("main") {
imageName.set("app")
verbose.set(true)
debug.set(true)
configurationFileDirectories.from(file("src/main/resources/META-INF/native-image"))
buildArgs("--color=always", "-H:+AddAllCharsets")
}
}
}
Error Log
org.springframework.jdbc.CannotGetJdbcConnectionException: Failed to obtain JDBC Connection
Caused by: java.sql.SQLException:
ORA-00604: Error occurred at recursive SQL level 1. Check subsequent errors.
ORA-01756: quoted string not properly terminated
Spring debug log shows the failure during connection acquisition:
Executing prepared SQL statement [select 1 from dual]
HikariPool-1 - Starting...
Here's a screenshot of the native binary execution and the error...
Request
Could this be related to Spring Boot JDBC / JdbcClient interaction with Oracle under native image?
Or perhaps to how JDBC metadata / session initialization is handled?
Any guidance on how to debug or mitigate would be greatly appreciated
If you want, I can also add:
- docker-compose.yaml
- start.sh that provisions user + schema
- the exact native image config
…but I kept the issue focused and minimal here.
Description
I am facing a persistent Oracle JDBC issue when running a Spring Boot 4.0.1 Kotlin application compiled as a native image (GraalVM 25 / Java 25).
When the application tries to obtain a JDBC connection and executes even the simplest query like:
select 1 from dualthe call fails with:
This happens before any of my queries actually run, meaning the failure occurs during Oracle session logon / recursive SQL execution.
The problem occurs:
The exact same code works fine when running the JVM JAR normally.
The error only occurs in native image.
Minimal Kotlin Sample
PostDemoSpringApplication.kt
application.properties
build.gradle.kts
Error Log
Spring debug log shows the failure during connection acquisition:
Here's a screenshot of the native binary execution and the error...
Request
Could this be related to Spring Boot JDBC / JdbcClient interaction with Oracle under native image?
Or perhaps to how JDBC metadata / session initialization is handled?
Any guidance on how to debug or mitigate would be greatly appreciated
If you want, I can also add:
…but I kept the issue focused and minimal here.