Skip to content

This Gradle plugin creates a fat JAR for your Java application by bundling and relocating specified dependencies. It helps to prevent dependency conflicts and ensures consistent application behavior by isolating these dependencies from downstream projects.

License

Notifications You must be signed in to change notification settings

remal-gradle-plugins/classes-relocation

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Tested on Java LTS versions from 11 to 21.

Tested on Gradle versions from 7.5 to 8.13.

name.remal.classes-relocation plugin

configuration cache: supported from v2

Usage:

plugins {
    id 'name.remal.classes-relocation' version '2.0.0-rc-6'
}

 

This Gradle plugin facilitates the creation of a fat JAR by bundling your Java application with specific dependencies. It relocates these dependencies to a new namespace within the JAR to prevent exposure to and conflicts with downstream projects.

This plugin is ideal for scenarios where your code relies on specific dependencies, but you need to ensure that these dependencies do not interfere with those in projects that consume your JAR.

By using this plugin, you can better manage dependency versions, leading to more reliable and predictable behavior of your Java applications across different environments.

Base configuration

classesRelocation {
  basePackageForRelocatedClasses = 'your.project.relocated' // specify the base package for relocated dependencies
}

dependencies {
  classesRelocation('com.google.guava:guava:XX.X.X-jre') { // relocate Guava with transitive dependencies
    exclude group: 'com.google.errorprone', module: 'error_prone_annotations' // but do NOT relocate Error Prone annotations
  }
}

Minimization

This plugin can automatically remove all classes of relocated dependencies that are not used by the project, minimizing the result JAR file. The minimization is implemented via extensive bytecode analysis.

These class members are always relocated:

  • Static initializer
    • It means that all initialized static fields will always be kept
    • See issue #37

Serialization-related members are relocated if any instance member is relocated:

  • serialVersionUID static field
  • writeObject(ObjectOutputStream) method
  • readObject(ObjectInputStream) method
  • readObjectNoData() method
  • writeReplace() method
  • readResolve() method

If you relocate a dependency that doesn't use reflection (Class.getMethod(), Class.getField(), etc), you don't need to configure minimization.

Keep class members annotated with configured annotations

All class members annotated by these annotations will be kept:

  • jakarta.inject.**
  • javax.inject.**
  • com.fasterxml.jackson.**
  • com.google.gson.**
  • jakarta.validation.**
  • javax.validation.**
  • org.hibernate.validator.**

You can configure other annotation type patterns:

classesRelocation {
  minimize {
    keepMembersAnnotatedWith('jakarta.inject.**') // all class members annotated with annotations matched to the provided patterns will be kept
  }
}

GraalVM's Reachability Metadata

To avoid the removal of necessary code, the plugin uses GraalVM's Reachability Metadata:

  • META-INF/native-image/<groupId>/<artifactId>/reachability-metadata.json files in the relocated dependencies are processed
    • only $.reflection field is supported
    • $.resources and $.bundles fields are NOT supported, so resource relocation can be broken is some cases
  • reflect-config.json files from oracle/graalvm-reachability-metadata are processed

Full minimization configuration

