Relocate projects to spring-boot-project

Move projects to better reflect the way that Spring Boot is released.

The following projects are under `spring-boot-project`:

  - `spring-boot`
  - `spring-boot-autoconfigure`
  - `spring-boot-tools`
  - `spring-boot-starters`
  - `spring-boot-actuator`
  - `spring-boot-actuator-autoconfigure`
  - `spring-boot-test`
  - `spring-boot-test-autoconfigure`
  - `spring-boot-devtools`
  - `spring-boot-cli`
  - `spring-boot-docs`

See gh-9316
This commit is contained in:
Phillip Webb
2017-09-19 14:29:46 -07:00
parent 0419d42b7c
commit 0ba4830b4f
4023 changed files with 0 additions and 0 deletions

View File

@@ -0,0 +1,108 @@
/*
* Copyright 2012-2017 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.devtools;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import org.springframework.boot.Banner;
import org.springframework.boot.ResourceBanner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.context.config.AnsiOutputApplicationListener;
import org.springframework.boot.context.config.ConfigFileApplicationListener;
import org.springframework.boot.context.logging.ClasspathLoggingApplicationListener;
import org.springframework.boot.context.logging.LoggingApplicationListener;
import org.springframework.boot.devtools.remote.client.RemoteClientConfiguration;
import org.springframework.boot.devtools.restart.RestartInitializer;
import org.springframework.boot.devtools.restart.RestartScopeInitializer;
import org.springframework.boot.devtools.restart.Restarter;
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ApplicationListener;
import org.springframework.core.io.ClassPathResource;
/**
* Application that can be used to establish a link to remotely running Spring Boot code.
* Allows remote updates (if enabled). This class should be launched from within your IDE
* and should have the same classpath configuration as the locally developed application.
* The remote URL of the application should be provided as a non-option argument.
*
* @author Phillip Webb
* @since 1.3.0
* @see RemoteClientConfiguration
*/
public final class RemoteSpringApplication {
private RemoteSpringApplication() {
}
private void run(String[] args) {
Restarter.initialize(args, RestartInitializer.NONE);
SpringApplication application = new SpringApplication(
RemoteClientConfiguration.class);
application.setWebApplicationType(WebApplicationType.NONE);
application.setBanner(getBanner());
application.setInitializers(getInitializers());
application.setListeners(getListeners());
application.run(args);
waitIndefinitely();
}
private Collection<ApplicationContextInitializer<?>> getInitializers() {
List<ApplicationContextInitializer<?>> initializers = new ArrayList<>();
initializers.add(new RestartScopeInitializer());
return initializers;
}
private Collection<ApplicationListener<?>> getListeners() {
List<ApplicationListener<?>> listeners = new ArrayList<>();
listeners.add(new AnsiOutputApplicationListener());
listeners.add(new ConfigFileApplicationListener());
listeners.add(new ClasspathLoggingApplicationListener());
listeners.add(new LoggingApplicationListener());
listeners.add(new RemoteUrlPropertyExtractor());
return listeners;
}
private Banner getBanner() {
ClassPathResource banner = new ClassPathResource("remote-banner.txt",
RemoteSpringApplication.class);
return new ResourceBanner(banner);
}
private void waitIndefinitely() {
while (true) {
try {
Thread.sleep(1000);
}
catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
}
}
/**
* Run the {@link RemoteSpringApplication}.
* @param args the program arguments (including the remote URL as a non-option
* argument)
*/
public static void main(String[] args) {
new RemoteSpringApplication().run(args);
}
}

View File

@@ -0,0 +1,75 @@
/*
* 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.devtools;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Collections;
import java.util.Map;
import org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.core.Ordered;
import org.springframework.core.env.CommandLinePropertySource;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.MapPropertySource;
import org.springframework.core.env.PropertySource;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* {@link ApplicationListener} to extract the remote URL for the
* {@link RemoteSpringApplication} to use.
*
* @author Phillip Webb
* @author Andy Wilkinson
*/
class RemoteUrlPropertyExtractor
implements ApplicationListener<ApplicationEnvironmentPreparedEvent>, Ordered {
private static final String NON_OPTION_ARGS = CommandLinePropertySource.DEFAULT_NON_OPTION_ARGS_PROPERTY_NAME;
@Override
public void onApplicationEvent(ApplicationEnvironmentPreparedEvent event) {
ConfigurableEnvironment environment = event.getEnvironment();
String url = cleanRemoteUrl(environment.getProperty(NON_OPTION_ARGS));
Assert.state(StringUtils.hasLength(url), "No remote URL specified");
Assert.state(url.indexOf(",") == -1, "Multiple URLs specified");
try {
new URI(url);
}
catch (URISyntaxException ex) {
throw new IllegalStateException("Malformed URL '" + url + "'");
}
Map<String, Object> source = Collections.singletonMap("remoteUrl", (Object) url);
PropertySource<?> propertySource = new MapPropertySource("remoteUrl", source);
environment.getPropertySources().addLast(propertySource);
}
private String cleanRemoteUrl(String url) {
if (StringUtils.hasText(url) && url.endsWith("/")) {
return url.substring(0, url.length() - 1);
}
return url;
}
@Override
public int getOrder() {
return Ordered.HIGHEST_PRECEDENCE;
}
}

View File

@@ -0,0 +1,186 @@
/*
* Copyright 2012-2017 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.devtools.autoconfigure;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import javax.sql.DataSource;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionMessage;
import org.springframework.boot.autoconfigure.condition.ConditionOutcome;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.SpringBootCondition;
import org.springframework.boot.autoconfigure.data.jpa.EntityManagerFactoryDependsOnPostProcessor;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
import org.springframework.boot.devtools.autoconfigure.DevToolsDataSourceAutoConfiguration.DevToolsDataSourceCondition;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ConfigurationCondition;
import org.springframework.core.type.AnnotatedTypeMetadata;
import org.springframework.orm.jpa.AbstractEntityManagerFactoryBean;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
/**
* {@link EnableAutoConfiguration Auto-configuration} for DevTools-specific
* {@link DataSource} configuration.
*
* @author Andy Wilkinson
* @since 1.3.3
*/
@AutoConfigureAfter(DataSourceAutoConfiguration.class)
@Conditional(DevToolsDataSourceCondition.class)
@Configuration
public class DevToolsDataSourceAutoConfiguration {
@Bean
NonEmbeddedInMemoryDatabaseShutdownExecutor inMemoryDatabaseShutdownExecutor(
DataSource dataSource, DataSourceProperties dataSourceProperties) {
return new NonEmbeddedInMemoryDatabaseShutdownExecutor(dataSource,
dataSourceProperties);
}
/**
* Additional configuration to ensure that
* {@link javax.persistence.EntityManagerFactory} beans depend on the
* {@code inMemoryDatabaseShutdownExecutor} bean.
*/
@Configuration
@ConditionalOnClass(LocalContainerEntityManagerFactoryBean.class)
@ConditionalOnBean(AbstractEntityManagerFactoryBean.class)
static class DatabaseShutdownExecutorJpaDependencyConfiguration
extends EntityManagerFactoryDependsOnPostProcessor {
DatabaseShutdownExecutorJpaDependencyConfiguration() {
super("inMemoryDatabaseShutdownExecutor");
}
}
static final class NonEmbeddedInMemoryDatabaseShutdownExecutor
implements DisposableBean {
private final DataSource dataSource;
private final DataSourceProperties dataSourceProperties;
NonEmbeddedInMemoryDatabaseShutdownExecutor(DataSource dataSource,
DataSourceProperties dataSourceProperties) {
this.dataSource = dataSource;
this.dataSourceProperties = dataSourceProperties;
}
@Override
public void destroy() throws Exception {
if (dataSourceRequiresShutdown()) {
this.dataSource.getConnection().createStatement().execute("SHUTDOWN");
}
}
private boolean dataSourceRequiresShutdown() {
for (InMemoryDatabase inMemoryDatabase : InMemoryDatabase.values()) {
if (inMemoryDatabase.matches(this.dataSourceProperties)) {
return true;
}
}
return false;
}
private enum InMemoryDatabase {
DERBY(null, "org.apache.derby.jdbc.EmbeddedDriver"),
H2("jdbc:h2:mem:", "org.h2.Driver", "org.h2.jdbcx.JdbcDataSource"),
HSQLDB("jdbc:hsqldb:mem:", "org.hsqldb.jdbcDriver",
"org.hsqldb.jdbc.JDBCDriver",
"org.hsqldb.jdbc.pool.JDBCXADataSource");
private final String urlPrefix;
private final Set<String> driverClassNames;
InMemoryDatabase(String urlPrefix, String... driverClassNames) {
this.urlPrefix = urlPrefix;
this.driverClassNames = new HashSet<>(Arrays.asList(driverClassNames));
}
boolean matches(DataSourceProperties properties) {
String url = properties.getUrl();
return (url == null || this.urlPrefix == null
|| url.startsWith(this.urlPrefix))
&& this.driverClassNames
.contains(properties.determineDriverClassName());
}
}
}
static class DevToolsDataSourceCondition extends SpringBootCondition
implements ConfigurationCondition {
@Override
public ConfigurationPhase getConfigurationPhase() {
return ConfigurationPhase.REGISTER_BEAN;
}
@Override
public ConditionOutcome getMatchOutcome(ConditionContext context,
AnnotatedTypeMetadata metadata) {
ConditionMessage.Builder message = ConditionMessage
.forCondition("DevTools DataSource Condition");
String[] dataSourceBeanNames = context.getBeanFactory()
.getBeanNamesForType(DataSource.class);
if (dataSourceBeanNames.length != 1) {
return ConditionOutcome
.noMatch(message.didNotFind("a single DataSource bean").atAll());
}
if (context.getBeanFactory()
.getBeanNamesForType(DataSourceProperties.class).length != 1) {
return ConditionOutcome.noMatch(
message.didNotFind("a single DataSourceProperties bean").atAll());
}
BeanDefinition dataSourceDefinition = context.getRegistry()
.getBeanDefinition(dataSourceBeanNames[0]);
if (dataSourceDefinition instanceof AnnotatedBeanDefinition
&& ((AnnotatedBeanDefinition) dataSourceDefinition)
.getFactoryMethodMetadata() != null
&& ((AnnotatedBeanDefinition) dataSourceDefinition)
.getFactoryMethodMetadata().getDeclaringClassName()
.startsWith(DataSourceAutoConfiguration.class.getPackage()
.getName() + ".DataSourceConfiguration$")) {
return ConditionOutcome
.match(message.foundExactly("auto-configured DataSource"));
}
return ConditionOutcome
.noMatch(message.didNotFind("an auto-configured DataSource").atAll());
}
}
}

View File

