V
Vel·ToolKit
Simple · Fast · Ready to use
EN
Chapter 20 of 20

Engineering Practice

Maven / Gradle commands, JUnit 5, SLF4J, packaging, the module system, LTS choice

Why Engineering Matters

Learning syntax only gets you small scripts. A real project needs: dependency management (no manual jar downloads), automated testing (no manual clicking), unified logging (no System.out), a deployable artifact (not source), and a version-selection strategy (deciding the LTS). This chapter covers the toolchain Java backend dev uses every day.

Maven: Dependency Management + Build (the Mainstream)

Use case: 90% of Java projects, especially the Spring ecosystem. One pom.xml describes the project, dependencies, and plugins; the mvn command-line builds/tests/packages in one go.

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.example</groupId>
    <artifactId>myapp</artifactId>
    <version>1.0.0</version>
    <packaging>jar</packaging>

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>2.0.13</version>
        </dependency>
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>1.5.6</version>
        </dependency>
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter</artifactId>
            <version>5.10.2</version>
            <scope>test</scope>
        </dependency>
    </dependencies>
</project>
$ mvn compile          # compile src/main/java
$ mvn test              # compile + run src/test/java
$ mvn package           # compile + test + build target/*.jar
$ mvn install           # install to local ~/.m2 repo (for other local projects)
$ mvn dependency:tree   # dependency tree, locate version conflicts
$ mvn clean package     # clean target/ first, then package

Gradle: Modern + Flexible

Gradle is also mainstream (Android default; Spring Boot officially supports both). Scripts use the Groovy / Kotlin DSL, more compact than XML. New projects can pick either — use whatever the team is used to.

// build.gradle.kts (Kotlin DSL)
plugins {
    java
    application
}

group = "com.example"
version = "1.0.0"
java { sourceCompatibility = JavaVersion.VERSION_17 }

repositories { mavenCentral() }

dependencies {
    implementation("org.slf4j:slf4j-api:2.0.13")
    implementation("ch.qos.logback:logback-classic:1.5.6")
    testImplementation("org.junit.jupiter:junit-jupiter:5.10.2")
}

application { mainClass = "com.example.App" }

tasks.test { useJUnitPlatform() }
$ ./gradlew build      # compile + test + package
$ ./gradlew test
$ ./gradlew run         # run directly via the application plugin
$ ./gradlew dependencies

JUnit 5: Unit Testing

Java's standard testing framework. Conventions: test classes go in src/test/java, class names end with Test, methods are annotated @Test. Assertions like assertEquals come from org.junit.jupiter.api.Assertions. The Maven / Gradle surefire plugin discovers and runs them automatically.

// src/main/java/com/example/MathUtil.java
package com.example;

public class MathUtil {
    public static int add(int a, int b) { return a + b; }
    public static int divide(int a, int b) {
        if (b == 0) throw new IllegalArgumentException("b=0");
        return a / b;
    }
}
// src/test/java/com/example/MathUtilTest.java
package com.example;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;

import static org.junit.jupiter.api.Assertions.*;

class MathUtilTest {

    @BeforeEach
    void setUp() { /* runs before each test */ }

    @Test
    @DisplayName("add: ordinary positives")
    void addPositive() {
        assertEquals(5, MathUtil.add(2, 3));
    }

    @ParameterizedTest
    @CsvSource({
        "1, 1, 2",
        "2, 3, 5",
        "-1, 1, 0"
    })
    void addTable(int a, int b, int expected) {
        assertEquals(expected, MathUtil.add(a, b));
    }

    @Test
    void divideByZeroThrows() {
        IllegalArgumentException e = assertThrows(
            IllegalArgumentException.class,
            () -> MathUtil.divide(1, 0)
        );
        assertEquals("b=0", e.getMessage());
    }
}

SLF4J + Logback: Standard Logging

