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