Add Restart auto-configuration

Add auto-configuration for application Restarts. Restarts are enabled
by default (when not running from a fat jar) and will be triggered when
any classpath folder changes.

The ClassPathRestartStrategy additional customization of when a full
restart is required. By default a PatternClassPathRestartStrategy with
patterns loaded from DeveloperToolsProperties.

Closes gh-3084
This commit is contained in:
Phillip Webb
2015-06-01 13:23:05 -07:00
parent a5c56ca482
commit 3d8db7cddb
13 changed files with 928 additions and 0 deletions

View File

@@ -0,0 +1,71 @@
/*
* Copyright 2012-2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.developertools.autoconfigure;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* Configuration properties for developer tools.
*
* @author Phillip Webb
* @since 1.3.0
*/
@ConfigurationProperties(prefix = "spring.developertools")
public class DeveloperToolsProperties {
private static final String DEFAULT_RESTART_EXCLUDES = "META-INF/resources/**,resource/**,static/**,public/**,templates/**";
private Restart restart = new Restart();
public Restart getRestart() {
return this.restart;
}
/**
* Restart properties
*/
public static class Restart {
/**
* Enable automatic restart.
*/
private boolean enabled = true;
/**
* Patterns that should be excluding for triggering a full restart.
*/
private String exclude = DEFAULT_RESTART_EXCLUDES;
public boolean isEnabled() {
return this.enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public String getExclude() {
return this.exclude;
}
public void setExclude(String exclude) {
this.exclude = exclude;
}
}
}

View File

@@ -16,10 +16,22 @@
package org.springframework.boot.developertools.autoconfigure;
import java.net.URL;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.developertools.classpath.ClassPathChangedEvent;
import org.springframework.boot.developertools.classpath.ClassPathFileSystemWatcher;
import org.springframework.boot.developertools.classpath.ClassPathRestartStrategy;
import org.springframework.boot.developertools.classpath.PatternClassPathRestartStrategy;
import org.springframework.boot.developertools.restart.ConditionalOnInitializedRestarter;
import org.springframework.boot.developertools.restart.Restarter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.event.EventListener;
/**
* {@link EnableAutoConfiguration Auto-configuration} for local development support.
@@ -29,11 +41,47 @@ import org.springframework.context.annotation.Configuration;
*/
@Configuration
@ConditionalOnInitializedRestarter
@EnableConfigurationProperties(DeveloperToolsProperties.class)
public class LocalDeveloperToolsAutoConfiguration {
@Autowired
private DeveloperToolsProperties properties;
@Bean
public static LocalDeveloperPropertyDefaultsPostProcessor localDeveloperPropertyDefaultsPostProcessor() {
return new LocalDeveloperPropertyDefaultsPostProcessor();
}
/**
* Local Restart Configuration.
*/
@ConditionalOnProperty(prefix = "spring.developertools.restart", name = "enabled", matchIfMissing = true)
static class RestartConfiguration {
@Autowired
private DeveloperToolsProperties properties;
@Bean
@ConditionalOnMissingBean
public ClassPathFileSystemWatcher classPathFileSystemWatcher() {
URL[] urls = Restarter.getInstance().getInitialUrls();
return new ClassPathFileSystemWatcher(classPathRestartStrategy(), urls);
}
@Bean
@ConditionalOnMissingBean
public ClassPathRestartStrategy classPathRestartStrategy() {
return new PatternClassPathRestartStrategy(this.properties.getRestart()
.getExclude());
}
@EventListener
public void onClassPathChanged(ClassPathChangedEvent event) {
if (event.isRestartRequired()) {
Restarter.getInstance().restart();
}
}
}
}

View File

@@ -0,0 +1,68 @@
/*
* Copyright 2012-2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.developertools.classpath;
import java.util.Set;
import org.springframework.boot.developertools.filewatch.ChangedFiles;
import org.springframework.context.ApplicationEvent;
import org.springframework.util.Assert;
/**
* {@link ApplicationEvent} containing details of a classpath change.
*
* @author Phillip Webb
* @since 1.3.0
* @see ClassPathFileChangeListener
*/
public class ClassPathChangedEvent extends ApplicationEvent {
private final Set<ChangedFiles> changeSet;
private final boolean restartRequired;
/**
* Create a new {@link ClassPathChangedEvent}.
* @param source the source of the event
* @param changeSet the changed files
* @param restartRequired if a restart is required due to the change
*/
public ClassPathChangedEvent(Object source, Set<ChangedFiles> changeSet,
boolean restartRequired) {
super(source);
Assert.notNull(changeSet, "ChangeSet must not be null");
this.changeSet = changeSet;
this.restartRequired = restartRequired;
}
/**
* Return details of the files that changed.
* @return the changed files
*/
public Set<ChangedFiles> getChangeSet() {
return this.changeSet;
}
/**
* Return if an application restart is required due to the change.
* @return if an application restart is required
*/
public boolean isRestartRequired() {
return this.restartRequired;
}
}

View File

@@ -0,0 +1,73 @@
/*
* Copyright 2012-2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.developertools.classpath;
import java.util.Set;
import org.springframework.boot.developertools.filewatch.ChangedFile;
import org.springframework.boot.developertools.filewatch.ChangedFiles;
import org.springframework.boot.developertools.filewatch.FileChangeListener;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.util.Assert;
/**
* A {@link FileChangeListener} to publish {@link ClassPathChangedEvent
* ClassPathChangedEvents}.
*
* @author Phillip Webb
* @since 1.3.0
* @see ClassPathFileSystemWatcher
*/
public class ClassPathFileChangeListener implements FileChangeListener {
private final ApplicationEventPublisher eventPublisher;
private final ClassPathRestartStrategy restartStrategy;
/**
* Create a new {@link ClassPathFileChangeListener} instance.
* @param eventPublisher the event publisher used send events
* @param restartStrategy the restart strategy to use
*/
public ClassPathFileChangeListener(ApplicationEventPublisher eventPublisher,
ClassPathRestartStrategy restartStrategy) {
Assert.notNull(eventPublisher, "EventPublisher must not be null");
Assert.notNull(restartStrategy, "RestartStrategy must not be null");
this.eventPublisher = eventPublisher;
this.restartStrategy = restartStrategy;
}
@Override
public void onChange(Set<ChangedFiles> changeSet) {
boolean restart = isRestartRequired(changeSet);
ApplicationEvent event = new ClassPathChangedEvent(this, changeSet, restart);
this.eventPublisher.publishEvent(event);
}
private boolean isRestartRequired(Set<ChangedFiles> changeSet) {
for (ChangedFiles changedFiles : changeSet) {
for (ChangedFile changedFile : changedFiles) {
if (this.restartStrategy.isRestartRequired(changedFile)) {
return true;
}
}
}
return false;
}
}

View File

@@ -0,0 +1,122 @@
/*
* Copyright 2012-2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.developertools.classpath;
import java.net.URL;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.boot.developertools.filewatch.FileSystemWatcher;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.util.Assert;
import org.springframework.util.ResourceUtils;
/**
* Encapsulates a {@link FileSystemWatcher} to watch the local classpath folders for
* changes.
*
* @author Phillip Webb
* @since 1.3.0
* @see ClassPathFileChangeListener
*/
public class ClassPathFileSystemWatcher implements InitializingBean, DisposableBean,
ApplicationContextAware {
private static final Log logger = LogFactory.getLog(ClassPathFileSystemWatcher.class);
private final FileSystemWatcher fileSystemWatcher;
private ClassPathRestartStrategy restartStrategy;
private ApplicationContext applicationContext;
/**
* Create a new {@link ClassPathFileSystemWatcher} instance.
* @param urls the classpath URLs to watch
*/
public ClassPathFileSystemWatcher(URL[] urls) {
this(new FileSystemWatcher(), null, urls);
}
/**
* Create a new {@link ClassPathFileSystemWatcher} instance.
* @param restartStrategy the classpath restart strategy
* @param urls the URLs to watch
*/
public ClassPathFileSystemWatcher(ClassPathRestartStrategy restartStrategy, URL[] urls) {
this(new FileSystemWatcher(), restartStrategy, urls);
}
/**
* Create a new {@link ClassPathFileSystemWatcher} instance.
* @param fileSystemWatcher the underlying {@link FileSystemWatcher} used to monitor
* the local file system
* @param restartStrategy the classpath restart strategy
* @param urls the URLs to watch
*/
protected ClassPathFileSystemWatcher(FileSystemWatcher fileSystemWatcher,
ClassPathRestartStrategy restartStrategy, URL[] urls) {
Assert.notNull(fileSystemWatcher, "FileSystemWatcher must not be null");
Assert.notNull(urls, "Urls must not be null");
this.fileSystemWatcher = new FileSystemWatcher();
this.restartStrategy = restartStrategy;
addUrls(urls);
}
private void addUrls(URL[] urls) {
for (URL url : urls) {
addUrl(url);
}
}
private void addUrl(URL url) {
if (url.getProtocol().equals("file") && url.getPath().endsWith("/")) {
try {
this.fileSystemWatcher.addSourceFolder(ResourceUtils.getFile(url));
}
catch (Exception ex) {
logger.warn("Unable to watch classpath URL " + url);
logger.trace("Unable to watch classpath URL " + url, ex);
}
}
}
@Override
public void setApplicationContext(ApplicationContext applicationContext)
throws BeansException {
this.applicationContext = applicationContext;
}
@Override
public void afterPropertiesSet() throws Exception {
if (this.restartStrategy != null) {
this.fileSystemWatcher.addListener(new ClassPathFileChangeListener(
this.applicationContext, this.restartStrategy));
}
this.fileSystemWatcher.start();
}
@Override
public void destroy() throws Exception {
this.fileSystemWatcher.stop();
}
}

View File

@@ -0,0 +1,39 @@
/*
* Copyright 2012-2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.developertools.classpath;
import org.springframework.boot.developertools.filewatch.ChangedFile;
/**
* Strategy interface used to determine when a changed classpath file should trigger a
* full application restart. For example, static web resources might not require a full
* restart where as class files would.
*
* @author Phillip Webb
* @since 1.3.0
* @see PatternClassPathRestartStrategy
*/
public interface ClassPathRestartStrategy {
/**
* Return true if a full restart is required.
* @param file the changed file
* @return {@code true} if a full restart is required
*/
boolean isRestartRequired(ChangedFile file);
}

View File

@@ -0,0 +1,51 @@
/*
* Copyright 2012-2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.developertools.classpath;
import org.springframework.boot.developertools.filewatch.ChangedFile;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.StringUtils;
/**
* Ant style pattern based {@link ClassPathRestartStrategy}.
*
* @author Phillip Webb
* @since 1.3.0
* @see ClassPathRestartStrategy
*/
public class PatternClassPathRestartStrategy implements ClassPathRestartStrategy {
private final AntPathMatcher matcher = new AntPathMatcher();
private final String[] excludePatterns;
public PatternClassPathRestartStrategy(String excludePatterns) {
this.excludePatterns = StringUtils
.commaDelimitedListToStringArray(excludePatterns);
}
@Override
public boolean isRestartRequired(ChangedFile file) {
for (String pattern : this.excludePatterns) {
if (this.matcher.match(pattern, file.getRelativeName())) {
return false;
}
}
return true;
}
}

View File

@@ -0,0 +1,21 @@
/*
* Copyright 2012-2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Support for classpath monitoring
*/
package org.springframework.boot.developertools.classpath;