@@ -0,0 +1,209 @@
/*
* Copyright 2012-2017 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.devtools.autoconfigure;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.NestedConfigurationProperty;
import org.springframework.util.StringUtils;
/**
* Configuration properties for developer tools.
*
* @author Phillip Webb
* @author Stephane Nicoll
* @since 1.3.0
*/
@ConfigurationProperties(prefix = "spring.devtools")
public class DevToolsProperties {
private Restart restart = new Restart();
private Livereload livereload = new Livereload();
@NestedConfigurationProperty
private RemoteDevToolsProperties remote = new RemoteDevToolsProperties();
public Restart getRestart() {
return this.restart;
}
public Livereload getLivereload() {
return this.livereload;
}
public RemoteDevToolsProperties getRemote() {
return this.remote;
}
/**
* Restart properties.
*/
public static class Restart {
private static final String DEFAULT_RESTART_EXCLUDES = "META-INF/maven/**,"
+ "META-INF/resources/**,resources/**,static/**,public/**,templates/**,"
+ "**/*Test.class,**/*Tests.class,git.properties,META-INF/build-info.properties";
private static final long DEFAULT_RESTART_POLL_INTERVAL = 1000;
private static final long DEFAULT_RESTART_QUIET_PERIOD = 400;
/**
* Enable automatic restart.
*/
private boolean enabled = true;
/**
* Patterns that should be excluded from triggering a full restart.
*/
private String exclude = DEFAULT_RESTART_EXCLUDES;
/**
* Additional patterns that should be excluded from triggering a full restart.
*/
private String additionalExclude;
/**
* Amount of time (in milliseconds) to wait between polling for classpath changes.
*/
private long pollInterval = DEFAULT_RESTART_POLL_INTERVAL;
/**
* Amount of quiet time (in milliseconds) required without any classpath changes
* before a restart is triggered.
*/
private long quietPeriod = DEFAULT_RESTART_QUIET_PERIOD;
/**
* Name of a specific file that when changed will trigger the restart check. If
* not specified any classpath file change will trigger the restart.
*/
private String triggerFile;
/**
* Additional paths to watch for changes.
*/
private List<File> additionalPaths = new ArrayList<>();
public boolean isEnabled() {
return this.enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public String[] getAllExclude() {
List<String> allExclude = new ArrayList<>();
if (StringUtils.hasText(this.exclude)) {
allExclude.addAll(StringUtils.commaDelimitedListToSet(this.exclude));
}
if (StringUtils.hasText(this.additionalExclude)) {
allExclude.addAll(
StringUtils.commaDelimitedListToSet(this.additionalExclude));
}
return allExclude.toArray(new String[allExclude.size()]);
}
public String getExclude() {
return this.exclude;
}
public void setExclude(String exclude) {
this.exclude = exclude;
}
public String getAdditionalExclude() {
return this.additionalExclude;
}
public void setAdditionalExclude(String additionalExclude) {
this.additionalExclude = additionalExclude;
}
public long getPollInterval() {
return this.pollInterval;
}
public void setPollInterval(long pollInterval) {
this.pollInterval = pollInterval;
}
public long getQuietPeriod() {
return this.quietPeriod;
}
public void setQuietPeriod(long quietPeriod) {
this.quietPeriod = quietPeriod;
}
public String getTriggerFile() {
return this.triggerFile;
}
public void setTriggerFile(String triggerFile) {
this.triggerFile = triggerFile;
}
public List<File> getAdditionalPaths() {
return this.additionalPaths;
}
public void setAdditionalPaths(List<File> additionalPaths) {
this.additionalPaths = additionalPaths;
}
}
/**
* LiveReload properties.
*/
public static class Livereload {
/**
* Enable a livereload.com compatible server.
*/
private boolean enabled = true;
/**
* Server port.
*/
private int port = 35729;
public boolean isEnabled() {
return this.enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public int getPort() {
return this.port;
}
public void setPort(int port) {
this.port = port;
}
}
}

View File

@@ -0,0 +1,75 @@
/*
* Copyright 2012-2016 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.devtools.autoconfigure;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import org.springframework.boot.devtools.classpath.ClassPathFolders;
import org.springframework.boot.devtools.filewatch.ChangedFiles;
import org.springframework.boot.devtools.filewatch.FileChangeListener;
import org.springframework.boot.devtools.filewatch.FileSystemWatcher;
import org.springframework.boot.devtools.filewatch.FileSystemWatcherFactory;
import org.springframework.boot.devtools.restart.FailureHandler;
import org.springframework.boot.devtools.restart.Restarter;
/**
* {@link FailureHandler} that waits for filesystem changes before retrying.
*
* @author Phillip Webb
*/
class FileWatchingFailureHandler implements FailureHandler {
private final FileSystemWatcherFactory fileSystemWatcherFactory;
FileWatchingFailureHandler(FileSystemWatcherFactory fileSystemWatcherFactory) {
this.fileSystemWatcherFactory = fileSystemWatcherFactory;
}
@Override
public Outcome handle(Throwable failure) {
CountDownLatch latch = new CountDownLatch(1);
FileSystemWatcher watcher = this.fileSystemWatcherFactory.getFileSystemWatcher();
watcher.addSourceFolders(
new ClassPathFolders(Restarter.getInstance().getInitialUrls()));
watcher.addListener(new Listener(latch));
watcher.start();
try {
latch.await();
}
catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
return Outcome.RETRY;
}
private static class Listener implements FileChangeListener {
private final CountDownLatch latch;
Listener(CountDownLatch latch) {
this.latch = latch;
}
@Override
public void onChange(Set<ChangedFiles> changeSet) {
this.latch.countDown();
}
}
}

View File

@@ -0,0 +1,88 @@
/*
* Copyright 2012-2017 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.devtools.autoconfigure;
import java.lang.reflect.Field;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.util.ClassUtils;
import org.springframework.util.ReflectionUtils;
/**
* Replaces the Objenesis instance in Spring HATEOAS's {@code DummyInvocationUtils} with
* one that does not perform any caching. The cache is problematic as it's keyed on class
* name which leads to {@code ClassCastExceptions} as the class loader changes across
* restarts.
*
* @author Andy Wilkinson
* @since 1.3.0
*/
class HateoasObjenesisCacheDisabler implements InitializingBean {
private static final Log logger = LogFactory
.getLog(HateoasObjenesisCacheDisabler.class);
private static boolean cacheDisabled;
@Override
public void afterPropertiesSet() {
disableCaching();
}
private void disableCaching() {
if (!cacheDisabled) {
cacheDisabled = true;
doDisableCaching();
}
}
private void doDisableCaching() {
try {
Class<?> type = ClassUtils.forName(
"org.springframework.hateoas.core.DummyInvocationUtils",
getClass().getClassLoader());
removeObjenesisCache(type);
}
catch (Exception ex) {
// Assume that Spring HATEOAS is not on the classpath and continue
}
}
private void removeObjenesisCache(Class<?> dummyInvocationUtils) {
try {
Field objenesisField = ReflectionUtils.findField(dummyInvocationUtils,
"OBJENESIS");
if (objenesisField != null) {
ReflectionUtils.makeAccessible(objenesisField);
Object objenesis = ReflectionUtils.getField(objenesisField, null);
Field cacheField = ReflectionUtils.findField(objenesis.getClass(),
"cache");
ReflectionUtils.makeAccessible(cacheField);
ReflectionUtils.setField(cacheField, objenesis, null);
}
}
catch (Exception ex) {
logger.warn(
"Failed to disable Spring HATEOAS's Objenesis cache. ClassCastExceptions may occur",
ex);
}
}
}

View File

@@ -0,0 +1,160 @@
/*
* Copyright 2012-2017 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.devtools.autoconfigure;
import java.io.File;
import java.net.URL;
import java.util.List;
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.devtools.autoconfigure.DevToolsProperties.Restart;
import org.springframework.boot.devtools.classpath.ClassPathChangedEvent;
import org.springframework.boot.devtools.classpath.ClassPathFileSystemWatcher;
import org.springframework.boot.devtools.classpath.ClassPathRestartStrategy;
import org.springframework.boot.devtools.classpath.PatternClassPathRestartStrategy;
import org.springframework.boot.devtools.filewatch.FileSystemWatcher;
import org.springframework.boot.devtools.filewatch.FileSystemWatcherFactory;
import org.springframework.boot.devtools.livereload.LiveReloadServer;
import org.springframework.boot.devtools.restart.ConditionalOnInitializedRestarter;
import org.springframework.boot.devtools.restart.RestartScope;
import org.springframework.boot.devtools.restart.Restarter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.context.event.EventListener;
import org.springframework.util.StringUtils;
/**
* {@link EnableAutoConfiguration Auto-configuration} for local development support.
*
* @author Phillip Webb
* @author Andy Wilkinson
* @author Vladimir Tsanev
* @since 1.3.0
*/
@Configuration
@ConditionalOnInitializedRestarter
@EnableConfigurationProperties(DevToolsProperties.class)
public class LocalDevToolsAutoConfiguration {
/**
* Local LiveReload configuration.
*/
@Configuration
@ConditionalOnProperty(prefix = "spring.devtools.livereload", name = "enabled", matchIfMissing = true)
static class LiveReloadConfiguration {
@Autowired
private DevToolsProperties properties;
@Autowired(required = false)
private LiveReloadServer liveReloadServer;
@Bean
@RestartScope
@ConditionalOnMissingBean
public LiveReloadServer liveReloadServer() {
return new LiveReloadServer(this.properties.getLivereload().getPort(),
Restarter.getInstance().getThreadFactory());
}
@EventListener
public void onContextRefreshed(ContextRefreshedEvent event) {
optionalLiveReloadServer().triggerReload();
}
@EventListener
public void onClassPathChanged(ClassPathChangedEvent event) {
if (!event.isRestartRequired()) {
optionalLiveReloadServer().triggerReload();
}
}
@Bean
public OptionalLiveReloadServer optionalLiveReloadServer() {
return new OptionalLiveReloadServer(this.liveReloadServer);
}
}
/**
* Local Restart Configuration.
*/
@ConditionalOnProperty(prefix = "spring.devtools.restart", name = "enabled", matchIfMissing = true)
static class RestartConfiguration {
@Autowired
private DevToolsProperties properties;
@EventListener
public void onClassPathChanged(ClassPathChangedEvent event) {
if (event.isRestartRequired()) {
Restarter.getInstance().restart(
new FileWatchingFailureHandler(fileSystemWatcherFactory()));
}
}
@Bean
@ConditionalOnMissingBean
public ClassPathFileSystemWatcher classPathFileSystemWatcher() {
URL[] urls = Restarter.getInstance().getInitialUrls();
ClassPathFileSystemWatcher watcher = new ClassPathFileSystemWatcher(
fileSystemWatcherFactory(), classPathRestartStrategy(), urls);
watcher.setStopWatcherOnRestart(true);
return watcher;
}
@Bean
@ConditionalOnMissingBean
public ClassPathRestartStrategy classPathRestartStrategy() {
return new PatternClassPathRestartStrategy(
this.properties.getRestart().getAllExclude());
}
@Bean
public HateoasObjenesisCacheDisabler hateoasObjenesisCacheDisabler() {
return new HateoasObjenesisCacheDisabler();
}
@Bean
public FileSystemWatcherFactory fileSystemWatcherFactory() {
return this::newFileSystemWatcher;
}
private FileSystemWatcher newFileSystemWatcher() {
Restart restartProperties = this.properties.getRestart();
FileSystemWatcher watcher = new FileSystemWatcher(true,
restartProperties.getPollInterval(),
restartProperties.getQuietPeriod());
String triggerFile = restartProperties.getTriggerFile();
if (StringUtils.hasLength(triggerFile)) {
watcher.setTriggerFilter(new TriggerFileFilter(triggerFile));
}
List<File> additionalPaths = restartProperties.getAdditionalPaths();
for (File path : additionalPaths) {
watcher.addSourceFolder(path.getAbsoluteFile());
}
return watcher;
}
}
}

View File

@@ -0,0 +1,77 @@
/*
* Copyright 2012-2017 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.devtools.autoconfigure;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.boot.devtools.livereload.LiveReloadServer;
/**
* Manages an optional {@link LiveReloadServer}. The {@link LiveReloadServer} may
* gracefully fail to start (e.g. because of a port conflict) or may be omitted entirely.
*
* @author Phillip Webb
* @since 1.3.0
*/
public class OptionalLiveReloadServer implements InitializingBean {
private static final Log logger = LogFactory.getLog(OptionalLiveReloadServer.class);
private LiveReloadServer server;
/**
* Create a new {@link OptionalLiveReloadServer} instance.
* @param server the server to manage or {@code null}
*/
public OptionalLiveReloadServer(LiveReloadServer server) {
this.server = server;
}
@Override
public void afterPropertiesSet() throws Exception {
startServer();
}
void startServer() throws Exception {
if (this.server != null) {
try {
if (!this.server.isStarted()) {
this.server.start();
}
logger.info(
"LiveReload server is running on port " + this.server.getPort());
}
catch (Exception ex) {
logger.warn("Unable to start LiveReload server");
logger.debug("Live reload start error", ex);
this.server = null;
}
}
}
/**
* Trigger LiveReload if the server is up an running.
*/
public void triggerReload() {
if (this.server != null) {
this.server.triggerReload();
}
}
}

View File

@@ -0,0 +1,140 @@
/*
* Copyright 2012-2017 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.devtools.autoconfigure;
import java.util.Collection;
import javax.servlet.Filter;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.web.ServerProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.devtools.remote.server.AccessManager;
import org.springframework.boot.devtools.remote.server.Dispatcher;
import org.springframework.boot.devtools.remote.server.DispatcherFilter;
import org.springframework.boot.devtools.remote.server.Handler;
import org.springframework.boot.devtools.remote.server.HandlerMapper;
import org.springframework.boot.devtools.remote.server.HttpHeaderAccessManager;
import org.springframework.boot.devtools.remote.server.HttpStatusHandler;
import org.springframework.boot.devtools.remote.server.UrlHandlerMapper;
import org.springframework.boot.devtools.restart.server.DefaultSourceFolderUrlFilter;
import org.springframework.boot.devtools.restart.server.HttpRestartServer;
import org.springframework.boot.devtools.restart.server.HttpRestartServerHandler;
import org.springframework.boot.devtools.restart.server.SourceFolderUrlFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.server.ServerHttpRequest;
/**
* {@link EnableAutoConfiguration Auto-configuration} for remote development support.
*
* @author Phillip Webb
* @author Rob Winch
* @author Andy Wilkinson
* @since 1.3.0
*/
@Configuration
@ConditionalOnProperty(prefix = "spring.devtools.remote", name = "secret")
@ConditionalOnClass({ Filter.class, ServerHttpRequest.class })
@EnableConfigurationProperties({ ServerProperties.class, DevToolsProperties.class })
public class RemoteDevToolsAutoConfiguration {
private static final Log logger = LogFactory
.getLog(RemoteDevToolsAutoConfiguration.class);
private final DevToolsProperties properties;
private final ServerProperties serverProperties;
public RemoteDevToolsAutoConfiguration(DevToolsProperties properties,
ServerProperties serverProperties) {
this.properties = properties;
this.serverProperties = serverProperties;
}
@Bean
@ConditionalOnMissingBean
public AccessManager remoteDevToolsAccessManager() {
RemoteDevToolsProperties remoteProperties = this.properties.getRemote();
return new HttpHeaderAccessManager(remoteProperties.getSecretHeaderName(),
remoteProperties.getSecret());
}
@Bean
public HandlerMapper remoteDevToolsHealthCheckHandlerMapper() {
Handler handler = new HttpStatusHandler();
return new UrlHandlerMapper(
(this.serverProperties.getServlet().getContextPath() == null ? ""
: this.serverProperties.getServlet().getContextPath())
+ this.properties.getRemote().getContextPath(),
handler);
}
@Bean
@ConditionalOnMissingBean
public DispatcherFilter remoteDevToolsDispatcherFilter(AccessManager accessManager,
Collection<HandlerMapper> mappers) {
Dispatcher dispatcher = new Dispatcher(accessManager, mappers);
return new DispatcherFilter(dispatcher);
}
/**
* Configuration for remote update and restarts.
*/
@ConditionalOnProperty(prefix = "spring.devtools.remote.restart", name = "enabled", matchIfMissing = true)
static class RemoteRestartConfiguration {
@Autowired
private DevToolsProperties properties;
@Autowired
private ServerProperties serverProperties;
@Bean
@ConditionalOnMissingBean
public SourceFolderUrlFilter remoteRestartSourceFolderUrlFilter() {
return new DefaultSourceFolderUrlFilter();
}
@Bean
@ConditionalOnMissingBean
public HttpRestartServer remoteRestartHttpRestartServer(
SourceFolderUrlFilter sourceFolderUrlFilter) {
return new HttpRestartServer(sourceFolderUrlFilter);
}
@Bean
@ConditionalOnMissingBean(name = "remoteRestartHandlerMapper")
public UrlHandlerMapper remoteRestartHandlerMapper(HttpRestartServer server) {
String url = (this.serverProperties.getServlet().getContextPath() == null ? ""
: this.serverProperties.getServlet().getContextPath())
+ this.properties.getRemote().getContextPath() + "/restart";
logger.warn("Listening for remote restart updates on " + url);
Handler handler = new HttpRestartServerHandler(server);
return new UrlHandlerMapper(url, handler);
}
}
}

View File

@@ -0,0 +1,132 @@
/*
* Copyright 2012-2017 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.devtools.autoconfigure;
/**
* Configuration properties for remote Spring Boot applications.
*
* @author Phillip Webb
* @author Rob Winch
* @since 1.3.0
* @see DevToolsProperties
*/
public class RemoteDevToolsProperties {
public static final String DEFAULT_CONTEXT_PATH = "/.~~spring-boot!~";
public static final String DEFAULT_SECRET_HEADER_NAME = "X-AUTH-TOKEN";
/**
* Context path used to handle the remote connection.
*/
private String contextPath = DEFAULT_CONTEXT_PATH;
/**
* A shared secret required to establish a connection (required to enable remote
* support).
*/
private String secret;
/**
* HTTP header used to transfer the shared secret.
*/
private String secretHeaderName = DEFAULT_SECRET_HEADER_NAME;
private Restart restart = new Restart();
private Proxy proxy = new Proxy();
public String getContextPath() {
return this.contextPath;
}
public void setContextPath(String contextPath) {
this.contextPath = contextPath;
}
public String getSecret() {
return this.secret;
}
public void setSecret(String secret) {
this.secret = secret;
}
public String getSecretHeaderName() {
return this.secretHeaderName;
}
public void setSecretHeaderName(String secretHeaderName) {
this.secretHeaderName = secretHeaderName;
}
public Restart getRestart() {
return this.restart;
}
public Proxy getProxy() {
return this.proxy;
}
public static class Restart {
/**
* Enable remote restart.
*/
private boolean enabled = true;
public boolean isEnabled() {
return this.enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
}
public static class Proxy {
/**
* The host of the proxy to use to connect to the remote application.
*/
private String host;
/**
* The port of the proxy to use to connect to the remote application.
*/
private Integer port;
public String getHost() {
return this.host;
}
public void setHost(String host) {
this.host = host;
}
public Integer getPort() {
return this.port;
}
public void setPort(Integer port) {
this.port = port;
}
}
}

View File

@@ -0,0 +1,44 @@
/*
* 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.devtools.autoconfigure;
import java.io.File;
import java.io.FileFilter;
import org.springframework.util.Assert;
/**
* {@link FileFilter} that accepts only a specific "trigger" file.
*
* @author Phillip Webb
* @since 1.3.0
*/
public class TriggerFileFilter implements FileFilter {
private final String name;
public TriggerFileFilter(String name) {
Assert.notNull(name, "Name must not be null");
this.name = name;
}
@Override
public boolean accept(File file) {
return file.getName().equals(this.name);
}
}

View File

@@ -0,0 +1,20 @@
/*
* 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.
*/
/**
* Auto-configuration for {@code spring-boot-devtools}.
*/
package org.springframework.boot.devtools.autoconfigure;

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.devtools.classpath;
import java.util.Set;
import org.springframework.boot.devtools.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,88 @@
/*
* 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.devtools.classpath;
import java.util.Set;
import org.springframework.boot.devtools.filewatch.ChangedFile;
import org.springframework.boot.devtools.filewatch.ChangedFiles;
import org.springframework.boot.devtools.filewatch.FileChangeListener;
import org.springframework.boot.devtools.filewatch.FileSystemWatcher;
import org.springframework.boot.devtools.restart.AgentReloader;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.util.Assert;
/**
* A {@link FileChangeListener} to publish {@link ClassPathChangedEvent
* ClassPathChangedEvents}.
*
* @author Phillip Webb
* @see ClassPathFileSystemWatcher
*/
class ClassPathFileChangeListener implements FileChangeListener {
private final ApplicationEventPublisher eventPublisher;
private final ClassPathRestartStrategy restartStrategy;
private final FileSystemWatcher fileSystemWatcherToStop;
/**
* Create a new {@link ClassPathFileChangeListener} instance.
* @param eventPublisher the event publisher used send events
* @param restartStrategy the restart strategy to use
* @param fileSystemWatcherToStop the file system watcher to stop on a restart (or
* {@code null})
*/
ClassPathFileChangeListener(ApplicationEventPublisher eventPublisher,
ClassPathRestartStrategy restartStrategy,
FileSystemWatcher fileSystemWatcherToStop) {
Assert.notNull(eventPublisher, "EventPublisher must not be null");
Assert.notNull(restartStrategy, "RestartStrategy must not be null");
this.eventPublisher = eventPublisher;
this.restartStrategy = restartStrategy;
this.fileSystemWatcherToStop = fileSystemWatcherToStop;
}
@Override
public void onChange(Set<ChangedFiles> changeSet) {
boolean restart = isRestartRequired(changeSet);
publishEvent(new ClassPathChangedEvent(this, changeSet, restart));
}
private void publishEvent(ClassPathChangedEvent event) {
this.eventPublisher.publishEvent(event);
if (event.isRestartRequired() && this.fileSystemWatcherToStop != null) {
this.fileSystemWatcherToStop.stop();
}
}
private boolean isRestartRequired(Set<ChangedFiles> changeSet) {
if (AgentReloader.isActive()) {
return false;
}
for (ChangedFiles changedFiles : changeSet) {
for (ChangedFile changedFile : changedFiles) {
if (this.restartStrategy.isRestartRequired(changedFile)) {
return true;
}
}
}
return false;
}
}

View File

@@ -0,0 +1,98 @@
/*
* 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.devtools.classpath;
import java.net.URL;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.boot.devtools.filewatch.FileSystemWatcher;
import org.springframework.boot.devtools.filewatch.FileSystemWatcherFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.util.Assert;
/**
* 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 final FileSystemWatcher fileSystemWatcher;
private ClassPathRestartStrategy restartStrategy;
private ApplicationContext applicationContext;
private boolean stopWatcherOnRestart;
/**
* Create a new {@link ClassPathFileSystemWatcher} instance.
* @param fileSystemWatcherFactory a factory to create the underlying
* {@link FileSystemWatcher} used to monitor the local file system
* @param restartStrategy the classpath restart strategy
* @param urls the URLs to watch
*/
public ClassPathFileSystemWatcher(FileSystemWatcherFactory fileSystemWatcherFactory,
ClassPathRestartStrategy restartStrategy, URL[] urls) {
Assert.notNull(fileSystemWatcherFactory,
"FileSystemWatcherFactory must not be null");
Assert.notNull(urls, "Urls must not be null");
this.fileSystemWatcher = fileSystemWatcherFactory.getFileSystemWatcher();
this.restartStrategy = restartStrategy;
this.fileSystemWatcher.addSourceFolders(new ClassPathFolders(urls));
}
/**
* Set if the {@link FileSystemWatcher} should be stopped when a full restart occurs.
* @param stopWatcherOnRestart if the watcher should be stopped when a restart occurs
*/
public void setStopWatcherOnRestart(boolean stopWatcherOnRestart) {
this.stopWatcherOnRestart = stopWatcherOnRestart;
}
@Override
public void setApplicationContext(ApplicationContext applicationContext)
throws BeansException {
this.applicationContext = applicationContext;
}
@Override
public void afterPropertiesSet() throws Exception {
if (this.restartStrategy != null) {
FileSystemWatcher watcherToStop = null;
if (this.stopWatcherOnRestart) {
watcherToStop = this.fileSystemWatcher;
}
this.fileSystemWatcher.addListener(new ClassPathFileChangeListener(
this.applicationContext, this.restartStrategy, watcherToStop));
}
this.fileSystemWatcher.start();
}
@Override
public void destroy() throws Exception {
this.fileSystemWatcher.stop();
}
}

View File

@@ -0,0 +1,72 @@
/*
* Copyright 2012-2017 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.devtools.classpath;
import java.io.File;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.util.ResourceUtils;
/**
* Provides access to entries on the classpath that refer to folders.
*
* @author Phillip Webb
* @since 1.3.0
*/
public class ClassPathFolders implements Iterable<File> {
private static final Log logger = LogFactory.getLog(ClassPathFolders.class);
private final List<File> folders = new ArrayList<>();
public ClassPathFolders(URL[] urls) {
if (urls != null) {
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.folders.add(ResourceUtils.getFile(url));
}
catch (Exception ex) {
logger.warn("Unable to get classpath URL " + url);
logger.trace("Unable to get classpath URL " + url, ex);
}
}
}
@Override
public Iterator<File> iterator() {
return Collections.unmodifiableList(this.folders).iterator();
}
}

View File

@@ -0,0 +1,40 @@
/*
* Copyright 2012-2017 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.devtools.classpath;
import org.springframework.boot.devtools.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
*/
@FunctionalInterface
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,54 @@
/*
* 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.devtools.classpath;
import org.springframework.boot.devtools.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 = excludePatterns;
}
public PatternClassPathRestartStrategy(String excludePatterns) {
this(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,20 @@
/*
* 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.devtools.classpath;

View File

@@ -0,0 +1,70 @@
/*
* 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.devtools.env;
import java.io.File;
import java.io.IOException;
import java.util.Properties;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.env.EnvironmentPostProcessor;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.PropertiesPropertySource;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.support.PropertiesLoaderUtils;
import org.springframework.util.StringUtils;
/**
* {@link EnvironmentPostProcessor} to add devtools properties from the user's home
* folder.
*
* @author Phillip Webb
* @author Andy Wilkinson
* @since 1.3.0
*/
public class DevToolsHomePropertiesPostProcessor implements EnvironmentPostProcessor {
private static final String FILE_NAME = ".spring-boot-devtools.properties";
@Override
public void postProcessEnvironment(ConfigurableEnvironment environment,
SpringApplication application) {
File home = getHomeFolder();
File propertyFile = (home == null ? null : new File(home, FILE_NAME));
if (propertyFile != null && propertyFile.exists() && propertyFile.isFile()) {
FileSystemResource resource = new FileSystemResource(propertyFile);
Properties properties;
try {
properties = PropertiesLoaderUtils.loadProperties(resource);
environment.getPropertySources().addFirst(
new PropertiesPropertySource("devtools-local", properties));
}
catch (IOException ex) {
throw new IllegalStateException("Unable to load " + FILE_NAME, ex);
}
}
}
protected File getHomeFolder() {
String home = System.getProperty("user.home");
if (StringUtils.hasLength(home)) {
return new File(home);
}
return null;
}
}

View File

@@ -0,0 +1,96 @@
/*
* Copyright 2012-2017 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.devtools.env;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.devtools.restart.Restarter;
import org.springframework.boot.env.EnvironmentPostProcessor;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.Environment;
import org.springframework.core.env.MapPropertySource;
import org.springframework.core.env.PropertySource;
/**
* {@link EnvironmentPostProcessor} to add properties that make sense when working at
* development time.
*
* @author Phillip Webb
* @author Andy Wilkinson
* @author Madhura Bhave
* @since 1.3.0
*/
@Order(Ordered.LOWEST_PRECEDENCE)
public class DevToolsPropertyDefaultsPostProcessor implements EnvironmentPostProcessor {
private static final Map<String, Object> properties;
static {
Map<String, Object> devToolsProperties = new HashMap<>();
devToolsProperties.put("spring.thymeleaf.cache", "false");
devToolsProperties.put("spring.freemarker.cache", "false");
devToolsProperties.put("spring.groovy.template.cache", "false");
devToolsProperties.put("spring.mustache.cache", "false");
devToolsProperties.put("server.session.persistent", "true");
devToolsProperties.put("spring.h2.console.enabled", "true");
devToolsProperties.put("spring.resources.cache-period", "0");
devToolsProperties.put("spring.resources.chain.cache", "false");
devToolsProperties.put("spring.template.provider.cache", "false");
devToolsProperties.put("spring.mvc.log-resolved-exception", "true");
devToolsProperties.put("server.servlet.jsp.init-parameters.development", "true");
devToolsProperties.put("spring.reactor.stacktrace-mode.enabled", "true");
properties = Collections.unmodifiableMap(devToolsProperties);
}
@Override
public void postProcessEnvironment(ConfigurableEnvironment environment,
SpringApplication application) {
if (isLocalApplication(environment) && canAddProperties(environment)) {
PropertySource<?> propertySource = new MapPropertySource("refresh",
properties);
environment.getPropertySources().addLast(propertySource);
}
}
private boolean isLocalApplication(ConfigurableEnvironment environment) {
return environment.getPropertySources().get("remoteUrl") == null;
}
private boolean canAddProperties(Environment environment) {
return isRestarterInitialized() || isRemoteRestartEnabled(environment);
}
private boolean isRestarterInitialized() {
try {
Restarter restarter = Restarter.getInstance();
return (restarter != null && restarter.getInitialUrls() != null);
}
catch (Exception ex) {
return false;
}
}
private boolean isRemoteRestartEnabled(Environment environment) {
return environment.containsProperty("spring.devtools.remote.secret");
}
}

View File

@@ -0,0 +1,131 @@
/*
* 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.devtools.filewatch;
import java.io.File;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* A single file that has changed.
*
* @author Phillip Webb
* @since 1.3.0
* @see ChangedFiles
*/
public final class ChangedFile {
private final File sourceFolder;
private final File file;
private final Type type;
/**
* Create a new {@link ChangedFile} instance.
* @param sourceFolder the source folder
* @param file the file
* @param type the type of change
*/
public ChangedFile(File sourceFolder, File file, Type type) {
Assert.notNull(sourceFolder, "SourceFolder must not be null");
Assert.notNull(file, "File must not be null");
Assert.notNull(type, "Type must not be null");
this.sourceFolder = sourceFolder;
this.file = file;
this.type = type;
}
/**
* Return the file that was changed.
* @return the file
*/
public File getFile() {
return this.file;
}
/**
* Return the type of change.
* @return the type of change
*/
public Type getType() {
return this.type;
}
/**
* Return the name of the file relative to the source folder.
* @return the relative name
*/
public String getRelativeName() {
File folder = this.sourceFolder.getAbsoluteFile();
File file = this.file.getAbsoluteFile();
String folderName = StringUtils.cleanPath(folder.getPath());
String fileName = StringUtils.cleanPath(file.getPath());
Assert.state(fileName.startsWith(folderName), "The file " + fileName
+ " is not contained in the source folder " + folderName);
return fileName.substring(folderName.length() + 1);
}
@Override
public int hashCode() {
return this.file.hashCode() * 31 + this.type.hashCode();
}
@Override
public boolean equals(Object obj) {
if (obj == this) {
return true;
}
if (obj == null) {
return false;
}
if (obj instanceof ChangedFile) {
ChangedFile other = (ChangedFile) obj;
return this.file.equals(other.file) && this.type.equals(other.type);
}
return super.equals(obj);
}
@Override
public String toString() {
return this.file + " (" + this.type + ")";
}
/**
* Change types.
*/
public enum Type {
/**
* A new file has been added.
*/
ADD,
/**
* An existing file has been modified.
*/
MODIFY,
/**
* An existing file has been deleted.
*/
DELETE
}
}

View File

@@ -0,0 +1,90 @@
/*
* Copyright 2012-2016 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.devtools.filewatch;
import java.io.File;
import java.util.Collections;
import java.util.Iterator;
import java.util.Set;
/**
* A collections of files from a specific source folder that have changed.
*
* @author Phillip Webb
* @since 1.3.0
* @see FileChangeListener
* @see ChangedFiles
*/
public final class ChangedFiles implements Iterable<ChangedFile> {
private final File sourceFolder;
private final Set<ChangedFile> files;
public ChangedFiles(File sourceFolder, Set<ChangedFile> files) {
this.sourceFolder = sourceFolder;
this.files = Collections.unmodifiableSet(files);
}
/**
* The source folder being watched.
* @return the source folder
*/
public File getSourceFolder() {
return this.sourceFolder;
}
@Override
public Iterator<ChangedFile> iterator() {
return getFiles().iterator();
}
/**
* The files that have been changed.
* @return the changed files
*/
public Set<ChangedFile> getFiles() {
return this.files;
}
@Override
public int hashCode() {
return this.files.hashCode();
}
@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (obj == this) {
return true;
}
if (obj instanceof ChangedFiles) {
ChangedFiles other = (ChangedFiles) obj;
return this.sourceFolder.equals(other.sourceFolder)
&& this.files.equals(other.files);
}
return super.equals(obj);
}
@Override
public String toString() {
return this.sourceFolder + " " + this.files;
}
}

View File

@@ -0,0 +1,37 @@
/*
* Copyright 2012-2017 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.devtools.filewatch;
import java.util.Set;
/**
* Callback interface when file changes are detected.
*
* @author Andy Clement
* @author Phillip Webb
* @since 1.3.0
*/
@FunctionalInterface
public interface FileChangeListener {
/**
* Called when files have been changed.
* @param changeSet a set of the {@link ChangedFiles}
*/
void onChange(Set<ChangedFiles> changeSet);
}

View File

@@ -0,0 +1,84 @@
/*
* Copyright 2012-2017 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.devtools.filewatch;
import java.io.File;
import org.springframework.util.Assert;
/**
* A snapshot of a File at a given point in time.
*
* @author Phillip Webb
*/
class FileSnapshot {
private final File file;
private final boolean exists;
private final long length;
private final long lastModified;
FileSnapshot(File file) {
Assert.notNull(file, "File must not be null");
Assert.isTrue(file.isFile() || !file.exists(), "File must not be a folder");
this.file = file;
this.exists = file.exists();
this.length = file.length();
this.lastModified = file.lastModified();
}
public File getFile() {
return this.file;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (obj instanceof FileSnapshot) {
FileSnapshot other = (FileSnapshot) obj;
boolean equals = this.file.equals(other.file);
equals = equals && this.exists == other.exists;
equals = equals && this.length == other.length;
equals = equals && this.lastModified == other.lastModified;
return equals;
}
return super.equals(obj);
}
@Override
public int hashCode() {
int hashCode = this.file.hashCode();
hashCode = 31 * hashCode + Boolean.hashCode(this.exists);
hashCode = 31 * hashCode + Long.hashCode(this.length);
hashCode = 31 * hashCode + Long.hashCode(this.lastModified);
return hashCode;
}
@Override
public String toString() {
return this.file.toString();
}
}

View File

@@ -0,0 +1,312 @@
/*
* Copyright 2012-2017 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.devtools.filewatch;
import java.io.File;
import java.io.FileFilter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import org.springframework.util.Assert;
/**
* Watches specific folders for file changes.
*
* @author Andy Clement
* @author Phillip Webb
* @since 1.3.0
* @see FileChangeListener
*/
public class FileSystemWatcher {
private static final long DEFAULT_POLL_INTERVAL = 1000;
private static final long DEFAULT_QUIET_PERIOD = 400;
private final List<FileChangeListener> listeners = new ArrayList<>();
private final boolean daemon;
private final long pollInterval;
private final long quietPeriod;
private final AtomicInteger remainingScans = new AtomicInteger(-1);
private final Map<File, FolderSnapshot> folders = new HashMap<>();
private Thread watchThread;
private FileFilter triggerFilter;
private final Object monitor = new Object();
/**
* Create a new {@link FileSystemWatcher} instance.
*/
public FileSystemWatcher() {
this(true, DEFAULT_POLL_INTERVAL, DEFAULT_QUIET_PERIOD);
}
/**
* Create a new {@link FileSystemWatcher} instance.
* @param daemon if a daemon thread used to monitor changes
* @param pollInterval the amount of time to wait between checking for changes
* @param quietPeriod the amount of time required after a change has been detected to
* ensure that updates have completed
*/
public FileSystemWatcher(boolean daemon, long pollInterval, long quietPeriod) {
Assert.isTrue(pollInterval > 0, "PollInterval must be positive");
Assert.isTrue(quietPeriod > 0, "QuietPeriod must be positive");
Assert.isTrue(pollInterval > quietPeriod,
"PollInterval must be greater than QuietPeriod");
this.daemon = daemon;
this.pollInterval = pollInterval;
this.quietPeriod = quietPeriod;
}
/**
* Add listener for file change events. Cannot be called after the watcher has been
* {@link #start() started}.
* @param fileChangeListener the listener to add
*/
public void addListener(FileChangeListener fileChangeListener) {
Assert.notNull(fileChangeListener, "FileChangeListener must not be null");
synchronized (this.monitor) {
checkNotStarted();
this.listeners.add(fileChangeListener);
}
}
/**
* Add source folders to monitor. Cannot be called after the watcher has been
* {@link #start() started}.
* @param folders the folders to monitor
*/
public void addSourceFolders(Iterable<File> folders) {
Assert.notNull(folders, "Folders must not be null");
synchronized (this.monitor) {
for (File folder : folders) {
addSourceFolder(folder);
}
}
}
/**
* Add a source folder to monitor. Cannot be called after the watcher has been
* {@link #start() started}.
* @param folder the folder to monitor
*/
public void addSourceFolder(File folder) {
Assert.notNull(folder, "Folder must not be null");
Assert.isTrue(folder.isDirectory(),
"Folder '" + folder + "' must exist and must" + " be a directory");
synchronized (this.monitor) {
checkNotStarted();
this.folders.put(folder, null);
}
}
/**
* Set an optional {@link FileFilter} used to limit the files that trigger a change.
* @param triggerFilter a trigger filter or null
*/
public void setTriggerFilter(FileFilter triggerFilter) {
synchronized (this.monitor) {
this.triggerFilter = triggerFilter;
}
}
private void checkNotStarted() {
synchronized (this.monitor) {
Assert.state(this.watchThread == null, "FileSystemWatcher already started");
}
}
/**
* Start monitoring the source folder for changes.
*/
public void start() {
synchronized (this.monitor) {
saveInitialSnapshots();
if (this.watchThread == null) {
Map<File, FolderSnapshot> localFolders = new HashMap<>();
localFolders.putAll(this.folders);
this.watchThread = new Thread(new Watcher(this.remainingScans,
new ArrayList<>(this.listeners), this.triggerFilter,
this.pollInterval, this.quietPeriod, localFolders));
this.watchThread.setName("File Watcher");
this.watchThread.setDaemon(this.daemon);
this.watchThread.start();
}
}
}
private void saveInitialSnapshots() {
for (File folder : this.folders.keySet()) {
this.folders.put(folder, new FolderSnapshot(folder));
}
}
/**
* Stop monitoring the source folders.
*/
public void stop() {
stopAfter(0);
}
/**
* Stop monitoring the source folders.
* @param remainingScans the number of remaining scans
*/
void stopAfter(int remainingScans) {
Thread thread = null;
synchronized (this.monitor) {
thread = this.watchThread;
if (thread != null) {
this.remainingScans.set(remainingScans);
if (remainingScans <= 0) {
thread.interrupt();
}
}
this.watchThread = null;
}
if (Thread.currentThread() != thread) {
try {
thread.join();
}
catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
}
}
private static final class Watcher implements Runnable {
private final AtomicInteger remainingScans;
private final List<FileChangeListener> listeners;
private final FileFilter triggerFilter;
private final long pollInterval;
private final long quietPeriod;
private Map<File, FolderSnapshot> folders;
private Watcher(AtomicInteger remainingScans, List<FileChangeListener> listeners,
FileFilter triggerFilter, long pollInterval, long quietPeriod,
Map<File, FolderSnapshot> folders) {
this.remainingScans = remainingScans;
this.listeners = listeners;
this.triggerFilter = triggerFilter;
this.pollInterval = pollInterval;
this.quietPeriod = quietPeriod;
this.folders = folders;
}
@Override
public void run() {
int remainingScans = this.remainingScans.get();
while (remainingScans > 0 || remainingScans == -1) {
try {
if (remainingScans > 0) {
this.remainingScans.decrementAndGet();
}
scan();
}
catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
remainingScans = this.remainingScans.get();
}
}
private void scan() throws InterruptedException {
Thread.sleep(this.pollInterval - this.quietPeriod);
Map<File, FolderSnapshot> previous;
Map<File, FolderSnapshot> current = this.folders;
do {
previous = current;
current = getCurrentSnapshots();
Thread.sleep(this.quietPeriod);
}
while (isDifferent(previous, current));
if (isDifferent(this.folders, current)) {
updateSnapshots(current.values());
}
}
private boolean isDifferent(Map<File, FolderSnapshot> previous,
Map<File, FolderSnapshot> current) {
if (!previous.keySet().equals(current.keySet())) {
return true;
}
for (Map.Entry<File, FolderSnapshot> entry : previous.entrySet()) {
FolderSnapshot previousFolder = entry.getValue();
FolderSnapshot currentFolder = current.get(entry.getKey());
if (!previousFolder.equals(currentFolder, this.triggerFilter)) {
return true;
}
}
return false;
}
private Map<File, FolderSnapshot> getCurrentSnapshots() {
Map<File, FolderSnapshot> snapshots = new LinkedHashMap<>();
for (File folder : this.folders.keySet()) {
snapshots.put(folder, new FolderSnapshot(folder));
}
return snapshots;
}
private void updateSnapshots(Collection<FolderSnapshot> snapshots) {
Map<File, FolderSnapshot> updated = new LinkedHashMap<>();
Set<ChangedFiles> changeSet = new LinkedHashSet<>();
for (FolderSnapshot snapshot : snapshots) {
FolderSnapshot previous = this.folders.get(snapshot.getFolder());
updated.put(snapshot.getFolder(), snapshot);
ChangedFiles changedFiles = previous.getChangedFiles(snapshot,
this.triggerFilter);
if (!changedFiles.getFiles().isEmpty()) {
changeSet.add(changedFiles);
}
}
if (!changeSet.isEmpty()) {
fireListeners(Collections.unmodifiableSet(changeSet));
}
this.folders = updated;
}
private void fireListeners(Set<ChangedFiles> changeSet) {
for (FileChangeListener listener : this.listeners) {
listener.onChange(changeSet);
}
}
}
}

