Chapter 08Lesson 02~180 minutes

Maven Properties, Profiles, settings.xml, Mirrors, Proxies, Servers, and Environment-Specific Builds: Guided Hands-On Workflow and Core Operations

Build a disposable Maven project, inspect effective profiles/settings/properties, compare project, settings-profile, and command-line property values, and route one fixed dependency through a local authenticated mirror without embedding credentials.

Effective SettingsProperty PrecedenceLocal MirrorServersIsolated Cache

Learning objectives

  • Create a disposable Maven project and a local authenticated Maven-style repository using only the JDK plus the project wrapper.
  • Prove project/profile/settings/CLI property values with help:evaluate rather than guessing precedence.
  • Inspect active profiles, effective settings, effective POM state, and dependency tree before and after changes.
  • Route one fixed dependency through a narrow localhost mirror while keeping credentials outside project files.
  • Compare warm resolution with a fresh isolated local repository and explain exactly which cache/repository state changed.
Current baseline — verified 2026-08-23. Labs use Maven 3.9.16 via Maven Wrapper 3.3.4, JDK 21 to run Maven, Java 17 as the compiler release target, Help Plugin 3.5.2, Dependency Plugin 3.11.0, Compiler Plugin 3.15.0, Resources Plugin 3.5.0, Surefire 3.5.6, and JAR Plugin 3.5.1. Lab resolution uses project-relative isolated local repositories. No normal ~/.m2, global settings, shared CI settings, or real credentials are modified.
Cross-platform note: POSIX examples use ./mvnw, grep, sha256sum, and shell environment variables. On Windows use mvnw.cmd, Select-String, Get-FileHash, and $env:NAME. Maven profile/settings semantics are cross-platform; file paths and shell quoting are not.

1. Lab contract and directory layout

The mandatory path is local and disposable. Maven still needs normal access to Central for its own plugins unless your normal organization policy already mirrors Central; the illustrative dependency itself comes from a JDK-served localhost repository. The mirror matches only repository ID academy-remote, so it cannot accidentally capture Central.

mkdir -p maven-environment-lab/{src/main/java/dev/academy,fixture-src/dev/academy/fixture,fixture-classes,lab-repository,lab,evidence}
cd maven-environment-lab
export REPO="$PWD/.lab-m2/repository"
mkdir -p "$REPO"

If you do not already have the trusted wrapper from Chapter 04, bootstrap it once with an installed Maven and then use the wrapper for all subsequent commands:

mvn org.apache.maven.plugins:maven-wrapper-plugin:3.3.4:wrapper   -Dmaven=3.9.16 -Dtype=only-script
./mvnw -v

2. Author the portable project first

The POM intentionally declares a repository ID with an unusable .invalid URL. The lab will prove that settings can redirect this ID to the localhost mirror without changing the dependency coordinate or committing environment credentials.

<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>dev.academy</groupId>
  <artifactId>environment-boundary-lab</artifactId>
  <version>1.0.0</version>

  <properties>
    <maven.compiler.release>17</maven.compiler.release>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <project.build.outputTimestamp>2026-08-23T00:00:00Z</project.build.outputTimestamp>
    <academy.channel>project-default</academy.channel>
    <academy.environment>portable</academy.environment>
  </properties>

  <repositories>
    <repository>
      <id>academy-remote</id>
      <url>https://academy.invalid/maven2</url>
      <releases><enabled>true</enabled></releases>
      <snapshots><enabled>false</enabled></snapshots>
    </repository>
  </repositories>

  <dependencies>
    <dependency>
      <groupId>dev.academy.fixture</groupId>
      <artifactId>academy-fixture</artifactId>
      <version>1.0.0</version>
    </dependency>
  </dependencies>

  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-clean-plugin</artifactId>
        <version>3.5.0</version>
      </plugin>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-resources-plugin</artifactId>
        <version>3.5.0</version>
      </plugin>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>3.15.0</version>
        <configuration><release>${maven.compiler.release}</release></configuration>
      </plugin>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-surefire-plugin</artifactId>
        <version>3.5.6</version>
      </plugin>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-jar-plugin</artifactId>
        <version>3.5.1</version>
      </plugin>
    </plugins>
  </build>

  <profiles>
    <profile>
      <id>dev</id>
      <properties>
        <academy.environment>dev</academy.environment>
        <academy.channel>pom-dev</academy.channel>
      </properties>
    </profile>
    <profile>
      <id>ci</id>
      <properties>
        <academy.environment>ci</academy.environment>
        <academy.channel>pom-ci</academy.channel>
      </properties>
    </profile>
  </profiles>
