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.