Add kotlin module: null safety, coroutines, allopen/noarg, JSpecify interop (post #42)

This commit is contained in:
Claude
2026-10-08 05:35:15 +00:00
parent 83d245e726
commit f736108067
21 changed files with 659 additions and 1 deletions
@@ -0,0 +1,24 @@
Real, captured output: removing the "spring" compilerPlugin entry from pom.xml's
kotlin-maven-plugin configuration (keeping only "jpa"), then running:
mvn -q -B clean package -DskipTests && java -jar target/kotlin-demo.jar
Failure #1 -- BookService's @Transactional needs a CGLIB subclass proxy, but
BookService is a final Kotlin class (and Application, as a @Configuration class,
has the same problem even before any bean method runs):
org.springframework.beans.factory.parsing.BeanDefinitionParsingException: Configuration problem: @Configuration class 'Application' may not be final. Remove the final modifier to continue.
Offending resource: com.ankurm.kotlin.Application
Failure #2 -- after manually adding `open` to just the Application class to get
past failure #1 (sed -i 's/^class Application {/open class Application {/' on
Application.kt), rebuilding and rerunning hits the next thing the "spring"
plugin would otherwise have handled automatically: the @Bean method itself.
org.springframework.beans.factory.parsing.BeanDefinitionParsingException: Configuration problem: @Bean method 'seed' must not be private or final; change the method's modifiers to continue.
Offending resource: com.ankurm.kotlin.Application
Restoring the "spring" compilerPlugin entry (alongside "jpa") and reverting
Application back to a plain, non-open `class Application` -- i.e. the
checked-in state of this repository -- builds and starts cleanly. See
02-fixed-startup.txt for that run.
+19
View File
@@ -0,0 +1,19 @@
Real, captured output of a clean startup with the checked-in pom.xml (both the
"spring" and "jpa" compilerPlugin entries present) and the checked-in
Application.kt (a plain, non-open `class Application`):
mvn -q -B clean package -DskipTests
java -jar target/kotlin-demo.jar
2026-10-08T10:59:52.685+05:30 INFO 1034 --- [kotlin-demo] [ main] com.ankurm.kotlin.ApplicationKt : Starting ApplicationKt v1.0.0 using Java 25.0.4.1.1 with PID 1034 (/home/claude/spring-boot-demo/kotlin/target/kotlin-demo.jar started by root in /home/claude/spring-boot-demo/kotlin)
2026-10-08T10:59:52.708+05:30 INFO 1034 --- [kotlin-demo] [ main] com.ankurm.kotlin.ApplicationKt : No active profile set, falling back to 1 default profile: "default"
2026-10-08T10:59:55.393+05:30 INFO 1034 --- [kotlin-demo] [ main] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data JPA repositories in DEFAULT mode.
2026-10-08T10:59:57.021+05:30 INFO 1034 --- [kotlin-demo] [ main] o.s.boot.tomcat.TomcatWebServer : Tomcat initialized with port 8090 (http)
2026-10-08T10:59:57.044+05:30 INFO 1034 --- [kotlin-demo] [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat]
2026-10-08T10:59:57.044+05:30 INFO 1034 --- [kotlin-demo] [ main] o.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/11.0.24]
2026-10-08T11:00:02.258+05:30 INFO 1034 --- [kotlin-demo] [ main] com.ankurm.kotlin.ApplicationKt : Started ApplicationKt in 10.673 seconds (process running for 11.919)
No BeanDefinitionParsingException, no CGLIB proxy failure: @Service-annotated
BookService and the @Configuration Application class are both final Kotlin
classes in source, but the "spring" compiler plugin (kotlin-maven-allopen under
the hood) rewrote them to be open at compile time -- before Spring ever saw them.
+18
View File
@@ -0,0 +1,18 @@
=== GET /books ===
[
{
"id": 1,
"title": "Kotlin in Action",
"author": "Jemerov & Isakova"
},
{
"id": 2,
"title": "Effective Kotlin",
"author": "Marcin Moskala"
},
{
"id": 3,
"author": "Unknown"
}
]
@@ -0,0 +1,17 @@
=== GET /books/flow ===
[
{
"id": 1,
"title": "Kotlin in Action",
"author": "Jemerov & Isakova"
},
{
"id": 2,
"title": "Effective Kotlin",
"author": "Marcin Moskala"
},
{
"id": 3,
"author": "Unknown"
}
]
+17
View File
@@ -0,0 +1,17 @@
=== GET /books/thread-check (run 1) ===
{
"beforeSuspend": "http-nio-8090-exec-4",
"afterSuspend": "kotlinx.coroutines.DefaultExecutor"
}
=== GET /books/thread-check (run 2) ===
{
"beforeSuspend": "http-nio-8090-exec-6",
"afterSuspend": "kotlinx.coroutines.DefaultExecutor"
}
=== GET /books/thread-check (run 3) ===
{
"beforeSuspend": "http-nio-8090-exec-9",
"afterSuspend": "kotlinx.coroutines.DefaultExecutor"
}
@@ -0,0 +1,11 @@
=== GET /books/null-demo/legacy/0 (expect 500, NPE) ===
HTTP/1.1 500
Content-Type: application/json
Transfer-Encoding: chunked
Date: Thu, 08 Oct 2026 05:30:22 GMT
Connection: close
{"timestamp":"2026-10-08T05:30:22.108Z","status":500,"error":"Internal Server Error","path":"/books/null-demo/legacy/0"}
--- server log (stack trace) ---
java.lang.NullPointerException: titleForId(...) must not be null
at com.ankurm.kotlin.BookController.legacyNullDemo(BookController.kt:47) ~[!/:1.0.0]
@@ -0,0 +1,11 @@
=== GET /books/null-demo/jspecify/0 (expect 500, NPE despite @NullMarked non-null contract) ===
HTTP/1.1 500
Content-Type: application/json
Transfer-Encoding: chunked
Date: Thu, 08 Oct 2026 05:30:33 GMT
Connection: close
{"timestamp":"2026-10-08T05:30:33.758Z","status":500,"error":"Internal Server Error","path":"/books/null-demo/jspecify/0"}
--- server log (stack trace) ---
java.lang.NullPointerException: titleForId(...) must not be null
at com.ankurm.kotlin.BookController.jspecifyNullDemo(BookController.kt:57) ~[!/:1.0.0]
@@ -0,0 +1,13 @@
=== GET /books/null-demo/jspecify-honest/0 (expect 200, no NPE -- compiler forced the null check) ===
HTTP/1.1 200
Content-Type: application/json
Content-Length: 13
Date: Thu, 08 Oct 2026 05:30:33 GMT
{"length":-1}=== GET /books/null-demo/jspecify-honest/5 (non-null id, expect real length) ===
HTTP/1.1 200
Content-Type: application/json
Content-Length: 12
Date: Thu, 08 Oct 2026 05:30:33 GMT
{"length":7}
@@ -0,0 +1,25 @@
Real, verified absence: Spring Boot 4.1.1's spring-boot-test module does not
ship org.springframework.boot.test.web.client.TestRestTemplate at all.
Resolved test-scope dependency tree (mvn dependency:tree), relevant slice:
[INFO] +- org.springframework.boot:spring-boot-starter-test:jar:4.1.1:test
[INFO] | +- org.springframework.boot:spring-boot-test:jar:4.1.1:test
[INFO] | +- org.springframework.boot:spring-boot-test-autoconfigure:jar:4.1.1:test
...
[INFO] | +- org.springframework:spring-test:jar:7.0.9:test
Checked directly against the jar itself (not docs, not a blog post):
$ unzip -l spring-boot-test-4.1.1.jar | grep -i "resttemplate\|restclient\|webtestclient"
(no output -- none of those classes are in the jar)
Confirmed via javac/kotlinc failing to resolve it: a first draft of
BookControllerTest.kt that imported
org.springframework.boot.test.web.client.TestRestTemplate failed to compile with
"Unresolved reference 'TestRestTemplate'" and "Unresolved reference
'getForEntity'" -- not a missing dependency, a genuinely absent class.
This repository's tests use plain java.net.http.HttpClient against
@LocalServerPort instead. The direct replacement in Spring Framework 7 is
RestTestClient, covered in its own post.