The smallmind-artifact module is a thin, opinionated Java facade over the
Eclipse Aether / Maven Resolver
stack. It gives a running JVM the ability to:
-
read the user’s
~/.m2/settings.xmland construct a fully configured Aether repository system from it — mirrors, proxies, server credentials, profile-activated repositories, and local-cache path included; -
resolve Maven artifacts by coordinate (groupId / artifactId / version / classifier / extension), downloading them to the local repository if they are not already present;
-
resolve the full transitive compile-scope dependency closure of an artifact, producing a flat array of local files suitable for classpath construction;
-
monitor a set of coordinates on a polling cycle, detect when any of them has been re-published (release upgrade or snapshot redeploy), and hand the caller a
ClassLoaderover the new artifact set so code can be reloaded without restarting the JVM.
The module is small by design: two data-transfer objects, one repository facade, one scanner, one event, and one listener interface. It exists as the narrow slice of Aether that most applications actually need, packaged so that a caller never has to write their own settings parser, session builder, selector chain, or dependency visitor.
Maven Resolver (Aether) is the programmatic artifact-resolution engine that
Maven itself uses, and it is the canonical way to fetch Maven artifacts from
within a JVM. It is also, out of the box, hostile to casual integration.
Building a working RepositorySystemSession from scratch requires assembling
a RepositorySystemSupplier, parsing settings.xml with
DefaultSettingsBuilderFactory, translating every settings-level entity
(Mirror, Proxy, Server, Profile, Repository) into its Aether
equivalent, wiring four different selectors (ProxySelector,
MirrorSelector, AuthenticationSelector, and a LocalRepositoryManager),
and finally stitching a sensible RepositoryPolicy onto each remote
repository. The public API surface for this wiring is large, and the
"correct" arrangement of its parts is mostly tribal knowledge.
smallmind-artifact does that wiring once, the way a normal Maven command
line would do it, and exposes a small facade — MavenRepository — whose
two methods cover the vast majority of what non-build-tool code ever wants
from Aether: fetch one artifact, or fetch one artifact and its transitive
dependencies.
On top of that, the module provides MavenScanner, which turns the same
facade into a hot-reload primitive: a polling component that resolves a
set of monitored coordinates on a configurable interval and delivers a
ClassLoader whenever the underlying artifacts change. This is the
mechanism a long-running service uses to swap in a new plugin jar,
a new rules module, or a new integration adapter, without a JVM restart
and without the service needing to know anything about Maven internals.
-
Reads
settings.xml— global or per-directory, falling back to~/.m2/settings.xml. Active profiles, mirrors, proxies, server credentials (password or SSH private key), and the<localRepository>element are all honored. -
Builds a reusable
RepositorySystembacked by the officialRepositorySystemSupplierfrom Maven Resolver, with the HTTP and file transports and the basic connector already on the classpath. -
Produces fresh
DefaultRepositorySystemSessioninstances on demand. Each session gets a newDefaultRepositoryCache, the configured offline flag, a merged view of all active-profileproperties, and the fully wired selector chain. -
Resolves single artifacts to local files via
MavenRepository.acquireArtifact. -
Resolves full dependency closures via
MavenRepository.resolve, walking the dependency graph depth-first, skipping optional dependencies, deduplicating by artifact identity, and downloading any node whose local file is missing. -
Polls for artifact changes via
MavenScanner, distinguishing release changes (coordinate identity) from snapshot redeploys (coordinate identity plus file modification time). -
Delivers a
GatingClassLoader— the nutsnbolts class-loader abstraction — over every changed artifact’s transitive compile dependency set, so listeners canClass.forName(…)against the new code without restarting.
-
It does not build projects. There is no compile step, no
maven-invokerintegration, no plugin execution. The module resolves pre-existing artifacts; it does not produce them. -
It does not resolve runtime or test scope by itself.
MavenRepository.resolverequests thecompilescope throughCollectRequest.setRoot(new Dependency(artifact, "compile")); callers who need other scopes or richer filters should invoke Aether directly using the session produced bygenerateSession(). -
It does not manage passwords, tokens, or secrets outside of
settings.xml. Whatever credentials exist in the user’s Maven configuration are what the facade uses; there is no separate credential store. -
It does not cache parsed settings across instances. Each
MavenRepositoryconstructor reads and resolvessettings.xmlonce; share the instance if you want that parse reused. -
It does not, in
MavenScanner, enforce any particular classloading discipline on listeners. What a listener does with the suppliedClassLoader— install it as the thread context loader, instantiate a specific class through it, register its contents with a plugin framework — is the listener’s decision.
| Goal | Entry point |
|---|---|
Resolve one artifact jar and get its local file |
|
Resolve an artifact plus all compile-scope dependencies |
|
Re-fetch the same artifacts from inside a long-lived process, noticing snapshot redeploys |
|
Build a classpath at runtime from a list of Maven coordinates |
|
Run against a pre-populated local repository, no network |
Construct |
Use a Maven settings file from a location other than |
Three-arg |
| Class | Role |
|---|---|
|
Mutable DTO describing a Maven artifact by |
|
The facade. Constructed once per |
|
Change-detection token. Pairs a resolved Aether |
|
Polling component. Monitors an array of |
|
Observer interface. Single method |
|
|
A handful of terms recur through the API and the Aether stack underneath, and naming them explicitly makes the examples much easier to read.
- Coordinate
-
A five-field address for a Maven artifact:
groupId,artifactId,version,classifier,extension.MavenCoordinateis the module’s representation;org.eclipse.aether.artifact.Artifactis Aether’s. The facade translates from the former to the latter on demand. - Session
-
An Aether
DefaultRepositorySystemSessioncarrying the selectors, the local-repository manager, the cache, and per-request configuration properties. A session is cheap to build; it is not thread-safe.MavenRepository.generateSession()produces a new one per call. - Remote repository
-
An Aether
RemoteRepository— the destination for HTTP or file-transport requests. The facade derives one per active-profile<repository>and<pluginRepository>insettings.xml, applies the mirror selector, and attaches authentication. Every remote repository is forced toUPDATE_POLICY_ALWAYS/CHECKSUM_POLICY_WARNso that snapshot redeployments are actually fetched. - Artifact tag
-
The
MavenScannerchange-detection unit. A tag is (resolved artifact, file mtime). Two tags compare equal if they wrap the same coordinate and, when the coordinate is a snapshot, the file mtimes match. This is the distinction that lets the scanner tell1.2.3-SNAPSHOTfrom1.2.3-SNAPSHOTafter a redeploy. - Delta map
-
The payload of a
MavenScannerEvent. Keys are the newly resolved artifacts; values are the artifacts they replaced (ornullon first observation). Unchanged coordinates do not appear in the map. - Gating class loader
-
The
GatingClassLoaderfromsmallmind-nutsnbolts. The scanner assembles one per notification, with aClasspathClassGatefor each changed artifact and each of its transitive compile dependencies. The loader’s parent is the thread context class loader at the time the worker runs, so existing framework classes remain resolvable; only the updated artifacts are reloaded.
The module is published as a single artifact. The Aether dependencies
ship with compile scope, because applications using the module almost
always want them on the runtime classpath. The three maven-* artifacts
are declared provided, because they are usually contributed by whatever
Maven-aware runtime is hosting the code; if your environment does not
already supply them, add them explicitly.
<dependency>
<groupId>org.smallmind</groupId>
<artifactId>artifact-maven</artifactId>
<version>${project.version}</version>
</dependency>Additional direct dependencies in a standalone deployment:
<dependency>
<groupId>org.apache.maven</groupId>
<artifactId>maven-model-builder</artifactId>
</dependency>
<dependency>
<groupId>org.apache.maven</groupId>
<artifactId>maven-resolver-provider</artifactId>
</dependency>
<dependency>
<groupId>org.apache.maven</groupId>
<artifactId>maven-settings</artifactId>
</dependency>
<dependency>
<groupId>org.apache.maven</groupId>
<artifactId>maven-settings-builder</artifactId>
</dependency>The module also pulls in smallmind-nutsnbolts (for Stint, ClassGate,
ClasspathClassGate, GatingClassLoader, and ComponentStatus) and
scribe-pen (for logging from the scanner worker). Both are standard
transitive dependencies.
|
Note
|
The module requires a valid |
The most basic operation: fetch an artifact’s jar into the local Maven
repository and get back a java.io.File pointing at it.
import org.eclipse.aether.DefaultRepositorySystemSession;
import org.eclipse.aether.artifact.Artifact;
import org.smallmind.artifact.maven.MavenCoordinate;
import org.smallmind.artifact.maven.MavenRepository;
MavenRepository repository = new MavenRepository("my-service", false); // (1)
DefaultRepositorySystemSession session = repository.generateSession(); // (2)
MavenCoordinate coordinate = new MavenCoordinate(
"com.example", "plugin-foo", "1.0.0"); // (3)
Artifact artifact = repository.acquireArtifact(session, coordinate); // (4)
java.io.File jar = artifact.getFile(); // (5)-
Reads
~/.m2/settings.xml."my-service"is theUser-Agentidentifier sent to remote repositories.offline=falsepermits network access. -
Produce a fresh session. A session is single-use in the sense that it is not thread-safe — do not share one between threads — but reusing one across several serial calls on a single thread is fine.
-
Default extension is
"jar"and classifier isnull, matching the 99% case. -
Returns the resolved Aether artifact with
getFile()populated. If the coordinate is not present in any configured remote repository (or the local cache when offline), this throwsArtifactResolutionException. -
artifact.getFile()is the local cache location, typically under~/.m2/repository/com/example/plugin-foo/1.0.0/plugin-foo-1.0.0.jar.
Use this when you need a classpath, not just one jar — e.g. to construct a child class loader for dynamically loaded code.
import org.eclipse.aether.DefaultRepositorySystemSession;
import org.eclipse.aether.artifact.Artifact;
import org.eclipse.aether.artifact.DefaultArtifact;
import org.smallmind.artifact.maven.MavenRepository;
MavenRepository repository = new MavenRepository("my-service", false);
DefaultRepositorySystemSession session = repository.generateSession();
Artifact root = new DefaultArtifact("com.example:plugin-foo:1.0.0"); // (1)
Artifact[] closure = repository.resolve(session, root); // (2)
for (Artifact resolved : closure) {
System.out.println(resolved.getFile().getAbsolutePath()); // (3)
}-
DefaultArtifactunderstands Aether’sgroup:artifact:versionshorthand. You may also pass aMavenCoordinatethroughacquireArtifactfirst if you prefer the DTO form; the root just needs to be an AetherArtifact. -
The returned array is the deduplicated dependency closure walked depth-first. Order is not guaranteed. Optional dependencies are excluded.
-
Every artifact in the closure has a populated
getFile().
|
Important
|
|
Feeding the closure into a child class loader is the standard plugin-loader
pattern. The module does not ship a helper for this step because the
shape of the child loader varies by application — most callers want either
a plain URLClassLoader or a GatingClassLoader from
smallmind-nutsnbolts.
import java.net.URL;
import java.net.URLClassLoader;
import org.eclipse.aether.artifact.Artifact;
URL[] urls = new URL[closure.length];
for (int i = 0; i < closure.length; i++) {
urls[i] = closure[i].getFile().toURI().toURL();
}
ClassLoader pluginLoader = new URLClassLoader(
urls, Thread.currentThread().getContextClassLoader());
Class<?> pluginClass = pluginLoader.loadClass("com.example.plugin.Entry");
Object plugin = pluginClass.getDeclaredConstructor().newInstance();If you need the gating semantics (per-class filtering, per-jar tickets)
that the scanner uses internally, use ClasspathClassGate and
GatingClassLoader from smallmind-nutsnbolts directly.
This is the primary reason to reach past MavenRepository and use
MavenScanner. Given one or more coordinates, the scanner polls the
repositories at the configured interval and hands you an updated class
loader whenever a snapshot has been redeployed or a release version has
changed.
import org.smallmind.artifact.maven.MavenCoordinate;
import org.smallmind.artifact.maven.MavenScanner;
import org.smallmind.artifact.maven.MavenScannerEvent;
import org.smallmind.artifact.maven.MavenScannerListener;
import org.smallmind.nutsnbolts.time.Stint;
import java.util.concurrent.TimeUnit;
MavenCoordinate plugin = new MavenCoordinate(
"com.example", "rules-module", "2.0.0-SNAPSHOT");
MavenScanner scanner = new MavenScanner(
"rules-host", // (1)
false, // (2)
new Stint(30, TimeUnit.SECONDS), // (3)
plugin); // (4)
scanner.addMavenScannerListener(new MavenScannerListener() {
@Override
public void artifactChange (MavenScannerEvent event) {
ClassLoader loader = event.getClassLoader(); // (5)
try {
Class<?> entry = loader.loadClass("com.example.rules.Entry");
Object rules = entry.getDeclaredConstructor().newInstance();
RulesHostRegistry.install(rules); // (6)
} catch (ReflectiveOperationException reflectiveException) {
// Handle or log.
}
}
});
scanner.start(); // (7)
// ... application runs ...
scanner.stop(); // (8)-
User-Agentidentifier for outbound HTTP requests. -
offline=falsepermits network access;truerestricts the scanner to whatever is already in the local~/.m2/repository. -
Cycle interval. Any
java.util.concurrent.TimeUnit-basedStintworks. -
One or more coordinates to monitor. Each is tracked independently.
-
The loader is already wired with the changed artifacts and their compile dependencies; its parent is the thread context loader that was active when the worker invoked the scan.
-
What to do with the loader is application-specific; the module takes no opinion beyond supplying it.
-
Performs an initial synchronous scan — which will always fire the listener because every tag starts as
null, so every coordinate registers as "changed" the first time — and then launches a daemon worker thread. -
Signals the worker to stop and blocks until it exits.
A CI runner or a reproducible-build environment may want to guarantee that no network traffic happens — all artifacts must be pre-populated in the local repository.
MavenRepository offline = new MavenRepository("ci-runner", true); // (1)
DefaultRepositorySystemSession session = offline.generateSession();
Artifact artifact = offline.acquireArtifact(session, coordinate); // (2)-
offline=truepropagates to every session produced bygenerateSession()viasession.setOffline(true). -
If the artifact is missing from the local cache this throws
ArtifactResolutionExceptionwithout attempting a remote fetch.
Packaged environments often carry their own settings.xml — a container
image that pre-configures mirrors for an internal proxy, for example.
MavenRepository repository = new MavenRepository(
"/opt/app/conf", // directory containing settings.xml (1)
"my-service",
false);-
The path is the containing directory, not the file itself. The constructor appends
/settings.xml. Passingnullor an empty string reverts to~/.m2.
A mutable DTO. The no-argument constructor leaves every field unset; setters must be called before the coordinate is usable. The convenience constructors progressively fill more fields:
| Constructor | Use when |
|---|---|
|
Building up a coordinate from external data (e.g. parsed config). Every
setter must be called except |
|
Standard jar artifacts. Extension defaults to |
|
Sources jars, tests jars, javadoc jars — anywhere the classifier is the only deviation from the jar default. |
|
Non-jar packaging: |
equals and hashCode include all five fields, with null classifier
and null extension treated as equal to each other but distinct from any
non-null value.
Two constructors, parallel except for the settings directory:
// Reads ~/.m2/settings.xml
MavenRepository (String repositoryId, boolean offline)
// Reads <settingsDirectory>/settings.xml; null/empty falls back to ~/.m2
MavenRepository (String settingsDirectory, String repositoryId, boolean offline)Three operations:
generateSession()-
Returns a fresh
DefaultRepositorySystemSession. Each session gets its ownDefaultRepositoryCache, merged user properties from every active profile, the shared selector chain, and aLocalRepositoryManageranchored at the settings<localRepository>(falling back to~/.m2/repository). acquireArtifact(session, coordinate)andacquireArtifact(session, artifact)-
Resolves a single artifact. The coordinate-accepting overload constructs a
DefaultArtifactand delegates to the artifact-accepting overload. ThrowsArtifactResolutionExceptionon any resolution failure. resolve(session, artifact)-
Resolves the root artifact and its full compile-scope dependency closure. Returns a deduplicated
Artifact[]. See the Behavioral Reference section for exact graph-walk semantics.
Two constructors, parallel except for the settings directory:
MavenScanner (String repositoryId, boolean offline,
Stint cycleStint, MavenCoordinate... mavenCoordinates)
MavenScanner (String settingsDirectory, String repositoryId, boolean offline,
Stint cycleStint, MavenCoordinate... mavenCoordinates)cycleStint is required and must be non-null; mavenCoordinates is
required and must be non-null (though an empty array is technically
allowed, a scanner with nothing to monitor is a degenerate case).
Lifecycle:
addMavenScannerListener(listener)/removeMavenScannerListener(listener)-
Register and deregister. May be called before or after
start(). Listeners registered beforestart()receive the initial "everything is new" notification. start()-
Idempotent. If the scanner is already running, returns immediately. Otherwise performs one synchronous scan on the calling thread — any resolution failures propagate here — and launches a daemon worker for subsequent cycles.
stop()-
Idempotent. Signals the worker to finish the current cycle and exit, then blocks on the exit latch. Callers that want non-blocking shutdown should invoke
stop()from a thread that can afford to wait.
Three getters:
getArtifactDeltaMap()-
Only the artifacts that changed this cycle. Keys are the new resolved artifacts, values are the prior artifacts they replaced (or
nullon first observation). getArtifacts()-
Every monitored coordinate, in registration order. Entries are
nullfor coordinates that have not been successfully resolved yet. getClassLoader()-
A
GatingClassLoaderover the changed artifacts plus their transitive compile dependencies. Parent is the thread context class loader active when the scan ran.
Single method artifactChange(MavenScannerEvent). Called on the scanner’s
worker thread. Implementations must be thread-safe or must hand the event
off to a dedicated executor.
Sessions are intentionally cheap and short-lived. generateSession() does
no I/O of its own — it wires pre-computed selectors, a new cache, and a
fresh local-repo manager onto a DefaultRepositorySystemSession. A
session is valid for as long as the MavenRepository instance that
produced it is alive; sharing selectors, settings, and
RepositorySystem across sessions is what makes the facade inexpensive
to reuse.
A session is not thread-safe. Calling acquireArtifact or resolve
with the same session from multiple threads is undefined. The
recommended pattern is one session per resolution operation (or per
sequential operation chain) on a single thread.
MavenRepository.resolve walks the root’s dependency graph with a
custom DependencyVisitor:
-
Nodes whose
Dependency.isOptional()returnstrueare skipped entirely; their children are also skipped becausevisitEnterreturns the membership-check result, not the optional flag, so a reference that is already invisitedSetwill also causevisitEnterto returnfalseand cut off that branch. -
A
HashSet<DependencyNode>keyed by object identity prevents the same node from being processed twice in diamond graphs. -
A
HashSet<Artifact>deduplicates by artifact identity across branches — the same coordinate reached through two different paths appears once in the returned array. -
For each kept node, if the dependency’s artifact has no
Fileyet,acquireArtifactis called to download it. AnArtifactResolutionExceptionduring this download is wrapped in aRuntimeExceptionbecause the visitor’s contract does not allow checked exceptions. -
After the depth-first walk, Aether’s
repositorySystem.resolveDependenciesis invoked on the root node to perform a final top-down resolution pass. This is how Aether normally verifies the graph, and it may raiseDependencyResolutionExceptionif any node that survived the walk cannot be resolved.
The practical implication: the returned array is what Aether actually made available as local files for the compile scope, minus optional branches, deduplicated.
Each cycle resolves every monitored coordinate fresh (no per-coordinate
caching between cycles) and wraps the result in an ArtifactTag. The
tag is compared to the stored one for that coordinate index:
-
On first scan (tag is
null) every coordinate registers as "changed." The initialstart()call will always fire the listener for every coordinate, withnullvalues in the delta map. -
For release artifacts (
Artifact.isSnapshot() == false) the comparison is artifact identity only. A release redeployed with the same version will not be detected, because Maven releases are conceptually immutable. If you really mean to redeploy a release and want it observed, bump the version. -
For snapshot artifacts both the artifact identity and the local file’s
lastModified()timestamp must match. A snapshot redeploy, which refreshes the local file’s mtime (becauseUPDATE_POLICY_ALWAYSforces a download), will register as changed.
When at least one coordinate has changed, all changed coordinates'
transitive dependencies are resolved and combined into a single
GatingClassLoader. Unchanged coordinates are not re-walked that cycle.
The initial scan runs on the calling thread and propagates any resolution exception:
-
ArtifactResolutionException -
DependencyCollectionException -
DependencyResolutionException
Subsequent scans run on the daemon worker. Any exception thrown inside
updateArtifact() is caught, logged at ERROR level through
LoggerManager.getLogger(MavenScanner.class), and swallowed. The cycle
loop continues unless the worker is interrupted or stop() is called.
This is the deliberate choice that a transient network failure should
not kill the scanner.
A failed scan leaves the stored ArtifactTag array unchanged, so the
next successful scan will still detect the change that the failed scan
missed.
MavenRepository-
Construction is single-threaded. After construction,
generateSession()and the two resolution methods are safe to call from multiple threads provided each thread uses its own session. The sharedRepositorySystem,Settings, selectors, andremoteRepositoryListare effectively immutable after the constructor returns. MavenScanner-
All four public lifecycle methods (
start,stop,addMavenScannerListener,removeMavenScannerListener) aresynchronized.updateArtifact()is alsosynchronized, so a caller that invokesstop()while a scan is in progress will wait for that scan to finish before the status flag flips. Listeners are invoked on the worker thread, not on the thread that calledaddMavenScannerListener. MavenScannerListenerimplementations-
Must tolerate being called from a daemon worker thread. If the listener needs to run on a specific thread (an application’s event loop, a Swing EDT, a reactor thread), it must marshal the event itself.
The constructor builds an effective Settings object by running
DefaultSettingsBuilderFactory().newInstance().build(request) over a
DefaultSettingsBuildingRequest whose global settings file is the file
at <settingsDirectory>/settings.xml. This is Maven’s own settings
builder, so profile activation, interpolation, and global/user merging
all behave the way mvn would.
From the effective settings the facade extracts:
-
All
profiles. Profiles are considered active if their id appears in<activeProfiles>or if their<activation>block sets<activeByDefault>true</activeByDefault>. -
Every active profile’s
<repositories>and<pluginRepositories>, each translated into aRemoteRepositorywith both release and snapshot policies forced toUPDATE_POLICY_ALWAYS/CHECKSUM_POLICY_WARN. -
All
mirrors, installed on aDefaultMirrorSelector. -
All
proxies, installed on aDefaultProxySelectorwith optional basic auth and non-proxy-hosts patterns. -
All
servers, installed on aDefaultAuthenticationSelectorwrapped inConservativeAuthenticationSelector. Each server contributes a username/password pair and/or an SSH private-key path with passphrase. -
The
<localRepository>path, or~/.m2/repositoryif unset.
Profile <properties> blocks are merged into each session’s user
properties every time generateSession() is called — see the
Defaults Reference for the subtle
last-profile-wins behavior this implies.
| Setting | Default |
|---|---|
|
|
|
|
Settings directory when constructor arg is |
|
Local repository when |
|
|
|
|
|
|
|
|
Skipped |
|
|
|
Note
|
The session-user-properties population loop in |
- No plugin execution
-
This module resolves artifacts; it does not run Maven plugins. A coordinate that only exists as a Maven plugin binding (not as a consumable artifact) is still fetchable as a jar, but nothing in
smallmind-artifactwill invoke its mojos. - Compile scope only
-
MavenRepository.resolvehard-codes the root dependency scope to"compile". To pullruntimeortestscope, or to apply aDependencyFilter, use therepositorySystemand session produced by the facade directly — the facade does not expose the root scope as a parameter. - Release redeploys not detected
-
MavenScannerintentionally compares release artifacts by identity only. If your workflow republishes a release version in place, the scanner will not notice. This matches Maven’s semantic model of releases as immutable; it is a design choice, not an oversight. - Last-profile-wins user properties
-
generateSessionoverwrites user properties per active profile rather than merging them. If two active profiles define the same property with different values, the value from the profile encountered last in the iteration order wins. - Exception boxing during transitive resolution
-
An
ArtifactResolutionExceptionthrown while downloading a transitive dependency insideresolveis wrapped in an uncheckedRuntimeExceptionbecause the AetherDependencyVisitorinterface does not permit checked exceptions. Callers that want to react specifically to resolution failures during traversal must unwrap the cause. - Scanner listener errors are not handled
-
The scanner does not catch exceptions thrown by listeners. A listener that throws will propagate through
updateArtifact()into the worker loop’s catch-all, which logs and continues — but the remaining listeners registered for that event will not be invoked. - Single cycle interval
-
Every coordinate shares the same cycle
Stint. There is no way to poll one coordinate more frequently than another within the same scanner. If you need heterogeneous intervals, run multiple scanners.
The three maven-* artifacts (maven-model-builder,
maven-resolver-provider, maven-settings, maven-settings-builder)
are declared provided in the module POM. When embedding the module in
a standalone application, you must put them on the runtime classpath
yourself. In most container or plugin hosts they are already present.
Aether’s HTTP transport depends on Apache HttpClient. The
maven-resolver-transport-http dependency pulls it in transitively. If
your application already ships a different HttpClient major version,
verify that the versions are compatible.
Credentials are taken exclusively from the <servers> block of
settings.xml. There is no mechanism for programmatic credential
injection. To override credentials for a specific deployment, ship a
dedicated settings.xml and point the second constructor at it.
Each MavenScannerEvent carries a fresh GatingClassLoader. Previous
class loaders handed to earlier listeners will be garbage-collected only
once every reference to them — including any instances they loaded, any
classes still reachable from the JVM’s class metadata, and any
resources they opened — is released. A listener that installs the loader
globally without releasing the previous one will retain every generation
of classes ever loaded, which is a classpath leak. The standard
remediation is to discard the previous loader explicitly as part of the
listener’s install step.
The scanner’s worker swallows resolution exceptions and keeps polling.
This is usually the right choice for a long-running service — a five-minute
artifact-repository outage should not take the plugin host down — but it
means a misconfigured repository will fail silently after the initial
scan succeeds. Watch the ERROR-level log output of
org.smallmind.artifact.maven.MavenScanner in production.
offline=true sets RepositorySystemSession.setOffline(true). Aether
interprets this as "do not attempt any remote operation" — missing
artifacts raise ArtifactResolutionException without trying the
network. The User-Agent string is still constructed (it is assigned
to configProps regardless) but will never be sent anywhere.
Use this list as a pre-flight before shipping a service that embeds the module:
-
❏ Confirm
settings.xmlexists at the configured location and parses (construct aMavenRepositoryin a startup health check). -
❏ Confirm every expected remote repository appears in
repository.generateSession()output (inspect the `LocalRepositoryManager’s parent session, or enable debug logging on Aether). -
❏ For scanners, register listeners before calling
start()so that the initial "everything is new" notification is delivered. -
❏ For scanners, ensure
stop()is called from a shutdown hook or container lifecycle callback; the worker is a daemon thread and will not keep the JVM alive, but in-flight downloads may be abandoned mid-transfer. -
❏ For scanners, verify listener code releases references to the previous
ClassLoaderbefore installing the new one. -
❏ For offline deployments, pre-populate
~/.m2/repository(or the configured local repo) with every expected artifact and its transitive dependencies.
-
Maven Resolver (Aether) — the underlying resolution engine.
-
Maven settings reference — the element names consumed by
MavenRepository. -
org.smallmind.nutsnbolts.lang.GatingClassLoaderandorg.smallmind.nutsnbolts.lang.ClasspathClassGate— the class-loading primitives the scanner hands to listeners. -
org.smallmind.nutsnbolts.time.Stint— the duration abstraction used for the scanner cycle interval.