Search code examples
javaspringspring-bootkotlin

Spring Boot won't pickup application.properties in custom class loader environment


I am currently working on a project which includes running Spring Boot within a Minecraft Spigot / Paper plugin.

My project is setup to run a custom Framework that I built on plugin initialization:

// MyPlugin class

override fun onEnable() {
    MyFramework.run(javaClass, name)
}

My framework is responsible for initializing the Spring application, which works fine...
Dependency injection and any other part of Spring itself works without problems, but one thing bothers me.

I can't get the application.properties file to work with Spring Boot.

Spring seems to just ignore my properties file and any other external configuration file, when declared in my Minecraft plugin....

I have tried multiple things already:

  • Custom resource loader when building the Spring Application using SpringApplicationBuilder

  • Setting the properties file location using spring.config.location when building the application using SpringApplicationBuilder

  • Adding @PropertySources to the main class of my Spring Application

  • Manually adding the application.properties file to the output JAR ROOT before running

I have made sure that the configuration file is correctly placed in the ROOT directory of my plugin.

The file is also found by the application proven by the following test:

public static void checkPropertiesFile() {
    ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
    URL resource = classLoader.getResource("application.properties");
    if (resource != null) {
        System.out.println("Found application.properties at " + resource.getPath());
    } else {
        System.out.println("application.properties not found in classpath.");
    }
}

This is the code snippet relevant to running the spring application:

public static void run(Class<?> plugin, String pluginName) {
    final ClassLoader pluginClassLoader = plugin.getClassLoader();

    // needed or else spring startup fails
    Thread.currentThread().setContextClassLoader(pluginClassLoader);

    final ResourceLoader loader = new DefaultResourceLoader(pluginClassLoader);
    final SpringApplicationBuilder builder = new SpringApplicationBuilder();
    // auto generated "main spring class"
    // needed since when using the actual plugin main class, spring tries to instantiate a new instance of our plugin class, which fails
    final Class<?> pluginConfiguration = Class.forName(plugin.getPackageName() + ".PluginConfiguration");
    final Class<?>[] sources = {pluginConfiguration, SpigotConfiguration.class, BungeecordConfiguration.class};

    context = builder.sources(sources)
            .initializers(applicationContext -> {
                applicationContext.setClassLoader(pluginClassLoader);
            })
            .resourceLoader(loader)
            .properties("plugin.name=" + pluginName)
            .properties("plugin.main=" + plugin.getName())
            .run();
}

I am really lost here, you guys are my last hope, I am slowly losing my mind


Solution

  • I have found a way to kind of resolve my issue.

    When telling Spring to use „StandardEnvironment“ while building, Spring picks up any System Property set before starting:

    builder.initializers(applicationContext -> {
        applicationContext.setEnvironment(new StandardEnvironment());
    })
    

    Now I can use @mlecz s suggestion (Loading the application.properties with a Properties Object and iterating over every key-value pair, then setting a System Property for every pair).

    This is a workaround and not directly the functionality I was hoping for, but it works.

    Here’s the edited code snippet for anyone having similar issues with implementing Spring in such an environment:

    public static void run(Class<?> plugin, String pluginName) {
        final ClassLoader pluginClassLoader = plugin.getClassLoader();
        // needed or else spring startup fails
        Thread.currentThread().setContextClassLoader(pluginClassLoader);
    
        final ResourceLoader loader = new DefaultResourceLoader(pluginClassLoader);
        final SpringApplicationBuilder builder = new SpringApplicationBuilder();
        final Class<?> pluginConfiguration = Class.forName(plugin.getPackageName() + ".PluginConfiguration");
        final Class<?>[] sources = {pluginConfiguration, SpigotConfiguration.class, BungeecordConfiguration.class};
    
        System.setProperty("plugin.name", pluginName);
        System.setProperty("plugin.main", plugin.getName());
    
        try {
            final Properties properties = new Properties();
    
            properties.load(pluginClassLoader.getResourceAsStream("application.properties"));
    
            properties.forEach((key, value) -> {
                System.setProperty(key.toString(), value.toString());
            });
        } catch (Exception ignored) {}
    
        context = builder.sources(sources)
                .initializers(applicationContext -> {
                    applicationContext.setClassLoader(pluginClassLoader);
                    // this somehow enables spring to pickup the system properties    
                    applicationContext.setEnvironment(new StandardEnvironment());
                })
                .resourceLoader(loader)
                .run();
    }
    
    

    With this implementation I was able to set my active profile with the „application.properties“ which is located in the ROOT of my built JAR