Skip to content

VOL 00 / CH 08 / LESSON 01

8.1 From Source Code to Runnable Artifact ​

Prerequisites: Terminal, Git, Maven, Docker

Estimated time: 30 minutes

Exercise environment: Bash, curl 7.71+, JDK 17+, Maven 3.6.3+; the image section uses Docker Engine 28+ with default bridge/NAT networking

You are ready to continue when: you can distinguish between source code, test code, artifacts, images, deployment, release, and runtime, and locally validate a Java 17 artifact

The terminal, Git, debugger, build tools, and Docker are ready. Now turn source code into a service you can request locally: build an artifact, start a process, expose a port, and retain verifiable release evidence.

One Chain, Nine Different Actions ​

ActionOutput or State
WriteSource code and configuration changes
CompileBytecode or machine code
TestVerification results for the candidate version
PackageJAR, archive, software package, etc., artifacts
Build imageOCI/Docker image containing runtime configuration
Publish artifactArtifact enters the repository, obtaining a version or digest
DeployThe candidate version begins to be used in the target environment
Release (release)User traffic or feature switches are directed to this version
Run and observeHealth, logs, metrics, tracing, and alerts

Deployment does not necessarily mean that users have already seen the new version. You can first deploy to an instance with no traffic, validate it, and then release traffic; or you can first deploy the code and then use a feature flag to enable the feature.

From an Empty Directory Make a Java 17 Service ​

This chapter uses the HTTP server built into JDK to avoid mixing in framework dependencies into the first release chain. First, set up the project:

bash
workdir=$(mktemp -d)
release_lab_dir="$workdir/release-lab"
mkdir -p "$release_lab_dir/src/main/java/dev/atlas"
cd "$release_lab_dir"

Write the complete pom.xml:

bash
cat > pom.xml <<'XML'
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
  <modelVersion>4.0.0</modelVersion>
  <groupId>dev.atlas</groupId>
  <artifactId>release-lab</artifactId>
  <version>1.0.0</version>
  <properties>
    <maven.compiler.release>17</maven.compiler.release>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  </properties>
  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>3.13.0</version>
      </plugin>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-jar-plugin</artifactId>
        <version>3.4.1</version>
        <configuration>
          <archive>
            <manifest>
              <mainClass>dev.atlas.App</mainClass>
            </manifest>
          </archive>
        </configuration>
      </plugin>
    </plugins>
  </build>
</project>
XML

Then write the complete service entry point:

bash
cat > src/main/java/dev/atlas/App.java <<'JAVA'
package dev.atlas;

import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpServer;
import java.io.IOException;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;

public final class App {
    private App() {}

    public static void main(String[] args) throws IOException {
        String host = System.getenv().getOrDefault("HOST", "127.0.0.1");
        int port = Integer.parseInt(System.getenv().getOrDefault("PORT", "8080"));
        HttpServer server = HttpServer.create(new InetSocketAddress(host, port), 0);
        server.createContext("/", exchange -> respond(exchange, 200, "Hello Atlas! Let us go!\n"));
        server.createContext("/health", exchange -> respond(exchange, 200, "ok\n"));
        String version = System.getenv().getOrDefault("APP_VERSION", "v1");
        server.createContext("/version", exchange -> respond(exchange, 200, version + "\n"));
        server.setExecutor(null);
        Runtime.getRuntime().addShutdownHook(new Thread(() -> server.stop(1)));
        server.start();
        System.out.printf("listening=%d%n", server.getAddress().getPort());
    }

    private static void respond(HttpExchange exchange, int status, String text)
            throws IOException {
        byte[] body = text.getBytes(StandardCharsets.UTF_8);
        exchange.getResponseHeaders().set("Content-Type", "text/plain; charset=utf-8");
        exchange.sendResponseHeaders(status, body.length);
        try (OutputStream output = exchange.getResponseBody()) {
            output.write(body);
        }
    }
}
JAVA

/health only shows that the process can handle this request. /version returns the environment's version marker, defaulting to v1. Neither checks a database or external service. JDK HttpServer matches context path prefixes; this example does not implement exact routing, authentication, or production traffic controls. See the JDK HttpServer documentation.

The service defaults to listening on 127.0.0.1. When running directly on the host, it won't be exposed to the local network. After building, let the operating system assign a temporary port and complete validation and stopping in the same terminal:

