Search code examples
javaspring-bootpluginsdynamic-loading

How to add external JARs to Spring application without restarting JVM?


I have a Spring Boot application which copies external JAR files to a folder, depending on certain conditions. These JARs can contain many Spring components (i.e. classes annotated or meta-annotated with @Component) and the Spring application should be able scan and instantiate for these beans. Is it possible, based on certain conditions, to dynamically load the contents of the JAR files and make them available to the Spring application context? I am fully aware of the security implications this has.

I have read about the different types of Launchers which Spring provides for its executable JAR format, such as JarLauncher and PropertiesLauncher, but it looks like that these launchers do not detect changes to the classpath, but instead only scan the directories once for JAR files.

The following simple application demonstrates the problem:

// .../Application.java
@SpringBootApplication
public class Application {
  public static void main(String[] args) {
    System.out.println("Please copy JAR files and press Enter ...");
    System.in.read();
    SpringApplication.run(Application.class, args);
  }
}

Replace the default JarLauncher with PropertiesLauncher:

// build.gradle
tasks.named('bootJar') {
  manifest {
    attributes 'Main-Class': 'org.springframework.boot.loader.PropertiesLauncher',
      'Start-Class': 'com.example.customlauncher.Application'
  }
}

Specify the location to the external JARs in the properties file of the PropertiesLauncher:

# .../resources/loader.properties
loader.path=file:/path/to/dir

The application is a Spring Initializer Gradle application and packaged by running the bootJar task: ./gradlew bootJar.

It is then started with the following command:

java -jar build/libs/customlauncher-0.0.1-SNAPSHOT.jar

This works if the JAR file is already present at the specified location (/path/to/dir), but it does not work if the java command is executed while the directory is empty and the JAR file is then copied while the app waits for the user to copy the files and press Enter .

There are a couple of related questions, but it looks like they all assume that the JAR files already exist at the time of starting the JVM:

Is there a way to achieve this without too many awkard hacks? Or is recommended to utilize something like OSGi? Am I looking at this completely wrong and there is a better way to have JARs on the classpath that do not need always need loading (if the JAR is "disabled", it should not be loaded/compiled by the JVM, should not be picked up by Spring, etc.)?


Solution

  • It looks like this is possible if the JAR files are copied before starting the Spring application. It feels hackish, but it works. Use at your own risk!

    You need two classes, one for bootstrapping the external JARs, which will then start the second via a manually created PropertiesLauncher. The bootstrapping class can be a plain old regular Java class (but it can be a Spring Boot Application too) and only the second class needs to be a SpringBootApplication.

    // BootstrapApplication.java
    public class BootstrapApplication {
      public static void main(String[] args) {
        System.out.println("Please copy JAR files and press Enter ...");
        System.in.read();
    
        PropertiesLauncher.main(args);
      }
    }
    
    // Application.java
    @SpringBootApplication
    public class Application {
      public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
      }
    }
    

    In the gradle file, we can switch back to the default JarLauncher, by removing the bootJar task manifest configuration and applying settings via the springBoot configuration block. mainClass will end up as Start-Class in the MANIFEST.MF file.

    // build.gradle
    springBoot {
        mainClass = 'com.example.customlauncher.BootstrapApplication'
    }
    

    In the properties file for the loader, a new property needs to be set, which points to the real application class. The settings in this file are only picked up by PropertiesLauncher and ignored by JarLauncher. In other words: JarLauncher delegates to Start-Class from the manifest file and PropertiesLauncher delegates to loader.main from its properties file.

    # .../resources/loader.properties
    loader.path=file:/path/to/dir
    loader.main=com.example.customlauncher.Application
    

    Spring (Boot) will first call the main method of BootstrapApplication, as specified in the MANIFEST.MF file (controlled via springBoot configuration block in the build.gradle file). In the implementation of this main method, a new PropertiesLauncher is created with the main class set to the "real" application (i.e. Application).

    Executing the application is still done via the same invocation:

    java -jar build/libs/customlauncher-0.0.1-SNAPSHOT.jar
    

    Any JAR files added to /path/to/dir after the JVM has started, but before calling PropertiesLauncher#main in BootstrapApplication are then available in the classpath and application context as seen from Application.