</project>
package dev.academy;

import dev.academy.fixture.Fixture;

public final class App {
    private App() {}
    public static void main(String[] args) {
        System.out.println("environment-boundary-lab -> " + Fixture.message());
    }
}

The dev and ci profiles only change properties that are not consumed by compilation or packaging. This is deliberate: the build can expose environment selection without creating environment-specific artifact bytes.

3. Seed a tiny Maven-style repository with JDK tools

A remote Maven release artifact is just repository content arranged by coordinate path. Build a tiny fixed JAR and its POM, then place both under the canonical repository layout. This is not a publishing lesson; it is a transparent resolution fixture.

package dev.academy.fixture;

public final class Fixture {
    private Fixture() {}
    public static String message() {
        return "fixture-1.0.0";
    }
}
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>dev.academy.fixture</groupId>
  <artifactId>academy-fixture</artifactId>
  <version>1.0.0</version>
</project>
javac --release 17 -d fixture-classes fixture-src/dev/academy/fixture/Fixture.java
mkdir -p lab-repository/dev/academy/fixture/academy-fixture/1.0.0
jar --create   --file lab-repository/dev/academy/fixture/academy-fixture/1.0.0/academy-fixture-1.0.0.jar   -C fixture-classes .
cp fixture-pom.xml   lab-repository/dev/academy/fixture/academy-fixture/1.0.0/academy-fixture-1.0.0.pom

