Table of Contents

Introduction

I often have to build a small Java-based API or microservice. Using Jakarta EE or Spring sometimes feels like an overkill, especially when I work with teams which are not experienced with Java.

In this post, I’ll explain how to build an unopionated, production ready API which isn’t coupled with complex frameworks or requiring understanding of the Java enterprise ecosystem. We’ll cover how to do automate testing, formatting, code analysis, and vulnerability checks with Gradle commands.

The API starter code is on GitHub and in the rest of this post we’ll break it down.

Set up

Let’s start by setting up a local dev environment. I like SDKMAN, which is a utility for installing and managing different versions of Java and related tools:

1
2
3
4
5
6
7
8
9
# Install SDKMan, if you don't have it
curl -s "https://get.sdkman.io" | bash

# View all available Java distributions:
sdk list java

# Install the latest Java 17 Temurin distro
# (Temurin used to be called AdoptOpenJDK)
sdk install java 17.0.8-tem

If you cloned the GitHub project, then that’s all you need. If you want to follow along, then you’ll need gradle to initialise the project:

1
2
3
4
5
# View available Gradle distributions:
sdk list gradle

# Install a gradle version from the above list
sdk install gradle 8.2.1

Now let’s initialise a project with gradle init. This will open a terminal “wizard”, and we’ll have to select the following:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
> gradle init
Select type of project to generate:
  1: basic
  2: application
Enter selection (default: basic) [1..4] 2

Select implementation language:
  1: C++
  ...
  3: Java
Enter selection (default: Java) [1..6] 3

Split functionality across multiple subprojects?:
  1: no - only one application project
  2: yes - application and library projects
Enter selection (default: no - only one application project) [1..2] 1

Select build script DSL:
  1: Groovy
  2: Kotlin
Enter selection (default: Groovy) [1..2] 1

Select test framework:
  1: JUnit 4
  ...
  4: JUnit Jupiter
Enter selection (default: JUnit Jupiter) [1..4] 4

Project name (default: java-api-starter): 
Source package (default: java.api.starter): com.nikgrozev
Enter target version of Java (min. 7) (default: 17): 17

The above creates a single project gradle application which can run a main method and packages your code in an archive. If you’re new to gradle, you can have a look at the Gradle Quickstart tutorial. From now on, we’ll use the Gradle Wrapper script ./gradlew to ensure consistent builds across environments.

Open the new project in you favourite IDE. At this point, we have a functioning Java app, with a main method App.java and a unit test in AppTest.java. We can already build, test, run, and check the docs:

1
2
3
4
5
6
7
8
9
10
11
12
# Clean up, compile and rebuild
./gradlew clean build

# Run the JUnit5 tests
./gradlew test

# Run the app locally
./gradlew run

# Checkout all gradle tasks or get help for a task
./gradlew task
./gradlew help --task test

Next, let’s build a single executable uber jar file, which we can distribute. One approach is to modify the jar task in build.gradle:

1
2
3
4
5
6
7
8
9
10
// Based on https://www.baeldung.com/gradle-fat-jar
jar {
    duplicatesStrategy = DuplicatesStrategy.EXCLUDE
    manifest {
        attributes "Main-Class": application.mainClass
    }
    from {
        configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) }
    }
}

Now we can build and run the executable jar file:

1
2
3
4
5
# Clean up, compile and rebuild
./gradlew clean build

# The above creates an executable uber jar file
java -jar ./app/build/libs/app.jar

Javalin

Javalin is a lightweight and unopiniated web server built on top of Jetty. It’s easy to get started with and comes with convenient testing utilities. Let’s add the following to build.gradle and rebuild:

1
2
3
4
5
6
7
dependencies {
    //...
    // Javalin Web Server
    implementation "io.javalin:javalin:5.6.1"
    // Allows Javalin to parse and create JSON
    implementation 'com.fasterxml.jackson.core:jackson-databind:2.15.0'
}

Javalin is quite easy to start with. We just need to create a Javalin instance and add routes. I particularly like the CRUD handler, which allows you to create REST-ful endpoints and organise them in a separate class:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class App {
    public static Javalin makeJavalinServer() {
        var app = Javalin.create();
        app.get("/api/health", ctx -> ctx.result("Healthy"));
        app.routes(() -> crud("/api/items/{id}", 
          // ItemController is our implementation of Javalin's CrudHandler
          new ItemController())
        );
        return app;
    }

    public static void main(String[] args) {
        app.start(9090);
    }
}

For a full example, see the GitHub project. With the above, we have a fully functional web server:

1
2
3
# Run the app locally
# Checkout http://localhost:9090/api/health
./gradlew run

Beyond the basisc, Javalin offers many more advanced functionalities like before and after handlers, supports web sockets, validation and more.

Testing

Javalin supports two types of tests: unit tests for each handler and end-to-end tests. Let’s start by adding the following dependencies:

