Java Web Project Examples

This page contains examples of using Mill as a build tool for web-applications. It covers setting up a basic backend server with a variety of server frameworks

Simple Web Example

This example shows off running a simple webserver in a module configured via build.mill.yaml

build.mill.yaml (download, browse)
extends: JavaModule
mvnDeps: [org.springframework.boot:spring-boot-starter-web:3.2.0]

In this example, the webserver script uses the Spring Boot framework to run the web server

src/WebServer.java (download, browse)
package example;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.*;

@SpringBootApplication
@RestController
public class WebServer {
  public static void main(String[] args) {
    System.setProperty("server.port", System.getenv().getOrDefault("PORT", "8080"));
    SpringApplication.run(WebServer.class, args);
  }

  @PostMapping("/reverse-string")
  public String reverseString(@RequestBody String body) {
    return new StringBuilder(body).reverse().toString();
  }
}
> ./mill runBackground

> curl -d 'helloworld' localhost:${PORT:-8080}/reverse-string
dlrowolleh

> ./mill clean runBackground # shut down webserver

Testing Web Projects

This example web project comes with a small test suite in the test/ folder, configured by test/package.mill.yaml. This test suite spins up the server and makes a single HTTP request against and checks it behaves as expected.

test/package.mill.yaml (download, browse)
extends: [build.JavaTests, TestModule.Junit5]
mvnDeps:
- com.squareup.okhttp3:okhttp:4.12.0
- org.junit.jupiter:junit-jupiter-api:5.10.1
- org.junit.jupiter:junit-jupiter-engine:5.10.1
> ./mill test
Test example.WebServerTests#testReverseString() finished...

Spring Boot

Jetty Hello World App

build.mill.yaml (download, browse)
extends: JavaModule
mvnDeps:
- org.eclipse.jetty:jetty-server:9.4.43.v20210629
- javax.servlet:javax.servlet-api:4.0.1
object test:
  extends: [JavaTests, TestModule.Junit4]

This example demonstrates how to set up a simple Jetty webserver, able to handle a single HTTP request at / and reply with a single response.

> ./mill test
...HelloJettyTest.testHelloJetty finished...

> ./mill runBackground

> curl http://localhost:${PORT:-8080}
...<h1>Hello, World!</h1>...

> ./mill clean runBackground

Micronaut Hello World App

build.mill (download, browse)
package build

import mill.*, javalib.*

object `package` extends MicronautModule {
  def micronautVersion = "4.6.1"

  def mvnDeps = Seq(
    mvn"io.micronaut:micronaut-http-server-netty",
    mvn"io.micronaut.serde:micronaut-serde-jackson",
    mvn"ch.qos.logback:logback-classic:1.5.3"
  )

  object test extends MavenTests, TestModule.Junit5 {

    def mvnDeps = Seq(
      mvn"io.micronaut:micronaut-http-client",
      mvn"io.micronaut.test:micronaut-test-junit5"
    )

    // Micronaut test not compatible with running in parallel
    def testParallelism = false
  }
}

trait MicronautModule extends MavenModule {
  def micronautVersion: String

  override def bomMvnDeps = Seq(
    mvn"io.micronaut.platform:micronaut-platform:${micronautVersion}"
  )

  override def annotationProcessorsMvnDeps =
    Seq(
      mvn"io.micronaut.data:micronaut-data-processor",
      mvn"io.micronaut:micronaut-http-validation",
      mvn"io.micronaut.serde:micronaut-serde-processor",
      mvn"io.micronaut.validation:micronaut-validation-processor",
      mvn"io.micronaut:micronaut-inject-java"
    )

  override def annotationProcessorsJavacOptions = super.annotationProcessorsJavacOptions() ++ Seq(
    "-Amicronaut.processing.incremental=true",
    "-Amicronaut.processing.group=example.micronaut",
    "-Amicronaut.processing.module=hello",
    "-Amicronaut.processing.annotations=example.micronaut.*"
  )

  def javacOptions = super.javacOptions() ++ Seq(
    "-parameters"
  )
}

This example demonstrates how to set up a simple Micronaut example service, using the code from the Micronaut Tutorial.

To preserve compatibility with the file layout from the example project, we use MavenModule, which follows the src/main/java and src/test/java folder convention.

Although Mill does not have a built in MicronautModule, this example shows how easy it is to define it yourself as trait MicronautModule: setting up the annotation processor classpath as a JavaModule and setting up the annotation via javacOptions. Once defined, you can then use MicronautModule in your build just like you can use any builtin trait like JavaModule.

The MicronautModule shown here does not implement the full functionality of the micronaut CLI; in particular, support for Micronaut AOT compilation is missing. But it easily can be extended with more features as necessary.

> ./mill test
...example.micronaut.HelloControllerTest#testHello()...

> ./mill runBackground

> curl http://localhost:${PORT:-8080}/hello
...Hello World...

> ./mill clean runBackground

Micronaut TodoMvc App

build.mill (download, browse)
package build

import mill.*, javalib.*

object `package` extends MicronautModule {
  def micronautVersion = "4.4.3"
  def runMvnDeps = Seq(
    mvn"ch.qos.logback:logback-classic:1.5.3",
    mvn"com.h2database:h2:2.2.224"
  )