View File

@@ -0,0 +1,34 @@
/*
* Copyright 2012-2017 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.devtools.filewatch;
/**
* Factory used to create new {@link FileSystemWatcher} instances.
*
* @author Phillip Webb
* @since 1.3.0
*/
@FunctionalInterface
public interface FileSystemWatcherFactory {
/**
* Create a new {@link FileSystemWatcher}.
* @return a new {@link FileSystemWatcher}
*/
FileSystemWatcher getFileSystemWatcher();
}

View File

@@ -0,0 +1,173 @@
/*
* Copyright 2012-2017 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.devtools.filewatch;
import java.io.File;
import java.io.FileFilter;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import org.springframework.boot.devtools.filewatch.ChangedFile.Type;
import org.springframework.util.Assert;
/**
* A snapshot of a folder at a given point in time.
*
* @author Phillip Webb
*/
class FolderSnapshot {
private static final Set<String> DOT_FOLDERS = Collections
.unmodifiableSet(new HashSet<>(Arrays.asList(".", "..")));
private final File folder;
private final Date time;
private Set<FileSnapshot> files;
/**
* Create a new {@link FolderSnapshot} for the given folder.
* @param folder the source folder
*/
FolderSnapshot(File folder) {
Assert.notNull(folder, "Folder must not be null");
Assert.isTrue(folder.isDirectory(), "Folder must not be a file");
this.folder = folder;
this.time = new Date();
Set<FileSnapshot> files = new LinkedHashSet<>();
collectFiles(folder, files);
this.files = Collections.unmodifiableSet(files);
}
private void collectFiles(File source, Set<FileSnapshot> result) {
File[] children = source.listFiles();
if (children != null) {
for (File child : children) {
if (child.isDirectory() && !DOT_FOLDERS.contains(child.getName())) {
collectFiles(child, result);
}
else if (child.isFile()) {
result.add(new FileSnapshot(child));
}
}
}
}
public ChangedFiles getChangedFiles(FolderSnapshot snapshot,
FileFilter triggerFilter) {
Assert.notNull(snapshot, "Snapshot must not be null");
File folder = this.folder;
Assert.isTrue(snapshot.folder.equals(folder),
"Snapshot source folder must be '" + folder + "'");
Set<ChangedFile> changes = new LinkedHashSet<>();
Map<File, FileSnapshot> previousFiles = getFilesMap();
for (FileSnapshot currentFile : snapshot.files) {
if (acceptChangedFile(triggerFilter, currentFile)) {
FileSnapshot previousFile = previousFiles.remove(currentFile.getFile());
if (previousFile == null) {
changes.add(new ChangedFile(folder, currentFile.getFile(), Type.ADD));
}
else if (!previousFile.equals(currentFile)) {
changes.add(
new ChangedFile(folder, currentFile.getFile(), Type.MODIFY));
}
}
}
for (FileSnapshot previousFile : previousFiles.values()) {
if (acceptChangedFile(triggerFilter, previousFile)) {
changes.add(new ChangedFile(folder, previousFile.getFile(), Type.DELETE));
}
}
return new ChangedFiles(folder, changes);
}
private boolean acceptChangedFile(FileFilter triggerFilter, FileSnapshot file) {
return (triggerFilter == null || !triggerFilter.accept(file.getFile()));
}
private Map<File, FileSnapshot> getFilesMap() {
Map<File, FileSnapshot> files = new LinkedHashMap<>();
for (FileSnapshot file : this.files) {
files.put(file.getFile(), file);
}
return files;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (obj instanceof FolderSnapshot) {
return equals((FolderSnapshot) obj, null);
}
return super.equals(obj);
}
public boolean equals(FolderSnapshot other, FileFilter filter) {
if (this.folder.equals(other.folder)) {
Set<FileSnapshot> ourFiles = filter(this.files, filter);
Set<FileSnapshot> otherFiles = filter(other.files, filter);
return ourFiles.equals(otherFiles);
}
return false;
}
private Set<FileSnapshot> filter(Set<FileSnapshot> source, FileFilter filter) {
if (filter == null) {
return source;
}
Set<FileSnapshot> filtered = new LinkedHashSet<>();
for (FileSnapshot file : source) {
if (filter.accept(file.getFile())) {
filtered.add(file);
}
}
return filtered;
}
@Override
public int hashCode() {
int hashCode = this.folder.hashCode();
hashCode = 31 * hashCode + this.files.hashCode();
return hashCode;
}
/**
* Return the source folder of this snapshot.
* @return the source folder
*/
public File getFolder() {
return this.folder;
}
@Override
public String toString() {
return this.folder + " snapshot at " + this.time;
}
}

View File

@@ -0,0 +1,20 @@
/*
* 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.
*/
/**
* Class to watch the local filesystem for changes.
*/
package org.springframework.boot.devtools.filewatch;

View File

@@ -0,0 +1,166 @@
/*
* Copyright 2012-2016 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.devtools.livereload;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.util.Base64Utils;
/**
* A {@link LiveReloadServer} connection.
*
* @author Phillip Webb
*/
class Connection {
private static final Log logger = LogFactory.getLog(Connection.class);
private static final Pattern WEBSOCKET_KEY_PATTERN = Pattern
.compile("^Sec-WebSocket-Key:(.*)$", Pattern.MULTILINE);
public final static String WEBSOCKET_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
private final Socket socket;
private final ConnectionInputStream inputStream;
private final ConnectionOutputStream outputStream;
private final String header;
private volatile boolean webSocket;
private volatile boolean running = true;
/**
* Create a new {@link Connection} instance.
* @param socket the source socket
* @param inputStream the socket input stream
* @param outputStream the socket output stream
* @throws IOException in case of I/O errors
*/
Connection(Socket socket, InputStream inputStream, OutputStream outputStream)
throws IOException {
this.socket = socket;
this.inputStream = new ConnectionInputStream(inputStream);
this.outputStream = new ConnectionOutputStream(outputStream);
this.header = this.inputStream.readHeader();
logger.debug("Established livereload connection [" + this.header + "]");
}
/**
* Run the connection.
* @throws Exception in case of errors
*/
public void run() throws Exception {
if (this.header.contains("Upgrade: websocket")
&& this.header.contains("Sec-WebSocket-Version: 13")) {
runWebSocket(this.header);
}
if (this.header.contains("GET /livereload.js")) {
this.outputStream.writeHttp(getClass().getResourceAsStream("livereload.js"),
"text/javascript");
}
}
private void runWebSocket(String header) throws Exception {
String accept = getWebsocketAcceptResponse();
this.outputStream.writeHeaders("HTTP/1.1 101 Switching Protocols",
"Upgrade: websocket", "Connection: Upgrade",
"Sec-WebSocket-Accept: " + accept);
new Frame("{\"command\":\"hello\",\"protocols\":"
+ "[\"http://livereload.com/protocols/official-7\"],"
+ "\"serverName\":\"spring-boot\"}").write(this.outputStream);
Thread.sleep(100);
this.webSocket = true;
while (this.running) {
readWebSocketFrame();
}
}
private void readWebSocketFrame() throws IOException {
try {
Frame frame = Frame.read(this.inputStream);
if (frame.getType() == Frame.Type.PING) {
writeWebSocketFrame(new Frame(Frame.Type.PONG));
}
else if (frame.getType() == Frame.Type.CLOSE) {
throw new ConnectionClosedException();
}
else if (frame.getType() == Frame.Type.TEXT) {
logger.debug("Received LiveReload text frame " + frame);
}
else {
throw new IOException("Unexpected Frame Type " + frame.getType());
}
}
catch (SocketTimeoutException ex) {
writeWebSocketFrame(new Frame(Frame.Type.PING));
Frame frame = Frame.read(this.inputStream);
if (frame.getType() != Frame.Type.PONG) {
throw new IllegalStateException("No Pong");
}
}
}
/**
* Trigger livereload for the client using this connection.
* @throws IOException in case of I/O errors
*/
public void triggerReload() throws IOException {
if (this.webSocket) {
logger.debug("Triggering LiveReload");
writeWebSocketFrame(new Frame("{\"command\":\"reload\",\"path\":\"/\"}"));
}
}
private void writeWebSocketFrame(Frame frame) throws IOException {
frame.write(this.outputStream);
}
private String getWebsocketAcceptResponse() throws NoSuchAlgorithmException {
Matcher matcher = WEBSOCKET_KEY_PATTERN.matcher(this.header);
if (!matcher.find()) {
throw new IllegalStateException("No Sec-WebSocket-Key");
}
String response = matcher.group(1).trim() + WEBSOCKET_GUID;
MessageDigest messageDigest = MessageDigest.getInstance("SHA-1");
messageDigest.update(response.getBytes(), 0, response.length());
return Base64Utils.encodeToString(messageDigest.digest());
}
/**
* Close the connection.
* @throws IOException in case of I/O errors
*/
public void close() throws IOException {
this.running = false;
this.socket.close();
}
}

View File

@@ -0,0 +1,32 @@
/*
* 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.devtools.livereload;
import java.io.IOException;
/**
* Exception throw when the client closes the connection.
*
* @author Phillip Webb
*/
class ConnectionClosedException extends IOException {
ConnectionClosedException() {
super("Connection closed");
}
}

View File

@@ -0,0 +1,102 @@
/*
* Copyright 2012-2016 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.devtools.livereload;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
/**
* {@link InputStream} for a server connection.
*
* @author Phillip Webb
*/
class ConnectionInputStream extends FilterInputStream {
private static final String HEADER_END = "\r\n\r\n";
private static final int BUFFER_SIZE = 4096;
ConnectionInputStream(InputStream in) {
super(in);
}
/**
* Read the HTTP header from the {@link InputStream}. Note: This method doesn't expect
* any HTTP content after the header since the initial request is usually just a
* WebSocket upgrade.
* @return the HTTP header
* @throws IOException in case of I/O errors
*/
public String readHeader() throws IOException {
byte[] buffer = new byte[BUFFER_SIZE];
StringBuilder content = new StringBuilder(BUFFER_SIZE);
while (content.indexOf(HEADER_END) == -1) {
int amountRead = checkedRead(buffer, 0, BUFFER_SIZE);
content.append(new String(buffer, 0, amountRead));
}
return content.substring(0, content.indexOf(HEADER_END));
}
/**
* Repeatedly read the underlying {@link InputStream} until the requested number of
* bytes have been loaded.
* @param buffer the destination buffer
* @param offset the buffer offset
* @param length the amount of data to read
* @throws IOException in case of I/O errors
*/
public void readFully(byte[] buffer, int offset, int length) throws IOException {
while (length > 0) {
int amountRead = checkedRead(buffer, offset, length);
offset += amountRead;
length -= amountRead;
}
}
/**
* Read a single byte from the stream (checking that the end of the stream hasn't been
* reached.
* @return the content
* @throws IOException in case of I/O errors
*/
public int checkedRead() throws IOException {
int b = read();
if (b == -1) {
throw new IOException("End of stream");
}
return (b & 0xff);
}
/**
* Read a number of bytes from the stream (checking that the end of the stream hasn't
* been reached).
* @param buffer the destination buffer
* @param offset the buffer offset
* @param length the length to read
* @return the amount of data read
* @throws IOException in case of I/O errors
*/
public int checkedRead(byte[] buffer, int offset, int length) throws IOException {
int amountRead = read(buffer, offset, length);
if (amountRead == -1) {
throw new IOException("End of stream");
}
return amountRead;
}
}

View File

@@ -0,0 +1,59 @@
/*
* 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.devtools.livereload;
import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import org.springframework.util.FileCopyUtils;
/**
* {@link OutputStream} for a server connection.
*
* @author Phillip Webb
*/
class ConnectionOutputStream extends FilterOutputStream {
ConnectionOutputStream(OutputStream out) {
super(out);
}
@Override
public void write(byte[] b, int off, int len) throws IOException {
this.out.write(b, off, len);
}
public void writeHttp(InputStream content, String contentType) throws IOException {
byte[] bytes = FileCopyUtils.copyToByteArray(content);
writeHeaders("HTTP/1.1 200 OK", "Content-Type: " + contentType,
"Content-Length: " + bytes.length, "Connection: close");
write(bytes);
flush();
}
public void writeHeaders(String... headers) throws IOException {
StringBuilder response = new StringBuilder();
for (String header : headers) {
response.append(header).append("\r\n");
}
response.append("\r\n");
write(response.toString().getBytes());
}
}

View File

@@ -0,0 +1,162 @@
/*
* 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.devtools.livereload;
import java.io.IOException;
import java.io.OutputStream;
import org.springframework.util.Assert;
/**
* A limited implementation of a WebSocket Frame used to carry LiveReload data.
*
* @author Phillip Webb
*/
class Frame {
private static final byte[] NO_BYTES = new byte[0];
private final Type type;
private final byte[] payload;
/**
* Create a new {@link Type#TEXT text} {@link Frame} instance with the specified
* payload.
* @param payload the text payload
*/
Frame(String payload) {
Assert.notNull(payload, "Payload must not be null");
this.type = Type.TEXT;
this.payload = payload.getBytes();
}
Frame(Type type) {
Assert.notNull(type, "Type must not be null");
this.type = type;
this.payload = NO_BYTES;
}
Frame(Type type, byte[] payload) {
this.type = type;
this.payload = payload;
}
public Type getType() {
return this.type;
}
public byte[] getPayload() {
return this.payload;
}
@Override
public String toString() {
return new String(this.payload);
}
public void write(OutputStream outputStream) throws IOException {
outputStream.write(0x80 | this.type.code);
if (this.payload.length < 126) {
outputStream.write(0x00 | (this.payload.length & 0x7F));
}
else {
outputStream.write(0x7E);
outputStream.write(this.payload.length >> 8 & 0xFF);
outputStream.write(this.payload.length >> 0 & 0xFF);
}
outputStream.write(this.payload);
outputStream.flush();
}
public static Frame read(ConnectionInputStream inputStream) throws IOException {
int firstByte = inputStream.checkedRead();
Assert.state((firstByte & 0x80) != 0, "Fragmented frames are not supported");
int maskAndLength = inputStream.checkedRead();
boolean hasMask = (maskAndLength & 0x80) != 0;
int length = (maskAndLength & 0x7F);
Assert.state(length != 127, "Large frames are not supported");
if (length == 126) {
length = ((inputStream.checkedRead()) << 8 | inputStream.checkedRead());
}
byte[] mask = new byte[4];
if (hasMask) {
inputStream.readFully(mask, 0, mask.length);
}
byte[] payload = new byte[length];
inputStream.readFully(payload, 0, length);
if (hasMask) {
for (int i = 0; i < payload.length; i++) {
payload[i] ^= mask[i % 4];
}
}
return new Frame(Type.forCode(firstByte & 0x0F), payload);
}
/**
* Frame types.
*/
public enum Type {
/**
* Continuation frame.
*/
CONTINUATION(0x00),
/**
* Text frame.
*/
TEXT(0x01),
/**
* Binary frame.
*/
BINARY(0x02),
/**
* Close frame.
*/
CLOSE(0x08),
/**
* Ping frame.
*/
PING(0x09),
/**
* Pong frame.
*/
PONG(0x0A);
private final int code;
Type(int code) {
this.code = code;
}
public static Type forCode(int code) {
for (Type type : values()) {
if (type.code == code) {
return type;
}
}
throw new IllegalStateException("Unknown code " + code);
}
}
}

View File

@@ -0,0 +1,317 @@
/*
* Copyright 2012-2017 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.devtools.livereload;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.util.Assert;
/**
* A <a href="http://livereload.com">livereload</a> server.
*
* @author Phillip Webb
* @since 1.3.0
* @see <a href="http://livereload.com">livereload.com</a>
*/
public class LiveReloadServer {
/**
* The default live reload server port.
*/
public static final int DEFAULT_PORT = 35729;
private static final Log logger = LogFactory.getLog(LiveReloadServer.class);
private static final int READ_TIMEOUT = (int) TimeUnit.SECONDS.toMillis(4);
private final ExecutorService executor = Executors
.newCachedThreadPool(new WorkerThreadFactory());
private final List<Connection> connections = new ArrayList<>();
private final Object monitor = new Object();
private final int port;
private final ThreadFactory threadFactory;
private ServerSocket serverSocket;
private Thread listenThread;
/**
* Create a new {@link LiveReloadServer} listening on the default port.
*/
public LiveReloadServer() {
this(DEFAULT_PORT);
}
/**
* Create a new {@link LiveReloadServer} listening on the default port with a specific
* {@link ThreadFactory}.
* @param threadFactory the thread factory
*/
public LiveReloadServer(ThreadFactory threadFactory) {
this(DEFAULT_PORT, threadFactory);
}
/**
* Create a new {@link LiveReloadServer} listening on the specified port.
* @param port the listen port
*/
public LiveReloadServer(int port) {
this(port, Thread::new);
}
/**
* Create a new {@link LiveReloadServer} listening on the specified port with a
* specific {@link ThreadFactory}.
* @param port the listen port
* @param threadFactory the thread factory
*/
public LiveReloadServer(int port, ThreadFactory threadFactory) {
this.port = port;
this.threadFactory = threadFactory;
}
/**
* Start the livereload server and accept incoming connections.
* @return the port on which the server is listening
* @throws IOException in case of I/O errors
*/
public int start() throws IOException {
synchronized (this.monitor) {
Assert.state(!isStarted(), "Server already started");
logger.debug("Starting live reload server on port " + this.port);
this.serverSocket = new ServerSocket(this.port);
int localPort = this.serverSocket.getLocalPort();
this.listenThread = this.threadFactory.newThread(this::acceptConnections);
this.listenThread.setDaemon(true);
this.listenThread.setName("Live Reload Server");
this.listenThread.start();
return localPort;
}
}
/**
* Return if the server has been started.
* @return {@code true} if the server is running
*/
public boolean isStarted() {
synchronized (this.monitor) {
return this.listenThread != null;
}
}
/**
* Return the port that the server is listening on.
* @return the server port
*/
public int getPort() {
return this.port;
}
private void acceptConnections() {
do {
try {
Socket socket = this.serverSocket.accept();
socket.setSoTimeout(READ_TIMEOUT);
this.executor.execute(new ConnectionHandler(socket));
}
catch (SocketTimeoutException ex) {
// Ignore
}
catch (Exception ex) {
if (logger.isDebugEnabled()) {
logger.debug("LiveReload server error", ex);
}
}
}
while (!this.serverSocket.isClosed());
}
/**
* Gracefully stop the livereload server.
* @throws IOException in case of I/O errors
*/
public void stop() throws IOException {
synchronized (this.monitor) {
if (this.listenThread != null) {
closeAllConnections();
try {
this.executor.shutdown();
this.executor.awaitTermination(1, TimeUnit.MINUTES);
}
catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
this.serverSocket.close();
try {
this.listenThread.join();
}
catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
this.listenThread = null;
this.serverSocket = null;
}
}
}
private void closeAllConnections() throws IOException {
synchronized (this.connections) {
for (Connection connection : this.connections) {
connection.close();
}
}
}
/**
* Trigger livereload of all connected clients.
*/
public void triggerReload() {
synchronized (this.monitor) {
synchronized (this.connections) {
for (Connection connection : this.connections) {
try {
connection.triggerReload();
}
catch (Exception ex) {
logger.debug("Unable to send reload message", ex);
}
}
}
}
}
private void addConnection(Connection connection) {
synchronized (this.connections) {
this.connections.add(connection);
}
}
private void removeConnection(Connection connection) {
synchronized (this.connections) {
this.connections.remove(connection);
}
}
/**
* Factory method used to create the {@link Connection}.
* @param socket the source socket
* @param inputStream the socket input stream
* @param outputStream the socket output stream
* @return a connection
* @throws IOException in case of I/O errors
*/
protected Connection createConnection(Socket socket, InputStream inputStream,
OutputStream outputStream) throws IOException {
return new Connection(socket, inputStream, outputStream);
}
/**
* {@link Runnable} to handle a single connection.
*
* @see Connection
*/
private class ConnectionHandler implements Runnable {
private final Socket socket;
private final InputStream inputStream;
ConnectionHandler(Socket socket) throws IOException {
this.socket = socket;
this.inputStream = socket.getInputStream();
}
@Override
public void run() {
try {
handle();
}
catch (ConnectionClosedException ex) {
logger.debug("LiveReload connection closed");
}
catch (Exception ex) {
if (logger.isDebugEnabled()) {
logger.debug("LiveReload error", ex);
}
}
}
private void handle() throws Exception {
try {
try (OutputStream outputStream = this.socket.getOutputStream()) {
Connection connection = createConnection(this.socket,
this.inputStream, outputStream);
runConnection(connection);
}
finally {
this.inputStream.close();
}
}
finally {
this.socket.close();
}
}
private void runConnection(Connection connection) throws IOException, Exception {
try {
addConnection(connection);
connection.run();
}
finally {
removeConnection(connection);
}
}
}
/**
* {@link ThreadFactory} to create the worker threads.
*/
private static class WorkerThreadFactory implements ThreadFactory {
private final AtomicInteger threadNumber = new AtomicInteger(1);
@Override
public Thread newThread(Runnable r) {
Thread thread = new Thread(r);
thread.setDaemon(true);
thread.setName("Live Reload #" + this.threadNumber.getAndIncrement());
return thread;
}
}
}

View File

@@ -0,0 +1,20 @@
/*
* 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 the livereload protocol.
*/
package org.springframework.boot.devtools.livereload;

View File

@@ -0,0 +1,20 @@
/*
* 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.
*/
/**
* Spring Boot developer tools.
*/
package org.springframework.boot.devtools;

View File