bash
(
set -e
mvn --batch-mode verify
sha256sum target/release-lab-1.0.0.jar

HOST=127.0.0.1 PORT=0 java -jar target/release-lab-1.0.0.jar >app.log 2>&1 &
app_pid=$!
cleanup_app() {
  kill "$app_pid" 2>/dev/null || true
  wait "$app_pid" 2>/dev/null || true
}
trap cleanup_app EXIT
trap 'exit 130' INT
trap 'exit 143' TERM

for attempt in 1 2 3 4 5; do
  grep -q '^listening=' app.log && break
  sleep 1
done
app_port=$(sed -n 's/^listening=//p' app.log)
test -n "$app_port"
curl --fail --silent --show-error "http://127.0.0.1:$app_port/health"
curl --fail --silent --show-error "http://127.0.0.1:$app_port/"

)

The parentheses create a subshell whose exit cleanup also runs after a failure; SIGKILL cannot trigger a trap. Expect ok followed by the greeting. On macOS, use shasum -a 256. The first time Maven parses the plugin, it may need to connect to the internet. When the project is officially used, it is also recommended to submit the Maven Wrapper. In this case, verify will go through compilation, testing, and packaging. However, it does not generate tests on its own; "No tests to run" still indicates that additional regression test cases are needed.

Whether java -jar can run the JAR depends on how the manifest and dependencies are packaged. In this example, only JDK modules are used, so a regular JAR is sufficient. However, when there are third-party dependencies, a regular Maven JAR will not automatically include them. A Spring Boot executable JAR is repackaged by the corresponding plugin, and the two cannot be confused. Keep $workdir and $release_lab_dir, as the end-of-chapter experiment will also use them.

Putting Verified JARs into the Image ​

Save this as $release_lab_dir/Dockerfile. APP_VERSION is a public exercise marker that can be overridden at build time. Do not bake credentials into an image through ARG or ENV:

dockerfile
FROM eclipse-temurin:17-jre
ARG APP_VERSION=v1
ENV APP_VERSION=$APP_VERSION
WORKDIR /app
COPY target/release-lab-1.0.0.jar /app/app.jar
USER 10001:10001
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "/app/app.jar"]
bash
(
set -e
image_tag="workshop-ch08-release:${RANDOM}-${RANDOM}"
container_id=
cleanup_image() {
  if [ -n "$container_id" ]; then
    docker stop "$container_id" >/dev/null 2>&1 || true
  fi
  docker image rm "$image_tag" >/dev/null 2>&1 || true
}
trap cleanup_image EXIT
trap 'exit 130' INT
trap 'exit 143' TERM
docker build -t "$image_tag" .
container_id=$(docker run --detach --rm --env HOST=0.0.0.0 \
  --publish 127.0.0.1::8080 "$image_tag")
published=$(docker port "$container_id" 8080/tcp)
host_port=${published##*:}
curl --fail --silent --show-error --retry 10 --retry-all-errors \
  --retry-delay 1 --max-time 2 "http://127.0.0.1:$host_port/health"
curl --fail --silent --show-error --max-time 2 "http://127.0.0.1:$host_port/version"
)

Startup can briefly refuse or reset connections. These read-only health GETs allow at most 10 retries, with a 2-second limit per request. --retry-all-errors requires curl 7.71+; see the curl retry option.

If the base image is not cached, the build will access the registry. In an offline environment, it's important to prepare and verify the image source in advance. Production images should also fix the version or digest of an approved image, rather than relying long-term on movable tags.

One Build, Incremental Advancement ​

Build one candidate artifact for a release and promote it between environments. Production projects record the commit SHA, JAR checksum, image digest, build log, and test results. Development, pre-release, and production environments reference the same digest incrementally, rather than rebuilding for each environment.

This reduces dependency drift but does not automatically ensure security. Repeated builds, source provenance, signing, SBOMs, vulnerability scanning, and controlled configurations are still required.

Acceptance ​

  • Can explain the boundaries of nine actions;
  • Can explain the difference between mvn verify and "there are indeed tests";
  • Can explain the difference between a regular JAR and a Spring Boot executable JAR;
  • Can use /health to verify local JARs and containers;
  • Can explain why deployment should reference an immutable digest.

The manual sequence is complete once health and version requests pass and cleanup finishes. The next step is to hand over the verification to automation, while restricting its permissions.

→ Next Step: CI/CD

Built with VitePress | Software Systems Atlas