Skip to content

Latest commit

 

History

42 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

JExDependency

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.

License: MIT Java 21 Paper Spigot Version GitHub stars

Quick Start • Why JExDependency • Features • Configuration • FAQ


💡 Why JExDependency

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 libraries block, but portable, relocation-aware, and working on Spigot too.


✨ Features

  • 🧩 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 a CompletableFuture for 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-opens semantics 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.

🚀 Quick Start

1. Add the dependency

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>

2. Declare runtime libraries

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.

3. Bootstrap in your plugin

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.


🔁 Replace your ShadowJar relocate blocks

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.


🎯 Bootstrap modes

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.


⚙️ Configuration

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.

🧠 How it works

┌──────────────────────────────────────────────────────────────────┐
│  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.
   └───────────────────────┘

📁 Project layout

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.


🔍 Observability

  • 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.

🔐 Security practices

  • Maven checksum validation on every downloaded jar; corrupted files trigger a retry and cache purge.
  • Remapping runs inside a sandboxed URLClassLoader so 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.

❓ FAQ

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.

💬 Support & Contact

Need a hand, found a bug, or want to bounce ideas around?


🤝 Contributing

Issues, discussions, and PRs are welcome.

  1. Fork the repo and create a feature branch.
  2. Run ./gradlew build to verify the project compiles.
  3. Add tests or a reproduction case where applicable.
  4. 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.


📜 License

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.

About

No more Shading in bukkit/spigot/paper

Resources

Stars

13 stars

Watchers

5 watching

Forks

Releases

Packages

Used by

Contributors

Languages