@@ -0,0 +1,169 @@
/*
* Copyright 2012-2017 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.devtools.remote.client;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.net.ConnectException;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.boot.devtools.classpath.ClassPathChangedEvent;
import org.springframework.boot.devtools.filewatch.ChangedFile;
import org.springframework.boot.devtools.filewatch.ChangedFiles;
import org.springframework.boot.devtools.restart.classloader.ClassLoaderFile;
import org.springframework.boot.devtools.restart.classloader.ClassLoaderFile.Kind;
import org.springframework.boot.devtools.restart.classloader.ClassLoaderFiles;
import org.springframework.context.ApplicationListener;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.client.ClientHttpRequest;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.util.Assert;
import org.springframework.util.FileCopyUtils;
/**
* Listens and pushes any classpath updates to a remote endpoint.
*
* @author Phillip Webb
* @author Andy Wilkinson
* @since 1.3.0
*/
public class ClassPathChangeUploader
implements ApplicationListener<ClassPathChangedEvent> {
private static final Map<ChangedFile.Type, ClassLoaderFile.Kind> TYPE_MAPPINGS;
static {
Map<ChangedFile.Type, ClassLoaderFile.Kind> map = new HashMap<>();
map.put(ChangedFile.Type.ADD, ClassLoaderFile.Kind.ADDED);
map.put(ChangedFile.Type.DELETE, ClassLoaderFile.Kind.DELETED);
map.put(ChangedFile.Type.MODIFY, ClassLoaderFile.Kind.MODIFIED);
TYPE_MAPPINGS = Collections.unmodifiableMap(map);
}
private static final Log logger = LogFactory.getLog(ClassPathChangeUploader.class);
private final URI uri;
private final ClientHttpRequestFactory requestFactory;
public ClassPathChangeUploader(String url, ClientHttpRequestFactory requestFactory) {
Assert.hasLength(url, "URL must not be empty");
Assert.notNull(requestFactory, "RequestFactory must not be null");
try {
this.uri = new URL(url).toURI();
}
catch (URISyntaxException | MalformedURLException ex) {
throw new IllegalArgumentException("Malformed URL '" + url + "'");
}
this.requestFactory = requestFactory;
}
@Override
public void onApplicationEvent(ClassPathChangedEvent event) {
try {
ClassLoaderFiles classLoaderFiles = getClassLoaderFiles(event);
byte[] bytes = serialize(classLoaderFiles);
performUpload(classLoaderFiles, bytes);
}
catch (IOException ex) {
throw new IllegalStateException(ex);
}
}
private void performUpload(ClassLoaderFiles classLoaderFiles, byte[] bytes)
throws IOException {
try {
while (true) {
try {
ClientHttpRequest request = this.requestFactory
.createRequest(this.uri, HttpMethod.POST);
HttpHeaders headers = request.getHeaders();
headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
headers.setContentLength(bytes.length);
FileCopyUtils.copy(bytes, request.getBody());
ClientHttpResponse response = request.execute();
Assert.state(response.getStatusCode() == HttpStatus.OK,
"Unexpected " + response.getStatusCode()
+ " response uploading class files");
logUpload(classLoaderFiles);
return;
}
catch (ConnectException ex) {
logger.warn("Failed to connect when uploading to " + this.uri
+ ". Upload will be retried in 2 seconds");
Thread.sleep(2000);
}
}
}
catch (InterruptedException ex) {
Thread.currentThread().interrupt();
throw new IllegalStateException(ex);
}
}
private void logUpload(ClassLoaderFiles classLoaderFiles) {
int size = classLoaderFiles.size();
logger.info(
"Uploaded " + size + " class " + (size == 1 ? "resource" : "resources"));
}
private byte[] serialize(ClassLoaderFiles classLoaderFiles) throws IOException {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(outputStream);
objectOutputStream.writeObject(classLoaderFiles);
objectOutputStream.close();
return outputStream.toByteArray();
}
private ClassLoaderFiles getClassLoaderFiles(ClassPathChangedEvent event)
throws IOException {
ClassLoaderFiles files = new ClassLoaderFiles();
for (ChangedFiles changedFiles : event.getChangeSet()) {
String sourceFolder = changedFiles.getSourceFolder().getAbsolutePath();
for (ChangedFile changedFile : changedFiles) {
files.addFile(sourceFolder, changedFile.getRelativeName(),
asClassLoaderFile(changedFile));
}
}
return files;
}
private ClassLoaderFile asClassLoaderFile(ChangedFile changedFile)
throws IOException {
ClassLoaderFile.Kind kind = TYPE_MAPPINGS.get(changedFile.getType());
byte[] bytes = (kind == Kind.DELETED ? null
: FileCopyUtils.copyToByteArray(changedFile.getFile()));
long lastModified = (kind == Kind.DELETED ? System.currentTimeMillis()
: changedFile.getFile().lastModified());
return new ClassLoaderFile(kind, lastModified, bytes);
}
}

View File

@@ -0,0 +1,118 @@
/*
* Copyright 2012-2016 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.devtools.remote.client;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.boot.devtools.autoconfigure.OptionalLiveReloadServer;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.client.ClientHttpRequest;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.util.Assert;
/**
* {@link Runnable} that waits to triggers live reload until the remote server has
* restarted.
*
* @author Phillip Webb
*/
class DelayedLiveReloadTrigger implements Runnable {
private static final long SHUTDOWN_TIME = 1000;
private static final long SLEEP_TIME = 500;
private static final long TIMEOUT = 30000;
private static final Log logger = LogFactory.getLog(DelayedLiveReloadTrigger.class);
private final OptionalLiveReloadServer liveReloadServer;
private final ClientHttpRequestFactory requestFactory;
private final URI uri;
private long shutdownTime = SHUTDOWN_TIME;
private long sleepTime = SLEEP_TIME;
private long timeout = TIMEOUT;
DelayedLiveReloadTrigger(OptionalLiveReloadServer liveReloadServer,
ClientHttpRequestFactory requestFactory, String url) {
Assert.notNull(liveReloadServer, "LiveReloadServer must not be null");
Assert.notNull(requestFactory, "RequestFactory must not be null");
Assert.hasLength(url, "URL must not be empty");
this.liveReloadServer = liveReloadServer;
this.requestFactory = requestFactory;
try {
this.uri = new URI(url);
}
catch (URISyntaxException ex) {
throw new IllegalArgumentException(ex);
}
}
protected void setTimings(long shutdown, long sleep, long timeout) {
this.shutdownTime = shutdown;
this.sleepTime = sleep;
this.timeout = timeout;
}
@Override
public void run() {
try {
Thread.sleep(this.shutdownTime);
long start = System.currentTimeMillis();
while (!isUp()) {
long runTime = System.currentTimeMillis() - start;
if (runTime > this.timeout) {
return;
}
Thread.sleep(this.sleepTime);
}
logger.info("Remote server has changed, triggering LiveReload");
this.liveReloadServer.triggerReload();
}
catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
}
private boolean isUp() {
try {
ClientHttpRequest request = createRequest();
ClientHttpResponse response = request.execute();
return response.getStatusCode() == HttpStatus.OK;
}
catch (Exception ex) {
return false;
}
}
private ClientHttpRequest createRequest() throws IOException {
return this.requestFactory.createRequest(this.uri, HttpMethod.GET);
}
}

View File

@@ -0,0 +1,60 @@
/*
* 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.devtools.remote.client;
import java.io.IOException;
import org.springframework.http.HttpRequest;
import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.util.Assert;
/**
* {@link ClientHttpRequestInterceptor} to populate arbitrary HTTP headers with a value.
* For example, it might be used to provide an X-AUTH-TOKEN and value for security
* purposes.
*
* @author Rob Winch
* @since 1.3.0
*/
public class HttpHeaderInterceptor implements ClientHttpRequestInterceptor {
private final String name;
private final String value;
/**
* Creates a new {@link HttpHeaderInterceptor} instance.
* @param name the header name to populate. Cannot be null or empty.
* @param value the header value to populate. Cannot be null or empty.
*/
public HttpHeaderInterceptor(String name, String value) {
Assert.hasLength(name, "Name must not be empty");
Assert.hasLength(value, "Value" + " must not be empty");
this.name = name;
this.value = value;
}
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body,
ClientHttpRequestExecution execution) throws IOException {
request.getHeaders().add(this.name, this.value);
return execution.execute(request, body);
}
}

View File

@@ -0,0 +1,231 @@
/*
* Copyright 2012-2017 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.devtools.remote.client;
import java.net.InetSocketAddress;
import java.net.Proxy.Type;
import java.net.URL;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
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.devtools.autoconfigure.DevToolsProperties;
import org.springframework.boot.devtools.autoconfigure.DevToolsProperties.Restart;
import org.springframework.boot.devtools.autoconfigure.OptionalLiveReloadServer;
import org.springframework.boot.devtools.autoconfigure.RemoteDevToolsProperties;
import org.springframework.boot.devtools.autoconfigure.RemoteDevToolsProperties.Proxy;
import org.springframework.boot.devtools.autoconfigure.TriggerFileFilter;
import org.springframework.boot.devtools.classpath.ClassPathChangedEvent;
import org.springframework.boot.devtools.classpath.ClassPathFileSystemWatcher;
import org.springframework.boot.devtools.classpath.ClassPathRestartStrategy;
import org.springframework.boot.devtools.classpath.PatternClassPathRestartStrategy;
import org.springframework.boot.devtools.filewatch.FileSystemWatcher;
import org.springframework.boot.devtools.filewatch.FileSystemWatcherFactory;
import org.springframework.boot.devtools.livereload.LiveReloadServer;
import org.springframework.boot.devtools.restart.DefaultRestartInitializer;
import org.springframework.boot.devtools.restart.RestartScope;
import org.springframework.boot.devtools.restart.Restarter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.event.EventListener;
import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.InterceptingClientHttpRequestFactory;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* Configuration used to connect to remote Spring Boot applications.
*
* @author Phillip Webb
* @since 1.3.0
* @see org.springframework.boot.devtools.RemoteSpringApplication
*/
@Configuration
@EnableConfigurationProperties(DevToolsProperties.class)
public class RemoteClientConfiguration implements InitializingBean {
private static final Log logger = LogFactory.getLog(RemoteClientConfiguration.class);
private final DevToolsProperties properties;
@Value("${remoteUrl}")
private String remoteUrl;
public RemoteClientConfiguration(DevToolsProperties properties) {
this.properties = properties;
}
@Bean
public static PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() {
return new PropertySourcesPlaceholderConfigurer();
}
@Bean
public ClientHttpRequestFactory clientHttpRequestFactory() {
List<ClientHttpRequestInterceptor> interceptors = Arrays
.asList(getSecurityInterceptor());
SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();
Proxy proxy = this.properties.getRemote().getProxy();
if (proxy.getHost() != null && proxy.getPort() != null) {
requestFactory.setProxy(new java.net.Proxy(Type.HTTP,
new InetSocketAddress(proxy.getHost(), proxy.getPort())));
}
return new InterceptingClientHttpRequestFactory(requestFactory, interceptors);
}
private ClientHttpRequestInterceptor getSecurityInterceptor() {
RemoteDevToolsProperties remoteProperties = this.properties.getRemote();
String secretHeaderName = remoteProperties.getSecretHeaderName();
String secret = remoteProperties.getSecret();
Assert.state(secret != null,
"The environment value 'spring.devtools.remote.secret' "
+ "is required to secure your connection.");
return new HttpHeaderInterceptor(secretHeaderName, secret);
}
@Override
public void afterPropertiesSet() {
logWarnings();
}
private void logWarnings() {
RemoteDevToolsProperties remoteProperties = this.properties.getRemote();
if (!remoteProperties.getRestart().isEnabled()) {
logger.warn("Remote restart is disabled.");
}
if (!this.remoteUrl.startsWith("https://")) {
logger.warn("The connection to " + this.remoteUrl
+ " is insecure. You should use a URL starting with 'https://'.");
}
}
/**
* LiveReload configuration.
*/
@ConditionalOnProperty(prefix = "spring.devtools.livereload", name = "enabled", matchIfMissing = true)
static class LiveReloadConfiguration {
@Autowired
private DevToolsProperties properties;
@Autowired(required = false)
private LiveReloadServer liveReloadServer;
@Autowired
private ClientHttpRequestFactory clientHttpRequestFactory;
@Value("${remoteUrl}")
private String remoteUrl;
private ExecutorService executor = Executors.newSingleThreadExecutor();
@Bean
@RestartScope
@ConditionalOnMissingBean
public LiveReloadServer liveReloadServer() {
return new LiveReloadServer(this.properties.getLivereload().getPort(),
Restarter.getInstance().getThreadFactory());
}
@EventListener
public void onClassPathChanged(ClassPathChangedEvent event) {
String url = this.remoteUrl + this.properties.getRemote().getContextPath();
this.executor.execute(new DelayedLiveReloadTrigger(optionalLiveReloadServer(),
this.clientHttpRequestFactory, url));
}
@Bean
public OptionalLiveReloadServer optionalLiveReloadServer() {
return new OptionalLiveReloadServer(this.liveReloadServer);
}
final ExecutorService getExecutor() {
return this.executor;
}
}
/**
* Client configuration for remote update and restarts.
*/
@ConditionalOnProperty(prefix = "spring.devtools.remote.restart", name = "enabled", matchIfMissing = true)
static class RemoteRestartClientConfiguration {
@Autowired
private DevToolsProperties properties;
@Value("${remoteUrl}")
private String remoteUrl;
@Bean
public ClassPathFileSystemWatcher classPathFileSystemWatcher() {
DefaultRestartInitializer restartInitializer = new DefaultRestartInitializer();
URL[] urls = restartInitializer.getInitialUrls(Thread.currentThread());
if (urls == null) {
urls = new URL[0];
}
return new ClassPathFileSystemWatcher(getFileSystemWatcherFactory(),
classPathRestartStrategy(), urls);
}
@Bean
public FileSystemWatcherFactory getFileSystemWatcherFactory() {
return this::newFileSystemWatcher;
}
private FileSystemWatcher newFileSystemWatcher() {
Restart restartProperties = this.properties.getRestart();
FileSystemWatcher watcher = new FileSystemWatcher(true,
restartProperties.getPollInterval(),
restartProperties.getQuietPeriod());
String triggerFile = restartProperties.getTriggerFile();
if (StringUtils.hasLength(triggerFile)) {
watcher.setTriggerFilter(new TriggerFileFilter(triggerFile));
}
return watcher;
}
@Bean
public ClassPathRestartStrategy classPathRestartStrategy() {
return new PatternClassPathRestartStrategy(
this.properties.getRestart().getAllExclude());
}
@Bean
public ClassPathChangeUploader classPathChangeUploader(
ClientHttpRequestFactory requestFactory) {
String url = this.remoteUrl + this.properties.getRemote().getContextPath()
+ "/restart";
return new ClassPathChangeUploader(url, requestFactory);
}
}
}

View File

@@ -0,0 +1,20 @@
/*
* 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.
*/
/**
* Client support for a remotely running Spring Boot application.
*/
package org.springframework.boot.devtools.remote.client;

View File

@@ -0,0 +1,43 @@
/*
* Copyright 2012-2017 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.devtools.remote.server;
import org.springframework.http.server.ServerHttpRequest;
/**
* Provides access control for a {@link Dispatcher}.
*
* @author Phillip Webb
* @since 1.3.0
*/
@FunctionalInterface
public interface AccessManager {
/**
* {@link AccessManager} that permits all requests.
*/
AccessManager PERMIT_ALL = (request) -> true;
/**
* Determine if the specific request is allowed to be handled by the
* {@link Dispatcher}.
* @param request the request to check
* @return {@code true} if access is allowed.
*/
boolean isAllowed(ServerHttpRequest request);
}

View File

@@ -0,0 +1,81 @@
/*
* Copyright 2012-2017 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.devtools.remote.server;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.util.Assert;
/**
* Dispatcher used to route incoming remote server requests to a {@link Handler}. Similar
* to {@code DispatchServlet} in Spring MVC but separate to ensure that remote support can
* be used regardless of any web framework.
*
* @author Phillip Webb
* @since 1.3.0
* @see HandlerMapper
*/
public class Dispatcher {
private final AccessManager accessManager;
private final List<HandlerMapper> mappers;
public Dispatcher(AccessManager accessManager, Collection<HandlerMapper> mappers) {
Assert.notNull(accessManager, "AccessManager must not be null");
Assert.notNull(mappers, "Mappers must not be null");
this.accessManager = accessManager;
this.mappers = new ArrayList<>(mappers);
AnnotationAwareOrderComparator.sort(this.mappers);
}
/**
* Dispatch the specified request to an appropriate {@link Handler}.
* @param request the request
* @param response the response
* @return {@code true} if the request was dispatched
* @throws IOException in case of I/O errors
*/
public boolean handle(ServerHttpRequest request, ServerHttpResponse response)
throws IOException {
for (HandlerMapper mapper : this.mappers) {
Handler handler = mapper.getHandler(request);
if (handler != null) {
handle(handler, request, response);
return true;
}
}
return false;
}
private void handle(Handler handler, ServerHttpRequest request,
ServerHttpResponse response) throws IOException {
if (!this.accessManager.isAllowed(request)) {
response.setStatusCode(HttpStatus.FORBIDDEN);
return;
}
handler.handle(request, response);
}
}

View File

@@ -0,0 +1,81 @@
/*
* 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.devtools.remote.server;
import java.io.IOException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.http.server.ServletServerHttpResponse;
import org.springframework.util.Assert;
/**
* Servlet filter providing integration with the remote server {@link Dispatcher}.
*
* @author Phillip Webb
* @author Rob Winch
* @since 1.3.0
*/
public class DispatcherFilter implements Filter {
private final Dispatcher dispatcher;
public DispatcherFilter(Dispatcher dispatcher) {
Assert.notNull(dispatcher, "Dispatcher must not be null");
this.dispatcher = dispatcher;
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
if (request instanceof HttpServletRequest
&& response instanceof HttpServletResponse) {
doFilter((HttpServletRequest) request, (HttpServletResponse) response, chain);
}
else {
chain.doFilter(request, response);
}
}
private void doFilter(HttpServletRequest request, HttpServletResponse response,
FilterChain chain) throws IOException, ServletException {
ServerHttpRequest serverRequest = new ServletServerHttpRequest(request);
ServerHttpResponse serverResponse = new ServletServerHttpResponse(response);
if (!this.dispatcher.handle(serverRequest, serverResponse)) {
chain.doFilter(request, response);
}
}
@Override
public void destroy() {
}
}

View File

@@ -0,0 +1,42 @@
/*
* Copyright 2012-2017 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.devtools.remote.server;
import java.io.IOException;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
/**
* A single handler that is able to process an incoming remote server request.
*
* @author Phillip Webb
* @since 1.3.0
*/
@FunctionalInterface
public interface Handler {
/**
* Handle the request.
* @param request the request
* @param response the response
* @throws IOException in case of I/O errors
*/
void handle(ServerHttpRequest request, ServerHttpResponse response)
throws IOException;
}

View File

@@ -0,0 +1,38 @@
/*
* Copyright 2012-2017 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.devtools.remote.server;
import org.springframework.http.server.ServerHttpRequest;
/**
* Interface to provide a mapping between a {@link ServerHttpRequest} and a
* {@link Handler}.
*
* @author Phillip Webb
* @since 1.3.0
*/
@FunctionalInterface
public interface HandlerMapper {
/**
* Return the handler for the given request or {@code null}.
* @param request the request
* @return a {@link Handler} or {@code null}
*/
Handler getHandler(ServerHttpRequest request);
}

View File

@@ -0,0 +1,48 @@
/*
* 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.devtools.remote.server;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.util.Assert;
/**
* {@link AccessManager} that checks for the presence of a HTTP header secret.
*
* @author Rob Winch
* @author Phillip Webb
* @since 1.3.0
*/
public class HttpHeaderAccessManager implements AccessManager {
private final String headerName;
private final String expectedSecret;
public HttpHeaderAccessManager(String headerName, String expectedSecret) {
Assert.hasLength(headerName, "HeaderName must not be empty");
Assert.hasLength(expectedSecret, "ExpectedSecret must not be empty");
this.headerName = headerName;
this.expectedSecret = expectedSecret;
}
@Override
public boolean isAllowed(ServerHttpRequest request) {
String providedSecret = request.getHeaders().getFirst(this.headerName);
return this.expectedSecret.equals(providedSecret);
}
}

View File

@@ -0,0 +1,59 @@
/*
* 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.devtools.remote.server;
import java.io.IOException;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.util.Assert;
/**
* {@link Handler} that responds with a specific {@link HttpStatus}.
*
* @author Phillip Webb
*/
public class HttpStatusHandler implements Handler {
private final HttpStatus status;
/**
* Create a new {@link HttpStatusHandler} instance that will respond with a HTTP OK
* 200 status.
*/
public HttpStatusHandler() {
this(HttpStatus.OK);
}
/**
* Create a new {@link HttpStatusHandler} instance that will respond with the
* specified status.
* @param status the status
*/
public HttpStatusHandler(HttpStatus status) {
Assert.notNull(status, "Status must not be null");
this.status = status;
}
@Override
public void handle(ServerHttpRequest request, ServerHttpResponse response)
throws IOException {
response.setStatusCode(this.status);
}
}

View File

@@ -0,0 +1,55 @@
/*
* 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.devtools.remote.server;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.util.Assert;
/**
* {@link HandlerMapper} implementation that maps incoming URLs.
*
* @author Rob Winch
* @author Phillip Webb
* @since 1.3.0
*/
public class UrlHandlerMapper implements HandlerMapper {
private final String requestUri;
private final Handler handler;
/**
* Create a new {@link UrlHandlerMapper}.
* @param url the URL to map
* @param handler the handler to use
*/
public UrlHandlerMapper(String url, Handler handler) {
Assert.hasLength(url, "URL must not be empty");
Assert.isTrue(url.startsWith("/"), "URL must start with '/'");
this.requestUri = url;
this.handler = handler;
}
@Override
public Handler getHandler(ServerHttpRequest request) {
if (this.requestUri.equals(request.getURI().getPath())) {
return this.handler;
}
return null;
}
}

View File

@@ -0,0 +1,20 @@
/*
* 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.
*/
/**
* Server support for a remotely running Spring Boot application.
*/
package org.springframework.boot.devtools.remote.server;

View File

@@ -0,0 +1,64 @@
/*
* Copyright 2012-2017 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.devtools.restart;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Set;
import org.springframework.util.ClassUtils;
/**
* Utility to determine if an Java agent based reloader (e.g. JRebel) is being used.
*
* @author Phillip Webb
* @since 1.3.0
*/
public abstract class AgentReloader {
private static final Set<String> AGENT_CLASSES;
static {
Set<String> agentClasses = new LinkedHashSet<>();
agentClasses.add("org.zeroturnaround.javarebel.Integration");
agentClasses.add("org.zeroturnaround.javarebel.ReloaderFactory");
agentClasses.add("org.hotswap.agent.HotswapAgent");
AGENT_CLASSES = Collections.unmodifiableSet(agentClasses);
}
private AgentReloader() {
}
/**
* Determine if any agent reloader is active.
* @return true if agent reloading is active
*/
public static boolean isActive() {
return isActive(null) || isActive(AgentReloader.class.getClassLoader())
|| isActive(ClassLoader.getSystemClassLoader());
}
private static boolean isActive(ClassLoader classLoader) {
for (String agentClass : AGENT_CLASSES) {
if (ClassUtils.isPresent(agentClass, classLoader)) {
return true;
}
}
return false;
}
}

View File

@@ -0,0 +1,196 @@
/*
* Copyright 2012-2017 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.devtools.restart;
import java.io.File;
import java.io.IOException;
import java.lang.management.ManagementFactory;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.jar.Attributes;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
import java.util.stream.Stream;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.boot.devtools.settings.DevToolsSettings;
import org.springframework.util.StringUtils;
/**
* A filtered collection of URLs which can change after the application has started.
*
* @author Phillip Webb
* @author Andy Wilkinson
*/
final class ChangeableUrls implements Iterable<URL> {
private static final Log logger = LogFactory.getLog(ChangeableUrls.class);
private final List<URL> urls;
private ChangeableUrls(URL... urls) {
DevToolsSettings settings = DevToolsSettings.get();
List<URL> reloadableUrls = new ArrayList<>(urls.length);
for (URL url : urls) {
if ((settings.isRestartInclude(url) || isFolderUrl(url.toString()))
&& !settings.isRestartExclude(url)) {
reloadableUrls.add(url);
}
}
if (logger.isDebugEnabled()) {
logger.debug("Matching URLs for reloading : " + reloadableUrls);
}
this.urls = Collections.unmodifiableList(reloadableUrls);
}
private boolean isFolderUrl(String urlString) {
return urlString.startsWith("file:") && urlString.endsWith("/");
}
@Override
public Iterator<URL> iterator() {
return this.urls.iterator();
}
public int size() {
return this.urls.size();
}
public URL[] toArray() {
return this.urls.toArray(new URL[this.urls.size()]);
}
public List<URL> toList() {
return Collections.unmodifiableList(this.urls);
}
@Override
public String toString() {
return this.urls.toString();
}
public static ChangeableUrls fromClassLoader(ClassLoader classLoader) {
List<URL> urls = new ArrayList<>();
for (URL url : urlsFromClassLoader(classLoader)) {
urls.add(url);
urls.addAll(getUrlsFromClassPathOfJarManifestIfPossible(url));
}
return fromUrls(urls);
}
private static URL[] urlsFromClassLoader(ClassLoader classLoader) {
if (classLoader instanceof URLClassLoader) {
return ((URLClassLoader) classLoader).getURLs();
}
return Stream
.of(ManagementFactory.getRuntimeMXBean().getClassPath()
.split(File.pathSeparator))
.map(ChangeableUrls::toURL).toArray(URL[]::new);
}
private static URL toURL(String classPathEntry) {
try {
return new File(classPathEntry).toURI().toURL();
}
catch (MalformedURLException ex) {
throw new IllegalArgumentException(
"URL could not be created from '" + classPathEntry + "'", ex);
}
}
private static List<URL> getUrlsFromClassPathOfJarManifestIfPossible(URL url) {
JarFile jarFile = getJarFileIfPossible(url);
if (jarFile == null) {
return Collections.<URL>emptyList();
}
try {
return getUrlsFromManifestClassPathAttribute(url, jarFile);
}
catch (IOException ex) {
throw new IllegalStateException(
"Failed to read Class-Path attribute from manifest of jar " + url,
ex);
}
}
private static JarFile getJarFileIfPossible(URL url) {
try {
File file = new File(url.toURI());
if (file.isFile()) {
return new JarFile(file);
}
}
catch (Exception ex) {
// Assume it's not a jar and continue
}
return null;
}
private static List<URL> getUrlsFromManifestClassPathAttribute(URL jarUrl,
JarFile jarFile) throws IOException {
Manifest manifest = jarFile.getManifest();
if (manifest == null) {
return Collections.<URL>emptyList();
}
String classPath = manifest.getMainAttributes()
.getValue(Attributes.Name.CLASS_PATH);
if (!StringUtils.hasText(classPath)) {
return Collections.emptyList();
}
String[] entries = StringUtils.delimitedListToStringArray(classPath, " ");
List<URL> urls = new ArrayList<>(entries.length);
List<URL> nonExistentEntries = new ArrayList<>();
for (String entry : entries) {
try {
URL referenced = new URL(jarUrl, entry);
if (new File(referenced.getFile()).exists()) {
urls.add(referenced);
}
else {
nonExistentEntries.add(referenced);
}
}
catch (MalformedURLException ex) {
throw new IllegalStateException(
"Class-Path attribute contains malformed URL", ex);
}
}
if (!nonExistentEntries.isEmpty()) {
System.out.println("The Class-Path manifest attribute in " + jarFile.getName()
+ " referenced one or more files that do not exist: "
+ StringUtils.collectionToCommaDelimitedString(nonExistentEntries));
}
return urls;
}
public static ChangeableUrls fromUrls(Collection<URL> urls) {
return fromUrls(new ArrayList<>(urls).toArray(new URL[urls.size()]));
}
public static ChangeableUrls fromUrls(URL... urls) {
return new ChangeableUrls(urls);
}
}

