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
pull/3077/merge
Phillip Webb 10 years ago
parent a5c56ca482
commit 3d8db7cddb

@ -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;
}
}
}

@ -16,10 +16,22 @@
package org.springframework.boot.developertools.autoconfigure; 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.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.ConditionalOnInitializedRestarter;
import org.springframework.boot.developertools.restart.Restarter;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.context.event.EventListener;
/** /**
* {@link EnableAutoConfiguration Auto-configuration} for local development support. * {@link EnableAutoConfiguration Auto-configuration} for local development support.
@ -29,11 +41,47 @@ import org.springframework.context.annotation.Configuration;
*/ */
@Configuration @Configuration
@ConditionalOnInitializedRestarter @ConditionalOnInitializedRestarter
@EnableConfigurationProperties(DeveloperToolsProperties.class)
public class LocalDeveloperToolsAutoConfiguration { public class LocalDeveloperToolsAutoConfiguration {
@Autowired
private DeveloperToolsProperties properties;
@Bean @Bean
public static LocalDeveloperPropertyDefaultsPostProcessor localDeveloperPropertyDefaultsPostProcessor() { public static LocalDeveloperPropertyDefaultsPostProcessor localDeveloperPropertyDefaultsPostProcessor() {
return new 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();
}
}
}
} }

@ -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;
}
}

@ -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;
}
}

@ -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();
}
}

@ -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);
}

@ -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;
}
}

@ -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;

@ -24,8 +24,12 @@ import org.junit.After;
import org.junit.Rule; import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.junit.rules.ExpectedException; import org.junit.rules.ExpectedException;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.thymeleaf.ThymeleafAutoConfiguration; import org.springframework.boot.autoconfigure.thymeleaf.ThymeleafAutoConfiguration;
import org.springframework.boot.developertools.classpath.ClassPathChangedEvent;
import org.springframework.boot.developertools.classpath.ClassPathFileSystemWatcher;
import org.springframework.boot.developertools.filewatch.ChangedFiles;
import org.springframework.boot.developertools.restart.MockRestartInitializer; import org.springframework.boot.developertools.restart.MockRestartInitializer;
import org.springframework.boot.developertools.restart.MockRestarter; import org.springframework.boot.developertools.restart.MockRestarter;
import org.springframework.boot.developertools.restart.Restarter; import org.springframework.boot.developertools.restart.Restarter;
@ -36,7 +40,10 @@ import org.springframework.util.SocketUtils;
import org.thymeleaf.templateresolver.TemplateResolver; import org.thymeleaf.templateresolver.TemplateResolver;
import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.notNullValue;
import static org.junit.Assert.assertThat; import static org.junit.Assert.assertThat;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
/** /**
* Tests for {@link LocalDeveloperToolsAutoConfiguration}. * Tests for {@link LocalDeveloperToolsAutoConfiguration}.
@ -70,6 +77,41 @@ public class LocalDeveloperToolsAutoConfigurationTests {
assertThat(resolver.isCacheable(), equalTo(false)); assertThat(resolver.isCacheable(), equalTo(false));
} }
@Test
public void restartTriggerdOnClassPathChangeWithRestart() throws Exception {
this.context = initializeAndRun(Config.class);
ClassPathChangedEvent event = new ClassPathChangedEvent(this.context,
Collections.<ChangedFiles> emptySet(), true);
this.context.publishEvent(event);
verify(this.mockRestarter.getMock()).restart();
}
@Test
public void restartNotTriggerdOnClassPathChangeWithRestart() throws Exception {
this.context = initializeAndRun(Config.class);
ClassPathChangedEvent event = new ClassPathChangedEvent(this.context,
Collections.<ChangedFiles> emptySet(), false);
this.context.publishEvent(event);
verify(this.mockRestarter.getMock(), never()).restart();
}
@Test
public void restartWatchingClassPath() throws Exception {
this.context = initializeAndRun(Config.class);
ClassPathFileSystemWatcher watcher = this.context
.getBean(ClassPathFileSystemWatcher.class);
assertThat(watcher, notNullValue());
}
@Test
public void restartDisabled() throws Exception {
Map<String, Object> properties = new HashMap<String, Object>();
properties.put("spring.developertools.restart.enabled", false);
this.context = initializeAndRun(Config.class, properties);
this.thrown.expect(NoSuchBeanDefinitionException.class);
this.context.getBean(ClassPathFileSystemWatcher.class);
}
private ConfigurableApplicationContext initializeAndRun(Class<?> config) { private ConfigurableApplicationContext initializeAndRun(Class<?> config) {
return initializeAndRun(config, Collections.<String, Object> emptyMap()); return initializeAndRun(config, Collections.<String, Object> emptyMap());
} }

@ -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.LinkedHashSet;
import java.util.Set;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.springframework.boot.developertools.filewatch.ChangedFiles;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.sameInstance;
import static org.junit.Assert.assertThat;
/**
* Tests for {@link ClassPathChangedEvent}.
*
* @author Phillip Webb
*/
public class ClassPathChangedEventTests {
@Rule
public ExpectedException thrown = ExpectedException.none();
private Object source = new Object();
@Test
public void changeSetMustNotBeNull() throws Exception {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("ChangeSet must not be null");
new ClassPathChangedEvent(this.source, null, false);
}
@Test
public void getChangeSet() throws Exception {
Set<ChangedFiles> changeSet = new LinkedHashSet<ChangedFiles>();
ClassPathChangedEvent event = new ClassPathChangedEvent(this.source, changeSet,
false);
assertThat(event.getChangeSet(), sameInstance(changeSet));
}
@Test
public void getRestartRequired() throws Exception {
Set<ChangedFiles> changeSet = new LinkedHashSet<ChangedFiles>();
ClassPathChangedEvent event;
event = new ClassPathChangedEvent(this.source, changeSet, false);
assertThat(event.isRestartRequired(), equalTo(false));
event = new ClassPathChangedEvent(this.source, changeSet, true);
assertThat(event.isRestartRequired(), equalTo(true));
}
}

