3

I am running my spring-boot app in a Docker container, trying to use remote LiveReload.

The spring-boot DevTools documentation states that

Developer tools are automatically disabled when running a fully packaged application. If your application is launched using java -jar or if it’s started using a special classloader, then it is considered a “production application”.

Is there any way to force the enabling of DevTools?

GBC
  • 3,246
  • 2
  • 19
  • 18

2 Answers2

2

The solution is sketchy, so you decide if it's good for you. The final solution is the last part of this post

It's hard to just throw the solution, I first need to explain how I got there. First, why livereload is not enabled when launching outside the IDE:


Understand what is going on

(1) LocalDevToolsAutoConfiguration configuration is conditional on @ConditionalOnInitializedRestarter/OnInitializedRestarterCondition:

    @Configuration
    @ConditionalOnInitializedRestarter
    @EnableConfigurationProperties(DevToolsProperties.class)
    public class LocalDevToolsAutoConfiguration {
    ...

(2) OnInitializedRestarterCondition retrieves a Restarter instance and checks if it's null otherwise restarter.getInitialUrls() returns null. In my case, restarter.getInitialUrls() was returning null.

class OnInitializedRestarterCondition extends SpringBootCondition {
    @Override
    public ConditionOutcome getMatchOutcome(ConditionContext context,
            AnnotatedTypeMetadata metadata) {
        Restarter restarter = getRestarter();
        if (restarter == null) {
            return ConditionOutcome.noMatch("Restarter unavailable");
        }
        if (restarter.getInitialUrls() == null) {
            return ConditionOutcome.noMatch("Restarter initialized without URLs");
        }
        return ConditionOutcome.match("Restarter available and initialized");
    }

(3) initialUrls is initialized in Restarter.class through DefaultRestartInitializer.getInitialUrls(..)

class Restarter{
    this.initialUrls = initializer.getInitialUrls(thread);
}

class DefaultRestartInitializer{
    @Override
    public URL[] getInitialUrls(Thread thread) {
        if (!isMain(thread)) {
            return null;
        }
        for (StackTraceElement element : thread.getStackTrace()) {
            if (isSkippedStackElement(element)) {
                return null;
            }
        }
        return getUrls(thread);
    }

    protected boolean isMain(Thread thread) {
    return thread.getName().equals("main") && thread.getContextClassLoader()
            .getClass().getName().contains("AppClassLoader");
    }
}

thread.getContextClassLoader() .getClass().getName().contains("AppClassLoader")

is only true when running from Eclipse (possibly any IDE? + springboot-maven-plugin?). To Recap:

  • isMain() returns false;

  • the initialUrls is not initialized;

  • the conditional LocalDevToolsAutoConfiguration is not configured;

  • no livereload.


A SOLUTION:

Make sure the classloader name is "AppClassLoader" by Creating your own AppClassLoader classloader. At the very first line of your spring-boot main, replace the classloader with yours:

URLClassLoader originalClassLoader = (URLClassLoader)Thread.currentThread().getContextClassLoader();
Thread.currentThread().setContextClassLoader(new CustomAppClassLoader(originalClassLoader));

Our custom classloader implementation simply delegates to the original one:

public class CustomAppClassLoader extends URLClassLoader{

private URLClassLoader contextClassLoader;

public CustomAppClassLoader(URLClassLoader contextClassLoader) {
    super(contextClassLoader.getURLs(), contextClassLoader.getParent());
    this.contextClassLoader = contextClassLoader;
}

public int hashCode() {
    return contextClassLoader.hashCode();
}

public boolean equals(Object obj) {
    return contextClassLoader.equals(obj);
}

public InputStream getResourceAsStream(String name) {
    return contextClassLoader.getResourceAsStream(name);
}

public String toString() {
    return contextClassLoader.toString();
}

public void close() throws IOException {
    contextClassLoader.close();
}

public URL[] getURLs() {
    return contextClassLoader.getURLs();
}

public Class<?> loadClass(String name) throws ClassNotFoundException {
    return contextClassLoader.loadClass(name);
}

public URL findResource(String name) {
    return contextClassLoader.findResource(name);
}

public Enumeration<URL> findResources(String name) throws IOException {
    return contextClassLoader.findResources(name);
}

public URL getResource(String name) {
    return contextClassLoader.getResource(name);
}

public Enumeration<URL> getResources(String name) throws IOException {
    return contextClassLoader.getResources(name);
}

public void setDefaultAssertionStatus(boolean enabled) {
    contextClassLoader.setDefaultAssertionStatus(enabled);
}

public void setPackageAssertionStatus(String packageName, boolean enabled) {
    contextClassLoader.setPackageAssertionStatus(packageName, enabled);
}

public void setClassAssertionStatus(String className, boolean enabled) {
    contextClassLoader.setClassAssertionStatus(className, enabled);
}

public void clearAssertionStatus() {
    contextClassLoader.clearAssertionStatus();
}

}

I configured CustomAppClassLoader as much as I could (calling super with 'url' and 'parent' from the original classloader), but anyway I still delegate all public methods to the original classloader.

It works for me. Now, the better question is do I really want this :)

A better option

I think Spring Cloud's RestartEndpoint is a better option: Programmatically restart Spring Boot application However, RestartEndPoint does not handle the detection of changes in the classpath.

Community
  • 1
  • 1
alexbt
  • 16,415
  • 6
  • 78
  • 87
0

make sure that devtools is included in the repackaged archive, like this:

<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
            <configuration>
                <excludeDevtools>false</excludeDevtools>
            </configuration>
        </plugin>
    </plugins>
</build>
cyp
  • 1
  • 1