View File

@@ -0,0 +1,289 @@
/*
* Copyright 2012-2017 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.devtools.restart;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.Map.Entry;
import org.springframework.boot.devtools.restart.classloader.ClassLoaderFile;
import org.springframework.boot.devtools.restart.classloader.ClassLoaderFile.Kind;
import org.springframework.boot.devtools.restart.classloader.ClassLoaderFileURLStreamHandler;
import org.springframework.boot.devtools.restart.classloader.ClassLoaderFiles;
import org.springframework.boot.devtools.restart.classloader.ClassLoaderFiles.SourceFolder;
import org.springframework.context.ApplicationContext;
import org.springframework.core.io.AbstractResource;
import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.core.io.ProtocolResolver;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.core.io.UrlResource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.ClassUtils;
import org.springframework.util.PathMatcher;
import org.springframework.util.ReflectionUtils;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.ServletContextResource;
import org.springframework.web.context.support.ServletContextResourcePatternResolver;
/**
* A {@code ResourcePatternResolver} that considers {@link ClassLoaderFiles} when
* resolving resources.
*
* @author Andy Wilkinson
* @author Phillip Webb
* @author Stephane Nicoll
*/
final class ClassLoaderFilesResourcePatternResolver implements ResourcePatternResolver {
private static final String[] LOCATION_PATTERN_PREFIXES = { CLASSPATH_ALL_URL_PREFIX,
CLASSPATH_URL_PREFIX };
private static final String WEB_CONTEXT_CLASS = "org.springframework.web.context."
+ "WebApplicationContext";
private final ResourcePatternResolver patternResolverDelegate;
private final PathMatcher antPathMatcher = new AntPathMatcher();
private final ClassLoaderFiles classLoaderFiles;
ClassLoaderFilesResourcePatternResolver(ApplicationContext applicationContext,
ClassLoaderFiles classLoaderFiles) {
this.classLoaderFiles = classLoaderFiles;
this.patternResolverDelegate = getResourcePatternResolverFactory()
.getResourcePatternResolver(applicationContext,
retrieveResourceLoader(applicationContext));
}
private ResourceLoader retrieveResourceLoader(ApplicationContext applicationContext) {
Field field = ReflectionUtils.findField(applicationContext.getClass(),
"resourceLoader", ResourceLoader.class);
if (field == null) {
return null;
}
ReflectionUtils.makeAccessible(field);
return (ResourceLoader) ReflectionUtils.getField(field, applicationContext);
}
private ResourcePatternResolverFactory getResourcePatternResolverFactory() {
if (ClassUtils.isPresent(WEB_CONTEXT_CLASS, null)) {
return new WebResourcePatternResolverFactory();
}
return new ResourcePatternResolverFactory();
}
@Override
public ClassLoader getClassLoader() {
return this.patternResolverDelegate.getClassLoader();
}
@Override
public Resource getResource(String location) {
Resource candidate = this.patternResolverDelegate.getResource(location);
if (isDeleted(candidate)) {
return new DeletedClassLoaderFileResource(location);
}
return candidate;
}
@Override
public Resource[] getResources(String locationPattern) throws IOException {
List<Resource> resources = new ArrayList<>();
Resource[] candidates = this.patternResolverDelegate
.getResources(locationPattern);
for (Resource candidate : candidates) {
if (!isDeleted(candidate)) {
resources.add(candidate);
}
}
resources.addAll(getAdditionalResources(locationPattern));
return resources.toArray(new Resource[resources.size()]);
}
private List<Resource> getAdditionalResources(String locationPattern)
throws MalformedURLException {
List<Resource> additionalResources = new ArrayList<>();
String trimmedLocationPattern = trimLocationPattern(locationPattern);
for (SourceFolder sourceFolder : this.classLoaderFiles.getSourceFolders()) {
for (Entry<String, ClassLoaderFile> entry : sourceFolder.getFilesEntrySet()) {
String name = entry.getKey();
ClassLoaderFile file = entry.getValue();
if (file.getKind() == Kind.ADDED
&& this.antPathMatcher.match(trimmedLocationPattern, name)) {
URL url = new URL("reloaded", null, -1, "/" + name,
new ClassLoaderFileURLStreamHandler(file));
UrlResource resource = new UrlResource(url);
additionalResources.add(resource);
}
}
}
return additionalResources;
}
private String trimLocationPattern(String pattern) {
for (String prefix : LOCATION_PATTERN_PREFIXES) {
if (pattern.startsWith(prefix)) {
return pattern.substring(prefix.length());
}
}
return pattern;
}
private boolean isDeleted(Resource resource) {
for (SourceFolder sourceFolder : this.classLoaderFiles.getSourceFolders()) {
for (Entry<String, ClassLoaderFile> entry : sourceFolder.getFilesEntrySet()) {
try {
String name = entry.getKey();
ClassLoaderFile file = entry.getValue();
if (file.getKind() == Kind.DELETED && resource.exists()
&& resource.getURI().toString().endsWith(name)) {
return true;
}
}
catch (IOException ex) {
throw new IllegalStateException(
"Failed to retrieve URI from '" + resource + "'", ex);
}
}
}
return false;
}
/**
* A {@link Resource} that represents a {@link ClassLoaderFile} that has been
* {@link Kind#DELETED deleted}.
*/
static final class DeletedClassLoaderFileResource extends AbstractResource {
private final String name;
private DeletedClassLoaderFileResource(String name) {
this.name = name;
}
@Override
public boolean exists() {
return false;
}
@Override
public String getDescription() {
return "Deleted: " + this.name;
}
@Override
public InputStream getInputStream() throws IOException {
throw new IOException(this.name + " has been deleted");
}
}
/**
* Factory used to create the {@link ResourcePatternResolver} delegate.
*/
private static class ResourcePatternResolverFactory {
public ResourcePatternResolver getResourcePatternResolver(
ApplicationContext applicationContext, ResourceLoader resourceLoader) {
if (resourceLoader == null) {
resourceLoader = new DefaultResourceLoader();
copyProtocolResolvers(applicationContext, resourceLoader);
}
return new PathMatchingResourcePatternResolver(resourceLoader);
}
protected final void copyProtocolResolvers(ApplicationContext applicationContext,
ResourceLoader resourceLoader) {
if (applicationContext instanceof DefaultResourceLoader
&& resourceLoader instanceof DefaultResourceLoader) {
copyProtocolResolvers((DefaultResourceLoader) applicationContext,
(DefaultResourceLoader) resourceLoader);
}
}
protected final void copyProtocolResolvers(DefaultResourceLoader source,
DefaultResourceLoader destination) {
for (ProtocolResolver resolver : source.getProtocolResolvers()) {
destination.addProtocolResolver(resolver);
}
}
}
/**
* {@link ResourcePatternResolverFactory} to be used when the classloader can access
* {@link WebApplicationContext}.
*/
private static class WebResourcePatternResolverFactory
extends ResourcePatternResolverFactory {
@Override
public ResourcePatternResolver getResourcePatternResolver(
ApplicationContext applicationContext, ResourceLoader resourceLoader) {
if (applicationContext instanceof WebApplicationContext) {
return getResourcePatternResolver(
(WebApplicationContext) applicationContext, resourceLoader);
}
return super.getResourcePatternResolver(applicationContext, resourceLoader);
}
private ResourcePatternResolver getResourcePatternResolver(
WebApplicationContext applicationContext, ResourceLoader resourceLoader) {
if (resourceLoader == null) {
resourceLoader = new WebApplicationContextResourceLoader(
applicationContext);
copyProtocolResolvers(applicationContext, resourceLoader);
}
return new ServletContextResourcePatternResolver(resourceLoader);
}
}
/**
* {@link ResourceLoader} that optionally supports {@link ServletContextResource
* ServletContextResources}.
*/
private static class WebApplicationContextResourceLoader
extends DefaultResourceLoader {
private final WebApplicationContext applicationContext;
WebApplicationContextResourceLoader(WebApplicationContext applicationContext) {
this.applicationContext = applicationContext;
}
@Override
protected Resource getResourceByPath(String path) {
if (this.applicationContext.getServletContext() != null) {
return new ServletContextResource(
this.applicationContext.getServletContext(), path);
}
return super.getResourceByPath(path);
}
}
}

View File

@@ -0,0 +1,40 @@
/*
* 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.devtools.restart;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.context.annotation.Conditional;
/**
* {@link Conditional} that only matches when the {@link RestartInitializer} has been
* applied with non {@code null} URLs.
*
* @author Phillip Webb
* @since 1.3.0
*/
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(OnInitializedRestarterCondition.class)
public @interface ConditionalOnInitializedRestarter {
}

View File

@@ -0,0 +1,97 @@
/*
* Copyright 2012-2017 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.devtools.restart;
import java.net.URL;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.stream.Stream;
/**
* Default {@link RestartInitializer} that only enable initial restart when running a
* standard "main" method. Skips initialization when running "fat" jars (included
* exploded) or when running from a test.
*
* @author Phillip Webb
* @author Andy Wilkinson
* @since 1.3.0
*/
public class DefaultRestartInitializer implements RestartInitializer {
private static final Set<String> SKIPPED_STACK_ELEMENTS;
static {
Set<String> skipped = new LinkedHashSet<>();
skipped.add("org.junit.runners.");
skipped.add("org.springframework.boot.test.");
skipped.add("cucumber.runtime.");
SKIPPED_STACK_ELEMENTS = Collections.unmodifiableSet(skipped);
}
@Override
public URL[] getInitialUrls(Thread thread) {
if (!isMain(thread)) {
return null;
}
for (StackTraceElement element : thread.getStackTrace()) {
if (isSkippedStackElement(element)) {
return null;
}
}
URL[] urls = getUrls(thread);
Stream.of(urls).forEach(System.out::println);
return urls;
}
/**
* Returns if the thread is for a main invocation. By default checks the name of the
* thread and the context classloader.
* @param thread the thread to check
* @return {@code true} if the thread is a main invocation
*/
protected boolean isMain(Thread thread) {
return thread.getName().equals("main") && thread.getContextClassLoader()
.getClass().getName().contains("AppClassLoader");
}
/**
* Checks if a specific {@link StackTraceElement} should cause the initializer to be
* skipped.
* @param element the stack element to check
* @return {@code true} if the stack element means that the initializer should be
* skipped
*/
protected boolean isSkippedStackElement(StackTraceElement element) {
for (String skipped : SKIPPED_STACK_ELEMENTS) {
if (element.getClassName().startsWith(skipped)) {
return true;
}
}
return false;
}
/**
* Return the URLs that should be used with initialization.
* @param thread the source thread
* @return the URLs
*/
protected URL[] getUrls(Thread thread) {
return ChangeableUrls.fromClassLoader(thread.getContextClassLoader()).toArray();
}
}

View File

@@ -0,0 +1,58 @@
/*
* Copyright 2012-2017 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.devtools.restart;
/**
* Strategy used to handle launch failures.
*
* @author Phillip Webb
* @since 1.3.0
*/
@FunctionalInterface
public interface FailureHandler {
/**
* {@link FailureHandler} that always aborts.
*/
FailureHandler NONE = (failure) -> Outcome.ABORT;
/**
* Handle a run failure. Implementations may block, for example to wait until specific
* files are updated.
* @param failure the exception
* @return the outcome
*/
Outcome handle(Throwable failure);
/**
* Various outcomes for the handler.
*/
enum Outcome {
/**
* Abort the relaunch.
*/
ABORT,
/**
* Try again to relaunch the application.
*/
RETRY
}
}

View File

@@ -0,0 +1,84 @@
/*
* 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.devtools.restart;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import org.springframework.util.Assert;
/**
* The "main" method located from a running thread.
*
* @author Phillip Webb
*/
class MainMethod {
private final Method method;
MainMethod() {
this(Thread.currentThread());
}
MainMethod(Thread thread) {
Assert.notNull(thread, "Thread must not be null");
this.method = getMainMethod(thread);
}
private Method getMainMethod(Thread thread) {
for (StackTraceElement element : thread.getStackTrace()) {
if ("main".equals(element.getMethodName())) {
Method method = getMainMethod(element);
if (method != null) {
return method;
}
}
}
throw new IllegalStateException("Unable to find main method");
}
private Method getMainMethod(StackTraceElement element) {
try {
Class<?> elementClass = Class.forName(element.getClassName());
Method method = elementClass.getDeclaredMethod("main", String[].class);
if (Modifier.isStatic(method.getModifiers())) {
return method;
}
}
catch (Exception ex) {
// Ignore
}
return null;
}
/**
* Returns the actual main method.
* @return the main method
*/
public Method getMethod() {
return this.method;
}
/**
* Return the name of the declaring class.
* @return the declaring class name
*/
public String getDeclaringClassName() {
return this.method.getDeclaringClass().getName();
}
}

View File

@@ -0,0 +1,58 @@
/*
* Copyright 2012-2016 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.devtools.restart;
import org.springframework.boot.autoconfigure.condition.ConditionMessage;
import org.springframework.boot.autoconfigure.condition.ConditionOutcome;
import org.springframework.boot.autoconfigure.condition.SpringBootCondition;
import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.type.AnnotatedTypeMetadata;
/**
* {@link Condition} that checks that a {@link Restarter} is available and initialized.
*
* @author Phillip Webb
* @see ConditionalOnInitializedRestarter
*/
class OnInitializedRestarterCondition extends SpringBootCondition {
@Override
public ConditionOutcome getMatchOutcome(ConditionContext context,
AnnotatedTypeMetadata metadata) {
ConditionMessage.Builder message = ConditionMessage
.forCondition("Initialized Restarter Condition");
Restarter restarter = getRestarter();
if (restarter == null) {
return ConditionOutcome.noMatch(message.because("unavailable"));
}
if (restarter.getInitialUrls() == null) {
return ConditionOutcome.noMatch(message.because("initialized without URLs"));
}
return ConditionOutcome.match(message.because("available and initialized"));
}
private Restarter getRestarter() {
try {
return Restarter.getInstance();
}
catch (Exception ex) {
return null;
}
}
}

View File

@@ -0,0 +1,95 @@
/*
* Copyright 2012-2016 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.devtools.restart;
import org.springframework.boot.context.event.ApplicationFailedEvent;
import org.springframework.boot.context.event.ApplicationPreparedEvent;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.boot.context.event.ApplicationStartingEvent;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.core.Ordered;
/**
* {@link ApplicationListener} to initialize the {@link Restarter}.
*
* @author Phillip Webb
* @author Andy Wilkinson
* @since 1.3.0
* @see Restarter
*/
public class RestartApplicationListener
implements ApplicationListener<ApplicationEvent>, Ordered {
private int order = HIGHEST_PRECEDENCE;
private static final String ENABLED_PROPERTY = "spring.devtools.restart.enabled";
@Override
public void onApplicationEvent(ApplicationEvent event) {
if (event instanceof ApplicationStartingEvent) {
onApplicationStartingEvent((ApplicationStartingEvent) event);
}
if (event instanceof ApplicationPreparedEvent) {
onApplicationPreparedEvent((ApplicationPreparedEvent) event);
}
if (event instanceof ApplicationReadyEvent
|| event instanceof ApplicationFailedEvent) {
Restarter.getInstance().finish();
}
if (event instanceof ApplicationFailedEvent) {
onApplicationFailedEvent((ApplicationFailedEvent) event);
}
}
private void onApplicationStartingEvent(ApplicationStartingEvent event) {
// It's too early to use the Spring environment but we should still allow
// users to disable restart using a System property.
String enabled = System.getProperty(ENABLED_PROPERTY);
if (enabled == null || Boolean.parseBoolean(enabled)) {
String[] args = event.getArgs();
DefaultRestartInitializer initializer = new DefaultRestartInitializer();
boolean restartOnInitialize = !AgentReloader.isActive();
Restarter.initialize(args, false, initializer, restartOnInitialize);
}
else {
Restarter.disable();
}
}
private void onApplicationPreparedEvent(ApplicationPreparedEvent event) {
Restarter.getInstance().prepare(event.getApplicationContext());
}
private void onApplicationFailedEvent(ApplicationFailedEvent event) {
Restarter.getInstance().remove(event.getApplicationContext());
}
@Override
public int getOrder() {
return this.order;
}
/**
* Set the order of the listener.
* @param order the order of the listener
*/
public void setOrder(int order) {
this.order = order;
}
}

View File

@@ -0,0 +1,44 @@
/*
* Copyright 2012-2017 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.devtools.restart;
import java.net.URL;
/**
* Strategy interface used to initialize a {@link Restarter}.
*
* @author Phillip Webb
* @since 1.3.0
* @see DefaultRestartInitializer
*/
@FunctionalInterface
public interface RestartInitializer {
/**
* {@link RestartInitializer} that doesn't return any URLs.
*/
RestartInitializer NONE = (thread) -> null;
/**
* Return the initial set of URLs for the {@link Restarter} or {@code null} if no
* initial restart is required.
* @param thread the source thread
* @return initial URLs or {@code null}
*/
URL[] getInitialUrls(Thread thread);
}

View File

@@ -0,0 +1,61 @@
/*
* 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.devtools.restart;
import java.lang.reflect.Method;
/**
* Thread used to launch a restarted application.
*
* @author Phillip Webb
*/
class RestartLauncher extends Thread {
private final String mainClassName;
private final String[] args;
private Throwable error;
RestartLauncher(ClassLoader classLoader, String mainClassName, String[] args,
UncaughtExceptionHandler exceptionHandler) {
this.mainClassName = mainClassName;
this.args = args;
setName("restartedMain");
setUncaughtExceptionHandler(exceptionHandler);
setDaemon(false);
setContextClassLoader(classLoader);
}
@Override
public void run() {
try {
Class<?> mainClass = getContextClassLoader().loadClass(this.mainClassName);
Method mainMethod = mainClass.getDeclaredMethod("main", String[].class);
mainMethod.invoke(null, new Object[] { this.args });
}
catch (Throwable ex) {
this.error = ex;
getUncaughtExceptionHandler().uncaughtException(this, ex);
}
}
public Throwable getError() {
return this.error;
}
}

View File

@@ -0,0 +1,33 @@
/*
* Copyright 2012-2017 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.devtools.restart;
/**
* Listener that is notified of application restarts.
*
* @author Andy Wilkinson
* @since 1.3.0
*/
@FunctionalInterface
public interface RestartListener {
/**
* Called before an application restart.
*/
void beforeRestart();
}

View File

@@ -0,0 +1,41 @@
/*
* 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.devtools.restart;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.context.annotation.Scope;
/**
* Restart {@code @Scope} Annotation used to indicate that a bean should remain between
* restarts.
*
* @author Phillip Webb
* @since 1.3.0
* @see RestartScopeInitializer
*/
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Scope("restart")
public @interface RestartScope {
}

View File

@@ -0,0 +1,69 @@
/*
* Copyright 2012-2016 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.devtools.restart;
import org.springframework.beans.factory.ObjectFactory;
import org.springframework.beans.factory.config.Scope;
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ConfigurableApplicationContext;
/**
* Support for a 'restart' {@link Scope} that allows beans to remain between restarts.
*
* @author Phillip Webb
* @since 1.3.0
*/
public class RestartScopeInitializer
implements ApplicationContextInitializer<ConfigurableApplicationContext> {
@Override
public void initialize(ConfigurableApplicationContext applicationContext) {
applicationContext.getBeanFactory().registerScope("restart", new RestartScope());
}
/**
* {@link Scope} that stores beans as {@link Restarter} attributes.
*/
private static class RestartScope implements Scope {
@Override
public Object get(String name, ObjectFactory<?> objectFactory) {
return Restarter.getInstance().getOrAddAttribute(name, objectFactory);
}
@Override
public Object remove(String name) {
return Restarter.getInstance().removeAttribute(name);
}
@Override
public void registerDestructionCallback(String name, Runnable callback) {
}
@Override
public Object resolveContextualObject(String key) {
return null;
}
@Override
public String getConversationId() {
return null;
}
}
}

View File