for f in lab-repository/dev/academy/fixture/academy-fixture/1.0.0/*.{jar,pom}; do
  sha1sum "$f" | awk '{print $1}' > "$f.sha1"
done
find lab-repository -type f -maxdepth 8 -print

The immutable dependency identity is now dev.academy.fixture:academy-fixture:1.0.0. Its bytes are visible before Maven downloads anything.

4. Serve the fixture with local Basic authentication

The JDK includes the jdk.httpserver module. The fixture server binds only to loopback, prevents path traversal, requires Basic authentication, and never logs the Authorization header. It is not a production repository manager; it exists only to make server-ID behavior observable.

import com.sun.net.httpserver.Headers;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpServer;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Base64;

public final class AuthRepoServer {
    private AuthRepoServer() {}

    public static void main(String[] args) throws Exception {
        if (args.length != 1) throw new IllegalArgumentException("repository root required");
        String user = System.getenv("ACADEMY_REPO_USER");
        String pass = System.getenv("ACADEMY_REPO_PASSWORD");
        if (user == null || pass == null) throw new IllegalStateException("lab credentials not set");
        Path root = Paths.get(args[0]).toAbsolutePath().normalize();
        String expected = "Basic " + Base64.getEncoder().encodeToString((user + ":" + pass).getBytes(StandardCharsets.UTF_8));

        HttpServer server = HttpServer.create(new InetSocketAddress("127.0.0.1", 18080), 0);
        server.createContext("/", exchange -> serve(exchange, root, expected));
        server.start();
        System.out.println("Academy fixture repository: http://127.0.0.1:18080/");
    }

    private static void serve(HttpExchange exchange, Path root, String expected) throws IOException {
        String auth = exchange.getRequestHeaders().getFirst("Authorization");
        if (!expected.equals(auth)) {
            exchange.getResponseHeaders().set("WWW-Authenticate", "Basic realm=academy-lab");
            exchange.sendResponseHeaders(401, -1);
            exchange.close();
            return;
        }
        String requestPath = exchange.getRequestURI().getPath();
        Path file = root.resolve(requestPath.substring(1)).normalize();
        if (!file.startsWith(root) || !Files.isRegularFile(file)) {
            exchange.sendResponseHeaders(404, -1);
            exchange.close();
            return;
        }
        Headers headers = exchange.getResponseHeaders();
        headers.set("Content-Type", "application/octet-stream");
        long size = Files.size(file);
        exchange.sendResponseHeaders(200, size);
        if (!"HEAD".equalsIgnoreCase(exchange.getRequestMethod())) {
            Files.copy(file, exchange.getResponseBody());
        }
        exchange.close();
    }
}
javac --add-modules jdk.httpserver AuthRepoServer.java
export ACADEMY_REPO_USER=academy-lab
read -r -s -p "Disposable lab password: " ACADEMY_REPO_PASSWORD; echo
export ACADEMY_REPO_PASSWORD
java --add-modules jdk.httpserver AuthRepoServer lab-repository
Safety: choose a disposable value that is not used anywhere else. Do not reuse a repository, GitHub, cloud, database, or corporate password for this localhost-only exercise.

5. Create alternate settings with mirror and credential indirection

The repository contains no credential value. Settings references process environment variables. The mirrorOf pattern is an exact ID, so only academy-remote is redirected.

<settings xmlns="http://maven.apache.org/SETTINGS/1.2.0"
          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
          xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.2.0 https://maven.apache.org/xsd/settings-1.2.0.xsd">
  <mirrors>
    <mirror>
      <id>academy-mirror</id>
      <name>Disposable localhost Maven mirror</name>
      <url>http://127.0.0.1:18080/</url>
      <mirrorOf>academy-remote</mirrorOf>
    </mirror>
  </mirrors>
  <servers>
    <server>
      <id>academy-mirror</id>
      <username>${env.ACADEMY_REPO_USER}</username>
      <password>${env.ACADEMY_REPO_PASSWORD}</password>
    </server>
  </servers>
  <profiles>
    <profile>
      <id>lab-settings</id>
      <properties>
        <academy.channel>settings-profile</academy.channel>
      </properties>
    </profile>
  </profiles>
  <activeProfiles>
    <activeProfile>lab-settings</activeProfile>
  </activeProfiles>
</settings>

Notice the identity chain: POM repository ID academy-remote → mirror match → mirror ID academy-mirror → server credentials keyed to academy-mirror.

6. Inspect settings and profiles before building

Use -s lab/settings-good.xml on every Maven invocation so there is no ambiguity about which user settings are in scope.

mkdir -p evidence
./mvnw -s lab/settings-good.xml   org.apache.maven.plugins:maven-help-plugin:3.5.2:active-profiles   -Doutput=evidence/active-profiles-settings.txt

./mvnw -s lab/settings-good.xml   org.apache.maven.plugins:maven-help-plugin:3.5.2:effective-settings   -Doutput=evidence/effective-settings.xml

./mvnw -s lab/settings-good.xml   org.apache.maven.plugins:maven-help-plugin:3.5.2:effective-pom   -Dverbose -Doutput=evidence/effective-pom-settings.xml

grep -nE 'academy-mirror|academy-remote|lab-settings|academy.channel'   evidence/effective-settings.xml evidence/effective-pom-settings.xml
Expected: passwords remain hidden in effective settings. If a troubleshooting recipe tells you to enable password display merely to prove which server is selected, reject that recipe; server IDs and mirror IDs are enough.

7. Observe property layering with one expression

The POM declares project-default. The active settings profile contributes settings-profile. The explicit POM profile dev contributes pom-dev. Current Maven profile guidance documents that settings profile effects can take priority over POM profile values in the effective build; a CLI user property can override the final value. Verify rather than memorize.

HELP=org.apache.maven.plugins:maven-help-plugin:3.5.2

./mvnw -s lab/settings-good.xml "$HELP:evaluate"   -Dexpression=academy.channel -q -DforceStdout

./mvnw -s lab/settings-good.xml -Pdev "$HELP:evaluate"   -Dexpression=academy.channel -q -DforceStdout

./mvnw -s lab/settings-good.xml -Pdev "$HELP:evaluate"   -Dexpression=academy.channel -Dacademy.channel=cli -q -DforceStdout
Invocation Expected value Why
Settings active, no POM profile settings-profile Settings profile contributes the property.
Settings active + -Pdev settings-profile The active settings-profile contribution wins over the conflicting POM-profile property.
Same + -Dacademy.channel=cli cli CLI user property is the explicit invocation override.

8. Resolve/build through the mirror

Now Maven must obtain academy-fixture:1.0.0. The declared .invalid URL should never be contacted because the exact repository ID is mirrored to localhost.

set -o pipefail
./mvnw -s lab/settings-good.xml -Dmaven.repo.local="$REPO"   -Pdev clean package | tee evidence/build-cold.log

./mvnw -s lab/settings-good.xml -Dmaven.repo.local="$REPO"   org.apache.maven.plugins:maven-dependency-plugin:3.11.0:tree   -DoutputFile=evidence/dependency-tree.txt

cat evidence/dependency-tree.txt
find "$REPO/dev/academy/fixture/academy-fixture/1.0.0" -maxdepth 1 -type f -print
java -cp "target/environment-boundary-lab-1.0.0.jar:$REPO/dev/academy/fixture/academy-fixture/1.0.0/academy-fixture-1.0.0.jar" dev.academy.App

Expected application output is environment-boundary-lab -> fixture-1.0.0. The local repository now contains a cached copy plus resolver metadata. The authoritative fixture remains the localhost repository; the cache is only derived machine/job state.

9. Warm-cache versus fresh-cache causality

Run the same package command again. Dependency transfer should be reduced because the isolated local repository is warm. Then prove the remote path again with a second empty repository instead of deleting the first.

set -o pipefail
./mvnw -s lab/settings-good.xml -Dmaven.repo.local="$REPO"   -Pdev package | tee evidence/build-warm.log

export REPO_FRESH="$PWD/.lab-m2-fresh/repository"
./mvnw -s lab/settings-good.xml -Dmaven.repo.local="$REPO_FRESH"   -Pdev package | tee evidence/build-fresh.log

This distinction matters in CI: a warm cache can hide mirror/proxy/server failures. A second isolated repository gives you a clean-room resolution test without destroying evidence from the first run.

10. Challenge — choose the correct control

A teammate wants the dev build to use a different repository URL and proposes putting that URL inside the dev POM profile. Choose a better control and justify it.

Target reasoning: keep the dependency coordinate/repository identity portable in the POM; put environment routing in controlled settings/mirror policy. Activate dev only for a build-model difference that truly belongs to the project.

11. Cleanup

Stop the localhost server with Ctrl+C. Remove only the disposable maven-environment-lab tree when finished. Do not delete your normal Maven local repository or rewrite normal user/global settings. Preserve evidence/ elsewhere first if you want to compare later chapters.

Knowledge check

Why does the POM use an unusable .invalid repository URL in this lab?

What changed after the first successful cold build?

Why is a second isolated repository better than deleting the first cache for a fresh-resolution test?

Where is the real lab password stored in the project files?

Which command proves that -Dacademy.channel=cli won?

12. Bridge to design choices

You now have a concrete boundary: project model, settings model, repository routing, credentials, and cache state are separately visible. Lesson 3 asks where each kind of variation should live in a real team and what tradeoffs follow from that placement.

Official references and version notes

Version-sensitive statements in this lesson were checked against Apache Maven primary documentation on 2026-08-23. The mandatory path uses Maven 3.9.16 through Maven Wrapper 3.3.4, JDK 21 to run Maven, Java 17 as the project release target, Help Plugin 3.5.2, Dependency Plugin 3.11.0, Compiler Plugin 3.15.0, Resources Plugin 3.5.0, Surefire 3.5.6, and JAR Plugin 3.5.1. Maven 4-only profile syntax and preview behavior are not required here.

The localhost Basic-auth server is a teaching fixture built from JDK APIs; it is not an Apache Maven component and must not be treated as a production repository manager.

Keep the academy open

Support free, practical DevOps education.

Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.

Ethereum / ERC-20
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0 Send only Ethereum/ERC-20 compatible assets to this address.