Ship lightweight Paper & Spigot plugins. Let your players download the libraries.
Runtime dependency resolution, downloading, relocation, and classpath injection β purpose-built for modern Minecraft servers.
Quick Start β’ Why JExDependency β’ Features β’ Configuration β’ FAQ
Minecraft plugin jars keep getting larger. Hibernate, Jackson, Caffeine, JDBC drivers β shading them all bloats your jar to 20 MB+ and guarantees classpath collisions the moment another plugin ships a different version.
JExDependency flips that model. Declare dependencies in a tiny YAML file, ship a 50 KB plugin, and let the library download, verify, (optionally) relocate, and inject the JARs at runtime β on every flavour of Paper and Spigot, transparently.
Think of it as Paper's
librariesblock, but portable, relocation-aware, and working on Spigot too.
- π§© Universal loader β Paper plugin-loader handshake on 1.20+, legacy bootstrap on Spigot/older Paper. Auto-detected.
- π¦ YAML-first descriptors β merge generic / Paper-only / Spigot-only files, deduplicate versions, normalise coordinates.
- π Optional ASM relocation β isolate conflicting packages without paying the CPU cost when you don't need it.
- β‘ Sync or async bootstrap β block
onLoad()for simplicity, or return aCompletableFuturefor non-blocking startup. - π Checksum-verified downloads β corrupted artifacts are retried; temp dirs are wiped on every exit path.
- β Java 21 ready β module de-encapsulation and
--add-openssemantics handled for reflective libraries. - π§Ή Deterministic cache β artefacts live under
plugins/<Plugin>/libraries/and.../libraries/remapped/; safe to prune. - πͺ΅ First-class logging β every bootstrap cycle reports counts, timings, and redacted paths through the plugin logger.
Replace VERSION with the latest release tag.
Gradle (Kotlin DSL)
repositories {
maven("https://repo.jexcellence.de/releases")
}
dependencies {
implementation("de.jexcellence.dependency:jexdependency:VERSION")
}Gradle (Groovy)
repositories {
maven { url 'https://repo.jexcellence.de/releases' }
}
dependencies {
implementation 'de.jexcellence.dependency:jexdependency:VERSION'
}Maven
<repositories>
<repository>
<id>jexcellence</id>
<url>https://repo.jexcellence.de/releases</url>
</repository>
</repositories>
<dependency>
<groupId>de.jexcellence.dependency</groupId>
<artifactId>jexdependency</artifactId>
<version>VERSION</version>
</dependency>Create src/main/resources/dependency/dependencies.yml:
dependencies:
- "com.github.ben-manes.caffeine:caffeine:3.2.2"
- "com.fasterxml.jackson.core:jackson-databind:2.18.2"
- "com.mysql:mysql-connector-j:9.2.0"Need platform-specific sets? Add dependencies-paper.yml or dependencies-spigot.yml β they are merged automatically.
public final class ExamplePlugin extends JavaPlugin {
@Override
public void onLoad() {
// Synchronous β simplest; blocks onLoad while JARs download.
JEDependency.initialize(this, ExamplePlugin.class);
}
}That's it. Libraries land under plugins/ExamplePlugin/libraries/ and are injected into your plugin's classloader before onEnable() fires.
If your build.gradle.kts currently looks like this:
tasks.named<com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar>("shadowJar") {
archiveBaseName.set("RDQ")
archiveVersion.set(rdqVersion)
archiveClassifier.set("Free")
relocate("com.github.benmanes", "de.jexcellence.remapped.com.github.benmanes")
relocate("me.devnatan.inventoryframework", "de.jexcellence.remapped.me.devnatan.inventoryframework")
relocate("com.tcoded", "de.jexcellence.remapped.com.tcoded")
relocate("com.cryptomorin.xseries", "de.jexcellence.remapped.com.cryptomorin.xseries")
configurations = listOf(project.configurations.getByName("runtimeClasspath"))
mergeServiceFiles()
}β¦you can throw it all away. No shading. No relocate block. No 20 MB fat jar.
With JExDependency, the entire setup becomes:
1) build.gradle.kts β no shadow plugin required:
dependencies {
implementation("de.jexcellence.dependency:jexdependency:2.0.0")
// These stay as compileOnly β they're downloaded at runtime, not shaded.
compileOnly("com.github.ben-manes.caffeine:caffeine:3.2.2")
compileOnly("me.devnatan:inventory-framework-paper:3.3.0")
compileOnly("com.tcoded:FoliaLib:0.5.1")
compileOnly("com.github.cryptomorin:XSeries:11.3.0")
}2) src/main/resources/dependency/dependencies.yml:
dependencies:
- "com.github.ben-manes.caffeine:caffeine:3.2.2"
- "me.devnatan:inventory-framework-paper:3.3.0"
- "com.tcoded:FoliaLib:0.5.1"
- "com.github.cryptomorin:XSeries:11.3.0"3) Bootstrap with relocation forced on:
public final class RDQ extends JavaPlugin {
@Override
public void onLoad() {
// Equivalent to your old shadow relocate(...) block, at runtime.
JEDependency.initializeWithRemapping(this, RDQ.class);
}
}4) (Optional) Control the relocation prefix via a JVM flag β no rebuild needed:
-Djedependency.relocations.prefix=de.jexcellence.remapped
Result: a 50 KB plugin jar instead of 20 MB, a clean Git diff instead of a shaded-classes explosion, and per-server-operator control over relocations.
| Method | Blocking | Forces relocation | When to use |
|---|---|---|---|
JEDependency.initialize(plugin, anchor) |
β | β | Default. Safe inside onLoad(). |
JEDependency.initializeWithRemapping(plugin, anchor) |
β | β | You need guaranteed isolation from other plugins' libs. |
JEDependency.initializeAsync(plugin, anchor) |
β | β | Non-blocking startup; await the returned CompletableFuture<Void> before touching injected classes. |
All three methods accept an optional String[] of extra Maven coordinates (group:artifact:version[:classifier]) appended to the YAML list.
JExDependency is driven by JVM system properties so server operators can tweak behaviour without recompiling your plugin.
| Property | Default | Purpose |
|---|---|---|
-Djedependency.remap |
false |
true / 1 / yes / on forces ASM relocation. |
-Djedependency.relocations |
β | Comma-separated pattern=target overrides, e.g. com.google.gson=mypkg.libs.gson. |
-Djedependency.relocations.prefix |
β | Global prefix applied to auto-relocated packages. |
-Djedependency.relocations.excludes |
β | Packages that must never be relocated. |
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β onLoad() β JEDependency.initialize(...) β
ββββββββββββββββ¬ββββββββββββββββββββββββββββββββββββββββββββββββββββ
βΌ
βββββββββββββββββββββββββ Paper 1.20+?
β Server detection ββββββββββ
βββββββββββββββββββββββββ βΌ
β βββββββββββββββββββββββββ
β β Inject pre-downloaded β
β β libs via Paper loader β
β βββββββββββββ¬ββββββββββββ
βΌ β
βββββββββββββββββββββββββ β
β Merge YAML sources ββββββββββββββ
β (generic + platform) β
βββββββββββββ¬ββββββββββββ
βΌ
βββββββββββββββββββββββββ
β Resolve + download β β checksum verify β cache under
β from Maven repos β plugins/<Plugin>/libraries/
βββββββββββββ¬ββββββββββββ
βΌ
βββββββββββββββββββββββββ (only when -Djedependency.remap=true
β Optional ASM remap β or initializeWithRemapping(...) is used)
βββββββββββββ¬ββββββββββββ
βΌ
βββββββββββββββββββββββββ
β URLClassLoader β β classes visible to your plugin
β injection β before onEnable() fires.
βββββββββββββββββββββββββ
src/main/java/de/jexcellence/dependency/
βββ JEDependency.java β public entrypoints
βββ manager/DependencyManager β core resolution pipeline
βββ remapper/ β ASM relocation (opt-in)
βββ downloader/ β Maven artefact retrieval + checksum
βββ injector/ClasspathInjector β runtime classloader injection
βββ loader/ β Paper / Spigot loader adapters
βββ repository/ β repository registry + mirrors
βββ resolver/ β coordinate + transitive resolution
βββ model/ β immutable data types
Full Javadoc lives next to the sources. Start from JEDependency and DependencyManager.
- Every stage logs through
plugin.getLogger()β no custom appenders required. - FINE level reveals per-artifact download progress, checksum results, and relocation summaries.
- Failure paths sanitise file system roots so logs are safe to share.
- Start / end timestamps and dependency counts are emitted for automation to diff across restarts.
- Maven checksum validation on every downloaded jar; corrupted files trigger a retry and cache purge.
- Remapping runs inside a sandboxed
URLClassLoaderso a half-relocated class can never leak into your plugin loader on failure. - Temporary directories are wiped on both success and failure β no stale bytecode survives restarts.
How is this different from Paper's built-in libraries block?
Paper's loader only works on Paper 1.20+ and gives you no relocation support. JExDependency runs on Spigot and older Paper too, adds optional ASM relocation, and β when the Paper loader is active β cooperates with it instead of duplicating work.
Will it download every startup?
No. Artefacts are cached under the plugin's data folder and reused across restarts. Only missing or checksum-invalid jars are re-downloaded.
Does async mode block onEnable()?
No.
initializeAsync returns a CompletableFuture<Void> immediately. You decide whether to .join() before using the dependencies or to schedule work after completion.
What Java versions are supported?
Java 21 is the primary target. Module de-encapsulation and
--add-opens semantics for reflective libraries are handled for you.
Does relocation rewrite my plugin's own classes?
No. Only downloaded dependency jars are visited. Your plugin bytecode is never touched.
Need a hand, found a bug, or want to bounce ideas around?
- π§ Email β justin.eiletz@jexcellence.de
- π¬ Discord β
jexcellence - π Issues β GitHub Issues for bugs and feature requests
Issues, discussions, and PRs are welcome.
- Fork the repo and create a feature branch.
- Run
./gradlew buildto verify the project compiles. - Add tests or a reproduction case where applicable.
- Open a PR describing the change and the motivation.
Please follow the existing code style β no wildcard imports, Javadoc on public API, nullability annotations from org.jetbrains.annotations.
Released under the MIT License. Use it, ship it, star it. β
Built with care by JExcellence. If JExDependency saves you a shaded jar today, consider giving it a star β it genuinely helps others find the project.