@@ -0,0 +1,643 @@
/*
* Copyright 2012-2017 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.devtools.restart;
import java.beans.Introspector;
import java.lang.Thread.UncaughtExceptionHandler;
import java.lang.reflect.Field;
import java.net.URL;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.BlockingDeque;
import java.util.concurrent.Callable;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.CachedIntrospectionResults;
import org.springframework.beans.factory.ObjectFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.devtools.restart.FailureHandler.Outcome;
import org.springframework.boot.devtools.restart.classloader.ClassLoaderFiles;
import org.springframework.boot.devtools.restart.classloader.RestartClassLoader;
import org.springframework.boot.logging.DeferredLog;
import org.springframework.cglib.core.ClassNameReader;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.support.GenericApplicationContext;
import org.springframework.core.ResolvableType;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.io.ResourceLoader;
import org.springframework.util.Assert;
import org.springframework.util.ReflectionUtils;
/**
* Allows a running application to be restarted with an updated classpath. The restarter
* works by creating a new application ClassLoader that is split into two parts. The top
* part contains static URLs that don't change (for example 3rd party libraries and Spring
* Boot itself) and the bottom part contains URLs where classes and resources might be
* updated.
* <p>
* The Restarter should be {@link #initialize(String[]) initialized} early to ensure that
* classes are loaded multiple times. Mostly the {@link RestartApplicationListener} can be
* relied upon to perform initialization, however, you may need to call
* {@link #initialize(String[])} directly if your SpringApplication arguments are not
* identical to your main method arguments.
* <p>
* By default, applications running in an IDE (i.e. those not packaged as "fat jars") will
* automatically detect URLs that can change. It's also possible to manually configure
* URLs or class file updates for remote restart scenarios.
*
* @author Phillip Webb
* @author Andy Wilkinson
* @since 1.3.0
* @see RestartApplicationListener
* @see #initialize(String[])
* @see #getInstance()
* @see #restart()
*/
public class Restarter {
private static final Object INSTANCE_MONITOR = new Object();
private static final String[] NO_ARGS = {};
private static Restarter instance;
private final Set<URL> urls = new LinkedHashSet<>();
private final ClassLoaderFiles classLoaderFiles = new ClassLoaderFiles();
private final Map<String, Object> attributes = new HashMap<>();
private final BlockingDeque<LeakSafeThread> leakSafeThreads = new LinkedBlockingDeque<>();
private final Lock stopLock = new ReentrantLock();
private final Object monitor = new Object();
private Log logger = new DeferredLog();
private final boolean forceReferenceCleanup;
private boolean enabled = true;
private URL[] initialUrls;
private final String mainClassName;
private final ClassLoader applicationClassLoader;
private final String[] args;
private final UncaughtExceptionHandler exceptionHandler;
private boolean finished = false;
private final List<ConfigurableApplicationContext> rootContexts = new CopyOnWriteArrayList<>();
/**
* Internal constructor to create a new {@link Restarter} instance.
* @param thread the source thread
* @param args the application arguments
* @param forceReferenceCleanup if soft/weak reference cleanup should be forced
* @param initializer the restart initializer
* @see #initialize(String[])
*/
protected Restarter(Thread thread, String[] args, boolean forceReferenceCleanup,
RestartInitializer initializer) {
Assert.notNull(thread, "Thread must not be null");
Assert.notNull(args, "Args must not be null");
Assert.notNull(initializer, "Initializer must not be null");
this.logger.debug("Creating new Restarter for thread " + thread);
SilentExitExceptionHandler.setup(thread);
this.forceReferenceCleanup = forceReferenceCleanup;
this.initialUrls = initializer.getInitialUrls(thread);
this.mainClassName = getMainClassName(thread);
this.applicationClassLoader = thread.getContextClassLoader();
this.args = args;
this.exceptionHandler = thread.getUncaughtExceptionHandler();
this.leakSafeThreads.add(new LeakSafeThread());
}
private String getMainClassName(Thread thread) {
try {
return new MainMethod(thread).getDeclaringClassName();
}
catch (Exception ex) {
return null;
}
}
protected void initialize(boolean restartOnInitialize) {
preInitializeLeakyClasses();
if (this.initialUrls != null) {
this.urls.addAll(Arrays.asList(this.initialUrls));
if (restartOnInitialize) {
this.logger.debug("Immediately restarting application");
immediateRestart();
}
}
}
private void immediateRestart() {
try {
getLeakSafeThread().callAndWait(() -> {
start(FailureHandler.NONE);
cleanupCaches();
return null;
});
}
catch (Exception ex) {
this.logger.warn("Unable to initialize restarter", ex);
}
SilentExitExceptionHandler.exitCurrentThread();
}
/**
* CGLIB has a private exception field which needs to initialized early to ensure that
* the stacktrace doesn't retain a reference to the RestartClassLoader.
*/
private void preInitializeLeakyClasses() {
try {
Class<?> readerClass = ClassNameReader.class;
Field field = readerClass.getDeclaredField("EARLY_EXIT");
field.setAccessible(true);
((Throwable) field.get(null)).fillInStackTrace();
}
catch (Exception ex) {
this.logger.warn("Unable to pre-initialize classes", ex);
}
}
/**
* Set if restart support is enabled.
* @param enabled if restart support is enabled
*/
private void setEnabled(boolean enabled) {
this.enabled = enabled;
}
/**
* Add additional URLs to be includes in the next restart.
* @param urls the urls to add
*/
public void addUrls(Collection<URL> urls) {
Assert.notNull(urls, "Urls must not be null");
this.urls.addAll(urls);
}
/**
* Add additional {@link ClassLoaderFiles} to be included in the next restart.
* @param classLoaderFiles the files to add
*/
public void addClassLoaderFiles(ClassLoaderFiles classLoaderFiles) {
Assert.notNull(classLoaderFiles, "ClassLoaderFiles must not be null");
this.classLoaderFiles.addAll(classLoaderFiles);
}
/**
* Return a {@link ThreadFactory} that can be used to create leak safe threads.
* @return a leak safe thread factory
*/
public ThreadFactory getThreadFactory() {
return new LeakSafeThreadFactory();
}
/**
* Restart the running application.
*/
public void restart() {
restart(FailureHandler.NONE);
}
/**
* Restart the running application.
* @param failureHandler a failure handler to deal with application that doesn't start
*/
public void restart(final FailureHandler failureHandler) {
if (!this.enabled) {
this.logger.debug("Application restart is disabled");
return;
}
this.logger.debug("Restarting application");
getLeakSafeThread().call(() -> {
Restarter.this.stop();
Restarter.this.start(failureHandler);
return null;
});
}
/**
* Start the application.
* @param failureHandler a failure handler for application that won't start
* @throws Exception in case of errors
*/
protected void start(FailureHandler failureHandler) throws Exception {
do {
Throwable error = doStart();
if (error == null) {
return;
}
if (failureHandler.handle(error) == Outcome.ABORT) {
return;
}
}
while (true);
}
private Throwable doStart() throws Exception {
Assert.notNull(this.mainClassName, "Unable to find the main class to restart");
ClassLoader parent = this.applicationClassLoader;
URL[] urls = this.urls.toArray(new URL[this.urls.size()]);
ClassLoaderFiles updatedFiles = new ClassLoaderFiles(this.classLoaderFiles);
ClassLoader classLoader = new RestartClassLoader(parent, urls, updatedFiles,
this.logger);
if (this.logger.isDebugEnabled()) {
this.logger.debug("Starting application " + this.mainClassName + " with URLs "
+ Arrays.asList(urls));
}
return relaunch(classLoader);
}
/**
* Relaunch the application using the specified classloader.
* @param classLoader the classloader to use
* @return any exception that caused the launch to fail or {@code null}
* @throws Exception in case of errors
*/
protected Throwable relaunch(ClassLoader classLoader) throws Exception {
RestartLauncher launcher = new RestartLauncher(classLoader, this.mainClassName,
this.args, this.exceptionHandler);
launcher.start();
launcher.join();
return launcher.getError();
}
/**
* Stop the application.
* @throws Exception in case of errors
*/
protected void stop() throws Exception {
this.logger.debug("Stopping application");
this.stopLock.lock();
try {
for (ConfigurableApplicationContext context : this.rootContexts) {
context.close();
this.rootContexts.remove(context);
}
cleanupCaches();
if (this.forceReferenceCleanup) {
forceReferenceCleanup();
}
}
finally {
this.stopLock.unlock();
}
System.gc();
System.runFinalization();
}
private void cleanupCaches() throws Exception {
Introspector.flushCaches();
cleanupKnownCaches();
}
private void cleanupKnownCaches() throws Exception {
// Whilst not strictly necessary it helps to cleanup soft reference caches
// early rather than waiting for memory limits to be reached
clear(ResolvableType.class, "cache");
clear("org.springframework.core.SerializableTypeWrapper", "cache");
clear(CachedIntrospectionResults.class, "acceptedClassLoaders");
clear(CachedIntrospectionResults.class, "strongClassCache");
clear(CachedIntrospectionResults.class, "softClassCache");
clear(ReflectionUtils.class, "declaredFieldsCache");
clear(ReflectionUtils.class, "declaredMethodsCache");
clear(AnnotationUtils.class, "findAnnotationCache");
clear(AnnotationUtils.class, "annotatedInterfaceCache");
clear("com.sun.naming.internal.ResourceManager", "propertiesCache");
}
private void clear(String className, String fieldName) {
try {
clear(Class.forName(className), fieldName);
}
catch (Exception ex) {
this.logger.debug("Unable to clear field " + className + " " + fieldName, ex);
}
}
private void clear(Class<?> type, String fieldName) throws Exception {
Field field = type.getDeclaredField(fieldName);
field.setAccessible(true);
Object instance = field.get(null);
if (instance instanceof Set) {
((Set<?>) instance).clear();
}
if (instance instanceof Map) {
Map<?, ?> map = ((Map<?, ?>) instance);
for (Iterator<?> iterator = map.keySet().iterator(); iterator.hasNext();) {
Object value = iterator.next();
if (value instanceof Class && ((Class<?>) value)
.getClassLoader() instanceof RestartClassLoader) {
iterator.remove();
}
}
}
}
/**
* Cleanup any soft/weak references by forcing an {@link OutOfMemoryError} error.
*/
private void forceReferenceCleanup() {
try {
final List<long[]> memory = new LinkedList<>();
while (true) {
memory.add(new long[102400]);
}
}
catch (final OutOfMemoryError ex) {
// Expected
}
}
/**
* Called to finish {@link Restarter} initialization when application logging is
* available.
*/
void finish() {
synchronized (this.monitor) {
if (!isFinished()) {
this.logger = DeferredLog.replay(this.logger,
LogFactory.getLog(getClass()));
this.finished = true;
}
}
}
boolean isFinished() {
synchronized (this.monitor) {
return this.finished;
}
}
void prepare(ConfigurableApplicationContext applicationContext) {
if (applicationContext != null && applicationContext.getParent() != null) {
return;
}
if (applicationContext instanceof GenericApplicationContext) {
prepare((GenericApplicationContext) applicationContext);
}
this.rootContexts.add(applicationContext);
}
void remove(ConfigurableApplicationContext applicationContext) {
if (applicationContext != null) {
this.rootContexts.remove(applicationContext);
}
}
private void prepare(GenericApplicationContext applicationContext) {
ResourceLoader resourceLoader = new ClassLoaderFilesResourcePatternResolver(
applicationContext, this.classLoaderFiles);
applicationContext.setResourceLoader(resourceLoader);
}
private LeakSafeThread getLeakSafeThread() {
try {
return this.leakSafeThreads.takeFirst();
}
catch (InterruptedException ex) {
Thread.currentThread().interrupt();
throw new IllegalStateException(ex);
}
}
public Object getOrAddAttribute(final String name,
final ObjectFactory<?> objectFactory) {
synchronized (this.attributes) {
if (!this.attributes.containsKey(name)) {
this.attributes.put(name, objectFactory.getObject());
}
return this.attributes.get(name);
}
}
public Object removeAttribute(String name) {
synchronized (this.attributes) {
return this.attributes.remove(name);
}
}
/**
* Return the initial set of URLs as configured by the {@link RestartInitializer}.
* @return the initial URLs or {@code null}
*/
public URL[] getInitialUrls() {
return this.initialUrls;
}
/**
* Initialize and disable restart support.
*/
public static void disable() {
initialize(NO_ARGS, false, RestartInitializer.NONE);
getInstance().setEnabled(false);
}
/**
* Initialize restart support. See
* {@link #initialize(String[], boolean, RestartInitializer)} for details.
* @param args main application arguments
* @see #initialize(String[], boolean, RestartInitializer)
*/
public static void initialize(String[] args) {
initialize(args, false, new DefaultRestartInitializer());
}
/**
* Initialize restart support. See
* {@link #initialize(String[], boolean, RestartInitializer)} for details.
* @param args main application arguments
* @param initializer the restart initializer
* @see #initialize(String[], boolean, RestartInitializer)
*/
public static void initialize(String[] args, RestartInitializer initializer) {
initialize(args, false, initializer, true);
}
/**
* Initialize restart support. See
* {@link #initialize(String[], boolean, RestartInitializer)} for details.
* @param args main application arguments
* @param forceReferenceCleanup if forcing of soft/weak reference should happen on
* @see #initialize(String[], boolean, RestartInitializer)
*/
public static void initialize(String[] args, boolean forceReferenceCleanup) {
initialize(args, forceReferenceCleanup, new DefaultRestartInitializer());
}
/**
* Initialize restart support. See
* {@link #initialize(String[], boolean, RestartInitializer)} for details.
* @param args main application arguments
* @param forceReferenceCleanup if forcing of soft/weak reference should happen on
* @param initializer the restart initializer
* @see #initialize(String[], boolean, RestartInitializer)
*/
public static void initialize(String[] args, boolean forceReferenceCleanup,
RestartInitializer initializer) {
initialize(args, forceReferenceCleanup, initializer, true);
}
/**
* Initialize restart support for the current application. Called automatically by
* {@link RestartApplicationListener} but can also be called directly if main
* application arguments are not the same as those passed to the
* {@link SpringApplication}.
* @param args main application arguments
* @param forceReferenceCleanup if forcing of soft/weak reference should happen on
* each restart. This will slow down restarts and is intended primarily for testing
* @param initializer the restart initializer
* @param restartOnInitialize if the restarter should be restarted immediately when
* the {@link RestartInitializer} returns non {@code null} results
*/
public static void initialize(String[] args, boolean forceReferenceCleanup,
RestartInitializer initializer, boolean restartOnInitialize) {
Restarter localInstance = null;
synchronized (INSTANCE_MONITOR) {
if (instance == null) {
localInstance = new Restarter(Thread.currentThread(), args,
forceReferenceCleanup, initializer);
instance = localInstance;
}
}
if (localInstance != null) {
localInstance.initialize(restartOnInitialize);
}
}
/**
* Return the active {@link Restarter} instance. Cannot be called before
* {@link #initialize(String[]) initialization}.
* @return the restarter
*/
public static Restarter getInstance() {
synchronized (INSTANCE_MONITOR) {
Assert.state(instance != null, "Restarter has not been initialized");
return instance;
}
}
/**
* Set the restarter instance (useful for testing).
* @param instance the instance to set
*/
final static void setInstance(Restarter instance) {
synchronized (INSTANCE_MONITOR) {
Restarter.instance = instance;
}
}
/**
* Clear the instance. Primarily provided for tests and not usually used in
* application code.
*/
public static void clearInstance() {
synchronized (INSTANCE_MONITOR) {
instance = null;
}
}
/**
* Thread that is created early so not to retain the {@link RestartClassLoader}.
*/
private class LeakSafeThread extends Thread {
private Callable<?> callable;
private Object result;
LeakSafeThread() {
setDaemon(false);
}
public void call(Callable<?> callable) {
this.callable = callable;
start();
}
@SuppressWarnings("unchecked")
public <V> V callAndWait(Callable<V> callable) {
this.callable = callable;
start();
try {
join();
return (V) this.result;
}
catch (InterruptedException ex) {
Thread.currentThread().interrupt();
throw new IllegalStateException(ex);
}
}
@Override
public void run() {
// We are safe to refresh the ActionThread (and indirectly call
// AccessController.getContext()) since our stack doesn't include the
// RestartClassLoader
try {
Restarter.this.leakSafeThreads.put(new LeakSafeThread());
this.result = this.callable.call();
}
catch (Exception ex) {
ex.printStackTrace();
System.exit(1);
}
}
}
/**
* {@link ThreadFactory} that creates a leak safe thread.
*/
private class LeakSafeThreadFactory implements ThreadFactory {
@Override
public Thread newThread(final Runnable runnable) {
return getLeakSafeThread().callAndWait(() -> {
Thread thread = new Thread(runnable);
thread.setContextClassLoader(Restarter.this.applicationClassLoader);
return thread;
});
}
}
}

View File

@@ -0,0 +1,97 @@
/*
* Copyright 2012-2016 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.devtools.restart;
import java.lang.Thread.UncaughtExceptionHandler;
import java.util.Arrays;
/**
* {@link UncaughtExceptionHandler} decorator that allows a thread to exit silently.
*
* @author Phillip Webb
* @author Andy Wilkinson
*/
class SilentExitExceptionHandler implements UncaughtExceptionHandler {
private final UncaughtExceptionHandler delegate;
SilentExitExceptionHandler(UncaughtExceptionHandler delegate) {
this.delegate = delegate;
}
@Override
public void uncaughtException(Thread thread, Throwable exception) {
if (exception instanceof SilentExitException) {
if (isJvmExiting(thread)) {
preventNonZeroExitCode();
}
return;
}
if (this.delegate != null) {
this.delegate.uncaughtException(thread, exception);
}
}
private boolean isJvmExiting(Thread exceptionThread) {
for (Thread thread : getAllThreads()) {
if (thread != exceptionThread && thread.isAlive() && !thread.isDaemon()) {
return false;
}
}
return true;
}
protected Thread[] getAllThreads() {
ThreadGroup rootThreadGroup = getRootThreadGroup();
Thread[] threads = new Thread[32];
int count = rootThreadGroup.enumerate(threads);
while (count == threads.length) {
threads = new Thread[threads.length * 2];
count = rootThreadGroup.enumerate(threads);
}
return Arrays.copyOf(threads, count);
}
private ThreadGroup getRootThreadGroup() {
ThreadGroup candidate = Thread.currentThread().getThreadGroup();
while (candidate.getParent() != null) {
candidate = candidate.getParent();
}
return candidate;
}
protected void preventNonZeroExitCode() {
System.exit(0);
}
public static void setup(Thread thread) {
UncaughtExceptionHandler handler = thread.getUncaughtExceptionHandler();
if (!(handler instanceof SilentExitExceptionHandler)) {
handler = new SilentExitExceptionHandler(handler);
thread.setUncaughtExceptionHandler(handler);
}
}
public static void exitCurrentThread() {
throw new SilentExitException();
}
private static class SilentExitException extends RuntimeException {
}
}

View File

@@ -0,0 +1,112 @@
/*
* 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.devtools.restart.classloader;
import java.io.Serializable;
import org.springframework.util.Assert;
/**
* A single file that may be served from a {@link ClassLoader}. Can be used to represent
* files that have been added, modified or deleted since the original JAR was created.
*
* @author Phillip Webb
* @since 1.3.0
* @see ClassLoaderFileRepository
*/
public class ClassLoaderFile implements Serializable {
private static final long serialVersionUID = 1;
private final Kind kind;
private final byte[] contents;
private final long lastModified;
/**
* Create a new {@link ClassLoaderFile} instance.
* @param kind the kind of file
* @param contents the file contents
*/
public ClassLoaderFile(Kind kind, byte[] contents) {
this(kind, System.currentTimeMillis(), contents);
}
/**
* Create a new {@link ClassLoaderFile} instance.
* @param kind the kind of file
* @param lastModified the last modified time
* @param contents the file contents
*/
public ClassLoaderFile(Kind kind, long lastModified, byte[] contents) {
Assert.notNull(kind, "Kind must not be null");
Assert.isTrue(kind == Kind.DELETED ? contents == null : contents != null,
"Contents must " + (kind == Kind.DELETED ? "" : "not ") + "be null");
this.kind = kind;
this.lastModified = lastModified;
this.contents = contents;
}
/**
* Return the file {@link Kind} (added, modified, deleted).
* @return the kind
*/
public Kind getKind() {
return this.kind;
}
/**
* Return the time that the file was last modified.
* @return the last modified time
*/
public long getLastModified() {
return this.lastModified;
}
/**
* Return the contents of the file as a byte array or {@code null} if
* {@link #getKind()} is {@link Kind#DELETED}.
* @return the contents or {@code null}
*/
public byte[] getContents() {
return this.contents;
}
/**
* The kinds of class load files.
*/
public enum Kind {
/**
* The file has been added since the original JAR was created.
*/
ADDED,
/**
* The file has been modified since the original JAR was created.
*/
MODIFIED,
/**
* The file has been deleted since the original JAR was created.
*/
DELETED
}
}

View File

@@ -0,0 +1,44 @@
/*
* Copyright 2012-2017 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.devtools.restart.classloader;
/**
* A container for files that may be served from a {@link ClassLoader}. Can be used to
* represent files that have been added, modified or deleted since the original JAR was
* created.
*
* @author Phillip Webb
* @since 1.3.0
* @see ClassLoaderFile
*/
@FunctionalInterface
public interface ClassLoaderFileRepository {
/**
* Empty {@link ClassLoaderFileRepository} implementation.
*/
ClassLoaderFileRepository NONE = (name) -> null;
/**
* Return a {@link ClassLoaderFile} for the given name or {@code null} if no file is
* contained in this collection.
* @param name the name of the file
* @return a {@link ClassLoaderFile} or {@code null}
*/
ClassLoaderFile getFile(String name);
}

View File

@@ -0,0 +1,69 @@
/*
* Copyright 2012-2016 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.devtools.restart.classloader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLStreamHandler;
/**
* {@link URLStreamHandler} for the contents of a {@link ClassLoaderFile}.
*
* @author Phillip Webb
* @since 1.5.0
*/
public class ClassLoaderFileURLStreamHandler extends URLStreamHandler {
private ClassLoaderFile file;
public ClassLoaderFileURLStreamHandler(ClassLoaderFile file) {
this.file = file;
}
@Override
protected URLConnection openConnection(URL url) throws IOException {
return new Connection(url);
}
private class Connection extends URLConnection {
Connection(URL url) {
super(url);
}
@Override
public void connect() throws IOException {
}
@Override
public InputStream getInputStream() throws IOException {
return new ByteArrayInputStream(
ClassLoaderFileURLStreamHandler.this.file.getContents());
}
@Override
public long getLastModified() {
return ClassLoaderFileURLStreamHandler.this.file.getLastModified();
}
}
}

View File

@@ -0,0 +1,201 @@
/*
* Copyright 2012-2017 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.devtools.restart.classloader;
import java.io.Serializable;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import javax.management.loading.ClassLoaderRepository;
import org.springframework.util.Assert;
/**
* {@link ClassLoaderFileRepository} that maintains a collection of
* {@link ClassLoaderFile} items grouped by source folders.
*
* @author Phillip Webb
* @since 1.3.0
* @see ClassLoaderFile
* @see ClassLoaderRepository
*/
public class ClassLoaderFiles implements ClassLoaderFileRepository, Serializable {
private static final long serialVersionUID = 1;
private final Map<String, SourceFolder> sourceFolders;
/**
* Create a new {@link ClassLoaderFiles} instance.
*/
public ClassLoaderFiles() {
this.sourceFolders = new LinkedHashMap<>();
}
/**
* Create a new {@link ClassLoaderFiles} instance.
* @param classLoaderFiles the source classloader files.
*/
public ClassLoaderFiles(ClassLoaderFiles classLoaderFiles) {
Assert.notNull(classLoaderFiles, "ClassLoaderFiles must not be null");
this.sourceFolders = new LinkedHashMap<>(classLoaderFiles.sourceFolders);
}
/**
* Add all elements items from the specified {@link ClassLoaderFiles} to this
* instance.
* @param files the files to add
*/
public void addAll(ClassLoaderFiles files) {
Assert.notNull(files, "Files must not be null");
for (SourceFolder folder : files.getSourceFolders()) {
for (Map.Entry<String, ClassLoaderFile> entry : folder.getFilesEntrySet()) {
addFile(folder.getName(), entry.getKey(), entry.getValue());
}
}
}
/**
* Add a single {@link ClassLoaderFile} to the collection.
* @param name the name of the file
* @param file the file to add
*/
public void addFile(String name, ClassLoaderFile file) {
addFile("", name, file);
}
/**
* Add a single {@link ClassLoaderFile} to the collection.
* @param sourceFolder the source folder of the file
* @param name the name of the file
* @param file the file to add
*/
public void addFile(String sourceFolder, String name, ClassLoaderFile file) {
Assert.notNull(sourceFolder, "SourceFolder must not be null");
Assert.notNull(name, "Name must not be null");
Assert.notNull(file, "File must not be null");
removeAll(name);
getOrCreateSourceFolder(sourceFolder).add(name, file);
}
private void removeAll(String name) {
for (SourceFolder sourceFolder : this.sourceFolders.values()) {
sourceFolder.remove(name);
}
}
/**
* Get or create a {@link SourceFolder} with the given name.
* @param name the name of the folder
* @return an existing or newly added {@link SourceFolder}
*/
protected final SourceFolder getOrCreateSourceFolder(String name) {
SourceFolder sourceFolder = this.sourceFolders.get(name);
if (sourceFolder == null) {
sourceFolder = new SourceFolder(name);
this.sourceFolders.put(name, sourceFolder);
}
return sourceFolder;
}
/**
* Return all {@link SourceFolder SourceFolders} that have been added to the
* collection.
* @return a collection of {@link SourceFolder} items
*/
public Collection<SourceFolder> getSourceFolders() {
return Collections.unmodifiableCollection(this.sourceFolders.values());
}
/**
* Return the size of the collection.
* @return the size of the collection
*/
public int size() {
int size = 0;
for (SourceFolder sourceFolder : this.sourceFolders.values()) {
size += sourceFolder.getFiles().size();
}
return size;
}
@Override
public ClassLoaderFile getFile(String name) {
for (SourceFolder sourceFolder : this.sourceFolders.values()) {
ClassLoaderFile file = sourceFolder.get(name);
if (file != null) {
return file;
}
}
return null;
}
/**
* An individual source folder that is being managed by the collection.
*/
public static class SourceFolder implements Serializable {
private static final long serialVersionUID = 1;
private final String name;
private final Map<String, ClassLoaderFile> files = new LinkedHashMap<>();
SourceFolder(String name) {
this.name = name;
}
public Set<Entry<String, ClassLoaderFile>> getFilesEntrySet() {
return this.files.entrySet();
}
protected final void add(String name, ClassLoaderFile file) {
this.files.put(name, file);
}
protected final void remove(String name) {
this.files.remove(name);
}
protected final ClassLoaderFile get(String name) {
return this.files.get(name);
}
/**
* Return the name of the source folder.
* @return the name of the source folder
*/
public String getName() {
return this.name;
}
/**
* Return all {@link ClassLoaderFile ClassLoaderFiles} in the collection that are
* contained in this source folder.
* @return the files contained in the source folder
*/
public Collection<ClassLoaderFile> getFiles() {
return Collections.unmodifiableCollection(this.files.values());
}
}
}

View File

@@ -0,0 +1,226 @@
/*
* Copyright 2012-2017 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.devtools.restart.classloader;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.Enumeration;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.boot.devtools.restart.classloader.ClassLoaderFile.Kind;
import org.springframework.core.SmartClassLoader;
import org.springframework.util.Assert;
/**
* Disposable {@link ClassLoader} used to support application restarting. Provides parent
* last loading for the specified URLs.
*
* @author Andy Clement
* @author Phillip Webb
* @since 1.3.0
*/
public class RestartClassLoader extends URLClassLoader implements SmartClassLoader {
private final Log logger;
private final ClassLoaderFileRepository updatedFiles;
/**
* Create a new {@link RestartClassLoader} instance.
* @param parent the parent classloader
* @param urls the urls managed by the classloader
*/
public RestartClassLoader(ClassLoader parent, URL[] urls) {
this(parent, urls, ClassLoaderFileRepository.NONE);
}
/**
* Create a new {@link RestartClassLoader} instance.
* @param parent the parent classloader
* @param updatedFiles any files that have been updated since the JARs referenced in
* URLs were created.
* @param urls the urls managed by the classloader
*/
public RestartClassLoader(ClassLoader parent, URL[] urls,
ClassLoaderFileRepository updatedFiles) {
this(parent, urls, updatedFiles, LogFactory.getLog(RestartClassLoader.class));
}
/**
* Create a new {@link RestartClassLoader} instance.
* @param parent the parent classloader
* @param updatedFiles any files that have been updated since the JARs referenced in
* URLs were created.
* @param urls the urls managed by the classloader
* @param logger the logger used for messages
*/
public RestartClassLoader(ClassLoader parent, URL[] urls,
ClassLoaderFileRepository updatedFiles, Log logger) {
super(urls, parent);
Assert.notNull(parent, "Parent must not be null");
Assert.notNull(updatedFiles, "UpdatedFiles must not be null");
Assert.notNull(logger, "Logger must not be null");
this.updatedFiles = updatedFiles;
this.logger = logger;
if (logger.isDebugEnabled()) {
logger.debug("Created RestartClassLoader " + toString());
}
}
@Override
public Enumeration<URL> getResources(String name) throws IOException {
// Use the parent since we're shadowing resource and we don't want duplicates
Enumeration<URL> resources = getParent().getResources(name);
ClassLoaderFile file = this.updatedFiles.getFile(name);
if (file != null) {
// Assume that we're replacing just the first item
if (resources.hasMoreElements()) {
resources.nextElement();
}
if (file.getKind() != Kind.DELETED) {
return new CompoundEnumeration<>(createFileUrl(name, file), resources);
}
}
return resources;
}
@Override
public URL getResource(String name) {
ClassLoaderFile file = this.updatedFiles.getFile(name);
if (file != null && file.getKind() == Kind.DELETED) {
return null;
}
URL resource = findResource(name);
if (resource != null) {
return resource;
}
return getParent().getResource(name);
}
@Override
public URL findResource(final String name) {
final ClassLoaderFile file = this.updatedFiles.getFile(name);
if (file == null) {
return super.findResource(name);
}
if (file.getKind() == Kind.DELETED) {
return null;
}
return AccessController
.doPrivileged((PrivilegedAction<URL>) () -> createFileUrl(name, file));
}
@Override
public Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
String path = name.replace('.', '/').concat(".class");
ClassLoaderFile file = this.updatedFiles.getFile(path);
if (file != null && file.getKind() == Kind.DELETED) {
throw new ClassNotFoundException(name);
}
Class<?> loadedClass = findLoadedClass(name);
if (loadedClass == null) {
try {
loadedClass = findClass(name);
}
catch (ClassNotFoundException ex) {
loadedClass = getParent().loadClass(name);
}
}
if (resolve) {
resolveClass(loadedClass);
}
return loadedClass;
}
@Override
protected Class<?> findClass(final String name) throws ClassNotFoundException {
String path = name.replace('.', '/').concat(".class");
final ClassLoaderFile file = this.updatedFiles.getFile(path);
if (file == null) {
return super.findClass(name);
}
if (file.getKind() == Kind.DELETED) {
throw new ClassNotFoundException(name);
}
return AccessController.doPrivileged((PrivilegedAction<Class<?>>) () -> {
byte[] bytes = file.getContents();
return defineClass(name, bytes, 0, bytes.length);
});
}
private URL createFileUrl(String name, ClassLoaderFile file) {
try {
return new URL("reloaded", null, -1, "/" + name,
new ClassLoaderFileURLStreamHandler(file));
}
catch (MalformedURLException ex) {
throw new IllegalStateException(ex);
}
}
@Override
protected void finalize() throws Throwable {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Finalized classloader " + toString());
}
super.finalize();
}
@Override
public boolean isClassReloadable(Class<?> classType) {
return (classType.getClassLoader() instanceof RestartClassLoader);
}
/**
* Compound {@link Enumeration} that adds an additional item to the front.
*/
private static class CompoundEnumeration<E> implements Enumeration<E> {
private E firstElement;
private final Enumeration<E> enumeration;
CompoundEnumeration(E firstElement, Enumeration<E> enumeration) {
this.firstElement = firstElement;
this.enumeration = enumeration;
}
@Override
public boolean hasMoreElements() {
return (this.firstElement != null || this.enumeration.hasMoreElements());
}
@Override
public E nextElement() {
if (this.firstElement == null) {
return this.enumeration.nextElement();
}
E element = this.firstElement;
this.firstElement = null;
return element;
}
}
}