@ -0,0 +1,109 @@
/*
* 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.io.File;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Set;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.MockitoAnnotations;
import org.springframework.boot.developertools.filewatch.ChangedFile;
import org.springframework.boot.developertools.filewatch.ChangedFiles;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationEventPublisher;
import static org.hamcrest.Matchers.equalTo;
import static org.junit.Assert.assertThat;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
/**
* Tests for {@link ClassPathFileChangeListener}.
*
* @author Phillip Webb
*/
public class ClassPathFileChangeListenerTests {
@Rule
public ExpectedException thrown = ExpectedException.none();
@Captor
private ArgumentCaptor<ApplicationEvent> eventCaptor;
@Before
public void setup() {
MockitoAnnotations.initMocks(this);
}
@Test
public void eventPublisherMustNotBeNull() throws Exception {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("EventPublisher must not be null");
new ClassPathFileChangeListener(null, mock(ClassPathRestartStrategy.class));
}
@Test
public void restartStrategyMustNotBeNull() throws Exception {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("RestartStrategy must not be null");
new ClassPathFileChangeListener(mock(ApplicationEventPublisher.class), null);
}
@Test
public void sendsEventWithoutRestart() throws Exception {
testSendsEvent(false);
}
@Test
public void sendsEventWithRestart() throws Exception {
testSendsEvent(true);
}
private void testSendsEvent(boolean restart) {
ApplicationEventPublisher eventPublisher = mock(ApplicationEventPublisher.class);
ClassPathRestartStrategy restartStrategy = mock(ClassPathRestartStrategy.class);
ClassPathFileChangeListener listener = new ClassPathFileChangeListener(
eventPublisher, restartStrategy);
File folder = new File("s1");
File file = new File("f1");
ChangedFile file1 = new ChangedFile(folder, file, ChangedFile.Type.ADD);
ChangedFile file2 = new ChangedFile(folder, file, ChangedFile.Type.ADD);
Set<ChangedFile> files = new LinkedHashSet<ChangedFile>();
files.add(file1);
files.add(file2);
ChangedFiles changedFiles = new ChangedFiles(new File("source"), files);
Set<ChangedFiles> changeSet = Collections.singleton(changedFiles);
if (restart) {
given(restartStrategy.isRestartRequired(file2)).willReturn(true);
}
listener.onChange(changeSet);
verify(eventPublisher).publishEvent(this.eventCaptor.capture());
ClassPathChangedEvent actualEvent = (ClassPathChangedEvent) this.eventCaptor
.getValue();
assertThat(actualEvent.getChangeSet(), equalTo(changeSet));
assertThat(actualEvent.isRestartRequired(), equalTo(restart));
}
}

