mill icon indicating copy to clipboard operation
mill copied to clipboard

Spring Boot: Document getting started docs

Open vaslabs opened this issue 2 months ago • 18 comments

This is to track the status quo while I'm testing various spring boot guides against mill, see what's working and any possible issues/difficulties in setting up.

I'll accumulate all my findings in this issue and then we can create a spring boot guide focused on mill in the docs.

For potential issues/fixes/missing features I'll open different issues

vaslabs avatar Oct 20 '25 07:10 vaslabs

Spring Initializr

  • Generate a sample from https://start.spring.io/ or using IntelliJ spring boot initializr plugin
  • Run ./mill init

What's working

  • ./mill run works
  • Test trait is created, but neither ./mill test or ./mill __.test work, a test target doesn't seem to exist
  trait test extends MavenTests {

    def mvnDeps =
      Seq(mvn"org.springframework.boot:spring-boot-starter-test:3.5.6")

    def testSandboxWorkingDir = false
    def testParallelism = false
    def forkWorkingDir = moduleDir
  }

On first look it seems the test framework cannot be detected (or maybe the maven migration does not support it?)

changing the code to


  object test extends MavenTests with Junit4 {

    def mvnDeps =
      Seq(mvn"org.springframework.boot:spring-boot-starter-test:3.5.6")

    def testSandboxWorkingDir = false
    def testParallelism = false
    def forkWorkingDir = moduleDir

  }

does not crash, but the tests are not detected.

Working configuration needs junit platform version. The below works properly

object test extends MavenTests, Junit5 {

    override def junitPlatformVersion: T[String] = "6.0.0"

    def mvnDeps =
      Seq(mvn"org.springframework.boot:spring-boot-starter-test:3.5.6")

    def testSandboxWorkingDir = false
    def testParallelism = false
    def forkWorkingDir = moduleDir

  }

vaslabs avatar Oct 20 '25 08:10 vaslabs

Setting up Spring boot with Kotlin

This one fails to run with:

./mill run
[59/59, 1 failed] ============================== run ==============================
1 tasks failed
[58] finalMainClass No main class specified or found

The reason for the failure is due to the init not configuring the module as KotlinMavenModule.

Setting it though results in a different error

[68] Compiling 1 Kotlin sources to /home/vnicolaou/Downloads/demo-kotlin/out/compile.dest/classes ...
[68] /home/vnicolaou/Downloads/demo-kotlin/src/main/kotlin/com/example/demo_kotlin/DemoKotlinApplication.kt:10:2: error: cannot inline bytecode built with JVM target 17 into bytecode that is being built with JVM target 1.8. Please specify proper '-jvm-target' option
[68] 	runApplication<DemoKotlinApplication>(*args)
[68]  ^
[75/75, 1 failed] ============================== run ============================== 2s
1 tasks failed
[68] compile Kotlin compiler failed with exit code 1 (COMPILATION_ERROR)

Adding
override def kotlincOptions: T[Seq[String]] = super.kotlincOptions() ++ Seq("-jvm-target", "17") works

Tests

After the main module fixes and setting tests with:

  object test extends KotlinMavenTests, Junit5 {

    def mvnDeps = Seq(
      mvn"org.jetbrains.kotlin:kotlin-test-junit5:1.9.25",
      mvn"org.springframework.boot:spring-boot-starter-test:3.5.6"
    )

    def testSandboxWorkingDir = false
    def testParallelism = false
    def forkWorkingDir = moduleDir

  }

The tests fail with

org.junit.platform.commons.JUnitException: OutputDirectoryProvider not available; probably due to unaligned versions of the junit-platform-engine and junit-platform-launcher jars on the classpath/module path.

This seems to not crash by changing to JUnit4 but no tests are discovered

The test configuration that works is

object test extends KotlinMavenTests, Junit5 {

    override def junitPlatformVersion: T[String] = "6.0.0"