View File

@@ -0,0 +1,20 @@
/*
* 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.
*/
/**
* Classloaders used for reload support.
*/
package org.springframework.boot.devtools.restart.classloader;

View File

@@ -0,0 +1,20 @@
/*
* 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.
*/
/**
* Application restart support.
*/
package org.springframework.boot.devtools.restart;

View File

@@ -0,0 +1,101 @@
/*
* Copyright 2012-2017 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.devtools.restart.server;
import java.net.URL;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.springframework.util.StringUtils;
/**
* Default implementation of {@link SourceFolderUrlFilter} that attempts to match URLs
* using common naming conventions.
*
* @author Phillip Webb
* @since 1.3.0
*/
public class DefaultSourceFolderUrlFilter implements SourceFolderUrlFilter {
private static final String[] COMMON_ENDINGS = { "/target/classes", "/bin" };
private static final Pattern URL_MODULE_PATTERN = Pattern.compile(".*\\/(.+)\\.jar");
private static final Pattern VERSION_PATTERN = Pattern
.compile("^-\\d+(?:\\.\\d+)*(?:[.-].+)?$");
private static final Set<String> SKIPPED_PROJECTS = new HashSet<>(Arrays.asList(
"spring-boot", "spring-boot-devtools", "spring-boot-autoconfigure",
"spring-boot-actuator", "spring-boot-starter"));
@Override
public boolean isMatch(String sourceFolder, URL url) {
String jarName = getJarName(url);
if (!StringUtils.hasLength(jarName)) {
return false;
}
return isMatch(sourceFolder, jarName);
}
private String getJarName(URL url) {
Matcher matcher = URL_MODULE_PATTERN.matcher(url.toString());
if (matcher.find()) {
return matcher.group(1);
}
return null;
}
private boolean isMatch(String sourceFolder, String jarName) {
sourceFolder = stripTrailingSlash(sourceFolder);
sourceFolder = stripCommonEnds(sourceFolder);
String[] folders = StringUtils.delimitedListToStringArray(sourceFolder, "/");
for (int i = folders.length - 1; i >= 0; i--) {
if (isFolderMatch(folders[i], jarName)) {
return true;
}
}
return false;
}
private boolean isFolderMatch(String folder, String jarName) {
if (!jarName.startsWith(folder) || SKIPPED_PROJECTS.contains(folder)) {
return false;
}
String version = jarName.substring(folder.length());
return version.isEmpty() || VERSION_PATTERN.matcher(version).matches();
}
private String stripTrailingSlash(String string) {
if (string.endsWith("/")) {
return string.substring(0, string.length() - 1);
}
return string;
}
private String stripCommonEnds(String string) {
for (String ending : COMMON_ENDINGS) {
if (string.endsWith(ending)) {
return string.substring(0, string.length() - ending.length());
}
}
return string;
}
}

View File

@@ -0,0 +1,87 @@
/*
* 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.devtools.restart.server;
import java.io.IOException;
import java.io.ObjectInputStream;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.boot.devtools.restart.classloader.ClassLoaderFiles;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.util.Assert;
/**
* A HTTP server that can be used to upload updated {@link ClassLoaderFiles} and trigger
* restarts.
*
* @author Phillip Webb
* @since 1.3.0
* @see RestartServer
*/
public class HttpRestartServer {
private static final Log logger = LogFactory.getLog(HttpRestartServer.class);
private final RestartServer server;
/**
* Create a new {@link HttpRestartServer} instance.
* @param sourceFolderUrlFilter the source filter used to link remote folder to the
* local classpath
*/
public HttpRestartServer(SourceFolderUrlFilter sourceFolderUrlFilter) {
Assert.notNull(sourceFolderUrlFilter, "SourceFolderUrlFilter must not be null");
this.server = new RestartServer(sourceFolderUrlFilter);
}
/**
* Create a new {@link HttpRestartServer} instance.
* @param restartServer the underlying restart server
*/
public HttpRestartServer(RestartServer restartServer) {
Assert.notNull(restartServer, "RestartServer must not be null");
this.server = restartServer;
}
/**
* Handle a server request.
* @param request the request
* @param response the response
* @throws IOException in case of I/O errors
*/
public void handle(ServerHttpRequest request, ServerHttpResponse response)
throws IOException {
try {
Assert.state(request.getHeaders().getContentLength() > 0, "No content");
ObjectInputStream objectInputStream = new ObjectInputStream(
request.getBody());
ClassLoaderFiles files = (ClassLoaderFiles) objectInputStream.readObject();
objectInputStream.close();
this.server.updateAndRestart(files);
response.setStatusCode(HttpStatus.OK);
}
catch (Exception ex) {
logger.warn("Unable to handler restart server HTTP request", ex);
response.setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR);
}
}
}

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.devtools.restart.server;
import java.io.IOException;
import org.springframework.boot.devtools.remote.server.Handler;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.util.Assert;
/**
* Adapts {@link HttpRestartServer} to a {@link Handler}.
*
* @author Phillip Webb
* @since 1.3.0
*/
public class HttpRestartServerHandler implements Handler {
private final HttpRestartServer server;
/**
* Create a new {@link HttpRestartServerHandler} instance.
* @param server the server to adapt
*/
public HttpRestartServerHandler(HttpRestartServer server) {
Assert.notNull(server, "Server must not be null");
this.server = server;
}
@Override
public void handle(ServerHttpRequest request, ServerHttpResponse response)
throws IOException {
this.server.handle(request, response);
}
}

View File

@@ -0,0 +1,182 @@
/*
* Copyright 2012-2017 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.devtools.restart.server;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Map.Entry;
import java.util.Set;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.boot.devtools.restart.Restarter;
import org.springframework.boot.devtools.restart.classloader.ClassLoaderFile;
import org.springframework.boot.devtools.restart.classloader.ClassLoaderFile.Kind;
import org.springframework.boot.devtools.restart.classloader.ClassLoaderFiles;
import org.springframework.boot.devtools.restart.classloader.ClassLoaderFiles.SourceFolder;
import org.springframework.util.Assert;
import org.springframework.util.FileCopyUtils;
import org.springframework.util.ResourceUtils;
/**
* Server used to {@link Restarter restart} the current application with updated
* {@link ClassLoaderFiles}.
*
* @author Phillip Webb
* @since 1.3.0
*/
public class RestartServer {
private static final Log logger = LogFactory.getLog(RestartServer.class);
private final SourceFolderUrlFilter sourceFolderUrlFilter;
private final ClassLoader classLoader;
/**
* Create a new {@link RestartServer} instance.
* @param sourceFolderUrlFilter the source filter used to link remote folder to the
* local classpath
*/
public RestartServer(SourceFolderUrlFilter sourceFolderUrlFilter) {
this(sourceFolderUrlFilter, Thread.currentThread().getContextClassLoader());
}
/**
* Create a new {@link RestartServer} instance.
* @param sourceFolderUrlFilter the source filter used to link remote folder to the
* local classpath
* @param classLoader the application classloader
*/
public RestartServer(SourceFolderUrlFilter sourceFolderUrlFilter,
ClassLoader classLoader) {
Assert.notNull(sourceFolderUrlFilter, "SourceFolderUrlFilter must not be null");
Assert.notNull(classLoader, "ClassLoader must not be null");
this.sourceFolderUrlFilter = sourceFolderUrlFilter;
this.classLoader = classLoader;
}
/**
* Update the current running application with the specified {@link ClassLoaderFiles}
* and trigger a reload.
* @param files updated class loader files
*/
public void updateAndRestart(ClassLoaderFiles files) {
Set<URL> urls = new LinkedHashSet<>();
Set<URL> classLoaderUrls = getClassLoaderUrls();
for (SourceFolder folder : files.getSourceFolders()) {
for (Entry<String, ClassLoaderFile> entry : folder.getFilesEntrySet()) {
for (URL url : classLoaderUrls) {
if (updateFileSystem(url, entry.getKey(), entry.getValue())) {
urls.add(url);
}
}
}
urls.addAll(getMatchingUrls(classLoaderUrls, folder.getName()));
}
updateTimeStamp(urls);
restart(urls, files);
}
private boolean updateFileSystem(URL url, String name,
ClassLoaderFile classLoaderFile) {
if (!isFolderUrl(url.toString())) {
return false;
}
try {
File folder = ResourceUtils.getFile(url);
File file = new File(folder, name);
if (file.exists() && file.canWrite()) {
if (classLoaderFile.getKind() == Kind.DELETED) {
return file.delete();
}
FileCopyUtils.copy(classLoaderFile.getContents(), file);
return true;
}
}
catch (IOException ex) {
// Ignore
}
return false;
}
private boolean isFolderUrl(String urlString) {
return urlString.startsWith("file:") && urlString.endsWith("/");
}
private Set<URL> getMatchingUrls(Set<URL> urls, String sourceFolder) {
Set<URL> matchingUrls = new LinkedHashSet<>();
for (URL url : urls) {
if (this.sourceFolderUrlFilter.isMatch(sourceFolder, url)) {
if (logger.isDebugEnabled()) {
logger.debug("URL " + url + " matched against source folder "
+ sourceFolder);
}
matchingUrls.add(url);
}
}
return matchingUrls;
}
private Set<URL> getClassLoaderUrls() {
Set<URL> urls = new LinkedHashSet<>();
ClassLoader classLoader = this.classLoader;
while (classLoader != null) {
if (classLoader instanceof URLClassLoader) {
Collections.addAll(urls, ((URLClassLoader) classLoader).getURLs());
}
classLoader = classLoader.getParent();
}
return urls;
}
private void updateTimeStamp(Iterable<URL> urls) {
for (URL url : urls) {
updateTimeStamp(url);
}
}
private void updateTimeStamp(URL url) {
try {
URL actualUrl = ResourceUtils.extractJarFileURL(url);
File file = ResourceUtils.getFile(actualUrl, "Jar URL");
file.setLastModified(System.currentTimeMillis());
}
catch (Exception ex) {
// Ignore
}
}
/**
* Called to restart the application.
* @param urls the updated URLs
* @param files the updated files
*/
protected void restart(Set<URL> urls, ClassLoaderFiles files) {
Restarter restarter = Restarter.getInstance();
restarter.addUrls(urls);
restarter.addClassLoaderFiles(files);
restarter.restart();
}
}

View File

@@ -0,0 +1,40 @@
/*
* Copyright 2012-2017 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.devtools.restart.server;
import java.net.URL;
/**
* Filter URLs based on a source folder name. Used to match URLs from the running
* classpath against source folders on a remote system.
*
* @author Phillip Webb
* @since 1.3.0
* @see DefaultSourceFolderUrlFilter
*/
@FunctionalInterface
public interface SourceFolderUrlFilter {
/**
* Determine if the specified URL matches a source folder.
* @param sourceFolder the source folder
* @param url the URL to check
* @return {@code true} if the URL matches
*/
boolean isMatch(String sourceFolder, URL url);
}

View File

@@ -0,0 +1,20 @@
/*
* 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.
*/
/**
* Remote restart server.
*/
package org.springframework.boot.devtools.restart.server;

View File

@@ -0,0 +1,127 @@
/*
* Copyright 2012-2017 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.devtools.settings;
import java.net.URL;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.core.io.UrlResource;
import org.springframework.core.io.support.PropertiesLoaderUtils;
/**
* DevTools settings loaded from {@literal /META-INF/spring-devtools.properties} files.
*
* @author Phillip Webb
* @since 1.3.0
*/
public class DevToolsSettings {
/**
* The location to look for settings properties. Can be present in multiple JAR files.
*/
public static final String SETTINGS_RESOURCE_LOCATION = "META-INF/spring-devtools.properties";
private static final Log logger = LogFactory.getLog(DevToolsSettings.class);
private static DevToolsSettings settings;
private final List<Pattern> restartIncludePatterns = new ArrayList<>();
private final List<Pattern> restartExcludePatterns = new ArrayList<>();
DevToolsSettings() {
}
void add(Map<?, ?> properties) {
Map<String, Pattern> includes = getPatterns(properties, "restart.include.");
this.restartIncludePatterns.addAll(includes.values());
Map<String, Pattern> excludes = getPatterns(properties, "restart.exclude.");
this.restartExcludePatterns.addAll(excludes.values());
}
private Map<String, Pattern> getPatterns(Map<?, ?> properties, String prefix) {
Map<String, Pattern> patterns = new LinkedHashMap<>();
for (Map.Entry<?, ?> entry : properties.entrySet()) {
String name = String.valueOf(entry.getKey());
if (name.startsWith(prefix)) {
Pattern pattern = Pattern.compile((String) entry.getValue());
patterns.put(name, pattern);
}
}
return patterns;
}
public boolean isRestartInclude(URL url) {
return isMatch(url.toString(), this.restartIncludePatterns);
}
public boolean isRestartExclude(URL url) {
return isMatch(url.toString(), this.restartExcludePatterns);
}
private boolean isMatch(String url, List<Pattern> patterns) {
for (Pattern pattern : patterns) {
if (pattern.matcher(url).find()) {
return true;
}
}
return false;
}
public static DevToolsSettings get() {
if (settings == null) {
settings = load();
}
return settings;
}
static DevToolsSettings load() {
return load(SETTINGS_RESOURCE_LOCATION);
}
static DevToolsSettings load(String location) {
try {
DevToolsSettings settings = new DevToolsSettings();
Enumeration<URL> urls = Thread.currentThread().getContextClassLoader()
.getResources(location);
while (urls.hasMoreElements()) {
settings.add(PropertiesLoaderUtils
.loadProperties(new UrlResource(urls.nextElement())));
}
if (logger.isDebugEnabled()) {
logger.debug("Included patterns for restart : "
+ settings.restartIncludePatterns);
logger.debug("Excluded patterns for restart : "
+ settings.restartExcludePatterns);
}
return settings;
}
catch (Exception ex) {
throw new IllegalStateException("Unable to load devtools settings from "
+ "location [" + location + "]", ex);
}
}
}

View File

@@ -0,0 +1,223 @@
/*
* Copyright 2012-2017 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.devtools.tunnel.client;
import java.io.Closeable;
import java.io.IOException;
import java.net.ConnectException;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.ByteBuffer;
import java.nio.channels.WritableByteChannel;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicLong;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.boot.devtools.tunnel.payload.HttpTunnelPayload;
import org.springframework.boot.devtools.tunnel.payload.HttpTunnelPayloadForwarder;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.client.ClientHttpRequest;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.util.Assert;
/**
* {@link TunnelConnection} implementation that uses HTTP to transfer data.
*
* @author Phillip Webb
* @author Rob Winch
* @author Andy Wilkinson
* @since 1.3.0
* @see TunnelClient
* @see org.springframework.boot.devtools.tunnel.server.HttpTunnelServer
*/
public class HttpTunnelConnection implements TunnelConnection {
private static final Log logger = LogFactory.getLog(HttpTunnelConnection.class);
private final URI uri;
private final ClientHttpRequestFactory requestFactory;
private final Executor executor;
/**
* Create a new {@link HttpTunnelConnection} instance.
* @param url the URL to connect to
* @param requestFactory the HTTP request factory
*/
public HttpTunnelConnection(String url, ClientHttpRequestFactory requestFactory) {
this(url, requestFactory, null);
}
/**
* Create a new {@link HttpTunnelConnection} instance.
* @param url the URL to connect to
* @param requestFactory the HTTP request factory
* @param executor the executor used to handle connections
*/
protected HttpTunnelConnection(String url, ClientHttpRequestFactory requestFactory,
Executor executor) {
Assert.hasLength(url, "URL must not be empty");
Assert.notNull(requestFactory, "RequestFactory must not be null");
try {
this.uri = new URL(url).toURI();
}
catch (URISyntaxException | MalformedURLException ex) {
throw new IllegalArgumentException("Malformed URL '" + url + "'");
}
this.requestFactory = requestFactory;
this.executor = (executor == null
? Executors.newCachedThreadPool(new TunnelThreadFactory()) : executor);
}
@Override
public TunnelChannel open(WritableByteChannel incomingChannel, Closeable closeable)
throws Exception {
logger.trace("Opening HTTP tunnel to " + this.uri);
return new TunnelChannel(incomingChannel, closeable);
}
protected final ClientHttpRequest createRequest(boolean hasPayload)
throws IOException {
HttpMethod method = (hasPayload ? HttpMethod.POST : HttpMethod.GET);
return this.requestFactory.createRequest(this.uri, method);
}
/**
* A {@link WritableByteChannel} used to transfer traffic.
*/
protected class TunnelChannel implements WritableByteChannel {
private final HttpTunnelPayloadForwarder forwarder;
private final Closeable closeable;
private boolean open = true;
private AtomicLong requestSeq = new AtomicLong();
public TunnelChannel(WritableByteChannel incomingChannel, Closeable closeable) {
this.forwarder = new HttpTunnelPayloadForwarder(incomingChannel);
this.closeable = closeable;
openNewConnection(null);
}
@Override
public boolean isOpen() {
return this.open;
}
@Override
public void close() throws IOException {
if (this.open) {
this.open = false;
this.closeable.close();
}
}
@Override
public int write(ByteBuffer src) throws IOException {
int size = src.remaining();
if (size > 0) {
openNewConnection(
new HttpTunnelPayload(this.requestSeq.incrementAndGet(), src));
}
return size;
}
private void openNewConnection(final HttpTunnelPayload payload) {
HttpTunnelConnection.this.executor.execute(new Runnable() {
@Override
public void run() {
try {
sendAndReceive(payload);
}
catch (IOException ex) {
if (ex instanceof ConnectException) {
logger.warn("Failed to connect to remote application at "
+ HttpTunnelConnection.this.uri);
}
else {
logger.trace("Unexpected connection error", ex);
}
closeQuietly();
}
}
private void closeQuietly() {
try {
close();
}
catch (IOException ex) {
// Ignore
}
}
});
}
private void sendAndReceive(HttpTunnelPayload payload) throws IOException {
ClientHttpRequest request = createRequest(payload != null);
if (payload != null) {
payload.logIncoming();
payload.assignTo(request);
}
handleResponse(request.execute());
}
private void handleResponse(ClientHttpResponse response) throws IOException {
if (response.getStatusCode() == HttpStatus.GONE) {
close();
return;
}
if (response.getStatusCode() == HttpStatus.OK) {
HttpTunnelPayload payload = HttpTunnelPayload.get(response);
if (payload != null) {
this.forwarder.forward(payload);
}
}
if (response.getStatusCode() != HttpStatus.TOO_MANY_REQUESTS) {
openNewConnection(null);
}
}
}
/**
* {@link ThreadFactory} used to create the tunnel thread.
*/
private static class TunnelThreadFactory implements ThreadFactory {
@Override
public Thread newThread(Runnable runnable) {
Thread thread = new Thread(runnable, "HTTP Tunnel Connection");
thread.setDaemon(true);
return thread;
}
}
}

View File

@@ -0,0 +1,223 @@
/*
* Copyright 2012-2017 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.devtools.tunnel.client;
import java.io.Closeable;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousCloseException;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.channels.WritableByteChannel;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.SmartInitializingSingleton;
import org.springframework.util.Assert;
/**
* The client side component of a socket tunnel. Starts a {@link ServerSocket} of the
* specified port for local clients to connect to.
*
* @author Phillip Webb
* @author Andy Wilkinson
* @since 1.3.0
*/
public class TunnelClient implements SmartInitializingSingleton {
private static final int BUFFER_SIZE = 1024 * 100;
private static final Log logger = LogFactory.getLog(TunnelClient.class);
private final TunnelClientListeners listeners = new TunnelClientListeners();
private final Object monitor = new Object();
private final int listenPort;
private final TunnelConnection tunnelConnection;
private ServerThread serverThread;
public TunnelClient(int listenPort, TunnelConnection tunnelConnection) {
Assert.isTrue(listenPort >= 0, "ListenPort must be greater than or equal to 0");
Assert.notNull(tunnelConnection, "TunnelConnection must not be null");
this.listenPort = listenPort;
this.tunnelConnection = tunnelConnection;
}
@Override
public void afterSingletonsInstantiated() {
synchronized (this.monitor) {
if (this.serverThread == null) {
try {
start();
}
catch (IOException ex) {
throw new IllegalStateException(ex);
}
}
}
}
/**
* Start the client and accept incoming connections.
* @return the port on which the client is listening
* @throws IOException in case of I/O errors
*/
public int start() throws IOException {
synchronized (this.monitor) {
Assert.state(this.serverThread == null, "Server already started");
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.socket().bind(new InetSocketAddress(this.listenPort));
int port = serverSocketChannel.socket().getLocalPort();
logger.trace("Listening for TCP traffic to tunnel on port " + port);
this.serverThread = new ServerThread(serverSocketChannel);
this.serverThread.start();
return port;
}
}
/**
* Stop the client, disconnecting any servers.
* @throws IOException in case of I/O errors
*/
public void stop() throws IOException {
synchronized (this.monitor) {
if (this.serverThread != null) {
this.serverThread.close();
try {
this.serverThread.join(2000);
}
catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
this.serverThread = null;
}
}
}
protected final ServerThread getServerThread() {
synchronized (this.monitor) {
return this.serverThread;
}
}
public void addListener(TunnelClientListener listener) {
this.listeners.addListener(listener);
}
public void removeListener(TunnelClientListener listener) {
this.listeners.removeListener(listener);
}
/**
* The main server thread.
*/
protected class ServerThread extends Thread {
private final ServerSocketChannel serverSocketChannel;
private boolean acceptConnections = true;
public ServerThread(ServerSocketChannel serverSocketChannel) {
this.serverSocketChannel = serverSocketChannel;
setName("Tunnel Server");
setDaemon(true);
}
public void close() throws IOException {
logger.trace("Closing tunnel client on port "
+ this.serverSocketChannel.socket().getLocalPort());
this.serverSocketChannel.close();
this.acceptConnections = false;
interrupt();
}
@Override
public void run() {
try {
while (this.acceptConnections) {
try (SocketChannel socket = this.serverSocketChannel.accept()) {
handleConnection(socket);
}
catch (AsynchronousCloseException ex) {
// Connection has been closed. Keep the server running
}
}
}
catch (Exception ex) {
logger.trace("Unexpected exception from tunnel client", ex);
}
}
private void handleConnection(SocketChannel socketChannel) throws Exception {
Closeable closeable = new SocketCloseable(socketChannel);
TunnelClient.this.listeners.fireOpenEvent(socketChannel);
try (WritableByteChannel outputChannel = TunnelClient.this.tunnelConnection
.open(socketChannel, closeable)) {
logger.trace("Accepted connection to tunnel client from "
+ socketChannel.socket().getRemoteSocketAddress());
while (true) {
ByteBuffer buffer = ByteBuffer.allocate(BUFFER_SIZE);
int amountRead = socketChannel.read(buffer);
if (amountRead == -1) {
outputChannel.close();
return;
}
if (amountRead > 0) {
buffer.flip();
outputChannel.write(buffer);
}
}
}
}
protected void stopAcceptingConnections() {
this.acceptConnections = false;
}
}
/**
* {@link Closeable} used to close a {@link SocketChannel} and fire an event.
*/
private class SocketCloseable implements Closeable {
private final SocketChannel socketChannel;
private boolean closed = false;
SocketCloseable(SocketChannel socketChannel) {
this.socketChannel = socketChannel;
}
@Override
public void close() throws IOException {
if (!this.closed) {
this.socketChannel.close();
TunnelClient.this.listeners.fireCloseEvent(this.socketChannel);
this.closed = true;
}
}
}
}

View File

@@ -0,0 +1,41 @@
/*
* 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.devtools.tunnel.client;
import java.nio.channels.SocketChannel;
/**
* Listener that can be used to receive {@link TunnelClient} events.
*
* @author Phillip Webb
* @since 1.3.0
*/
public interface TunnelClientListener {
/**
* Called when a socket channel is opened.
* @param socket the socket channel
*/
void onOpen(SocketChannel socket);
/**
* Called when a socket channel is closed.
* @param socket the socket channel
*/
void onClose(SocketChannel socket);
}

View File