1
2
3
4
5
6
dependencies {
  // Mockito for unit testing Javalin
  testImplementation 'org.mockito:mockito-inline:5.2.0'
  // Javalin test utility
  testImplementation 'io.javalin:javalin-testtools:5.6.1'
}

For unit testing our handlers, we’ll use Mockito to mock all external dependencies, as in the following example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class ItemControllerTest {
    private final Context ctx = mock(Context.class);
    private ItemController controller = new ItemController();

    @BeforeEach
    void setUp() {
        controller.setItems(List.of());
    }

    @Test
    void testCreate() {
        var newItem = new Item("id1", "item1");
        when(ctx.bodyAsClass(Item.class)).thenReturn(newItem);
        controller.create(ctx);
        verify(ctx).json(newItem, Item.class);
        assertEquals(1, controller.getItems().size());
        assertEquals("id1", controller.getItems().get(0).getId());
    }
}

This is great, but I prefer to mock as little as possible. Mocking excessively makes tests unreleastic. Fortunately, Javalin Test Tools allows us to test an actual instance of a Javalin webserver and make realistic API calls:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class AppTest {
    private Javalin app;

    @BeforeEach
    void setUp() throws Exception {
        app = App.makeJavalinServer();
    }

    @Test
    void testHealthEndpoint() {
        JavalinTest.test(
                app,
                (server, client) -> {
                    var response = client.get("/api/health");
                    assertEquals(200, response.code());
                    assertEquals("Healthy", response.body().string());
                });
    }
}

Once again, the full code with the unit tests is available on GitHub.

Externalisation

Dotenv is a Java alternative of the popular npm package dotenv. It allows us to keep all dev env variables in a local file ./app/.env and load them from the app. In production, you wouldn’t have a .env and then the library would load actual environment variables instead. Let’s add it as a dependency:

1
2
3
4
dependencies {
    // To read vars from .env file or environment
    implementation 'io.github.cdimascio:dotenv-java:3.0.0'
}

Now we can use it in our code:

1
2
3
4
5
6
7
8
9
public class App {
    public static void main(String[] args) {
        // load env vars via https://github.com/cdimascio/dotenv-java
        var dotenv = Dotenv.configure().ignoreIfMissing().systemProperties().load();

        var app = makeJavalinServer();
        app.start(Integer.parseInt(dotenv.get("PORT", "7070")));
    }
}

Code Formatting with Spotless

Spotless is a popular linter for Java and it comes with a handy Gradle Plugin. Let’s add it to our build.gradle:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
plugins {
    // ...
    id "com.diffplug.spotless" version "6.20.0"
}

// Based on https://github.com/diffplug/spotless/tree/main/plugin-gradle
 spotless {
   format 'misc', {
     // define the files to apply `misc` to
     target '*.gradle', '*.md', '.gitignore'

     // define the steps to apply to those files
     trimTrailingWhitespace()
     // or tabs/spaces. Takes an integer argument if you don't like 4
     indentWithSpaces()
     endWithNewline()
   }
   java {
     // apply a specific flavor of google-java-format
     googleJavaFormat('1.17.0').aosp().reflowLongStrings()
     // fix formatting of type annotations
     formatAnnotations()
   }
 }

In the above, we used out-of-the-box formatter from Google. Have a look at the documentation to see all configuration options.

Now we can format the code or check if it complies with the format:

1
2
3
4
5
# Check for violations (useful in CI)
./gradlew spotlessJavaCheck

# Format the code with Spotless
./gradlew spotlessJavaApply

Code Analysis with Spotbugs

Spotbugs is a great tool for code analysis and it has a Gradle Plugin as well. Let’s configure it with the default:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
plugins {
    // ....
    id "com.github.spotbugs" version "5.0.14"
}

// Extensions - https://spotbugs-gradle-plugin.netlify.app/com/github/spotbugs/snom/spotbugsextension
spotbugs {
    ignoreFailures = false
    showStackTraces = true
    showProgress = true
    effort = 'default'
    reportLevel = 'default'
    reportsDir = file("$buildDir/reports/spotbugs")
}
spotbugsMain {
    reports {
        html {
            required = true
        }
    }
}
spotbugsTest {
    reports {
        html {
            required = true
        }
    }
}

Now we can analyse our code and generate HTML reports for every violation:

1
2
3
# Analyse the code for issues with SpotBugs
# HTML reports are generated in ./app/build/reports/spotbugs
./gradlew spotbugsMain spotbugsTest

Spotless is very configurable, and you can check their documentation about the various options.

SonarQube

SonarQube is a popular code analysis tool. It usually runs in a central server within an organisation. Developers and CI tools can submit code for analysis, which is then validated against a predefined Quality Gate.

Let’s configure a Gradle Plugin to push code to a SonarQube instance. Before that, we’ll need to congirue test coverage reports, so that SonaQube can inspect that too (SonarQube won’t run your tests). We’ll use the popular JaCoCo code coverate tool: :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
plugins {
    // ...
    id "org.sonarqube" version "4.3.0.3225"
    id 'jacoco'
}