classesRelocation {
  minimize {
    keepClasses('com.google.common.*') // all classes in the `com.google.common` package will be fully relocated; subpackages (like `com.google.common.base`) will NOT be minimized
    keepClasses('com.google.common.**') // all classes in the `com.google.common` package in its subpackages (like `com.google.common.base`) will be fully relocated

    keepMembersAnnotatedWith('jakarta.inject.**') // all class members annotated with annotations matched to the provided patterns will be kept

    graalvmReachabilityMetadataVersion = 'x.x.x' // set release of /~https://github.com/oracle/graalvm-reachability-metadata

    addClassReachabilityConfig { // add GraalVM's Reachability Metadata programmatically
      className('package.Class') // for `$.reflection[*].type`
      onReachedClass('other.package.reached.Class') // for `$.reflection[*].condition.typeReached`
      onReached() // equals to `onReachedClass()` with the same class name as `className()`
      field('fieldName') // for `$.reflection[*].fields[*].name`
      fields(['fieldName1', 'fieldName2']) // for `$.reflection[*].fields[*].name`
      method('methodName', '(Ljava/lang/String)') // for `$.reflection[*].methods[*].name` and `$.reflection[*].methods[*].parameterTypes`; `(Ljava/lang/String)` is a method descriptor (see https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.3.3), return type is OPTIONAL
      allDeclaredConstructors(true) // for `$.reflection[*].allDeclaredConstructors`
      allPublicConstructors(true) // for `$.reflection[*].allPublicConstructors`
      allDeclaredMethods(true) // for `$.reflection[*].allDeclaredMethods`
      allPublicMethods(true) // for `$.reflection[*].allPublicMethods`
      allDeclaredFields(true) // for `$.reflection[*].allDeclaredFields`
      allPublicFields(true) // for `$.reflection[*].allPublicFields`
      allPermittedSubclasses(true) // for `$.reflection[*].allPermittedSubclasses`
    }

    addClassReachabilityConfig { /* ... */ } // add more GraalVM's Reachability Metadata
  }
}

Used minimization configuration is stored in the result JAR file. So, if the result JAR file is used as a relocated dependency in another project, all kept classes/members will be relocated.

How the plugin works

This plugin adds a new action to the jar task. This action recreates the JAR file relocating dependencies from the classesRelocation configuration.

Only directly used classes will be relocated.

The result JAR file will be used by Test, PluginUnderTestMetadata, ValidatePlugins, JacocoReportBase tasks (instead of build/classes/main/* directories).

Other projects in Multi-Project builds will consume the result JAR file if the current project is declared as a dependency (the same way it happens by default).

The result JAR file will be published to Maven repositories (the same way it happens by default).

By default, the jar task is not cacheable. This plugin makes this task cacheable.

Alternatives

The classic Gradle plugin for fat-JAR creation.

Benefits of name.remal.classes-relocation

  • Simpler configuration.
  • Other projects in Multi-Project builds will consume the relocated JAR file automatically.
  • Publication of the relocated JAR is configured automatically.
  • Minimization is relatively simple and enabled by default

Benefits of com.gradleup.shadow

  • Used in many projects, so it can be considered more stable and reliable.
  • Can be used not only for classes relocation but for creating fat-JARs.
  • Extendable (be implementing com.github.jengelman.gradle.plugins.shadow.transformers.Transformer)
  • More configuration options. For example, a capability of excluding specific files and packages from relocation.
  • Minimization requires tests to be written (by default, classes not used in test will be removed)
    • Code coverage should be very high to work properly
    • Build will fail if there is a dependency like this: :prod - main source set (with relocation) > :test-utils - main source set (depends on :prod) > :prod > test source set (prod's tests depend on :test-utils)

Migration guide

Version 1.* to 2.*

The minimum Java version is 11.

The name.remal.gradle_plugins.classes_relocation.ClassesRelocationExtension project extension should be used instead of name.remal.gradle_plugins.plugins.classes_relocation.ClassesRelocationExtension.

The relocateClasses configuration is still supported but was deprecated. Use classesRelocation instead.

The @name.remal.gradle_plugins.api.RelocateClasses and @name.remal.gradle_plugins.api.RelocatePackages annotations are no longer supported. If you need to relocate a dependency of a specific class only (but not of other classes), create a separate module and include it in your build.

The excludeFromClassesRelocation and excludeFromForcedClassesRelocation configurations are no longer supported.

About

This Gradle plugin creates a fat JAR for your Java application by bundling and relocating specified dependencies. It helps to prevent dependency conflicts and ensures consistent application behavior by isolating these dependencies from downstream projects.

Topics

Resources

License

Stars

Watchers

Forks

Contributors 3

  •  
  •  
  •  

Languages