@@ -0,0 +1,56 @@
/*
* Copyright 2012-2017 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.devtools.tunnel.client;
import java.nio.channels.SocketChannel;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import org.springframework.util.Assert;
/**
* A collection of {@link TunnelClientListener}.
*
* @author Phillip Webb
*/
class TunnelClientListeners {
private final List<TunnelClientListener> listeners = new CopyOnWriteArrayList<>();
public void addListener(TunnelClientListener listener) {
Assert.notNull(listener, "Listener must not be null");
this.listeners.add(listener);
}
public void removeListener(TunnelClientListener listener) {
Assert.notNull(listener, "Listener must not be null");
this.listeners.remove(listener);
}
public void fireOpenEvent(SocketChannel socket) {
for (TunnelClientListener listener : this.listeners) {
listener.onOpen(socket);
}
}
public void fireCloseEvent(SocketChannel socket) {
for (TunnelClientListener listener : this.listeners) {
listener.onClose(socket);
}
}
}

View File

@@ -0,0 +1,43 @@
/*
* Copyright 2012-2017 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.devtools.tunnel.client;
import java.io.Closeable;
import java.nio.channels.WritableByteChannel;
/**
* Interface used to manage socket tunnel connections.
*
* @author Phillip Webb
* @since 1.3.0
*/
@FunctionalInterface
public interface TunnelConnection {
/**
* Open the tunnel connection.
* @param incomingChannel A {@link WritableByteChannel} that should be used to write
* any incoming data received from the remote server.
* @param closeable a closeable to call when the channel is closed
* @return A {@link WritableByteChannel} that should be used to send any outgoing data
* destined for the remote server
* @throws Exception in case of errors
*/
WritableByteChannel open(WritableByteChannel incomingChannel, Closeable closeable)
throws Exception;
}

View File

@@ -0,0 +1,20 @@
/*
* 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.
*/
/**
* Client side TCP tunnel support.
*/
package org.springframework.boot.devtools.tunnel.client;

View File

@@ -0,0 +1,22 @@
/*
* 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.
*/
/**
* Provides support for tunneling TCP traffic over HTTP. Tunneling is primarily designed
* for the Java Debug Wire Protocol (JDWP) and as such only expects a single connection
* and isn't particularly worried about resource usage.
*/
package org.springframework.boot.devtools.tunnel;

View File

@@ -0,0 +1,186 @@
/*
* 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.devtools.tunnel.payload;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.WritableByteChannel;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* Encapsulates a payload data sent via a HTTP tunnel.
*
* @author Phillip Webb
* @since 1.3.0
*/
public class HttpTunnelPayload {
private static final String SEQ_HEADER = "x-seq";
private static final int BUFFER_SIZE = 1024 * 100;
final protected static char[] HEX_CHARS = "0123456789ABCDEF".toCharArray();
private static final Log logger = LogFactory.getLog(HttpTunnelPayload.class);
private final long sequence;
private final ByteBuffer data;
/**
* Create a new {@link HttpTunnelPayload} instance.
* @param sequence the sequence number of the payload
* @param data the payload data
*/
public HttpTunnelPayload(long sequence, ByteBuffer data) {
Assert.isTrue(sequence > 0, "Sequence must be positive");
Assert.notNull(data, "Data must not be null");
this.sequence = sequence;
this.data = data;
}
/**
* Return the sequence number of the payload.
* @return the sequence
*/
public long getSequence() {
return this.sequence;
}
/**
* Assign this payload to the given {@link HttpOutputMessage}.
* @param message the message to assign this payload to
* @throws IOException in case of I/O errors
*/
public void assignTo(HttpOutputMessage message) throws IOException {
Assert.notNull(message, "Message must not be null");
HttpHeaders headers = message.getHeaders();
headers.setContentLength(this.data.remaining());
headers.add(SEQ_HEADER, Long.toString(getSequence()));
headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
WritableByteChannel body = Channels.newChannel(message.getBody());
while (this.data.hasRemaining()) {
body.write(this.data);
}
body.close();
}
/**
* Write the content of this payload to the given target channel.
* @param channel the channel to write to
* @throws IOException in case of I/O errors
*/
public void writeTo(WritableByteChannel channel) throws IOException {
Assert.notNull(channel, "Channel must not be null");
while (this.data.hasRemaining()) {
channel.write(this.data);
}
}
/**
* Return the {@link HttpTunnelPayload} for the given message or {@code null} if there
* is no payload.
* @param message the HTTP message
* @return the payload or {@code null}
* @throws IOException in case of I/O errors
*/
public static HttpTunnelPayload get(HttpInputMessage message) throws IOException {
long length = message.getHeaders().getContentLength();
if (length <= 0) {
return null;
}
String seqHeader = message.getHeaders().getFirst(SEQ_HEADER);
Assert.state(StringUtils.hasLength(seqHeader), "Missing sequence header");
ReadableByteChannel body = Channels.newChannel(message.getBody());
ByteBuffer payload = ByteBuffer.allocate((int) length);
while (payload.hasRemaining()) {
body.read(payload);
}
body.close();
payload.flip();
return new HttpTunnelPayload(Long.valueOf(seqHeader), payload);
}
/**
* Return the payload data for the given source {@link ReadableByteChannel} or null if
* the channel timed out whilst reading.
* @param channel the source channel
* @return payload data or {@code null}
* @throws IOException in case of I/O errors
*/
public static ByteBuffer getPayloadData(ReadableByteChannel channel)
throws IOException {
ByteBuffer buffer = ByteBuffer.allocate(BUFFER_SIZE);
try {
int amountRead = channel.read(buffer);
Assert.state(amountRead != -1, "Target server connection closed");
buffer.flip();
return buffer;
}
catch (InterruptedIOException ex) {
return null;
}
}
/**
* Log incoming payload information at trace level to aid diagnostics.
*/
public void logIncoming() {
log("< ");
}
/**
* Log incoming payload information at trace level to aid diagnostics.
*/
public void logOutgoing() {
log("> ");
}
private void log(String prefix) {
if (logger.isTraceEnabled()) {
logger.trace(prefix + toHexString());
}
}
/**
* Return the payload as a hexadecimal string.
* @return the payload as a hex string
*/
public String toHexString() {
byte[] bytes = this.data.array();
char[] hex = new char[this.data.remaining() * 2];
for (int i = this.data.position(); i < this.data.remaining(); i++) {
int b = bytes[i] & 0xFF;
hex[i * 2] = HEX_CHARS[b >>> 4];
hex[i * 2 + 1] = HEX_CHARS[b & 0x0F];
}
return new String(hex);
}
}

View File

@@ -0,0 +1,73 @@
/*
* Copyright 2012-2017 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.devtools.tunnel.payload;
import java.io.IOException;
import java.nio.channels.WritableByteChannel;
import java.util.HashMap;
import java.util.Map;
import org.springframework.util.Assert;
/**
* Utility class that forwards {@link HttpTunnelPayload} instances to a destination
* channel, respecting sequence order.
*
* @author Phillip Webb
* @since 1.3.0
*/
public class HttpTunnelPayloadForwarder {
private static final int MAXIMUM_QUEUE_SIZE = 100;
private final Map<Long, HttpTunnelPayload> queue = new HashMap<>();
private final Object monitor = new Object();
private final WritableByteChannel targetChannel;
private long lastRequestSeq = 0;
/**
* Create a new {@link HttpTunnelPayloadForwarder} instance.
* @param targetChannel the target channel
*/
public HttpTunnelPayloadForwarder(WritableByteChannel targetChannel) {
Assert.notNull(targetChannel, "TargetChannel must not be null");
this.targetChannel = targetChannel;
}
public void forward(HttpTunnelPayload payload) throws IOException {
synchronized (this.monitor) {
long seq = payload.getSequence();
if (this.lastRequestSeq != seq - 1) {
Assert.state(this.queue.size() < MAXIMUM_QUEUE_SIZE,
"Too many messages queued");
this.queue.put(seq, payload);
return;
}
payload.logOutgoing();
payload.writeTo(this.targetChannel);
this.lastRequestSeq = seq;
HttpTunnelPayload queuedItem = this.queue.get(seq + 1);
if (queuedItem != null) {
forward(queuedItem);
}
}
}
}

View File

@@ -0,0 +1,20 @@
/*
* 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.
*/
/**
* Classes to deal with payloads sent over a HTTP tunnel.
*/
package org.springframework.boot.devtools.tunnel.payload;

View File

@@ -0,0 +1,498 @@
/*
* Copyright 2012-2017 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.devtools.tunnel.server;
import java.io.IOException;
import java.net.ConnectException;
import java.nio.ByteBuffer;
import java.nio.channels.ByteChannel;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.Iterator;
import java.util.concurrent.atomic.AtomicLong;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.boot.devtools.tunnel.payload.HttpTunnelPayload;
import org.springframework.boot.devtools.tunnel.payload.HttpTunnelPayloadForwarder;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.server.ServerHttpAsyncRequestControl;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.util.Assert;
/**
* A server that can be used to tunnel TCP traffic over HTTP. Similar in design to the
* <a href="http://xmpp.org/extensions/xep-0124.html">Bidirectional-streams Over
* Synchronous HTTP (BOSH)</a> XMPP extension protocol, the server uses long polling with
* HTTP requests held open until a response is available. A typical traffic pattern would
* be as follows:
*
* <pre>
* [ CLIENT ] [ SERVER ]
* | (a) Initial empty request |
* |------------------------------&gt;|
* | (b) Data I |
* --&gt;|------------------------------&gt;|---&gt;
* | Response I (a) |
* &lt;--|&lt;------------------------------|&lt;---
* | |
* | (c) Data II |
* --&gt;|------------------------------&gt;|---&gt;
* | Response II (b) |
* &lt;--|&lt;------------------------------|&lt;---
* . .
* . .
* </pre>
*
* Each incoming request is held open to be used to carry the next available response. The
* server will hold at most two connections open at any given time.
* <p>
* Requests should be made using HTTP GET or POST (depending if there is a payload), with
* any payload contained in the body. The following response codes can be returned from
* the server:
* <table summary="Response Codes">
* <tr>
* <th>Status</th>
* <th>Meaning</th>
* </tr>
* <tr>
* <td>200 (OK)</td>
* <td>Data payload response.</td>
* </tr>
* <tr>
* <td>204 (No Content)</td>
* <td>The long poll has timed out and the client should start a new request.</td>
* </tr>
* <tr>
* <td>429 (Too many requests)</td>
* <td>There are already enough connections open, this one can be dropped.</td>
* </tr>
* <tr>
* <td>410 (Gone)</td>
* <td>The target server has disconnected.</td>
* </tr>
* <tr>
* <td>503 (Service Unavailable)</td>
* <td>The target server is unavailable</td>
* </tr>
* </table>
* <p>
* Requests and responses that contain payloads include a {@code x-seq} header that
* contains a running sequence number (used to ensure data is applied in the correct
* order). The first request containing a payload should have a {@code x-seq} value of
* {@code 1}.
*
* @author Phillip Webb
* @author Andy Wilkinson
* @since 1.3.0
* @see org.springframework.boot.devtools.tunnel.client.HttpTunnelConnection
*/
public class HttpTunnelServer {
private static final int SECONDS = 1000;
private static final int DEFAULT_LONG_POLL_TIMEOUT = 10 * SECONDS;
private static final long DEFAULT_DISCONNECT_TIMEOUT = 30 * SECONDS;
private static final MediaType DISCONNECT_MEDIA_TYPE = new MediaType("application",
"x-disconnect");
private static final Log logger = LogFactory.getLog(HttpTunnelServer.class);
private final TargetServerConnection serverConnection;
private int longPollTimeout = DEFAULT_LONG_POLL_TIMEOUT;
private long disconnectTimeout = DEFAULT_DISCONNECT_TIMEOUT;
private volatile ServerThread serverThread;
/**
* Creates a new {@link HttpTunnelServer} instance.
* @param serverConnection the connection to the target server
*/
public HttpTunnelServer(TargetServerConnection serverConnection) {
Assert.notNull(serverConnection, "ServerConnection must not be null");
this.serverConnection = serverConnection;
}
/**
* Handle an incoming HTTP connection.
* @param request the HTTP request
* @param response the HTTP response
* @throws IOException in case of I/O errors
*/
public void handle(ServerHttpRequest request, ServerHttpResponse response)
throws IOException {
handle(new HttpConnection(request, response));
}
/**
* Handle an incoming HTTP connection.
* @param httpConnection the HTTP connection
* @throws IOException in case of I/O errors
*/
protected void handle(HttpConnection httpConnection) throws IOException {
try {
getServerThread().handleIncomingHttp(httpConnection);
httpConnection.waitForResponse();
}
catch (ConnectException ex) {
httpConnection.respond(HttpStatus.GONE);
}
}
/**
* Returns the active server thread, creating and starting it if necessary.
* @return the {@code ServerThread} (never {@code null})
* @throws IOException in case of I/O errors
*/
protected ServerThread getServerThread() throws IOException {
synchronized (this) {
if (this.serverThread == null) {
ByteChannel channel = this.serverConnection.open(this.longPollTimeout);
this.serverThread = new ServerThread(channel);
this.serverThread.start();
}
return this.serverThread;
}
}
/**
* Called when the server thread exits.
*/
void clearServerThread() {
synchronized (this) {
this.serverThread = null;
}
}
/**
* Set the long poll timeout for the server.
* @param longPollTimeout the long poll timeout in milliseconds
*/
public void setLongPollTimeout(int longPollTimeout) {
Assert.isTrue(longPollTimeout > 0, "LongPollTimeout must be a positive value");
this.longPollTimeout = longPollTimeout;
}
/**
* Set the maximum amount of time to wait for a client before closing the connection.
* @param disconnectTimeout the disconnect timeout in milliseconds
*/
public void setDisconnectTimeout(long disconnectTimeout) {
Assert.isTrue(disconnectTimeout > 0,
"DisconnectTimeout must be a positive value");
this.disconnectTimeout = disconnectTimeout;
}
/**
* The main server thread used to transfer tunnel traffic.
*/
protected class ServerThread extends Thread {
private final ByteChannel targetServer;
private final Deque<HttpConnection> httpConnections;
private final HttpTunnelPayloadForwarder payloadForwarder;
private boolean closed;
private AtomicLong responseSeq = new AtomicLong();
private long lastHttpRequestTime;
public ServerThread(ByteChannel targetServer) {
Assert.notNull(targetServer, "TargetServer must not be null");
this.targetServer = targetServer;
this.httpConnections = new ArrayDeque<>(2);
this.payloadForwarder = new HttpTunnelPayloadForwarder(targetServer);
}
@Override
public void run() {
try {
try {
readAndForwardTargetServerData();
}
catch (Exception ex) {
logger.trace("Unexpected exception from tunnel server", ex);
}
}
finally {
this.closed = true;
closeHttpConnections();
closeTargetServer();
HttpTunnelServer.this.clearServerThread();
}
}
private void readAndForwardTargetServerData() throws IOException {
while (this.targetServer.isOpen()) {
closeStaleHttpConnections();
ByteBuffer data = HttpTunnelPayload.getPayloadData(this.targetServer);
synchronized (this.httpConnections) {
if (data != null) {
HttpTunnelPayload payload = new HttpTunnelPayload(
this.responseSeq.incrementAndGet(), data);
payload.logIncoming();
HttpConnection connection = getOrWaitForHttpConnection();
connection.respond(payload);
}
}
}
}
private HttpConnection getOrWaitForHttpConnection() {
synchronized (this.httpConnections) {
HttpConnection httpConnection = this.httpConnections.pollFirst();
while (httpConnection == null) {
try {
this.httpConnections.wait(HttpTunnelServer.this.longPollTimeout);
}
catch (InterruptedException ex) {
Thread.currentThread().interrupt();
closeHttpConnections();
}
httpConnection = this.httpConnections.pollFirst();
}
return httpConnection;
}
}
private void closeStaleHttpConnections() throws IOException {
synchronized (this.httpConnections) {
checkNotDisconnected();
Iterator<HttpConnection> iterator = this.httpConnections.iterator();
while (iterator.hasNext()) {
HttpConnection httpConnection = iterator.next();
if (httpConnection
.isOlderThan(HttpTunnelServer.this.longPollTimeout)) {
httpConnection.respond(HttpStatus.NO_CONTENT);
iterator.remove();
}
}
}
}
private void checkNotDisconnected() {
if (this.lastHttpRequestTime > 0) {
long timeout = HttpTunnelServer.this.disconnectTimeout;
long duration = System.currentTimeMillis() - this.lastHttpRequestTime;
Assert.state(duration < timeout,
"Disconnect timeout: " + timeout + " " + duration);
}
}
private void closeHttpConnections() {
synchronized (this.httpConnections) {
while (!this.httpConnections.isEmpty()) {
try {
this.httpConnections.removeFirst().respond(HttpStatus.GONE);
}
catch (Exception ex) {
logger.trace("Unable to close remote HTTP connection");
}
}
}
}
private void closeTargetServer() {
try {
this.targetServer.close();
}
catch (IOException ex) {
logger.trace("Unable to target server connection");
}
}
/**
* Handle an incoming {@link HttpConnection}.
* @param httpConnection the connection to handle.
* @throws IOException in case of I/O errors
*/
public void handleIncomingHttp(HttpConnection httpConnection) throws IOException {
if (this.closed) {
httpConnection.respond(HttpStatus.GONE);
}
synchronized (this.httpConnections) {
while (this.httpConnections.size() > 1) {
this.httpConnections.removeFirst()
.respond(HttpStatus.TOO_MANY_REQUESTS);
}
this.lastHttpRequestTime = System.currentTimeMillis();
this.httpConnections.addLast(httpConnection);
this.httpConnections.notify();
}
forwardToTargetServer(httpConnection);
}
private void forwardToTargetServer(HttpConnection httpConnection)
throws IOException {
if (httpConnection.isDisconnectRequest()) {
this.targetServer.close();
interrupt();
}
ServerHttpRequest request = httpConnection.getRequest();
HttpTunnelPayload payload = HttpTunnelPayload.get(request);
if (payload != null) {
this.payloadForwarder.forward(payload);
}
}
}
/**
* Encapsulates a HTTP request/response pair.
*/
protected static class HttpConnection {
private final long createTime;
private final ServerHttpRequest request;
private final ServerHttpResponse response;
private ServerHttpAsyncRequestControl async;
private volatile boolean complete = false;
public HttpConnection(ServerHttpRequest request, ServerHttpResponse response) {
this.createTime = System.currentTimeMillis();
this.request = request;
this.response = response;
this.async = startAsync();
}
/**
* Start asynchronous support or if unavailable return {@code null} to cause
* {@link #waitForResponse()} to block.
* @return the async request control
*/
protected ServerHttpAsyncRequestControl startAsync() {
try {
// Try to use async to save blocking
ServerHttpAsyncRequestControl async = this.request
.getAsyncRequestControl(this.response);
async.start();
return async;
}
catch (Exception ex) {
return null;
}
}
/**
* Return the underlying request.
* @return the request
*/
public final ServerHttpRequest getRequest() {
return this.request;
}
/**
* Return the underlying response.
* @return the response
*/
protected final ServerHttpResponse getResponse() {
return this.response;
}
/**
* Determine if a connection is older than the specified time.
* @param time the time to check
* @return {@code true} if the request is older than the time
*/
public boolean isOlderThan(int time) {
long runningTime = System.currentTimeMillis() - this.createTime;
return (runningTime > time);
}
/**
* Cause the request to block or use asynchronous methods to wait until a response
* is available.
*/
public void waitForResponse() {
if (this.async == null) {
while (!this.complete) {
try {
synchronized (this) {
wait(1000);
}
}
catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
}
}
}
/**
* Detect if the request is actually a signal to disconnect.
* @return if the request is a signal to disconnect
*/
public boolean isDisconnectRequest() {
return DISCONNECT_MEDIA_TYPE
.equals(this.request.getHeaders().getContentType());
}
/**
* Send a HTTP status response.
* @param status the status to send
* @throws IOException in case of I/O errors
*/
public void respond(HttpStatus status) throws IOException {
Assert.notNull(status, "Status must not be null");
this.response.setStatusCode(status);
complete();
}
/**
* Send a payload response.
* @param payload the payload to send
* @throws IOException in case of I/O errors
*/
public void respond(HttpTunnelPayload payload) throws IOException {
Assert.notNull(payload, "Payload must not be null");
this.response.setStatusCode(HttpStatus.OK);
payload.assignTo(this.response);
complete();
}
/**
* Called when a request is complete.
*/
protected void complete() {
if (this.async != null) {
this.async.complete();
}
else {
synchronized (this) {
this.complete = true;
notifyAll();
}
}
}
}
}

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.devtools.tunnel.server;
import java.io.IOException;
import org.springframework.boot.devtools.remote.server.Handler;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.util.Assert;
/**
* Adapts a {@link HttpTunnelServer} to a {@link Handler}.
*
* @author Phillip Webb
* @since 1.3.0
*/
public class HttpTunnelServerHandler implements Handler {
private HttpTunnelServer server;
/**
* Create a new {@link HttpTunnelServerHandler} instance.
* @param server the server to adapt
*/
public HttpTunnelServerHandler(HttpTunnelServer server) {
Assert.notNull(server, "Server must not be null");
this.server = server;
}
@Override
public void handle(ServerHttpRequest request, ServerHttpResponse response)
throws IOException {
this.server.handle(request, response);
}
}

View File

@@ -0,0 +1,35 @@
/*
* Copyright 2012-2017 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.devtools.tunnel.server;
/**
* Strategy interface to provide access to a port (which may change if an existing
* connection is closed).
*
* @author Phillip Webb
* @since 1.3.0
*/
@FunctionalInterface
public interface PortProvider {
/**
* Return the port number.
* @return the port number
*/
int getPort();
}

View File

@@ -0,0 +1,102 @@
/*
* 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.devtools.tunnel.server;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ByteChannel;
import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.SocketChannel;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.util.Assert;
/**
* Socket based {@link TargetServerConnection}.
*
* @author Phillip Webb
* @since 1.3.0
*/
public class SocketTargetServerConnection implements TargetServerConnection {
private static final Log logger = LogFactory
.getLog(SocketTargetServerConnection.class);
private final PortProvider portProvider;
/**
* Create a new {@link SocketTargetServerConnection}.
* @param portProvider the port provider
*/
public SocketTargetServerConnection(PortProvider portProvider) {
Assert.notNull(portProvider, "PortProvider must not be null");
this.portProvider = portProvider;
}
@Override
public ByteChannel open(int socketTimeout) throws IOException {
SocketAddress address = new InetSocketAddress(this.portProvider.getPort());
logger.trace("Opening tunnel connection to target server on " + address);
SocketChannel channel = SocketChannel.open(address);
channel.socket().setSoTimeout(socketTimeout);
return new TimeoutAwareChannel(channel);
}
/**
* Wrapper to expose the {@link SocketChannel} in such a way that
* {@code SocketTimeoutExceptions} are still thrown from read methods.
*/
private static class TimeoutAwareChannel implements ByteChannel {
private final SocketChannel socketChannel;
private final ReadableByteChannel readChannel;
TimeoutAwareChannel(SocketChannel socketChannel) throws IOException {
this.socketChannel = socketChannel;
this.readChannel = Channels
.newChannel(socketChannel.socket().getInputStream());
}
@Override
public int read(ByteBuffer dst) throws IOException {
return this.readChannel.read(dst);
}
@Override
public int write(ByteBuffer src) throws IOException {
return this.socketChannel.write(src);
}
@Override
public boolean isOpen() {
return this.socketChannel.isOpen();
}
@Override
public void close() throws IOException {
this.socketChannel.close();
}
}
}

View File

@@ -0,0 +1,41 @@
/*
* 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.devtools.tunnel.server;
import org.springframework.util.Assert;
/**
* {@link PortProvider} for a static port that won't change.
*
* @author Phillip Webb
* @since 1.3.0
*/
public class StaticPortProvider implements PortProvider {
private final int port;
public StaticPortProvider(int port) {
Assert.isTrue(port > 0, "Port must be positive");
this.port = port;
}
@Override
public int getPort() {
return this.port;
}
}

View File

@@ -0,0 +1,39 @@
/*
* Copyright 2012-2017 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.devtools.tunnel.server;
import java.io.IOException;
import java.nio.channels.ByteChannel;
/**
* Manages the connection to the ultimate tunnel target server.
*
* @author Phillip Webb
* @since 1.3.0
*/
@FunctionalInterface
public interface TargetServerConnection {
/**
* Open a connection to the target server with the specified timeout.
* @param timeout the read timeout
* @return a {@link ByteChannel} providing read/write access to the server
* @throws IOException in case of I/O errors
*/
ByteChannel open(int timeout) throws IOException;
}

View File

@@ -0,0 +1,20 @@
/*
* 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.
*/
/**
* Server side TCP tunnel support.
*/
package org.springframework.boot.devtools.tunnel.server;

View File

@@ -0,0 +1,24 @@
{
"properties": [
{
"name": "spring.devtools.remote.debug.enabled",
"type": "java.lang.Boolean",
"description": "Enable remote debug support.",
"defaultValue": true,
"deprecation": {
"reason": "Remote debug is no longer supported.",
"level": "error"
}
},
{
"name": "spring.devtools.remote.debug.local-port",
"type": "java.lang.Integer",
"description": "Local remote debug server port.",
"defaultValue": 8000,
"deprecation": {
"reason": "Remote debug is no longer supported.",
"level": "error"
}
}
]
}

View File

@@ -0,0 +1,6 @@
restart.exclude.spring-boot=/spring-boot/target/classes/
restart.exclude.spring-boot-devtools=/spring-boot-devtools/target/classes/
restart.exclude.spring-boot-autoconfigure=/spring-boot-autoconfigure/target/classes/
restart.exclude.spring-boot-actuator=/spring-boot-actuator/target/classes/
restart.exclude.spring-boot-starter=/spring-boot-starter/target/classes/
restart.exclude.spring-boot-starters=/spring-boot-starter-[\\w-]+/

Some files were not shown because too many files have changed in this diff Show More