@ -0,0 +1,136 @@
/*
* 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.io.File;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.rules.TemporaryFolder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.developertools.filewatch.ChangedFile;
import org.springframework.boot.developertools.filewatch.FileSystemWatcher;
import org.springframework.context.ApplicationListener;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.core.env.MapPropertySource;
import org.springframework.util.FileCopyUtils;
import static org.hamcrest.Matchers.equalTo;
import static org.junit.Assert.assertThat;
/**
* Tests for {@link ClassPathFileSystemWatcher}.
*
* @author Phillip Webb
*/
public class ClassPathFileSystemWatcherTests {
@Rule
public ExpectedException thrown = ExpectedException.none();
@Rule
public TemporaryFolder temp = new TemporaryFolder();
@Test
public void urlsMustNotBeNull() throws Exception {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("Urls must not be null");
URL[] urls = null;
new ClassPathFileSystemWatcher(urls);
}
@Test
public void configuredWithRestartStrategy() throws Exception {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
Map<String, Object> properties = new HashMap<String, Object>();
File folder = this.temp.newFolder();
List<URL> urls = new ArrayList<URL>();
urls.add(new URL("http://spring.io"));
urls.add(folder.toURI().toURL());
properties.put("urls", urls);
MapPropertySource propertySource = new MapPropertySource("test", properties);
context.getEnvironment().getPropertySources().addLast(propertySource);
context.register(Config.class);
context.refresh();
Thread.sleep(100);
File classFile = new File(folder, "Example.class");
FileCopyUtils.copy("file".getBytes(), classFile);
Thread.sleep(1100);
List<ClassPathChangedEvent> events = context.getBean(Listener.class).getEvents();
assertThat(events.size(), equalTo(1));
assertThat(events.get(0).getChangeSet().iterator().next().getFiles().iterator()
.next().getFile(), equalTo(classFile));
context.close();
}
@Configuration
public static class Config {
@Autowired
public Environment environemnt;
@Bean
public ClassPathFileSystemWatcher watcher() {
FileSystemWatcher watcher = new FileSystemWatcher(false, 100, 10);
URL[] urls = this.environemnt.getProperty("urls", URL[].class);
return new ClassPathFileSystemWatcher(watcher, restartStrategy(), urls);
}
@Bean
public ClassPathRestartStrategy restartStrategy() {
return new ClassPathRestartStrategy() {
@Override
public boolean isRestartRequired(ChangedFile file) {
return false;
}
};
}
@Bean
public Listener listener() {
return new Listener();
}
}
public static class Listener implements ApplicationListener<ClassPathChangedEvent> {
private List<ClassPathChangedEvent> events = new ArrayList<ClassPathChangedEvent>();
@Override
public void onApplicationEvent(ClassPathChangedEvent event) {
this.events.add(event);
}
public List<ClassPathChangedEvent> getEvents() {
return this.events;
}
}
}

@ -0,0 +1,80 @@
/*
* 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.io.File;
import org.junit.Test;
import org.springframework.boot.developertools.filewatch.ChangedFile;
import org.springframework.boot.developertools.filewatch.ChangedFile.Type;
import static org.hamcrest.Matchers.equalTo;
import static org.junit.Assert.assertThat;
/**
* Tests for {@link PatternClassPathRestartStrategy}.
*
* @author Phillip Webb
*/
public class PatternClassPathRestartStrategyTests {
@Test
public void nullPattern() throws Exception {
ClassPathRestartStrategy strategy = createStrategy(null);
assertRestartRequired(strategy, "a/b.txt", true);
}
@Test
public void emptyPattern() throws Exception {
ClassPathRestartStrategy strategy = createStrategy("");
assertRestartRequired(strategy, "a/b.txt", true);
}
@Test
public void singlePattern() throws Exception {
ClassPathRestartStrategy strategy = createStrategy("static/**");
assertRestartRequired(strategy, "static/file.txt", false);
assertRestartRequired(strategy, "static/folder/file.txt", false);
assertRestartRequired(strategy, "public/file.txt", true);
assertRestartRequired(strategy, "public/folder/file.txt", true);
}
@Test
public void multiplePatterns() throws Exception {
ClassPathRestartStrategy strategy = createStrategy("static/**,public/**");
assertRestartRequired(strategy, "static/file.txt", false);
assertRestartRequired(strategy, "static/folder/file.txt", false);
assertRestartRequired(strategy, "public/file.txt", false);
assertRestartRequired(strategy, "public/folder/file.txt", false);
assertRestartRequired(strategy, "src/file.txt", true);
assertRestartRequired(strategy, "src/folder/file.txt", true);
}
private ClassPathRestartStrategy createStrategy(String pattern) {
return new PatternClassPathRestartStrategy(pattern);
}
private void assertRestartRequired(ClassPathRestartStrategy strategy,
String relativeName, boolean expected) {
assertThat(strategy.isRestartRequired(mockFile(relativeName)), equalTo(expected));
}
private ChangedFile mockFile(String relativeName) {
return new ChangedFile(new File("."), new File("./" + relativeName), Type.ADD);
}
}
Loading…
Cancel
Save