    def mvnDeps = Seq(
      mvn"org.jetbrains.kotlin:kotlin-test-junit5:1.9.25",
      mvn"org.springframework.boot:spring-boot-starter-test:3.5.6"
    )

    def testSandboxWorkingDir = false
    def testParallelism = false
    def forkWorkingDir = moduleDir

  }
Image

vaslabs avatar Oct 20 '25 08:10 vaslabs

I think I can start by documenting these 2, I'm thinking creating a spring boot section in the docs and going at it with increasing complexity, what do you think @lihaoyi ?

It also doesn't seem very difficult implementing this in the initialzr project for mill and sending a PR, so the init + migration won't be needed once that is ready

vaslabs avatar Oct 20 '25 08:10 vaslabs

Thank you @vaslabs for these feedback. We currently document spring-related modules under the "{Language} Web Project Examples" section, Where we also cover other frameworks like Micronaut or Ktor. But having a more explicit navigation section for "frameworks" and dedicated pages for each framework might be indeed a better fit. Esp. since frameworks like Spring aren't dedicated to just Java but work with all supported languages: Java, Scala, Kotlin and later Groovy.

Of course, landing a PR to Spring Initializr would be the best. I don't think we want guide users to generate projects for different build tools and then require them to migrate to Mill. While we should try to make it work, there is probably no reasonable ratio between effort and result.

lefou avatar Oct 20 '25 09:10 lefou

thanks for the heads up, I did some exploration today, I'll aim for the initializr PR first then, I guess if it gets accepted, no point doing the docs twice

vaslabs avatar Oct 20 '25 09:10 vaslabs

Don't know, how common it is, but Mill currently does not support creating WARs out of the box, but Spring Initializr does have an option to select "JAR" or "WAR".

lefou avatar Oct 20 '25 09:10 lefou

Don't know, how common it is, but Mill currently does not support creating WARs out of the box, but Spring Initializr does have an option to select "JAR" or "WAR".

I'll give that a go as well

vaslabs avatar Oct 20 '25 09:10 vaslabs

I think I can start by documenting these 2, I'm thinking creating a spring boot section in the docs and going at it with increasing complexity, what do you think @lihaoyi ?

It also doesn't seem very difficult implementing this in the initialzr project for mill and sending a PR, so the init + migration won't be needed once that is ready

I think this sounds reasonable. 1st we want working examples in Mill, 2nd want to figure out why ./mill init isn't working and if there's anything we can do to make it work, 3rd is to send a PR upstream to Spring Initializr (that may or may not be accepted yet since Mill is still relatively small compared to Maven and Gradle, but we should send the PR anyway)

lihaoyi avatar Oct 20 '25 12:10 lihaoyi

mill init from gradle project

In contrast with generating a maven project with https://start.spring.io/ , initialising a gradle one has one additional hiccup for mill init

The gradle effective (meaning the bit we care about to translate in mill) config is

plugins {
	java
	id("org.springframework.boot") version "3.5.7"
	id("io.spring.dependency-management") version "1.1.7"
}
...
dependencies {
	implementation("org.springframework.boot:spring-boot-starter-websocket")
	testImplementation("org.springframework.boot:spring-boot-starter-test")
	testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}

Notice in dependencies that they have no versions, which is probably injected via a bom file by the declared spring boot plugin

So after init with mill 1.0.6 we have

//| mill-version: 1.0.6
package build

import mill._
import mill.javalib._
import mill.javalib.publish._

object `package` extends MavenModule {

  def artifactName = "websocketdemo"

  def javacOptions = Seq("-parameters")

  def mvnDeps = Seq(mvn"org.springframework.boot:spring-boot-starter-websocket")

  def publishVersion = "0.0.1-SNAPSHOT"

  trait test extends MavenTests {

    def mvnDeps = Seq(mvn"org.springframework.boot:spring-boot-starter-test")

    def testSandboxWorkingDir = false
    def testParallelism = false
  }
}

which has the issues previously stated + the missing version

With the maven project, the version detection worked with it having

	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>3.5.6</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>

