This document describes the runtime dependency system used by NextForge Core: how to declare, download, and inject external JARs at plugin startup without requiring them on the compile-time classpath.
Components:
DependencyArtifact,DependencyRepository,DependencyResolver,DependencyLoader, andForgedPluginlifecycle integration.
- Declare artifacts (GAV:
groupId:artifactId:version) and repositories. - On plugin enable, the resolver downloads missing JARs into your plugin data folder.
- The loader adds those JARs to a dedicated
URLClassLoaderso you can reflectively load classes.
- Declarative dependency list via
DependencyArtifactandDependencyRepository. - Downloads JARs at runtime and loads them into the classpath.
- Maven-style repository layout supported.
- Extensible for authentication or alternative storage backends.
+-----------------+ declares +----------------------+
| ForgedPlugin |----------------------->| DependencyResolver |
| (enable()) | | (download JARs) |
+--------+--------+ +--------+------------+
| |
| downloaded JAR paths |
v v
+-----------------+ inject URLs +----------------------+
| DependencyLoader|<----------------------- | dependencies/ |
| (URLClassLoader)| | (plugin data dir) |
+-----------------+ +----------------------+
|
| loadClass("com.example.Foo")
v
application/runtime usage
public record DependencyArtifact(String groupId, String artifactId, String version) {
@Override public String toString() { return groupId + ":" + artifactId + ":" + version; }
}public record DependencyRepository(String url, String username, String password) {
public DependencyRepository(String url) { this(url, null, null); }
}url: base repository URL (e.g.,https://repo1.maven.org/maven2).username/password: optional for private repos (currently not injected into HTTP calls in the snippet; see Auth below).
Downloads declared artifacts into a target directory using Maven-like paths.
Key behavior (excerpt):
String artifactPath = artifact.groupId() + "/" + artifact.artifactId() + "/" + artifact.version() +
"/" + artifact.artifactId() + "-" + artifact.version() + ".jar";
URL url = new URL(repo.url() + "/" + artifactPath);Note: Maven Central layout requires groupId with slashes (dots replaced by
/). See Path Mapping.
public class DependencyLoader {
private final URLClassLoader classLoader;
public DependencyLoader(List<Path> jarFiles) {
URL[] urls = jarFiles.stream().map(p -> p.toUri().toURL()).toArray(URL[]::new);
classLoader = new URLClassLoader(urls, getClass().getClassLoader());
}
public Class<?> loadClass(String className) throws ClassNotFoundException { return classLoader.loadClass(className); }
}public abstract class ForgedPlugin extends LicensedPlugin {
@Override public void enablePlugin() {
if (!this.getClass().isAnnotationPresent(NextForgePlugin.class))
throw new IllegalStateException("Plugin must be annotated with @NextForgePlugin");
Path libDir = this.getDataFolder().toPath().resolve("dependencies");
DependencyResolver resolver = resolveDependencies(DependencyResolver.create());
resolver.downloadDependencies(libDir);
List<Path> jars = Files.list(libDir).toList();
DependencyLoader loader = new DependencyLoader(jars); // keep reference if you need to load classes
enable();
}
public abstract DependencyResolver resolveDependencies(DependencyResolver dr);
}@NextForgePlugin
public final class MyPlugin extends ForgedPlugin {
@Override public DependencyResolver resolveDependencies(DependencyResolver dr) {
return dr
.addRepository(new DependencyRepository("https://repo1.maven.org/maven2"))
// Private repo (auth optional; see Security & Auth)
.addRepository(new DependencyRepository("https://repo.mycompany.com/releases", "user", "token"))
// Artifacts
.addArtifact(new DependencyArtifact("org.slf4j", "slf4j-api", "2.0.13"))
.addArtifact(new DependencyArtifact("com.fasterxml.jackson.core", "jackson-databind", "2.17.1"));
}
@Override public UUID pluginId() { return UUID.fromString("00000000-0000-0000-0000-000000000001"); }
@Override public void enable() { /* use classes after download */ }
@Override public void disable() { /* cleanup */ }
}Path libDir = getDataFolder().toPath().resolve("dependencies");
List<Path> jars = Files.list(libDir).toList();
DependencyLoader loader = new DependencyLoader(jars);
Class<?> mapper = loader.loadClass("com.fasterxml.jackson.databind.ObjectMapper");
Object instance = mapper.getDeclaredConstructor().newInstance();Maven repository layout expects slashes in the group path:
groupId: com.fasterxml.jackson.core
→ path: com/fasterxml/jackson/core/jackson-databind/2.17.1/jackson-databind-2.17.1.jar
Your current resolver builds:
artifact.groupId() + "/" + artifact.artifactId() + "/" + artifact.version() + "/..."If groupId contains dots, convert them:
String groupPath = artifact.groupId().replace('.', '/');
String artifactPath = groupPath + "/" + artifact.artifactId() + "/" + artifact.version() + "/" +
artifact.artifactId() + "-" + artifact.version() + ".jar";Recommendation: Update
DependencyResolveraccordingly to ensure compatibility with public Maven repos.
The DependencyRepository type supports username/password, but the HTTP request in DependencyResolver does not yet use them. To add basic auth:
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
if (repo.username() != null && repo.password() != null) {
String basic = Base64.getEncoder().encodeToString((repo.username() + ":" + repo.password()).getBytes(StandardCharsets.UTF_8));
conn.setRequestProperty("Authorization", "Basic " + basic);
}Suggested hardening:
- Set reasonable timeouts:
conn.setConnectTimeout(5000); conn.setReadTimeout(15000);
- Support proxies via JVM properties (
http.proxyHost,http.proxyPort, etc.). - Validate checksums (see Integrity below).
Speed up large dependency sets:
ExecutorService pool = Executors.newFixedThreadPool(Math.min(8, Runtime.getRuntime().availableProcessors()));
List<Future<Path>> tasks = artifacts.stream().map(a -> pool.submit(() -> downloadOne(a, repo, libDir))).toList();
for (Future<Path> f : tasks) f.get();
pool.shutdown();For Maven-compatible repos, you can fetch <jar>.sha1 or <jar>.md5 and verify after download.
Skip downloads if the target JAR exists and passes checksum verification.
Use exponential backoff on transient HTTP errors (e.g., 429/5xx).
| Symptom | Cause | Fix |
|---|---|---|
| 404 Not Found | Group path not slash-converted | Use groupId.replace('.', '/') |
| 401/403 | Private repo requires auth | Provide credentials; set Authorization header |
ClassNotFoundException |
JAR not on loader classpath | Ensure file exists; re-create DependencyLoader after downloads |
| Timeout | Slow or blocked network | Increase timeouts; configure proxies |
| Corrupted JAR | Partial download | Enable checksum verification and retry |
- Keep a dedicated
dependencies/directory per plugin. - Version-pin artifacts strictly (avoid
LATEST). - Use checksums to guarantee integrity.
- Log at
INFOfor success paths,DEBUGfor HTTP URLs and retries (avoid logging secrets). - Consider shading absolutely critical libs rather than downloading them at runtime.
addRepository(DependencyRepository repo)addArtifact(DependencyArtifact artifact)downloadDependencies(Path libraryDirectory)
DependencyLoader(List<Path> jarFiles)Class<?> loadClass(String className)
groupId(),artifactId(),version()
url(),username(),password()
Path libDir = plugin.getDataFolder().toPath().resolve("dependencies");
DependencyResolver resolver = DependencyResolver.create()
.addRepository(new DependencyRepository("https://repo1.maven.org/maven2"))
.addArtifact(new DependencyArtifact("org.mongodb", "mongodb-driver-sync", "5.1.0"));
resolver.downloadDependencies(libDir);
List<Path> jars = Files.list(libDir).toList();
DependencyLoader loader = new DependencyLoader(jars);
Class<?> mongoClient = loader.loadClass("com.mongodb.client.MongoClients");- Credentials support for HTTP(S) downloads out of the box (Basic/Bearer).
- Pluggable
Transport(HTTP client abstraction) to add retries and metrics. - Support for POM resolution and transitive dependencies.
- Local cache directory with checksum index.
- Optional OSGi-style classloader isolation per dependency group.
Generated for the NextForge Core project. Keep calm and inject dependencies at runtime.