  def mvnDeps = Seq(
    mvn"io.micronaut:micronaut-http-server-netty",
    mvn"io.micronaut.serde:micronaut-serde-jackson",
    mvn"io.micronaut.data:micronaut-data-jdbc",
    mvn"io.micronaut.sql:micronaut-jdbc-hikari",
    mvn"io.micronaut.validation:micronaut-validation",
    mvn"io.micronaut.views:micronaut-views-htmx",
    mvn"io.micronaut.views:micronaut-views-thymeleaf",
    mvn"org.webjars.npm:todomvc-common:1.0.5",
    mvn"org.webjars.npm:todomvc-app-css:2.4.1",
    mvn"org.webjars.npm:github-com-bigskysoftware-htmx:1.9.10"
  )

  object test extends MavenTests, TestModule.Junit5 {

    override def bomMvnDeps = Seq(
      mvn"io.micronaut.platform:micronaut-platform:${micronautVersion}"
    )

    def mvnDeps = Seq(
      mvn"com.h2database:h2:2.2.224",
      mvn"io.micronaut:micronaut-http-client",
      mvn"io.micronaut.test:micronaut-test-junit5"
    )

    // Micronaut test not compatible with running in parallel
    def testParallelism = false
  }
}

trait MicronautModule extends MavenModule {
  def micronautVersion: String

  override def bomMvnDeps = Seq(
    mvn"io.micronaut.platform:micronaut-platform:${micronautVersion}"
  )

  override def annotationProcessorsMvnDeps = Seq(
    mvn"io.micronaut.data:micronaut-data-processor",
    mvn"io.micronaut:micronaut-http-validation",
    mvn"io.micronaut.serde:micronaut-serde-processor",
    mvn"io.micronaut.validation:micronaut-validation-processor",
    mvn"io.micronaut:micronaut-inject-java"
  )

  override def annotationProcessorsJavacOptions = super.annotationProcessorsJavacOptions() ++ Seq(
    "-Amicronaut.processing.incremental=true",
    "-Amicronaut.processing.group=example.micronaut",
    "-Amicronaut.processing.module=todo",
    "-Amicronaut.processing.annotations=example.micronaut.*"
  )

  def javacOptions = super.javacOptions() ++ Seq(
    "-parameters"
  )
}

This example is a more complete example using Micronaut, adapted from https://github.com/sdelamo/todomvc. On top of the MicronautModule and annotation processing demonstrated by the previous example, this example shows how a "full stack" web application using Micronaut looks like:

  • Thymeleaf for HTML templating

  • Webjars for Javascript and CSS

  • HTMX for interactivity

  • Database interactions using JDBC and H2

  • Controllers, Repositories, Entities, Forms

  • A more detailed test suite

Again, the example MicronautModule is by no means complete, but it demonstrates how Mill can be integrated with Micronaut’s annotation processors and configuration, and can be extended to cover additional functionality in future

> ./mill test
...example.micronaut.LearnJsonTest...
...example.micronaut.TodoTest...
...example.micronaut.TodoItemMapperTest...
...example.micronaut.TodoItemControllerTest...
...example.micronaut.HtmxWebJarsTest...

> ./mill runBackground

> curl http://localhost:${PORT:-8080}
 ...<h1>todos</h1>...

> ./mill clean runBackground

Micronaut Native (GraalVM)

build.mill (download, browse)
package build

import mill.*
import javalib.*
import javalib.micronaut.MicronautNativeAotModule
import mill.api.{PathRef, Task}
import os.zip.ZipSource

import java.util.jar.JarInputStream

object `package` extends MavenModule, MicronautNativeAotModule {

  def micronautPackage = "hello.world"

  override def bomMvnDeps = Seq(
    mvn"io.micronaut.platform:micronaut-platform:4.10.3"
  )

  def mvnDeps = Seq(
    mvn"io.micronaut:micronaut-http-client",
    mvn"io.micronaut.serde:micronaut-serde-jackson"
  )

  def jvmVersion = "graalvm-community:21.0.2"

  def finalMainClass = "hello.world.Application"

  def annotationProcessorsMvnDeps = Seq(
    mvn"io.micronaut:micronaut-http-validation",
    mvn"io.micronaut.serde:micronaut-serde-processor"
  )

  override def annotationProcessorsJavacOptions = super.annotationProcessorsJavacOptions() ++ Seq(
    "-Amicronaut.processing.incremental=true",
    "-Amicronaut.processing.annotations=hello.world.*"
  )

  def javacOptions = super.javacOptions() ++ Seq(
    "-parameters"
  )

  def compileMvnDeps = Seq(
    mvn"io.micronaut:micronaut-http-client"
  )

  override def runMvnDeps = Seq(
    mvn"io.micronaut:micronaut-http-server-netty",
    mvn"ch.qos.logback:logback-classic"
  )

}

This example demonstrates how to configure mill for a simple micronaut native project. It uses Micronaut AOT to optimize the application for the native image generation.

> ./mill show nativeImage
...out/nativeImage.dest/native-executable...

> ./mill show nativeRunBackground

> curl http://localhost:${PORT:-9870}
{"_links":{"self":[{"href":"/","templated":false}]},"_embedded":{"errors":[{"message":"Page Not Found"}]},"message":"Not Found"}

> ./mill clean nativeRunBackground