vaslabs avatar Nov 02 '25 12:11 vaslabs

~The main change is ready, but I still try to figure out why some tests fail in CI.~

Edit: wrong issue

lefou avatar Nov 02 '25 13:11 lefou

another issue I've found is while mill cli works properly for compile and test

Mill version SNAPSHOT is different than configured for this directory!
Configured version is 1.0.6 (/home/vnicolaou/mill/example/springboot/java/2-web-socket-initializr/build.mill)
[61/64] compile
[61] [info] compiling 3 Java sources to /home/vnicolaou/mill/example/springboot/java/2-web-socket-initializr/out/compile.dest/classes ...
[61] [info] done compiling

[64/64] test.compile
[64] [info] compiling 1 Java source to /home/vnicolaou/mill/example/springboot/java/2-web-socket-initializr/out/test/compile.dest/classes ...
[64] [info] done compiling
[64/64] ============================== test.compile ==============================

, running the main class from the IDE while the tests are not compiling, fails. perhaps some bsp wiring gets both directories for src/main and src/test

vaslabs avatar Nov 02 '25 13:11 vaslabs

Would be good to detail the Spring Gradle / Maven plugin equivalents. Does AOT or native mode just work? AOT has some pre-processors that generate bytecode as a build step.

re-thc avatar Nov 14 '25 10:11 re-thc

By "AOT or native mode", you mean "Ahead of Time Optimizations" as documented here: https://docs.spring.io/spring-framework/reference/core/aot.html ?

lefou avatar Nov 14 '25 10:11 lefou

By "AOT or native mode", you mean "Ahead of Time Optimizations" as documented here: https://docs.spring.io/spring-framework/reference/core/aot.html ?

Yes, you can enable it with spring.aot.enabled flag to true even if not building a GraalVM native image. It reduces memory, improves startup time and overall performance e.g. by not requiring reflection on IoC. It's not so clear from the linked article but there definitely is a build step that gets run and is embedded in the plugins.

re-thc avatar Nov 14 '25 11:11 re-thc

Yes, you can enable it with spring.aot.enabled flag to true even if not building a GraalVM native image.

You mean mvn spring-boot:process-aot package and then running with java -Dspring.aot.enabled=true -jar <jar-name>. We currently don't have this in Mill. We already have a SpringBootModule which holds the spring boot tools and provided the repackage functionality, so it should be possible to add easily. But I don't know, what really happens under the hood, most likely some annotation processing.

lefou avatar Nov 14 '25 12:11 lefou

Yes, you can enable it with spring.aot.enabled flag to true even if not building a GraalVM native image.

You mean mvn spring-boot:process-aot package and then running with java -Dspring.aot.enabled=true -jar <jar-name>. We currently don't have this in Mill. We already have a SpringBootModule which holds the spring boot tools and provided the repackage functionality, so it should be possible to add easily. But I don't know, what really happens under the hood, most likely some annotation processing.

https://github.com/spring-projects/spring-boot/blob/main/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/aot/ProcessAot.java org.springframework.boot.SpringApplicationAotProcessor gets run as part of a build step that does this.

re-thc avatar Nov 14 '25 13:11 re-thc

Yes, you can enable it with spring.aot.enabled flag to true even if not building a GraalVM native image.

You mean mvn spring-boot:process-aot package and then running with java -Dspring.aot.enabled=true -jar <jar-name>. We currently don't have this in Mill. We already have a SpringBootModule which holds the spring boot tools and provided the repackage functionality, so it should be possible to add easily. But I don't know, what really happens under the hood, most likely some annotation processing.

https://github.com/spring-projects/spring-boot/blob/main/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/aot/ProcessAot.java org.springframework.boot.SpringApplicationAotProcessor gets run as part of a build step that does this.

Thank you for this, created an issue, I can start working on it early next week.

Probably we can also look at/test graalvm support separately

vaslabs avatar Nov 15 '25 10:11 vaslabs

started the work here

vaslabs avatar Nov 18 '25 10:11 vaslabs