// https://docs.gradle.org/current/userguide/jacoco_plugin.html
test {
     // report is always generated after tests run
    finalizedBy jacocoTestReport
}
jacocoTestReport {
    // tests are required to run before generating the report
    //  By default, reports go into app/build/reports/jacoco
    dependsOn test
    reports {
        xml.required = true
    }
}

// https://docs.sonarsource.com/sonarqube/latest/analyzing-source-code/scanners/sonarscanner-for-gradle/
sonar {
    properties {
        property "sonar.projectKey", rootProject.name
        property "sonar.projectName", rootProject.name
        property "sonar.host.url", "???"
    }
}

To run a SonaQube analsis, please change the SonaQube url in the above config. You’ll also need an access token:

1
2
3
4
5
# Push the code for analsys to a SonarQube instance
# Note 1: you need to edit "build.gradle" and add your "sonar.host.url"
# Note 2: you need an env var SONAR_TOKEN to authenticate
export SONAR_TOKEN=???
./gradlew sonar

Dependency Vulnerability Analysis

Dependency vulnerability analysis is essential to maintain a healthy and secure application. We’ll use the OWASP Gradle plugin to inspect our code - let’s configure it:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
plugins {
    // ...
    id "org.owasp.dependencycheck" version "8.1.2"
}

// Results will be in app/build/reports/dependency-check-report.html
dependencyCheck {
    // Low(0.1-3.9), Med(4.0-6.9), High(7.0-8.9), Critical(9.0-10.0)
    failBuildOnCVSS = 7

    // Based on https://github.com/dependency-check/dependency-check-gradle/issues/22#issuecomment-575568801
    def skipConfigurationPatterns = [
        "test.*",
        "spotbugs.*",
        "spotless.*",
        "sonar.*",
        "jacoco.*"
    ]
    allprojects {
        configurations.all { configuration ->
            if (configuration.name in skipConfigurations) {
                return
            }
            skipConfigurationPatterns.each { pattern ->
                if (configuration.name.matches(pattern)) {
                    skipConfigurations << configuration.name
                }
            }
        }
    }
}

In the above configuration, we’ve ignored medium and lower severity dependencies (failBuildOnCVSS = 7). We are also ignoring test and tool dependencies (e.g. of spotbugs and sonar) as we will not package them into our build. Now we can run a check, which will also generate an HTML report:

1
2
3
# Check the dependencies for known high or ciritcal vulnerabilities
# Results will be in app/build/reports/dependency-check-report.html
./gradlew dependencyCheckAnalyze

Docker Image

Finally, let’s package our application in a Docker image. We will use a two-stage Dockerfile. The first stage will build the executable jar file, and the second will run it.

I’ve chosen to use the Eclipse Temurin official Docker image a base image. If you want to minimise the resulting docker image you may use its Alpine variant or another base image. Furthermore, the Eclipse Temurin page documents how to use jlink to create a smaller and custom JRE, which would result in an even smaller image. For simplicity, let’s just use the out of the box image:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
FROM eclipse-temurin:17 as builder

# Set the working directory inside the container
WORKDIR /app

# Copy the code after installing dependencies - Note! .dockerignore
COPY . .

# Run the Gradle build to compile the app
RUN ./gradlew clean build

# Stage 2: Build the actual container from the builder's output
FROM eclipse-temurin:17

# The port we're listening on
# Copy build bundle from the builder container
COPY --from=builder /app/app/build/libs/app.jar /app/

# Make sure we don't run as root
RUN chown -R nobody /app
USER nobody

CMD ["java", "-jar", "/app/app.jar"]

It’s a good practice to add a .dockerignore file to your code base. This will speed up the build. I have added a sample one in the GitHub project.

Let’s proceed to build and tag our docker image:

1
docker build . -t java-api-starter

Now we can start the image locally. It’s often convenient to pass all required env variables in a file:

1
2
3
4
5
6
# Replace 9090 with the PORT from .env
# Visit http://localhost:9090/api/health
docker run \
  -p 9090:9090 \
  --env-file=./app/.env \
  java-api-starter

To debug the image locally, we can open a shell “into” it:

1
2
3
4
5
6
7
# Replace 9090 with the PORT from .env
# Will open a shell
docker run \
  -p 9090:9090 \
  --env-file=./app/.env \
  -it java-api-starter \
  /bin/sh

Docker Image: Vulnerability Analysis

Many CI/CD tools scan images for known vulnerabilities. I often find it useful to scan the image locally, so I can quickly experiment with different approaches to mitigate vulnerabilities. I find Rancher Desktop very useful. It allows me to pick a local docker image and scan it in the UI.

Let’s start by selecting an image to scan:

Choose a docker image.
Select a local docker image to scan.

And then we can preview all known vulnerabilities and their severity:

Docker image analysis.
Preview vulnerabilities.