Use case: all production code should write structured logs. SLF4J is the logging facade (API), Logback the concrete implementation (binding). Convention: `private static final Logger log = LoggerFactory.getLogger(XX.class);`, **stop writing System.out**.

// src/main/java/com/example/Service.java
package com.example;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class Service {
    private static final Logger log = LoggerFactory.getLogger(Service.class);

    public void handle(long userId) {
        log.info("start handle user_id={}", userId);     // {} placeholder, lazily evaluated
        try {
            doWork(userId);
        } catch (Exception e) {
            log.error("handle failed user_id={}", userId, e); // the last argument is treated as the exception
        }
    }

    private void doWork(long id) {
        log.debug("working on {}", id);
        if (id < 0) throw new IllegalArgumentException("bad id");
    }
}

Logback config file src/main/resources/logback.xml:

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%thread] %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>

    <root level="INFO">
        <appender-ref ref="STDOUT"/>
    </root>

    <logger name="com.example" level="DEBUG"/>
</configuration>

Packaging and Running

Use case: deliver a runnable fat jar or docker image. Maven's maven-shade-plugin / Gradle's shadow plugin can bundle dependencies into the jar so java -jar runs directly. Spring Boot ships spring-boot-maven-plugin.

# Maven build jar
$ mvn clean package
$ java -jar target/myapp-1.0.0.jar

# a Spring Boot fat jar is self-runnable by default
$ java -jar target/myapp.jar --server.port=8081

# inject runtime parameters
$ java -Xmx512m -Dspring.profiles.active=prod -jar app.jar

# build a docker image (Spring Boot 3 has built-in buildpacks)
$ mvn spring-boot:build-image
$ docker run -p 8080:8080 myapp:1.0.0

Choosing a Java Version (LTS Strategy)

Java has a new release every 6 months and an LTS (Long-Term Support, 8+ years of updates) every 2 years. Production should **use only LTS**: Java 8 / 11 / 17 / 21. New projects today pick 17 or 21 directly; Java 8 only for maintaining legacy systems.

  • Java 8 (2014, LTS): lambdas, Stream, Optional, the new date API. Still high adoption.
  • Java 11 (2018, LTS): var, HTTP Client, single-file source run, JavaFX removed. The baseline for Spring 5 / Spring Boot 2.
  • Java 17 (2021, LTS): record, sealed, pattern matching, text blocks. Minimum required by Spring Boot 3+. **The baseline of this tutorial.**
  • Java 21 (2023, LTS): virtual threads (Loom), sequenced collections, enhanced pattern matching. Preferred for new projects.

The Java Module System (Java 9+, awareness is enough)

The module system split the JDK itself (java.base / java.sql / java.xml…). Application-level modularization (writing module-info.java) is rarely used — most backend projects just split subprojects with Maven / Gradle. You only need to know it exists and recognize the common error messages.

// src/main/java/module-info.java
module com.example.myapp {
    requires java.sql;             // declare a required JDK module
    requires org.slf4j;             // third-party module
    exports com.example.api;        // which packages are visible externally
    opens   com.example.entity to hibernate.core; // open for reflection to a specific module
}

Engineering Practice Checklist

  • Use Maven or Gradle to manage dependencies, run tests, and package; don't manually download jars or run javac by hand
  • Put .editorconfig and .gitignore (target/, .idea/, *.class) in every project
  • CI (GitHub Actions / Jenkins) should at least run: mvn verify (with tests) + static analysis (SpotBugs/Checkstyle/SonarLint)
  • Dependency management: write an explicit version for each dep to avoid transitive conflicts; use mvn dependency:tree to investigate
  • Logging: always use SLF4J + placeholders, configure JSON format for ELK / Loki
  • Testing: table-driven + ParameterizedTest to cover business branches; JaCoCo for coverage
  • Code style: Google Java Style or a team convention, automated by tooling (Spotless / google-java-format)
  • Start new projects on Java 17 LTS; Spring Boot 3.x; JUnit 5