Merge branch 'gh-3082'
This commit is contained in:
commit
247d2d6a2d
1
pom.xml
1
pom.xml
|
|
@ -84,6 +84,7 @@
|
|||
<module>spring-boot</module>
|
||||
<module>spring-boot-autoconfigure</module>
|
||||
<module>spring-boot-actuator</module>
|
||||
<module>spring-boot-developer-tools</module>
|
||||
<module>spring-boot-docs</module>
|
||||
<module>spring-boot-starters</module>
|
||||
<module>spring-boot-cli</module>
|
||||
|
|
|
|||
|
|
@ -184,6 +184,11 @@
|
|||
<artifactId>spring-boot-configuration-processor</artifactId>
|
||||
<version>1.3.0.BUILD-SNAPSHOT</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-developer-tools</artifactId>
|
||||
<version>1.3.0.BUILD-SNAPSHOT</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-loader</artifactId>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,90 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-parent</artifactId>
|
||||
<version>1.3.0.BUILD-SNAPSHOT</version>
|
||||
<relativePath>../spring-boot-parent</relativePath>
|
||||
</parent>
|
||||
<artifactId>spring-boot-developer-tools</artifactId>
|
||||
<name>Spring Boot Developer Tools</name>
|
||||
<description>Spring Boot Developer Tools</description>
|
||||
<url>http://projects.spring.io/spring-boot/</url>
|
||||
<organization>
|
||||
<name>Pivotal Software, Inc.</name>
|
||||
<url>http://www.spring.io</url>
|
||||
</organization>
|
||||
<properties>
|
||||
<main.basedir>${basedir}/..</main.basedir>
|
||||
</properties>
|
||||
<dependencies>
|
||||
<!-- Compile -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-autoconfigure</artifactId>
|
||||
</dependency>
|
||||
<!-- Optional -->
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-web</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>javax.servlet</groupId>
|
||||
<artifactId>javax.servlet-api</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<!-- Annotation processing -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-configuration-processor</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<!-- Test -->
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-webmvc</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.tomcat.embed</groupId>
|
||||
<artifactId>tomcat-embed-core</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.tomcat.embed</groupId>
|
||||
<artifactId>tomcat-embed-logging-juli</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty.websocket</groupId>
|
||||
<artifactId>websocket-client</artifactId>
|
||||
<version>${jetty.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-thymeleaf</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.codehaus.mojo</groupId>
|
||||
<artifactId>animal-sniffer-maven-plugin</artifactId>
|
||||
<configuration>
|
||||
<ignores>
|
||||
<ignore>org.springframework.boot.developertools.tunnel.server.RemoteDebugPortProvider</ignore>
|
||||
</ignores>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
/*
|
||||
* Copyright 2012-2015 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.developertools;
|
||||
|
||||
import org.springframework.boot.Banner;
|
||||
import org.springframework.boot.ResourceBanner;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.developertools.remote.client.RemoteClientConfiguration;
|
||||
import org.springframework.boot.developertools.restart.RestartInitializer;
|
||||
import org.springframework.boot.developertools.restart.Restarter;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
|
||||
/**
|
||||
* Application that can be used to establish a link to remotely running Spring Boot code.
|
||||
* Allows remote debugging and 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 class RemoteSpringApplication {
|
||||
|
||||
private void run(String[] args) {
|
||||
Restarter.initialize(args, RestartInitializer.NONE);
|
||||
SpringApplication application = new SpringApplication(
|
||||
RemoteClientConfiguration.class);
|
||||
application.setWebEnvironment(false);
|
||||
application.setBanner(getBanner());
|
||||
application.addListeners(new RemoteUrlPropertyExtractor());
|
||||
application.run(args);
|
||||
waitIndefinitely();
|
||||
}
|
||||
|
||||
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) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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.developertools;
|
||||
|
||||
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.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
|
||||
*/
|
||||
class RemoteUrlPropertyExtractor implements
|
||||
ApplicationListener<ApplicationEnvironmentPreparedEvent> {
|
||||
|
||||
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 = 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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,116 @@
|
|||
/*
|
||||
* Copyright 2012-2015 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.developertools.autoconfigure;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
/**
|
||||
* Configuration properties for developer tools.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 1.3.0
|
||||
*/
|
||||
@ConfigurationProperties(prefix = "spring.developertools")
|
||||
public class DeveloperToolsProperties {
|
||||
|
||||
private static final String DEFAULT_RESTART_EXCLUDES = "META-INF/resources/**,resource/**,static/**,public/**,templates/**";
|
||||
|
||||
private Restart restart = new Restart();
|
||||
|
||||
private Livereload livereload = new Livereload();
|
||||
|
||||
private RemoteDeveloperToolsProperties remote = new RemoteDeveloperToolsProperties();
|
||||
|
||||
public Restart getRestart() {
|
||||
return this.restart;
|
||||
}
|
||||
|
||||
public Livereload getLivereload() {
|
||||
return this.livereload;
|
||||
}
|
||||
|
||||
public RemoteDeveloperToolsProperties getRemote() {
|
||||
return this.remote;
|
||||
}
|
||||
|
||||
/**
|
||||
* Restart properties
|
||||
*/
|
||||
public static class Restart {
|
||||
|
||||
/**
|
||||
* Enable automatic restart.
|
||||
*/
|
||||
private boolean enabled = true;
|
||||
|
||||
/**
|
||||
* Patterns that should be excluding for triggering a full restart.
|
||||
*/
|
||||
private String exclude = DEFAULT_RESTART_EXCLUDES;
|
||||
|
||||
public boolean isEnabled() {
|
||||
return this.enabled;
|
||||
}
|
||||
|
||||
public void setEnabled(boolean enabled) {
|
||||
this.enabled = enabled;
|
||||
}
|
||||
|
||||
public String getExclude() {
|
||||
return this.exclude;
|
||||
}
|
||||
|
||||
public void setExclude(String exclude) {
|
||||
this.exclude = exclude;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
/*
|
||||
* Copyright 2012-2015 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.developertools.autoconfigure;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
|
||||
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
|
||||
import org.springframework.context.EnvironmentAware;
|
||||
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 BeanFactoryPostProcessor} to add properties that make sense when working
|
||||
* locally.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class LocalDeveloperPropertyDefaultsPostProcessor implements BeanFactoryPostProcessor,
|
||||
EnvironmentAware {
|
||||
|
||||
private static final Map<String, Object> PROPERTIES;
|
||||
static {
|
||||
Map<String, Object> properties = new HashMap<String, Object>();
|
||||
properties.put("spring.thymeleaf.cache", "false");
|
||||
PROPERTIES = Collections.unmodifiableMap(properties);
|
||||
}
|
||||
|
||||
private Environment environment;
|
||||
|
||||
@Override
|
||||
public void setEnvironment(Environment environment) {
|
||||
this.environment = environment;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory)
|
||||
throws BeansException {
|
||||
if (this.environment instanceof ConfigurableEnvironment) {
|
||||
postProcessEnvironment((ConfigurableEnvironment) this.environment);
|
||||
}
|
||||
}
|
||||
|
||||
private void postProcessEnvironment(ConfigurableEnvironment environment) {
|
||||
PropertySource<?> propertySource = new MapPropertySource("refresh", PROPERTIES);
|
||||
environment.getPropertySources().addFirst(propertySource);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,129 @@
|
|||
/*
|
||||
* Copyright 2012-2015 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.developertools.autoconfigure;
|
||||
|
||||
import java.net.URL;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.boot.developertools.classpath.ClassPathChangedEvent;
|
||||
import org.springframework.boot.developertools.classpath.ClassPathFileSystemWatcher;
|
||||
import org.springframework.boot.developertools.classpath.ClassPathRestartStrategy;
|
||||
import org.springframework.boot.developertools.classpath.PatternClassPathRestartStrategy;
|
||||
import org.springframework.boot.developertools.livereload.LiveReloadServer;
|
||||
import org.springframework.boot.developertools.restart.ConditionalOnInitializedRestarter;
|
||||
import org.springframework.boot.developertools.restart.RestartScope;
|
||||
import org.springframework.boot.developertools.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;
|
||||
|
||||
/**
|
||||
* {@link EnableAutoConfiguration Auto-configuration} for local development support.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 1.3.0
|
||||
*/
|
||||
@Configuration
|
||||
@ConditionalOnInitializedRestarter
|
||||
@EnableConfigurationProperties(DeveloperToolsProperties.class)
|
||||
public class LocalDeveloperToolsAutoConfiguration {
|
||||
|
||||
@Autowired
|
||||
private DeveloperToolsProperties properties;
|
||||
|
||||
@Bean
|
||||
public static LocalDeveloperPropertyDefaultsPostProcessor localDeveloperPropertyDefaultsPostProcessor() {
|
||||
return new LocalDeveloperPropertyDefaultsPostProcessor();
|
||||
}
|
||||
|
||||
/**
|
||||
* Local LiveReload configuration.
|
||||
*/
|
||||
@ConditionalOnProperty(prefix = "spring.developertools.livereload", name = "enabled", matchIfMissing = true)
|
||||
static class LiveReloadConfiguration {
|
||||
|
||||
@Autowired
|
||||
private DeveloperToolsProperties 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.developertools.restart", name = "enabled", matchIfMissing = true)
|
||||
static class RestartConfiguration {
|
||||
|
||||
@Autowired
|
||||
private DeveloperToolsProperties properties;
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public ClassPathFileSystemWatcher classPathFileSystemWatcher() {
|
||||
URL[] urls = Restarter.getInstance().getInitialUrls();
|
||||
return new ClassPathFileSystemWatcher(classPathRestartStrategy(), urls);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public ClassPathRestartStrategy classPathRestartStrategy() {
|
||||
return new PatternClassPathRestartStrategy(this.properties.getRestart()
|
||||
.getExclude());
|
||||
}
|
||||
|
||||
@EventListener
|
||||
public void onClassPathChanged(ClassPathChangedEvent event) {
|
||||
if (event.isRestartRequired()) {
|
||||
Restarter.getInstance().restart();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
/*
|
||||
* Copyright 2012-2015 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.developertools.autoconfigure;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.springframework.boot.developertools.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 {
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link PostConstruct} method to start the server if possible.
|
||||
* @throws Exception
|
||||
*/
|
||||
@PostConstruct
|
||||
public 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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,154 @@
|
|||
/*
|
||||
* Copyright 2012-2015 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.developertools.autoconfigure;
|
||||
|
||||
import 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.beans.factory.annotation.Qualifier;
|
||||
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.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.boot.developertools.remote.server.AccessManager;
|
||||
import org.springframework.boot.developertools.remote.server.Dispatcher;
|
||||
import org.springframework.boot.developertools.remote.server.DispatcherFilter;
|
||||
import org.springframework.boot.developertools.remote.server.Handler;
|
||||
import org.springframework.boot.developertools.remote.server.HandlerMapper;
|
||||
import org.springframework.boot.developertools.remote.server.HttpHeaderAccessManager;
|
||||
import org.springframework.boot.developertools.remote.server.HttpStatusHandler;
|
||||
import org.springframework.boot.developertools.remote.server.UrlHandlerMapper;
|
||||
import org.springframework.boot.developertools.restart.server.DefaultSourceFolderUrlFilter;
|
||||
import org.springframework.boot.developertools.restart.server.HttpRestartServer;
|
||||
import org.springframework.boot.developertools.restart.server.HttpRestartServerHandler;
|
||||
import org.springframework.boot.developertools.restart.server.SourceFolderUrlFilter;
|
||||
import org.springframework.boot.developertools.tunnel.server.HttpTunnelServer;
|
||||
import org.springframework.boot.developertools.tunnel.server.HttpTunnelServerHandler;
|
||||
import org.springframework.boot.developertools.tunnel.server.RemoteDebugPortProvider;
|
||||
import org.springframework.boot.developertools.tunnel.server.SocketTargetServerConnection;
|
||||
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
|
||||
* @since 1.3.0
|
||||
*/
|
||||
@Configuration
|
||||
@ConditionalOnProperty(prefix = "spring.developertools.remote", name = "secret")
|
||||
@ConditionalOnClass({ Filter.class, ServerHttpRequest.class })
|
||||
@EnableConfigurationProperties(DeveloperToolsProperties.class)
|
||||
public class RemoteDeveloperToolsAutoConfiguration {
|
||||
|
||||
private static final Log logger = LogFactory
|
||||
.getLog(RemoteDeveloperToolsAutoConfiguration.class);
|
||||
|
||||
@Autowired
|
||||
private DeveloperToolsProperties properties;
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public AccessManager remoteDeveloperToolsAccessManager() {
|
||||
RemoteDeveloperToolsProperties remoteProperties = this.properties.getRemote();
|
||||
return new HttpHeaderAccessManager(remoteProperties.getSecretHeaderName(),
|
||||
remoteProperties.getSecret());
|
||||
}
|
||||
|
||||
@Bean
|
||||
public HandlerMapper remoteDeveloperToolsHealthCheckHandlerMapper() {
|
||||
Handler handler = new HttpStatusHandler();
|
||||
return new UrlHandlerMapper(this.properties.getRemote().getContextPath(), handler);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public DispatcherFilter remoteDeveloperToolsDispatcherFilter(
|
||||
AccessManager accessManager, Collection<HandlerMapper> mappers) {
|
||||
Dispatcher dispatcher = new Dispatcher(accessManager, mappers);
|
||||
return new DispatcherFilter(dispatcher);
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration for remote update and restarts.
|
||||
*/
|
||||
@ConditionalOnProperty(prefix = "spring.developertools.remote.restart", name = "enabled", matchIfMissing = true)
|
||||
static class RemoteRestartConfiguration {
|
||||
|
||||
@Autowired
|
||||
private DeveloperToolsProperties properties;
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public SourceFolderUrlFilter remoteRestartSourceFolderUrlFilter() {
|
||||
return new DefaultSourceFolderUrlFilter();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public HttpRestartServer remoteRestartHttpRestartServer(
|
||||
SourceFolderUrlFilter sourceFolderUrlFilter) {
|
||||
return new HttpRestartServer(sourceFolderUrlFilter);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(name = "remoteRestartHanderMapper")
|
||||
public UrlHandlerMapper remoteRestartHanderMapper(HttpRestartServer server) {
|
||||
String url = this.properties.getRemote().getContextPath() + "/restart";
|
||||
logger.warn("Listening for remote restart updates on " + url);
|
||||
Handler handler = new HttpRestartServerHandler(server);
|
||||
return new UrlHandlerMapper(url, handler);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration for remote debug HTTP tunneling.
|
||||
*/
|
||||
@ConditionalOnProperty(prefix = "spring.developertools.remote.debug", name = "enabled", matchIfMissing = true)
|
||||
static class RemoteDebugTunnelConfiguration {
|
||||
|
||||
@Autowired
|
||||
private DeveloperToolsProperties properties;
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(name = "remoteDebugHanderMapper")
|
||||
public UrlHandlerMapper remoteDebugHanderMapper(
|
||||
@Qualifier("remoteDebugHttpTunnelServer") HttpTunnelServer server) {
|
||||
String url = this.properties.getRemote().getContextPath() + "/debug";
|
||||
logger.warn("Listening for remote debug traffic on " + url);
|
||||
Handler handler = new HttpTunnelServerHandler(server);
|
||||
return new UrlHandlerMapper(url, handler);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(name = "remoteDebugHttpTunnelServer")
|
||||
public HttpTunnelServer remoteDebugHttpTunnelServer() {
|
||||
return new HttpTunnelServer(new SocketTargetServerConnection(
|
||||
new RemoteDebugPortProvider()));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,134 @@
|
|||
/*
|
||||
* Copyright 2012-2015 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.developertools.autoconfigure;
|
||||
|
||||
/**
|
||||
* Configuration properties for remote Spring Boot applications.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Rob Winch
|
||||
* @since 1.3.0
|
||||
* @see DeveloperToolsProperties
|
||||
*/
|
||||
public class RemoteDeveloperToolsProperties {
|
||||
|
||||
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 Debug debug = new Debug();
|
||||
|
||||
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 Debug getDebug() {
|
||||
return this.debug;
|
||||
}
|
||||
|
||||
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 Debug {
|
||||
|
||||
public static final Integer DEFAULT_LOCAL_PORT = 8000;
|
||||
|
||||
/**
|
||||
* Enable remote debug support.
|
||||
*/
|
||||
private boolean enabled = true;
|
||||
|
||||
/**
|
||||
* Local remote debug server port.
|
||||
*/
|
||||
private int localPort = DEFAULT_LOCAL_PORT;
|
||||
|
||||
public boolean isEnabled() {
|
||||
return this.enabled;
|
||||
}
|
||||
|
||||
public void setEnabled(boolean enabled) {
|
||||
this.enabled = enabled;
|
||||
}
|
||||
|
||||
public int getLocalPort() {
|
||||
return this.localPort;
|
||||
}
|
||||
|
||||
public void setLocalPort(int localPort) {
|
||||
this.localPort = localPort;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* Copyright 2012-2015 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Auto-configuration for {@code spring-boot-developer-tools}.
|
||||
*/
|
||||
package org.springframework.boot.developertools.autoconfigure;
|
||||
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
/*
|
||||
* Copyright 2012-2015 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.developertools.classpath;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
import org.springframework.boot.developertools.filewatch.ChangedFiles;
|
||||
import org.springframework.context.ApplicationEvent;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* {@link ApplicationEvent} containing details of a classpath change.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 1.3.0
|
||||
* @see ClassPathFileChangeListener
|
||||
*/
|
||||
public class ClassPathChangedEvent extends ApplicationEvent {
|
||||
|
||||
private final Set<ChangedFiles> changeSet;
|
||||
|
||||
private final boolean restartRequired;
|
||||
|
||||
/**
|
||||
* Create a new {@link ClassPathChangedEvent}.
|
||||
* @param source the source of the event
|
||||
* @param changeSet the changed files
|
||||
* @param restartRequired if a restart is required due to the change
|
||||
*/
|
||||
public ClassPathChangedEvent(Object source, Set<ChangedFiles> changeSet,
|
||||
boolean restartRequired) {
|
||||
super(source);
|
||||
Assert.notNull(changeSet, "ChangeSet must not be null");
|
||||
this.changeSet = changeSet;
|
||||
this.restartRequired = restartRequired;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return details of the files that changed.
|
||||
* @return the changed files
|
||||
*/
|
||||
public Set<ChangedFiles> getChangeSet() {
|
||||
return this.changeSet;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return if an application restart is required due to the change.
|
||||
* @return if an application restart is required
|
||||
*/
|
||||
public boolean isRestartRequired() {
|
||||
return this.restartRequired;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
/*
|
||||
* Copyright 2012-2015 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.developertools.classpath;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
import org.springframework.boot.developertools.filewatch.ChangedFile;
|
||||
import org.springframework.boot.developertools.filewatch.ChangedFiles;
|
||||
import org.springframework.boot.developertools.filewatch.FileChangeListener;
|
||||
import org.springframework.context.ApplicationEvent;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* A {@link FileChangeListener} to publish {@link ClassPathChangedEvent
|
||||
* ClassPathChangedEvents}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 1.3.0
|
||||
* @see ClassPathFileSystemWatcher
|
||||
*/
|
||||
public class ClassPathFileChangeListener implements FileChangeListener {
|
||||
|
||||
private final ApplicationEventPublisher eventPublisher;
|
||||
|
||||
private final ClassPathRestartStrategy restartStrategy;
|
||||
|
||||
/**
|
||||
* Create a new {@link ClassPathFileChangeListener} instance.
|
||||
* @param eventPublisher the event publisher used send events
|
||||
* @param restartStrategy the restart strategy to use
|
||||
*/
|
||||
public ClassPathFileChangeListener(ApplicationEventPublisher eventPublisher,
|
||||
ClassPathRestartStrategy restartStrategy) {
|
||||
Assert.notNull(eventPublisher, "EventPublisher must not be null");
|
||||
Assert.notNull(restartStrategy, "RestartStrategy must not be null");
|
||||
this.eventPublisher = eventPublisher;
|
||||
this.restartStrategy = restartStrategy;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChange(Set<ChangedFiles> changeSet) {
|
||||
boolean restart = isRestartRequired(changeSet);
|
||||
ApplicationEvent event = new ClassPathChangedEvent(this, changeSet, restart);
|
||||
this.eventPublisher.publishEvent(event);
|
||||
}
|
||||
|
||||
private boolean isRestartRequired(Set<ChangedFiles> changeSet) {
|
||||
for (ChangedFiles changedFiles : changeSet) {
|
||||
for (ChangedFile changedFile : changedFiles) {
|
||||
if (this.restartStrategy.isRestartRequired(changedFile)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,122 @@
|
|||
/*
|
||||
* Copyright 2012-2015 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.developertools.classpath;
|
||||
|
||||
import java.net.URL;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.beans.factory.DisposableBean;
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.boot.developertools.filewatch.FileSystemWatcher;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.ApplicationContextAware;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ResourceUtils;
|
||||
|
||||
/**
|
||||
* Encapsulates a {@link FileSystemWatcher} to watch the local classpath folders for
|
||||
* changes.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 1.3.0
|
||||
* @see ClassPathFileChangeListener
|
||||
*/
|
||||
public class ClassPathFileSystemWatcher implements InitializingBean, DisposableBean,
|
||||
ApplicationContextAware {
|
||||
|
||||
private static final Log logger = LogFactory.getLog(ClassPathFileSystemWatcher.class);
|
||||
|
||||
private final FileSystemWatcher fileSystemWatcher;
|
||||
|
||||
private ClassPathRestartStrategy restartStrategy;
|
||||
|
||||
private ApplicationContext applicationContext;
|
||||
|
||||
/**
|
||||
* Create a new {@link ClassPathFileSystemWatcher} instance.
|
||||
* @param urls the classpath URLs to watch
|
||||
*/
|
||||
public ClassPathFileSystemWatcher(URL[] urls) {
|
||||
this(new FileSystemWatcher(), null, urls);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link ClassPathFileSystemWatcher} instance.
|
||||
* @param restartStrategy the classpath restart strategy
|
||||
* @param urls the URLs to watch
|
||||
*/
|
||||
public ClassPathFileSystemWatcher(ClassPathRestartStrategy restartStrategy, URL[] urls) {
|
||||
this(new FileSystemWatcher(), restartStrategy, urls);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link ClassPathFileSystemWatcher} instance.
|
||||
* @param fileSystemWatcher the underlying {@link FileSystemWatcher} used to monitor
|
||||
* the local file system
|
||||
* @param restartStrategy the classpath restart strategy
|
||||
* @param urls the URLs to watch
|
||||
*/
|
||||
protected ClassPathFileSystemWatcher(FileSystemWatcher fileSystemWatcher,
|
||||
ClassPathRestartStrategy restartStrategy, URL[] urls) {
|
||||
Assert.notNull(fileSystemWatcher, "FileSystemWatcher must not be null");
|
||||
Assert.notNull(urls, "Urls must not be null");
|
||||
this.fileSystemWatcher = new FileSystemWatcher();
|
||||
this.restartStrategy = restartStrategy;
|
||||
addUrls(urls);
|
||||
}
|
||||
|
||||
private void addUrls(URL[] urls) {
|
||||
for (URL url : urls) {
|
||||
addUrl(url);
|
||||
}
|
||||
}
|
||||
|
||||
private void addUrl(URL url) {
|
||||
if (url.getProtocol().equals("file") && url.getPath().endsWith("/")) {
|
||||
try {
|
||||
this.fileSystemWatcher.addSourceFolder(ResourceUtils.getFile(url));
|
||||
}
|
||||
catch (Exception ex) {
|
||||
logger.warn("Unable to watch classpath URL " + url);
|
||||
logger.trace("Unable to watch classpath URL " + url, ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setApplicationContext(ApplicationContext applicationContext)
|
||||
throws BeansException {
|
||||
this.applicationContext = applicationContext;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterPropertiesSet() throws Exception {
|
||||
if (this.restartStrategy != null) {
|
||||
this.fileSystemWatcher.addListener(new ClassPathFileChangeListener(
|
||||
this.applicationContext, this.restartStrategy));
|
||||
}
|
||||
this.fileSystemWatcher.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() throws Exception {
|
||||
this.fileSystemWatcher.stop();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* Copyright 2012-2015 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.developertools.classpath;
|
||||
|
||||
import org.springframework.boot.developertools.filewatch.ChangedFile;
|
||||
|
||||
/**
|
||||
* Strategy interface used to determine when a changed classpath file should trigger a
|
||||
* full application restart. For example, static web resources might not require a full
|
||||
* restart where as class files would.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 1.3.0
|
||||
* @see PatternClassPathRestartStrategy
|
||||
*/
|
||||
public interface ClassPathRestartStrategy {
|
||||
|
||||
/**
|
||||
* Return true if a full restart is required.
|
||||
* @param file the changed file
|
||||
* @return {@code true} if a full restart is required
|
||||
*/
|
||||
boolean isRestartRequired(ChangedFile file);
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
* Copyright 2012-2015 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.developertools.classpath;
|
||||
|
||||
import org.springframework.boot.developertools.filewatch.ChangedFile;
|
||||
import org.springframework.util.AntPathMatcher;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Ant style pattern based {@link ClassPathRestartStrategy}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 1.3.0
|
||||
* @see ClassPathRestartStrategy
|
||||
*/
|
||||
public class PatternClassPathRestartStrategy implements ClassPathRestartStrategy {
|
||||
|
||||
private final AntPathMatcher matcher = new AntPathMatcher();
|
||||
|
||||
private final String[] excludePatterns;
|
||||
|
||||
public PatternClassPathRestartStrategy(String excludePatterns) {
|
||||
this.excludePatterns = StringUtils
|
||||
.commaDelimitedListToStringArray(excludePatterns);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRestartRequired(ChangedFile file) {
|
||||
for (String pattern : this.excludePatterns) {
|
||||
if (this.matcher.match(pattern, file.getRelativeName())) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* Copyright 2012-2015 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Support for classpath monitoring
|
||||
*/
|
||||
package org.springframework.boot.developertools.classpath;
|
||||
|
||||
|
|
@ -0,0 +1,128 @@
|
|||
/*
|
||||
* Copyright 2012-2015 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.developertools.filewatch;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* 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() {
|
||||
String folderName = this.sourceFolder.getAbsoluteFile().getPath();
|
||||
String fileName = this.file.getAbsoluteFile().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 static enum Type {
|
||||
|
||||
/**
|
||||
* A new file has been added.
|
||||
*/
|
||||
ADD,
|
||||
|
||||
/**
|
||||
* An existing file has been modified.
|
||||
*/
|
||||
MODIFY,
|
||||
|
||||
/**
|
||||
* An existing file has been deleted.
|
||||
*/
|
||||
DELETE
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
/*
|
||||
* Copyright 2012-2015 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.developertools.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;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* Copyright 2012-2015 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.developertools.filewatch;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Callback interface when file changes are detected.
|
||||
*
|
||||
* @author Andy Clement
|
||||
* @author Phillip Webb
|
||||
* @since 1.3.0
|
||||
*/
|
||||
public interface FileChangeListener {
|
||||
|
||||
/**
|
||||
* Called when files have been changed.
|
||||
* @param changeSet a set of the {@link ChangedFiles}
|
||||
*/
|
||||
void onChange(Set<ChangedFiles> changeSet);
|
||||
|
||||
}
|
||||
|
|
@ -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.developertools.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;
|
||||
|
||||
public 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 &= this.exists == other.exists;
|
||||
equals &= this.length == other.length;
|
||||
equals &= this.lastModified == other.lastModified;
|
||||
return equals;
|
||||
}
|
||||
return super.equals(obj);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int hashCode = this.file.hashCode();
|
||||
hashCode = 31 * hashCode + (this.exists ? 1231 : 1237);
|
||||
hashCode = 31 * hashCode + (int) (this.length ^ (this.length >>> 32));
|
||||
hashCode = 31 * hashCode + (int) (this.lastModified ^ (this.lastModified >>> 32));
|
||||
return hashCode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return this.file.toString();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,211 @@
|
|||
/*
|
||||
* Copyright 2012-2015 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.developertools.filewatch;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
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
|
||||
* @see FileChangeListener
|
||||
* @since 1.3.0
|
||||
*/
|
||||
public class FileSystemWatcher {
|
||||
|
||||
private static final long DEFAULT_IDLE_TIME = 400;
|
||||
|
||||
private static final long DEFAULT_QUIET_TIME = 200;
|
||||
|
||||
private List<FileChangeListener> listeners = new ArrayList<FileChangeListener>();
|
||||
|
||||
private final boolean daemon;
|
||||
|
||||
private final long idleTime;
|
||||
|
||||
private final long quietTime;
|
||||
|
||||
private Thread watchThread;
|
||||
|
||||
private AtomicInteger remainingScans = new AtomicInteger(-1);
|
||||
|
||||
private Map<File, FolderSnapshot> folders = new LinkedHashMap<File, FolderSnapshot>();
|
||||
|
||||
/**
|
||||
* Create a new {@link FileSystemWatcher} instance.
|
||||
*/
|
||||
public FileSystemWatcher() {
|
||||
this(true, DEFAULT_IDLE_TIME, DEFAULT_QUIET_TIME);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link FileSystemWatcher} instance.
|
||||
* @param daemon if a daemon thread used to monitor changes
|
||||
* @param idleTime the amount of time to wait between checking for changes
|
||||
* @param quietTime the amount of time required after a change has been detected to
|
||||
* ensure that updates have completed
|
||||
*/
|
||||
public FileSystemWatcher(boolean daemon, long idleTime, long quietTime) {
|
||||
this.daemon = daemon;
|
||||
this.idleTime = idleTime;
|
||||
this.quietTime = quietTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add listener for file change events. Cannot be called after the watcher has been
|
||||
* {@link #start() started}.
|
||||
* @param fileChangeListener the listener to add
|
||||
*/
|
||||
public synchronized void addListener(FileChangeListener fileChangeListener) {
|
||||
Assert.notNull(fileChangeListener, "FileChangeListener must not be null");
|
||||
checkNotStarted();
|
||||
this.listeners.add(fileChangeListener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a source folder to monitor. Cannot be called after the watcher has been
|
||||
* {@link #start() started}.
|
||||
* @param folder the folder to monitor
|
||||
*/
|
||||
public synchronized void addSourceFolder(File folder) {
|
||||
Assert.notNull(folder, "Folder must not be null");
|
||||
Assert.isTrue(folder.isDirectory(), "Folder must not be a file");
|
||||
checkNotStarted();
|
||||
this.folders.put(folder, null);
|
||||
}
|
||||
|
||||
private void checkNotStarted() {
|
||||
Assert.state(this.watchThread == null, "FileSystemWatcher already started");
|
||||
}
|
||||
|
||||
/**
|
||||
* Start monitoring the source folder for changes.
|
||||
*/
|
||||
public synchronized void start() {
|
||||
saveInitalSnapshots();
|
||||
if (this.watchThread == null) {
|
||||
this.watchThread = new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
int remainingScans = FileSystemWatcher.this.remainingScans.get();
|
||||
while (remainingScans > 0 || remainingScans == -1) {
|
||||
try {
|
||||
if (remainingScans > 0) {
|
||||
FileSystemWatcher.this.remainingScans.decrementAndGet();
|
||||
}
|
||||
scan();
|
||||
remainingScans = FileSystemWatcher.this.remainingScans.get();
|
||||
}
|
||||
catch (InterruptedException ex) {
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
this.watchThread.setName("File Watcher");
|
||||
this.watchThread.setDaemon(this.daemon);
|
||||
this.remainingScans = new AtomicInteger(-1);
|
||||
this.watchThread.start();
|
||||
}
|
||||
}
|
||||
|
||||
private void saveInitalSnapshots() {
|
||||
for (File folder : this.folders.keySet()) {
|
||||
this.folders.put(folder, new FolderSnapshot(folder));
|
||||
}
|
||||
}
|
||||
|
||||
private void scan() throws InterruptedException {
|
||||
Thread.sleep(this.idleTime - this.quietTime);
|
||||
Set<FolderSnapshot> previous;
|
||||
Set<FolderSnapshot> current = new HashSet<FolderSnapshot>(this.folders.values());
|
||||
do {
|
||||
previous = current;
|
||||
current = getCurrentSnapshots();
|
||||
Thread.sleep(this.quietTime);
|
||||
}
|
||||
while (!previous.equals(current));
|
||||
updateSnapshots(current);
|
||||
}
|
||||
|
||||
private Set<FolderSnapshot> getCurrentSnapshots() {
|
||||
Set<FolderSnapshot> snapshots = new LinkedHashSet<FolderSnapshot>();
|
||||
for (File folder : this.folders.keySet()) {
|
||||
snapshots.add(new FolderSnapshot(folder));
|
||||
}
|
||||
return snapshots;
|
||||
}
|
||||
|
||||
private void updateSnapshots(Set<FolderSnapshot> snapshots) {
|
||||
Map<File, FolderSnapshot> updated = new LinkedHashMap<File, FolderSnapshot>();
|
||||
Set<ChangedFiles> changeSet = new LinkedHashSet<ChangedFiles>();
|
||||
for (FolderSnapshot snapshot : snapshots) {
|
||||
FolderSnapshot previous = this.folders.get(snapshot.getFolder());
|
||||
updated.put(snapshot.getFolder(), snapshot);
|
||||
ChangedFiles changedFiles = previous.getChangedFiles(snapshot);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop monitoring the source folders.
|
||||
*/
|
||||
public synchronized void stop() {
|
||||
stopAfter(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop monitoring the source folders.
|
||||
* @param remainingScans the number of scans remaming
|
||||
*/
|
||||
synchronized void stopAfter(int remainingScans) {
|
||||
Thread thread = this.watchThread;
|
||||
if (thread != null) {
|
||||
this.remainingScans.set(remainingScans);
|
||||
try {
|
||||
thread.join();
|
||||
}
|
||||
catch (InterruptedException ex) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
this.watchThread = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,141 @@
|
|||
/*
|
||||
* Copyright 2012-2015 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.developertools.filewatch;
|
||||
|
||||
import java.io.File;
|
||||
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.developertools.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<String>(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
|
||||
*/
|
||||
public 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<FileSnapshot>();
|
||||
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) {
|
||||
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<ChangedFile>();
|
||||
Map<File, FileSnapshot> previousFiles = getFilesMap();
|
||||
for (FileSnapshot currentFile : snapshot.files) {
|
||||
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()) {
|
||||
changes.add(new ChangedFile(folder, previousFile.getFile(), Type.DELETE));
|
||||
}
|
||||
return new ChangedFiles(folder, changes);
|
||||
}
|
||||
|
||||
private Map<File, FileSnapshot> getFilesMap() {
|
||||
Map<File, FileSnapshot> files = new LinkedHashMap<File, FileSnapshot>();
|
||||
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) {
|
||||
FolderSnapshot other = (FolderSnapshot) obj;
|
||||
return this.folder.equals(other.folder) && this.files.equals(other.files);
|
||||
}
|
||||
return super.equals(obj);
|
||||
}
|
||||
|
||||
@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 + " snaphost at " + this.time;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* Copyright 2012-2015 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Class to watch the local filesystem for changes.
|
||||
*/
|
||||
package org.springframework.boot.developertools.filewatch;
|
||||
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
/*
|
||||
* Copyright 2012-2015 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.developertools.livereload;
|
||||
|
||||
import java.nio.charset.Charset;
|
||||
|
||||
/**
|
||||
* Simple Base64 Encoder.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class Base64Encoder {
|
||||
|
||||
private static final Charset UTF_8 = Charset.forName("UTF-8");
|
||||
|
||||
private static final String ALPHABET_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
+ "abcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||
|
||||
static final byte[] ALPHABET = ALPHABET_CHARS.getBytes(UTF_8);
|
||||
|
||||
private static final byte EQUALS_SIGN = '=';
|
||||
|
||||
public static String encode(String string) {
|
||||
return encode(string.getBytes(UTF_8));
|
||||
}
|
||||
|
||||
public static String encode(byte[] bytes) {
|
||||
byte[] encoded = new byte[bytes.length / 3 * 4 + (bytes.length % 3 == 0 ? 0 : 4)];
|
||||
for (int i = 0; i < encoded.length; i += 3) {
|
||||
encodeBlock(bytes, i, Math.min((bytes.length - i), 3), encoded, i / 3 * 4);
|
||||
}
|
||||
return new String(encoded, UTF_8);
|
||||
}
|
||||
|
||||
private static void encodeBlock(byte[] src, int srcPos, int blockLen, byte[] dest,
|
||||
int destPos) {
|
||||
if (blockLen > 0) {
|
||||
int inBuff = (blockLen > 0 ? ((src[srcPos] << 24) >>> 8) : 0)
|
||||
| (blockLen > 1 ? ((src[srcPos + 1] << 24) >>> 16) : 0)
|
||||
| (blockLen > 2 ? ((src[srcPos + 2] << 24) >>> 24) : 0);
|
||||
for (int i = 0; i < 4; i++) {
|
||||
dest[destPos + i] = (i > blockLen ? EQUALS_SIGN
|
||||
: ALPHABET[(inBuff >>> (6 * (3 - i))) & 0x3f]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,162 @@
|
|||
/*
|
||||
* Copyright 2012-2014 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.developertools.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;
|
||||
|
||||
/**
|
||||
* A {@link LiveReloadServer} connection.
|
||||
*/
|
||||
class Connection {
|
||||
|
||||
private static 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
|
||||
*/
|
||||
public 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
|
||||
*/
|
||||
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("Recieved 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
|
||||
*/
|
||||
public void triggerReload() throws IOException {
|
||||
if (this.webSocket) {
|
||||
logger.debug("Triggering LiveReload");
|
||||
writeWebSocketFrame(new Frame("{\"command\":\"reload\",\"path\":\"/\"}"));
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized 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 Base64Encoder.encode(messageDigest.digest());
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the connection.
|
||||
* @throws IOException
|
||||
*/
|
||||
public void close() throws IOException {
|
||||
this.running = false;
|
||||
this.socket.close();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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.developertools.livereload;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Exception throw when the client closes the connection.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class ConnectionClosedException extends IOException {
|
||||
|
||||
public ConnectionClosedException() {
|
||||
super("Connection closed");
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,102 @@
|
|||
/*
|
||||
* Copyright 2012-2014 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.developertools.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;
|
||||
|
||||
public 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
|
||||
*/
|
||||
public String readHeader() throws IOException {
|
||||
byte[] buffer = new byte[BUFFER_SIZE];
|
||||
StringBuffer content = new StringBuffer(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)).toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
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
|
||||
*/
|
||||
public int checkedRead() throws IOException {
|
||||
int b = read();
|
||||
if (b == -1) {
|
||||
throw new IOException("End of stream");
|
||||
}
|
||||
return (b & 0xff);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a 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
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
/*
|
||||
* Copyright 2012-2014 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.developertools.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 {
|
||||
|
||||
public 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());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,159 @@
|
|||
/*
|
||||
* Copyright 2012-2014 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.developertools.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
|
||||
*/
|
||||
public Frame(String payload) {
|
||||
Assert.notNull(payload, "Payload must not be null");
|
||||
this.type = Type.TEXT;
|
||||
this.payload = payload.getBytes();
|
||||
}
|
||||
|
||||
public Frame(Type type) {
|
||||
Assert.notNull(type, "Type must not be null");
|
||||
this.type = type;
|
||||
this.payload = NO_BYTES;
|
||||
}
|
||||
|
||||
private 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);
|
||||
}
|
||||
|
||||
public static 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;
|
||||
|
||||
private 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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,322 @@
|
|||
/*
|
||||
* Copyright 2012-2014 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.developertools.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
|
||||
* @see <a href="http://livereload.com">livereload.com</a>
|
||||
* @since 1.3.0
|
||||
*/
|
||||
public class LiveReloadServer {
|
||||
|
||||
/**
|
||||
* The default live reload server port.
|
||||
*/
|
||||
public static final int DEFAULT_PORT = 35729;
|
||||
|
||||
private static Log logger = LogFactory.getLog(LiveReloadServer.class);
|
||||
|
||||
private static final int READ_TIMEOUT = (int) TimeUnit.SECONDS.toMillis(4);
|
||||
|
||||
private final int port;
|
||||
|
||||
private final ThreadFactory threadFactory;
|
||||
|
||||
private ServerSocket serverSocket;
|
||||
|
||||
private Thread listenThread;
|
||||
|
||||
private ExecutorService executor = Executors
|
||||
.newCachedThreadPool(new WorkerThreadFactory());
|
||||
|
||||
private List<Connection> connections = new ArrayList<Connection>();
|
||||
|
||||
/**
|
||||
* 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, new ThreadFactory() {
|
||||
|
||||
@Override
|
||||
public Thread newThread(Runnable runnable) {
|
||||
return new Thread(runnable);
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* @throws IOException
|
||||
*/
|
||||
public synchronized void start() throws IOException {
|
||||
Assert.state(!isStarted(), "Server already started");
|
||||
logger.debug("Starting live reload server on port " + this.port);
|
||||
this.serverSocket = new ServerSocket(this.port);
|
||||
this.listenThread = this.threadFactory.newThread(new Runnable() {
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
acceptConnections();
|
||||
}
|
||||
|
||||
});
|
||||
this.listenThread.setDaemon(true);
|
||||
this.listenThread.setName("Live Reload Server");
|
||||
this.listenThread.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return if the server has been started.
|
||||
* @return {@code true} if the server is running
|
||||
*/
|
||||
public synchronized boolean isStarted() {
|
||||
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
|
||||
*/
|
||||
public synchronized void stop() throws IOException {
|
||||
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.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
|
||||
*/
|
||||
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;
|
||||
|
||||
public 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();
|
||||
try {
|
||||
Connection connection = createConnection(this.socket,
|
||||
this.inputStream, outputStream);
|
||||
runConnection(connection);
|
||||
}
|
||||
finally {
|
||||
outputStream.close();
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* Copyright 2012-2015 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Support for the livereload protocol.
|
||||
*/
|
||||
package org.springframework.boot.developertools.livereload;
|
||||
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* Copyright 2012-2015 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Spring Boot developer tools.
|
||||
*/
|
||||
package org.springframework.boot.developertools;
|
||||
|
||||
|
|
@ -0,0 +1,145 @@
|
|||
/*
|
||||
* Copyright 2012-2015 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.developertools.remote.client;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.ObjectOutputStream;
|
||||
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.developertools.classpath.ClassPathChangedEvent;
|
||||
import org.springframework.boot.developertools.filewatch.ChangedFile;
|
||||
import org.springframework.boot.developertools.filewatch.ChangedFiles;
|
||||
import org.springframework.boot.developertools.restart.classloader.ClassLoaderFile;
|
||||
import org.springframework.boot.developertools.restart.classloader.ClassLoaderFile.Kind;
|
||||
import org.springframework.boot.developertools.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
|
||||
* @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<ChangedFile.Type, ClassLoaderFile.Kind>();
|
||||
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 ex) {
|
||||
throw new IllegalArgumentException("Malformed URL '" + url + "'");
|
||||
}
|
||||
catch (MalformedURLException ex) {
|
||||
throw new IllegalArgumentException("Malformed URL '" + url + "'");
|
||||
}
|
||||
this.requestFactory = requestFactory;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onApplicationEvent(ClassPathChangedEvent event) {
|
||||
try {
|
||||
ClassLoaderFiles classLoaderFiles = getClassLoaderFiles(event);
|
||||
ClientHttpRequest request = this.requestFactory.createRequest(this.uri,
|
||||
HttpMethod.POST);
|
||||
byte[] bytes = serialize(classLoaderFiles);
|
||||
HttpHeaders headers = request.getHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
|
||||
headers.setContentLength(bytes.length);
|
||||
FileCopyUtils.copy(bytes, request.getBody());
|
||||
logUpload(classLoaderFiles);
|
||||
ClientHttpResponse response = request.execute();
|
||||
Assert.state(response.getStatusCode() == HttpStatus.OK, "Unexpected "
|
||||
+ response.getStatusCode() + " response uploading class files");
|
||||
}
|
||||
catch (IOException ex) {
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,116 @@
|
|||
/*
|
||||
* Copyright 2012-2015 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.developertools.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.developertools.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;
|
||||
|
||||
public 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) {
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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.developertools.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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
/*
|
||||
* Copyright 2012-2015 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.developertools.remote.client;
|
||||
|
||||
import javax.net.ServerSocketFactory;
|
||||
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionOutcome;
|
||||
import org.springframework.boot.autoconfigure.condition.SpringBootCondition;
|
||||
import org.springframework.boot.bind.RelaxedPropertyResolver;
|
||||
import org.springframework.boot.developertools.autoconfigure.RemoteDeveloperToolsProperties;
|
||||
import org.springframework.context.annotation.ConditionContext;
|
||||
import org.springframework.core.type.AnnotatedTypeMetadata;
|
||||
|
||||
/**
|
||||
* Condition used to check that the actual local port is available.
|
||||
*/
|
||||
class LocalDebugPortAvailableCondition extends SpringBootCondition {
|
||||
|
||||
@Override
|
||||
public ConditionOutcome getMatchOutcome(ConditionContext context,
|
||||
AnnotatedTypeMetadata metadata) {
|
||||
RelaxedPropertyResolver resolver = new RelaxedPropertyResolver(
|
||||
context.getEnvironment(), "spring.developertools.remote.debug.");
|
||||
Integer port = resolver.getProperty("local-port", Integer.class);
|
||||
if (port == null) {
|
||||
port = RemoteDeveloperToolsProperties.Debug.DEFAULT_LOCAL_PORT;
|
||||
}
|
||||
if (isPortAvailable(port)) {
|
||||
return ConditionOutcome.match("Local debug port availble");
|
||||
}
|
||||
return ConditionOutcome.noMatch("Local debug port unavailble");
|
||||
}
|
||||
|
||||
private boolean isPortAvailable(int port) {
|
||||
try {
|
||||
ServerSocketFactory.getDefault().createServerSocket(port).close();
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* Copyright 2012-2015 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.developertools.remote.client;
|
||||
|
||||
import java.nio.channels.SocketChannel;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.springframework.boot.developertools.tunnel.client.TunnelClientListener;
|
||||
|
||||
/**
|
||||
* {@link TunnelClientListener} to log open/close events.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class LoggingTunnelClientListener implements TunnelClientListener {
|
||||
|
||||
private static final Log logger = LogFactory
|
||||
.getLog(LoggingTunnelClientListener.class);
|
||||
|
||||
@Override
|
||||
public void onOpen(SocketChannel socket) {
|
||||
logger.info("Remote debug connection opened");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClose(SocketChannel socket) {
|
||||
logger.info("Remote debug connection closed");
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,228 @@
|
|||
/*
|
||||
* Copyright 2012-2015 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.developertools.remote.client;
|
||||
|
||||
import java.net.URL;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
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.beans.factory.annotation.Value;
|
||||
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.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.boot.developertools.autoconfigure.DeveloperToolsProperties;
|
||||
import org.springframework.boot.developertools.autoconfigure.OptionalLiveReloadServer;
|
||||
import org.springframework.boot.developertools.autoconfigure.RemoteDeveloperToolsProperties;
|
||||
import org.springframework.boot.developertools.classpath.ClassPathChangedEvent;
|
||||
import org.springframework.boot.developertools.classpath.ClassPathFileSystemWatcher;
|
||||
import org.springframework.boot.developertools.classpath.ClassPathRestartStrategy;
|
||||
import org.springframework.boot.developertools.classpath.PatternClassPathRestartStrategy;
|
||||
import org.springframework.boot.developertools.livereload.LiveReloadServer;
|
||||
import org.springframework.boot.developertools.restart.DefaultRestartInitializer;
|
||||
import org.springframework.boot.developertools.restart.RestartScope;
|
||||
import org.springframework.boot.developertools.restart.Restarter;
|
||||
import org.springframework.boot.developertools.tunnel.client.HttpTunnelConnection;
|
||||
import org.springframework.boot.developertools.tunnel.client.TunnelClient;
|
||||
import org.springframework.boot.developertools.tunnel.client.TunnelConnection;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Conditional;
|
||||
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;
|
||||
|
||||
/**
|
||||
* Configuration used to connect to remote Spring Boot applications.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 1.3.0
|
||||
* @see org.springframework.boot.developertools.RemoteSpringApplication
|
||||
*/
|
||||
@Configuration
|
||||
@EnableConfigurationProperties(DeveloperToolsProperties.class)
|
||||
public class RemoteClientConfiguration {
|
||||
|
||||
private static final Log logger = LogFactory.getLog(RemoteClientConfiguration.class);
|
||||
|
||||
@Autowired
|
||||
private DeveloperToolsProperties properties;
|
||||
|
||||
@Value("${remoteUrl}")
|
||||
private String remoteUrl;
|
||||
|
||||
@Bean
|
||||
public static PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() {
|
||||
return new PropertySourcesPlaceholderConfigurer();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ClientHttpRequestFactory clientHttpRequestFactory() {
|
||||
List<ClientHttpRequestInterceptor> interceptors = Arrays
|
||||
.asList(getSecurityInterceptor());
|
||||
return new InterceptingClientHttpRequestFactory(
|
||||
new SimpleClientHttpRequestFactory(), interceptors);
|
||||
}
|
||||
|
||||
private ClientHttpRequestInterceptor getSecurityInterceptor() {
|
||||
RemoteDeveloperToolsProperties remoteProperties = this.properties.getRemote();
|
||||
String secretHeaderName = remoteProperties.getSecretHeaderName();
|
||||
String secret = remoteProperties.getSecret();
|
||||
Assert.state(secret != null,
|
||||
"The environment value 'spring.developertools.remote.secret' "
|
||||
+ "is required to secure your connection.");
|
||||
return new HttpHeaderInterceptor(secretHeaderName, secret);
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
private void logWarnings() {
|
||||
RemoteDeveloperToolsProperties remoteProperties = this.properties.getRemote();
|
||||
if (!remoteProperties.getDebug().isEnabled()
|
||||
&& !remoteProperties.getRestart().isEnabled()) {
|
||||
logger.warn("Remote restart and debug are both 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.developertools.livereload", name = "enabled", matchIfMissing = true)
|
||||
static class LiveReloadConfiguration {
|
||||
|
||||
@Autowired
|
||||
private DeveloperToolsProperties 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.developertools.remote.restart", name = "enabled", matchIfMissing = true)
|
||||
static class RemoteRestartClientConfiguration {
|
||||
|
||||
@Autowired
|
||||
private DeveloperToolsProperties 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(classPathRestartStrategy(), urls);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ClassPathRestartStrategy classPathRestartStrategy() {
|
||||
return new PatternClassPathRestartStrategy(this.properties.getRestart()
|
||||
.getExclude());
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ClassPathChangeUploader classPathChangeUploader(
|
||||
ClientHttpRequestFactory requestFactory) {
|
||||
String url = this.remoteUrl + this.properties.getRemote().getContextPath()
|
||||
+ "/restart";
|
||||
return new ClassPathChangeUploader(url, requestFactory);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Client configuration for remote debug HTTP tunneling.
|
||||
*/
|
||||
@ConditionalOnProperty(prefix = "spring.developertools.remote.debug", name = "enabled", matchIfMissing = true)
|
||||
@ConditionalOnClass(Filter.class)
|
||||
@Conditional(LocalDebugPortAvailableCondition.class)
|
||||
static class RemoteDebugTunnelClientConfiguration {
|
||||
|
||||
@Autowired
|
||||
private DeveloperToolsProperties properties;
|
||||
|
||||
@Value("${remoteUrl}")
|
||||
private String remoteUrl;
|
||||
|
||||
@Bean
|
||||
public TunnelClient remoteDebugTunnelClient(
|
||||
ClientHttpRequestFactory requestFactory) {
|
||||
RemoteDeveloperToolsProperties remoteProperties = this.properties.getRemote();
|
||||
String url = this.remoteUrl + remoteProperties.getContextPath() + "/debug";
|
||||
TunnelConnection connection = new HttpTunnelConnection(url, requestFactory);
|
||||
int localPort = remoteProperties.getDebug().getLocalPort();
|
||||
TunnelClient client = new TunnelClient(localPort, connection);
|
||||
client.addListener(new LoggingTunnelClientListener());
|
||||
return client;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* Copyright 2012-2015 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Client support for a remotely running Spring Boot application.
|
||||
*/
|
||||
package org.springframework.boot.developertools.remote.client;
|
||||
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
* Copyright 2012-2015 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.developertools.remote.server;
|
||||
|
||||
import org.springframework.http.server.ServerHttpRequest;
|
||||
|
||||
/**
|
||||
* Provides access control for a {@link Dispatcher}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 1.3.0
|
||||
*/
|
||||
public interface AccessManager {
|
||||
|
||||
/**
|
||||
* {@link AccessManager} that permits all requests.
|
||||
*/
|
||||
public static final AccessManager PERMIT_ALL = new AccessManager() {
|
||||
|
||||
@Override
|
||||
public boolean isAllowed(ServerHttpRequest request) {
|
||||
return 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);
|
||||
|
||||
}
|
||||
|
|
@ -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.developertools.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<HandlerMapper>(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
|
||||
*/
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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.developertools.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() {
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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.developertools.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
|
||||
*/
|
||||
public interface Handler {
|
||||
|
||||
/**
|
||||
* Handle the request.
|
||||
* @param request the request
|
||||
* @param response the response
|
||||
* @throws IOException
|
||||
*/
|
||||
void handle(ServerHttpRequest request, ServerHttpResponse response)
|
||||
throws IOException;
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* Copyright 2012-2015 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.developertools.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
|
||||
*/
|
||||
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);
|
||||
|
||||
}
|
||||
|
|
@ -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.developertools.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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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.developertools.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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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.developertools.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 hander;
|
||||
|
||||
/**
|
||||
* 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.hander = handler;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Handler getHandler(ServerHttpRequest request) {
|
||||
if (this.requestUri.equals(request.getURI().getPath())) {
|
||||
return this.hander;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* Copyright 2012-2015 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Server support for a remotely running Spring Boot application.
|
||||
*/
|
||||
package org.springframework.boot.developertools.remote.server;
|
||||
|
||||
|
|
@ -0,0 +1,111 @@
|
|||
/*
|
||||
* Copyright 2012-2015 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.developertools.restart;
|
||||
|
||||
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.regex.Pattern;
|
||||
|
||||
/**
|
||||
* A filtered collections of URLs which can be change after the application has started.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class ChangeableUrls implements Iterable<URL> {
|
||||
|
||||
private static final String[] SKIPPED_PROJECTS = { "spring-boot",
|
||||
"spring-boot-developer-tools", "spring-boot-autoconfigure",
|
||||
"spring-boot-actuator", "spring-boot-starter" };
|
||||
|
||||
private static final Pattern STARTER_PATTERN = Pattern
|
||||
.compile("\\/spring-boot-starter-[\\w-]+\\/");
|
||||
|
||||
private final List<URL> urls;
|
||||
|
||||
private ChangeableUrls(URL... urls) {
|
||||
List<URL> reloadableUrls = new ArrayList<URL>(urls.length);
|
||||
for (URL url : urls) {
|
||||
if (isReloadable(url)) {
|
||||
reloadableUrls.add(url);
|
||||
}
|
||||
}
|
||||
this.urls = Collections.unmodifiableList(reloadableUrls);
|
||||
}
|
||||
|
||||
private boolean isReloadable(URL url) {
|
||||
String urlString = url.toString();
|
||||
return isFolderUrl(urlString) && !isSkipped(urlString);
|
||||
}
|
||||
|
||||
private boolean isFolderUrl(String urlString) {
|
||||
return urlString.startsWith("file:") && urlString.endsWith("/");
|
||||
}
|
||||
|
||||
private boolean isSkipped(String urlString) {
|
||||
// Skip certain spring-boot projects to allow them to be imported in the same IDE
|
||||
for (String skipped : SKIPPED_PROJECTS) {
|
||||
if (urlString.contains("/" + skipped + "/target/classes/")) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// Skip all starter projects
|
||||
if (STARTER_PATTERN.matcher(urlString).find()) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@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 fromUrlClassLoader(URLClassLoader classLoader) {
|
||||
return fromUrls(classLoader.getURLs());
|
||||
}
|
||||
|
||||
public static ChangeableUrls fromUrls(Collection<URL> urls) {
|
||||
return fromUrls(new ArrayList<URL>(urls).toArray(new URL[urls.size()]));
|
||||
}
|
||||
|
||||
public static ChangeableUrls fromUrls(URL... urls) {
|
||||
return new ChangeableUrls(urls);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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.developertools.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 {
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,94 @@
|
|||
/*
|
||||
* Copyright 2012-2015 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.developertools.restart;
|
||||
|
||||
import java.net.URL;
|
||||
import java.net.URLClassLoader;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @since 1.3.0
|
||||
*/
|
||||
public class DefaultRestartInitializer implements RestartInitializer {
|
||||
|
||||
private static final Set<String> SKIPPED_STACK_ELEMENTS;
|
||||
static {
|
||||
Set<String> skipped = new LinkedHashSet<String>();
|
||||
skipped.add("org.junit.runners.");
|
||||
skipped.add("org.springframework.boot.test.");
|
||||
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;
|
||||
}
|
||||
}
|
||||
return getUrls(thread);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.fromUrlClassLoader(
|
||||
(URLClassLoader) thread.getContextClassLoader()).toArray();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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.developertools.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;
|
||||
|
||||
public MainMethod() {
|
||||
this(Thread.currentThread());
|
||||
}
|
||||
|
||||
public 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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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.developertools.restart;
|
||||
|
||||
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 an initialized.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @see ConditionalOnInitializedRestarter
|
||||
*/
|
||||
class OnInitializedRestarterCondition extends SpringBootCondition {
|
||||
|
||||
@Override
|
||||
public ConditionOutcome getMatchOutcome(ConditionContext context,
|
||||
AnnotatedTypeMetadata metadata) {
|
||||
Restarter restarter = getRestarter();
|
||||
if (restarter == null) {
|
||||
return ConditionOutcome.noMatch("Restarter unavailable");
|
||||
}
|
||||
if (restarter.getInitialUrls() == null) {
|
||||
return ConditionOutcome.noMatch("Restarter initialized without URLs");
|
||||
}
|
||||
return ConditionOutcome.match("Restarter available and initialized");
|
||||
}
|
||||
|
||||
private Restarter getRestarter() {
|
||||
try {
|
||||
return Restarter.getInstance();
|
||||
}
|
||||
catch (Exception ex) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
/*
|
||||
* Copyright 2012-2015 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.developertools.restart;
|
||||
|
||||
import org.springframework.boot.context.event.ApplicationFailedEvent;
|
||||
import org.springframework.boot.context.event.ApplicationReadyEvent;
|
||||
import org.springframework.boot.context.event.ApplicationStartedEvent;
|
||||
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
|
||||
* @since 1.3.0
|
||||
* @see Restarter
|
||||
*/
|
||||
public class RestartApplicationListener implements ApplicationListener<ApplicationEvent>,
|
||||
Ordered {
|
||||
|
||||
private int order = HIGHEST_PRECEDENCE;
|
||||
|
||||
@Override
|
||||
public void onApplicationEvent(ApplicationEvent event) {
|
||||
if (event instanceof ApplicationStartedEvent) {
|
||||
Restarter.initialize(((ApplicationStartedEvent) event).getArgs());
|
||||
}
|
||||
if (event instanceof ApplicationReadyEvent
|
||||
|| event instanceof ApplicationFailedEvent) {
|
||||
Restarter.getInstance().finish();
|
||||
}
|
||||
}
|
||||
|
||||
@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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
* Copyright 2012-2015 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.developertools.restart;
|
||||
|
||||
import java.net.URL;
|
||||
|
||||
/**
|
||||
* Strategy interface used to initialize a {@link Restarter}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 1.3.0
|
||||
* @see DefaultRestartInitializer
|
||||
*/
|
||||
public interface RestartInitializer {
|
||||
|
||||
/**
|
||||
* {@link RestartInitializer} that doesn't return any URLs.
|
||||
*/
|
||||
public static final RestartInitializer NONE = new RestartInitializer() {
|
||||
|
||||
@Override
|
||||
public URL[] getInitialUrls(Thread thread) {
|
||||
return 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);
|
||||
|
||||
}
|
||||
|
|
@ -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.developertools.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;
|
||||
|
||||
public 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 (Exception ex) {
|
||||
throw new IllegalStateException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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.developertools.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 shoul remain beteen
|
||||
* restarts.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 1.3.0
|
||||
* @see RestartScopeInitializer
|
||||
*/
|
||||
@Target({ ElementType.TYPE, ElementType.METHOD })
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
@Scope("restart")
|
||||
public @interface RestartScope {
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
/*
|
||||
* Copyright 2012-2015 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.developertools.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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,568 @@
|
|||
/*
|
||||
* Copyright 2012-2015 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.developertools.restart;
|
||||
|
||||
import java.beans.Introspector;
|
||||
import java.lang.Thread.UncaughtExceptionHandler;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.net.URL;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.IdentityHashMap;
|
||||
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.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.developertools.restart.classloader.ClassLoaderFiles;
|
||||
import org.springframework.boot.developertools.restart.classloader.RestartClassLoader;
|
||||
import org.springframework.boot.logging.DeferredLog;
|
||||
import org.springframework.cglib.core.ClassNameReader;
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.core.annotation.AnnotationUtils;
|
||||
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
|
||||
* @since 1.3.0
|
||||
* @see RestartApplicationListener
|
||||
* @see #initialize(String[])
|
||||
* @see #getInstance()
|
||||
* @see #restart()
|
||||
*/
|
||||
public class Restarter {
|
||||
|
||||
private static Restarter instance;
|
||||
|
||||
private Log logger = new DeferredLog();
|
||||
|
||||
private final boolean forceReferenceCleanup;
|
||||
|
||||
private URL[] initialUrls;
|
||||
|
||||
private final String mainClassName;
|
||||
|
||||
private final ClassLoader applicationClassLoader;
|
||||
|
||||
private final String[] args;
|
||||
|
||||
private final UncaughtExceptionHandler exceptionHandler;
|
||||
|
||||
private final Set<URL> urls = new LinkedHashSet<URL>();
|
||||
|
||||
private final ClassLoaderFiles classLoaderFiles = new ClassLoaderFiles();
|
||||
|
||||
private final Map<String, Object> attributes = new HashMap<String, Object>();
|
||||
|
||||
private final BlockingDeque<LeakSafeThread> leakSafeThreads = new LinkedBlockingDeque<LeakSafeThread>();
|
||||
|
||||
private boolean finished = false;
|
||||
|
||||
private Lock stopLock = new ReentrantLock();
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @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(new Callable<Void>() {
|
||||
|
||||
@Override
|
||||
public Void call() throws Exception {
|
||||
start();
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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(ChangeableUrls.fromUrls(urls).toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* 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() {
|
||||
this.logger.debug("Restarting application");
|
||||
getLeakSafeThread().call(new Callable<Void>() {
|
||||
|
||||
@Override
|
||||
public Void call() throws Exception {
|
||||
Restarter.this.stop();
|
||||
Restarter.this.start();
|
||||
return null;
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the application.
|
||||
* @throws Exception
|
||||
*/
|
||||
protected void start() 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));
|
||||
}
|
||||
relaunch(classLoader);
|
||||
}
|
||||
|
||||
/**
|
||||
* Relaunch the application using the specified classloader.
|
||||
* @param classLoader the classloader to use
|
||||
* @throws Exception
|
||||
*/
|
||||
protected void relaunch(ClassLoader classLoader) throws Exception {
|
||||
RestartLauncher launcher = new RestartLauncher(classLoader, this.mainClassName,
|
||||
this.args, this.exceptionHandler);
|
||||
launcher.start();
|
||||
launcher.join();
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the application.
|
||||
* @throws Exception
|
||||
*/
|
||||
protected void stop() throws Exception {
|
||||
this.logger.debug("Stopping application");
|
||||
this.stopLock.lock();
|
||||
try {
|
||||
triggerShutdownHooks();
|
||||
cleanupCaches();
|
||||
if (this.forceReferenceCleanup) {
|
||||
forceReferenceCleanup();
|
||||
}
|
||||
}
|
||||
finally {
|
||||
this.stopLock.unlock();
|
||||
}
|
||||
System.gc();
|
||||
System.runFinalization();
|
||||
}
|
||||
|
||||
@SuppressWarnings("rawtypes")
|
||||
private void triggerShutdownHooks() throws Exception {
|
||||
Class<?> hooksClass = Class.forName("java.lang.ApplicationShutdownHooks");
|
||||
Method runHooks = hooksClass.getDeclaredMethod("runHooks");
|
||||
runHooks.setAccessible(true);
|
||||
runHooks.invoke(null);
|
||||
Field field = hooksClass.getDeclaredField("hooks");
|
||||
field.setAccessible(true);
|
||||
field.set(null, new IdentityHashMap());
|
||||
}
|
||||
|
||||
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<long[]>();
|
||||
while (true) {
|
||||
memory.add(new long[102400]);
|
||||
}
|
||||
}
|
||||
catch (final OutOfMemoryError ex) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called to finish {@link Restarter} initialization when application logging is
|
||||
* available.
|
||||
*/
|
||||
synchronized void finish() {
|
||||
if (!isFinished()) {
|
||||
this.logger = DeferredLog.replay(this.logger, LogFactory.getLog(getClass()));
|
||||
this.finished = true;
|
||||
}
|
||||
}
|
||||
|
||||
boolean isFinished() {
|
||||
return this.finished;
|
||||
}
|
||||
|
||||
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 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) {
|
||||
if (instance == null) {
|
||||
synchronized (Restarter.class) {
|
||||
instance = new Restarter(Thread.currentThread(), args,
|
||||
forceReferenceCleanup, initializer);
|
||||
}
|
||||
instance.initialize(restartOnInitialize);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the active {@link Restarter} instance. Cannot be called before
|
||||
* {@link #initialize(String[]) initialization}.
|
||||
* @return the restarter
|
||||
*/
|
||||
public synchronized static Restarter getInstance() {
|
||||
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) {
|
||||
Restarter.instance = instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the instance. Primarily provided for tests and not usually used in
|
||||
* application code.
|
||||
*/
|
||||
public static void clearInstance() {
|
||||
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;
|
||||
|
||||
public 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 thead.
|
||||
*/
|
||||
private class LeakSafeThreadFactory implements ThreadFactory {
|
||||
|
||||
@Override
|
||||
public Thread newThread(final Runnable runnable) {
|
||||
return getLeakSafeThread().callAndWait(new Callable<Thread>() {
|
||||
|
||||
@Override
|
||||
public Thread call() throws Exception {
|
||||
Thread thread = new Thread(runnable);
|
||||
thread.setContextClassLoader(Restarter.this.applicationClassLoader);
|
||||
return thread;
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
* Copyright 2012-2014 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.developertools.restart;
|
||||
|
||||
import java.lang.Thread.UncaughtExceptionHandler;
|
||||
|
||||
/**
|
||||
* {@link UncaughtExceptionHandler} decorator that allows a thread to exit silently.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class SilentExitExceptionHandler implements UncaughtExceptionHandler {
|
||||
|
||||
private final UncaughtExceptionHandler delegate;
|
||||
|
||||
public SilentExitExceptionHandler(UncaughtExceptionHandler delegate) {
|
||||
this.delegate = delegate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void uncaughtException(Thread thread, Throwable exception) {
|
||||
if (exception instanceof SilentExitException) {
|
||||
return;
|
||||
}
|
||||
if (this.delegate != null) {
|
||||
this.delegate.uncaughtException(thread, exception);
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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.developertools.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
|
||||
* @see ClassLoaderFileRepository
|
||||
* @since 1.3.0
|
||||
*/
|
||||
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 static 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
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
* Copyright 2012-2015 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.developertools.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
|
||||
*/
|
||||
public interface ClassLoaderFileRepository {
|
||||
|
||||
/**
|
||||
* Empty {@link ClassLoaderFileRepository} implementation.
|
||||
*/
|
||||
public static final ClassLoaderFileRepository NONE = new ClassLoaderFileRepository() {
|
||||
|
||||
@Override
|
||||
public ClassLoaderFile getFile(String name) {
|
||||
return 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);
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
* Copyright 2012-2015 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.developertools.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
|
||||
*/
|
||||
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 {
|
||||
|
||||
public 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();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,202 @@
|
|||
/*
|
||||
* Copyright 2012-2015 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.developertools.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<String, SourceFolder>();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<String, SourceFolder>(
|
||||
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<String, ClassLoaderFile>();
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,231 @@
|
|||
/*
|
||||
* Copyright 2012-2015 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.developertools.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.developertools.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<URL>(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(new PrivilegedAction<URL>() {
|
||||
@Override
|
||||
public URL run() {
|
||||
return 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(new PrivilegedAction<Class<?>>() {
|
||||
@Override
|
||||
public Class<?> run() {
|
||||
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;
|
||||
|
||||
public 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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* Copyright 2012-2015 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Classloaders used for reload support
|
||||
*/
|
||||
package org.springframework.boot.developertools.restart.classloader;
|
||||
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* Copyright 2012-2015 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Application restart support
|
||||
*/
|
||||
package org.springframework.boot.developertools.restart;
|
||||
|
||||
|
|
@ -0,0 +1,94 @@
|
|||
/*
|
||||
* Copyright 2012-2015 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.developertools.restart.server;
|
||||
|
||||
import java.net.URL;
|
||||
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+)*(?:[.-].+)?$");
|
||||
|
||||
@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)) {
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
/*
|
||||
* Copyright 2012-2015 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.developertools.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.developertools.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
|
||||
*/
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
* Copyright 2012-2015 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.developertools.restart.server;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.springframework.boot.developertools.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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,182 @@
|
|||
/*
|
||||
* Copyright 2012-2015 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.developertools.restart.server;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.net.URLClassLoader;
|
||||
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.developertools.restart.Restarter;
|
||||
import org.springframework.boot.developertools.restart.classloader.ClassLoaderFile;
|
||||
import org.springframework.boot.developertools.restart.classloader.ClassLoaderFile.Kind;
|
||||
import org.springframework.boot.developertools.restart.classloader.ClassLoaderFiles;
|
||||
import org.springframework.boot.developertools.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<URL>();
|
||||
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<URL>();
|
||||
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<URL>();
|
||||
ClassLoader classLoader = this.classLoader;
|
||||
while (classLoader != null) {
|
||||
if (classLoader instanceof URLClassLoader) {
|
||||
for (URL url : ((URLClassLoader) classLoader).getURLs()) {
|
||||
urls.add(url);
|
||||
}
|
||||
}
|
||||
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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* Copyright 2012-2015 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.developertools.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
|
||||
*/
|
||||
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);
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* Copyright 2012-2015 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Remote restart server
|
||||
*/
|
||||
package org.springframework.boot.developertools.restart.server;
|
||||
|
||||
|
|
@ -0,0 +1,216 @@
|
|||
/*
|
||||
* Copyright 2012-2015 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.developertools.tunnel.client;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
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.developertools.tunnel.payload.HttpTunnelPayload;
|
||||
import org.springframework.boot.developertools.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
|
||||
* @since 1.3.0
|
||||
* @see TunnelClient
|
||||
* @see org.springframework.boot.developertools.tunnel.server.HttpTunnelServer
|
||||
*/
|
||||
public class HttpTunnelConnection implements TunnelConnection {
|
||||
|
||||
private static 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 ex) {
|
||||
throw new IllegalArgumentException("Malformed URL '" + url + "'");
|
||||
}
|
||||
catch (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 synchronized void openNewConnection(final HttpTunnelPayload payload) {
|
||||
HttpTunnelConnection.this.executor.execute(new Runnable() {
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
sendAndReceive(payload);
|
||||
}
|
||||
catch (IOException ex) {
|
||||
logger.trace("Unexpected connection error", ex);
|
||||
closeQuitely();
|
||||
}
|
||||
}
|
||||
|
||||
private void closeQuitely() {
|
||||
try {
|
||||
close();
|
||||
}
|
||||
catch (IOException ex) {
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,207 @@
|
|||
/*
|
||||
* Copyright 2012-2015 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.developertools.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.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
|
||||
* @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 int listenPort;
|
||||
|
||||
private final TunnelConnection tunnelConnection;
|
||||
|
||||
private TunnelClientListeners listeners = new TunnelClientListeners();
|
||||
|
||||
private ServerThread serverThread;
|
||||
|
||||
public TunnelClient(int listenPort, TunnelConnection tunnelConnection) {
|
||||
Assert.isTrue(listenPort > 0, "ListenPort must be positive");
|
||||
Assert.notNull(tunnelConnection, "TunnelConnection must not be null");
|
||||
this.listenPort = listenPort;
|
||||
this.tunnelConnection = tunnelConnection;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterSingletonsInstantiated() {
|
||||
if (this.serverThread == null) {
|
||||
try {
|
||||
start();
|
||||
}
|
||||
catch (IOException ex) {
|
||||
throw new IllegalStateException(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the client and accept incoming connections on the port.
|
||||
* @throws IOException
|
||||
*/
|
||||
public synchronized void start() throws IOException {
|
||||
Assert.state(this.serverThread == null, "Server already started");
|
||||
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
|
||||
serverSocketChannel.socket().bind(new InetSocketAddress(this.listenPort));
|
||||
logger.trace("Listening for TCP traffic to tunnel on port " + this.listenPort);
|
||||
this.serverThread = new ServerThread(serverSocketChannel);
|
||||
this.serverThread.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the client, disconnecting any servers.
|
||||
* @throws IOException
|
||||
*/
|
||||
public synchronized void stop() throws IOException {
|
||||
if (this.serverThread != null) {
|
||||
logger.trace("Closing tunnel client on port " + this.listenPort);
|
||||
this.serverThread.close();
|
||||
try {
|
||||
this.serverThread.join(2000);
|
||||
}
|
||||
catch (InterruptedException ex) {
|
||||
}
|
||||
this.serverThread = null;
|
||||
}
|
||||
}
|
||||
|
||||
protected final ServerThread getServerThread() {
|
||||
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 {
|
||||
this.serverSocketChannel.close();
|
||||
this.acceptConnections = false;
|
||||
interrupt();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
while (this.acceptConnections) {
|
||||
SocketChannel socket = this.serverSocketChannel.accept();
|
||||
try {
|
||||
handleConnection(socket);
|
||||
}
|
||||
finally {
|
||||
socket.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex) {
|
||||
logger.trace("Unexpected exception from tunnel client", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleConnection(SocketChannel socketChannel) throws Exception {
|
||||
Closeable closeable = new SocketCloseable(socketChannel);
|
||||
WritableByteChannel outputChannel = TunnelClient.this.tunnelConnection.open(
|
||||
socketChannel, closeable);
|
||||
TunnelClient.this.listeners.fireOpenEvent(socketChannel);
|
||||
try {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
finally {
|
||||
outputChannel.close();
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
public 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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.developertools.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);
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
* Copyright 2012-2015 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.developertools.tunnel.client;
|
||||
|
||||
import java.nio.channels.SocketChannel;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* A collection of {@link TunnelClientListener}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class TunnelClientListeners {
|
||||
|
||||
private final List<TunnelClientListener> listeners = new ArrayList<TunnelClientListener>();
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* Copyright 2012-2015 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.developertools.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
|
||||
*/
|
||||
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
|
||||
* @return A {@link WritableByteChannel} that should be used to send any outgoing data
|
||||
* destined for the remote server
|
||||
* @throws Exception
|
||||
*/
|
||||
WritableByteChannel open(WritableByteChannel incomingChannel, Closeable closeable)
|
||||
throws Exception;
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* Copyright 2012-2015 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Client side TCP tunnel support.
|
||||
*/
|
||||
package org.springframework.boot.developertools.tunnel.client;
|
||||
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* 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.developertools.tunnel;
|
||||
|
||||
|
|
@ -0,0 +1,185 @@
|
|||
/*
|
||||
* Copyright 2012-2015 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.developertools.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
|
||||
*/
|
||||
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
|
||||
*/
|
||||
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
|
||||
*/
|
||||
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
|
||||
*/
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
/*
|
||||
* Copyright 2012-2015 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.developertools.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 WritableByteChannel targetChannel;
|
||||
|
||||
private long lastRequestSeq = 0;
|
||||
|
||||
private final Map<Long, HttpTunnelPayload> queue = new HashMap<Long, HttpTunnelPayload>();
|
||||
|
||||
/**
|
||||
* 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 synchronized void forward(HttpTunnelPayload payload) throws IOException {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* Copyright 2012-2015 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Classes to deal with payloads sent over a HTTP tunnel.
|
||||
*/
|
||||
package org.springframework.boot.developertools.tunnel.payload;
|
||||
|
||||
|
|
@ -0,0 +1,486 @@
|
|||
/*
|
||||
* Copyright 2012-2015 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.developertools.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.developertools.tunnel.payload.HttpTunnelPayload;
|
||||
import org.springframework.boot.developertools.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 |
|
||||
* |------------------------------}|
|
||||
* | (b) Data I |
|
||||
* --}|------------------------------}|---}
|
||||
* | Response I (a) |
|
||||
* {--|<------------------------------|{---
|
||||
* | |
|
||||
* | (c) Data II |
|
||||
* --}|------------------------------}|---}
|
||||
* | Response II (b) |
|
||||
* {--|{------------------------------|{---
|
||||
* . .
|
||||
* . .
|
||||
* </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:
|
||||
* <p>
|
||||
* <table>
|
||||
* <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>
|
||||
* </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
|
||||
* @since 1.3.0
|
||||
* @see org.springframework.boot.developertools.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
|
||||
*/
|
||||
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
|
||||
*/
|
||||
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
|
||||
*/
|
||||
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<HttpConnection>(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) {
|
||||
closeHttpConnections();
|
||||
}
|
||||
httpConnection = this.httpConnections.pollFirst();
|
||||
}
|
||||
return httpConnection;
|
||||
}
|
||||
}
|
||||
|
||||
private void closeStaleHttpConnections() throws IOException {
|
||||
checkNotDisconnected();
|
||||
synchronized (this.httpConnections) {
|
||||
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() {
|
||||
long timeout = HttpTunnelServer.this.disconnectTimeout;
|
||||
long duration = System.currentTimeMillis() - this.lastHttpRequestTime;
|
||||
Assert.state(duration < timeout, "Disconnect timeout");
|
||||
}
|
||||
|
||||
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
|
||||
*/
|
||||
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 unavailble 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) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
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
|
||||
*/
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
* Copyright 2012-2015 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.developertools.tunnel.server;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.springframework.boot.developertools.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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* Copyright 2012-2015 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.developertools.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
|
||||
*/
|
||||
public interface PortProvider {
|
||||
|
||||
/**
|
||||
* Return the port number
|
||||
* @return the port number
|
||||
*/
|
||||
int getPort();
|
||||
|
||||
}
|
||||
|
|
@ -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.developertools.tunnel.server;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.springframework.boot.lang.UsesUnsafeJava;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* {@link PortProvider} that provides the port being used by the Java remote debugging.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
public class RemoteDebugPortProvider implements PortProvider {
|
||||
|
||||
private static final String JDWP_ADDRESS_PROPERTY = "sun.jdwp.listenerAddress";
|
||||
|
||||
private static final Log logger = LogFactory.getLog(RemoteDebugPortProvider.class);
|
||||
|
||||
@Override
|
||||
public int getPort() {
|
||||
Assert.state(isRemoteDebugRunning(), "Remote debug is not running");
|
||||
return getRemoteDebugPort();
|
||||
}
|
||||
|
||||
public static boolean isRemoteDebugRunning() {
|
||||
return getRemoteDebugPort() != -1;
|
||||
}
|
||||
|
||||
@UsesUnsafeJava
|
||||
@SuppressWarnings("restriction")
|
||||
private static int getRemoteDebugPort() {
|
||||
String property = sun.misc.VMSupport.getAgentProperties().getProperty(
|
||||
JDWP_ADDRESS_PROPERTY);
|
||||
try {
|
||||
if (property != null && property.contains(":")) {
|
||||
return Integer.valueOf(property.split(":")[1]);
|
||||
}
|
||||
}
|
||||
catch (Exception ex) {
|
||||
logger.trace("Unable to get JDWP port from property value '" + property + "'");
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,101 @@
|
|||
/*
|
||||
* Copyright 2012-2015 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.developertools.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;
|
||||
|
||||
public 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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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.developertools.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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* Copyright 2012-2015 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.developertools.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
|
||||
*/
|
||||
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
|
||||
*/
|
||||
ByteChannel open(int timeout) throws IOException;
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* Copyright 2012-2015 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Server side TCP tunnel support.
|
||||
*/
|
||||
package org.springframework.boot.developertools.tunnel.server;
|
||||
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
# Application Initializers
|
||||
org.springframework.context.ApplicationContextInitializer=\
|
||||
org.springframework.boot.developertools.restart.RestartScopeInitializer
|
||||
|
||||
# Application Listeners
|
||||
org.springframework.context.ApplicationListener=\
|
||||
org.springframework.boot.developertools.restart.RestartApplicationListener
|
||||
|
||||
# Auto Configure
|
||||
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
|
||||
org.springframework.boot.developertools.autoconfigure.LocalDeveloperToolsAutoConfiguration,\
|
||||
org.springframework.boot.developertools.autoconfigure.RemoteDeveloperToolsAutoConfiguration
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,7 @@
|
|||
. ____ _ __ _ _
|
||||
/\\ / ___'_ __ _ _(_)_ __ __ _ ___ _ \ \ \ \
|
||||
( ( )\___ | '_ | '_| | '_ \/ _` | | _ \___ _ __ ___| |_ ___ \ \ \ \
|
||||
\\/ ___)| |_)| | | | | || (_| []::::::[] / -_) ' \/ _ \ _/ -_) ) ) ) )
|
||||
' |____| .__|_| |_|_| |_\__, | |_|_\___|_|_|_\___/\__\___|/ / / /
|
||||
=========|_|==============|___/===================================/_/_/_/
|
||||
:: Spring Boot Remote :: ${spring-boot.formatted-version}
|
||||
|
|
@ -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.developertools;
|
||||
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.ExpectedException;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.developertools.RemoteUrlPropertyExtractor;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
import static org.junit.Assert.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link RemoteUrlPropertyExtractor}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
public class RemoteUrlPropertyExtractorTests {
|
||||
|
||||
@Rule
|
||||
public ExpectedException thrown = ExpectedException.none();
|
||||
|
||||
@Test
|
||||
public void missingUrl() throws Exception {
|
||||
this.thrown.expect(IllegalStateException.class);
|
||||
this.thrown.expectMessage("No remote URL specified");
|
||||
doTest();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void malformedUrl() throws Exception {
|
||||
this.thrown.expect(IllegalStateException.class);
|
||||
this.thrown.expectMessage("Malformed URL '::://wibble'");
|
||||
doTest("::://wibble");
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void multipleUrls() throws Exception {
|
||||
this.thrown.expect(IllegalStateException.class);
|
||||
this.thrown.expectMessage("Multiple URLs specified");
|
||||
doTest("http://localhost:8080", "http://localhost:9090");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void validUrl() throws Exception {
|
||||
ApplicationContext context = doTest("http://localhost:8080");
|
||||
assertThat(context.getEnvironment().getProperty("remoteUrl"),
|
||||
equalTo("http://localhost:8080"));
|
||||
}
|
||||
|
||||
private ApplicationContext doTest(String... args) {
|
||||
SpringApplication application = new SpringApplication(Config.class);
|
||||
application.setWebEnvironment(false);
|
||||
application.addListeners(new RemoteUrlPropertyExtractor());
|
||||
return application.run(args);
|
||||
}
|
||||
|
||||
@Configuration
|
||||
static class Config {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,209 @@
|
|||
/*
|
||||
* Copyright 2012-2015 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.developertools.autoconfigure;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.After;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.ExpectedException;
|
||||
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.thymeleaf.ThymeleafAutoConfiguration;
|
||||
import org.springframework.boot.developertools.classpath.ClassPathChangedEvent;
|
||||
import org.springframework.boot.developertools.classpath.ClassPathFileSystemWatcher;
|
||||
import org.springframework.boot.developertools.filewatch.ChangedFiles;
|
||||
import org.springframework.boot.developertools.livereload.LiveReloadServer;
|
||||
import org.springframework.boot.developertools.restart.MockRestartInitializer;
|
||||
import org.springframework.boot.developertools.restart.MockRestarter;
|
||||
import org.springframework.boot.developertools.restart.Restarter;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.context.event.ContextRefreshedEvent;
|
||||
import org.springframework.util.SocketUtils;
|
||||
import org.thymeleaf.templateresolver.TemplateResolver;
|
||||
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
import static org.hamcrest.Matchers.notNullValue;
|
||||
import static org.junit.Assert.assertThat;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.reset;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
/**
|
||||
* Tests for {@link LocalDeveloperToolsAutoConfiguration}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
public class LocalDeveloperToolsAutoConfigurationTests {
|
||||
|
||||
@Rule
|
||||
public ExpectedException thrown = ExpectedException.none();
|
||||
|
||||
@Rule
|
||||
public MockRestarter mockRestarter = new MockRestarter();
|
||||
|
||||
private int liveReloadPort = SocketUtils.findAvailableTcpPort();
|
||||
|
||||
private ConfigurableApplicationContext context;
|
||||
|
||||
@After
|
||||
public void cleanup() {
|
||||
if (this.context != null) {
|
||||
this.context.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void thymeleafCacheIsFalse() throws Exception {
|
||||
this.context = initializeAndRun(Config.class);
|
||||
TemplateResolver resolver = this.context.getBean(TemplateResolver.class);
|
||||
resolver.initialize();
|
||||
assertThat(resolver.isCacheable(), equalTo(false));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void liveReloadServer() throws Exception {
|
||||
this.context = initializeAndRun(Config.class);
|
||||
LiveReloadServer server = this.context.getBean(LiveReloadServer.class);
|
||||
assertThat(server.isStarted(), equalTo(true));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void liveReloadTriggeredOnContextRefresh() throws Exception {
|
||||
this.context = initializeAndRun(ConfigWithMockLiveReload.class);
|
||||
LiveReloadServer server = this.context.getBean(LiveReloadServer.class);
|
||||
reset(server);
|
||||
this.context.publishEvent(new ContextRefreshedEvent(this.context));
|
||||
verify(server).triggerReload();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void liveReloadTriggerdOnClassPathChangeWithoutRestart() throws Exception {
|
||||
this.context = initializeAndRun(ConfigWithMockLiveReload.class);
|
||||
LiveReloadServer server = this.context.getBean(LiveReloadServer.class);
|
||||
reset(server);
|
||||
ClassPathChangedEvent event = new ClassPathChangedEvent(this.context,
|
||||
Collections.<ChangedFiles> emptySet(), false);
|
||||
this.context.publishEvent(event);
|
||||
verify(server).triggerReload();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void liveReloadNotTriggerdOnClassPathChangeWithRestart() throws Exception {
|
||||
this.context = initializeAndRun(ConfigWithMockLiveReload.class);
|
||||
LiveReloadServer server = this.context.getBean(LiveReloadServer.class);
|
||||
reset(server);
|
||||
ClassPathChangedEvent event = new ClassPathChangedEvent(this.context,
|
||||
Collections.<ChangedFiles> emptySet(), true);
|
||||
this.context.publishEvent(event);
|
||||
verify(server, never()).triggerReload();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void liveReloadDisabled() throws Exception {
|
||||
Map<String, Object> properties = new HashMap<String, Object>();
|
||||
properties.put("spring.developertools.livereload.enabled", false);
|
||||
this.context = initializeAndRun(Config.class, properties);
|
||||
this.thrown.expect(NoSuchBeanDefinitionException.class);
|
||||
this.context.getBean(OptionalLiveReloadServer.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void restartTriggerdOnClassPathChangeWithRestart() throws Exception {
|
||||
this.context = initializeAndRun(Config.class);
|
||||
ClassPathChangedEvent event = new ClassPathChangedEvent(this.context,
|
||||
Collections.<ChangedFiles> emptySet(), true);
|
||||
this.context.publishEvent(event);
|
||||
verify(this.mockRestarter.getMock()).restart();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void restartNotTriggerdOnClassPathChangeWithRestart() throws Exception {
|
||||
this.context = initializeAndRun(Config.class);
|
||||
ClassPathChangedEvent event = new ClassPathChangedEvent(this.context,
|
||||
Collections.<ChangedFiles> emptySet(), false);
|
||||
this.context.publishEvent(event);
|
||||
verify(this.mockRestarter.getMock(), never()).restart();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void restartWatchingClassPath() throws Exception {
|
||||
this.context = initializeAndRun(Config.class);
|
||||
ClassPathFileSystemWatcher watcher = this.context
|
||||
.getBean(ClassPathFileSystemWatcher.class);
|
||||
assertThat(watcher, notNullValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void restartDisabled() throws Exception {
|
||||
Map<String, Object> properties = new HashMap<String, Object>();
|
||||
properties.put("spring.developertools.restart.enabled", false);
|
||||
this.context = initializeAndRun(Config.class, properties);
|
||||
this.thrown.expect(NoSuchBeanDefinitionException.class);
|
||||
this.context.getBean(ClassPathFileSystemWatcher.class);
|
||||
}
|
||||
|
||||
private ConfigurableApplicationContext initializeAndRun(Class<?> config) {
|
||||
return initializeAndRun(config, Collections.<String, Object> emptyMap());
|
||||
}
|
||||
|
||||
private ConfigurableApplicationContext initializeAndRun(Class<?> config,
|
||||
Map<String, Object> properties) {
|
||||
Restarter.initialize(new String[0], false, new MockRestartInitializer(), false);
|
||||
SpringApplication application = new SpringApplication(config);
|
||||
application.setDefaultProperties(getDefaultProperties(properties));
|
||||
application.setWebEnvironment(false);
|
||||
ConfigurableApplicationContext context = application.run();
|
||||
return context;
|
||||
}
|
||||
|
||||
private Map<String, Object> getDefaultProperties(
|
||||
Map<String, Object> specifiedProperties) {
|
||||
Map<String, Object> properties = new HashMap<String, Object>();
|
||||
properties.put("spring.thymeleaf.check-template-location", false);
|
||||
properties.put("spring.developertools.livereload.port", this.liveReloadPort);
|
||||
properties.putAll(specifiedProperties);
|
||||
return properties;
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@Import({ LocalDeveloperToolsAutoConfiguration.class,
|
||||
ThymeleafAutoConfiguration.class })
|
||||
public static class Config {
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@Import({ LocalDeveloperToolsAutoConfiguration.class,
|
||||
ThymeleafAutoConfiguration.class })
|
||||
public static class ConfigWithMockLiveReload {
|
||||
|
||||
@Bean
|
||||
public LiveReloadServer liveReloadServer() {
|
||||
return mock(LiveReloadServer.class);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
* Copyright 2012-2015 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.developertools.autoconfigure;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.springframework.boot.developertools.livereload.LiveReloadServer;
|
||||
|
||||
import static org.mockito.BDDMockito.willThrow;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
/**
|
||||
* Tests for {@link OptionalLiveReloadServer}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
public class OptionalLiveReloadServerTests {
|
||||
|
||||
@Test
|
||||
public void nullServer() throws Exception {
|
||||
OptionalLiveReloadServer server = new OptionalLiveReloadServer(null);
|
||||
server.startServer();
|
||||
server.triggerReload();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void serverWontStart() throws Exception {
|
||||
LiveReloadServer delegate = mock(LiveReloadServer.class);
|
||||
OptionalLiveReloadServer server = new OptionalLiveReloadServer(delegate);
|
||||
willThrow(new RuntimeException("Error")).given(delegate).start();
|
||||
server.startServer();
|
||||
server.triggerReload();
|
||||
verify(delegate, never()).triggerReload();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,262 @@
|
|||
/*
|
||||
* Copyright 2012-2015 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.developertools.autoconfigure;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.ExpectedException;
|
||||
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
|
||||
import org.springframework.boot.autoconfigure.PropertyPlaceholderAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.web.ServerPropertiesAutoConfiguration;
|
||||
import org.springframework.boot.developertools.remote.server.DispatcherFilter;
|
||||
import org.springframework.boot.developertools.restart.MockRestarter;
|
||||
import org.springframework.boot.developertools.restart.server.HttpRestartServer;
|
||||
import org.springframework.boot.developertools.restart.server.SourceFolderUrlFilter;
|
||||
import org.springframework.boot.developertools.tunnel.server.HttpTunnelServer;
|
||||
import org.springframework.boot.developertools.tunnel.server.RemoteDebugPortProvider;
|
||||
import org.springframework.boot.developertools.tunnel.server.SocketTargetServerConnection;
|
||||
import org.springframework.boot.developertools.tunnel.server.TargetServerConnection;
|
||||
import org.springframework.boot.test.EnvironmentTestUtils;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.http.server.ServerHttpRequest;
|
||||
import org.springframework.http.server.ServerHttpResponse;
|
||||
import org.springframework.mock.web.MockFilterChain;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.mock.web.MockHttpServletResponse;
|
||||
import org.springframework.mock.web.MockServletContext;
|
||||
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
|
||||
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
import static org.junit.Assert.assertThat;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* Tests for {@link RemoteDeveloperToolsAutoConfiguration}.
|
||||
*
|
||||
* @author Rob Winch
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
public class RemoteDeveloperToolsAutoConfigurationTests {
|
||||
|
||||
private static final String DEFAULT_CONTEXT_PATH = RemoteDeveloperToolsProperties.DEFAULT_CONTEXT_PATH;
|
||||
|
||||
private static final String DEFAULT_SECRET_HEADER_NAME = RemoteDeveloperToolsProperties.DEFAULT_SECRET_HEADER_NAME;
|
||||
|
||||
@Rule
|
||||
public MockRestarter mockRestarter = new MockRestarter();
|
||||
|
||||
@Rule
|
||||
public ExpectedException thrown = ExpectedException.none();
|
||||
|
||||
private AnnotationConfigWebApplicationContext context;
|
||||
|
||||
private MockHttpServletRequest request;
|
||||
|
||||
private MockHttpServletResponse response;
|
||||
|
||||
private MockFilterChain chain;
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
this.request = new MockHttpServletRequest();
|
||||
this.response = new MockHttpServletResponse();
|
||||
this.chain = new MockFilterChain();
|
||||
}
|
||||
|
||||
@After
|
||||
public void close() {
|
||||
if (this.context != null) {
|
||||
this.context.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void disabledIfRemoteSecretIsMissing() throws Exception {
|
||||
loadContext("a:b");
|
||||
this.thrown.expect(NoSuchBeanDefinitionException.class);
|
||||
this.context.getBean(DispatcherFilter.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void ignoresUnmappedUrl() throws Exception {
|
||||
loadContext("spring.developertools.remote.secret:supersecret");
|
||||
DispatcherFilter filter = this.context.getBean(DispatcherFilter.class);
|
||||
this.request.setRequestURI("/restart");
|
||||
this.request.addHeader(DEFAULT_SECRET_HEADER_NAME, "supersecret");
|
||||
filter.doFilter(this.request, this.response, this.chain);
|
||||
assertRestartInvoked(false);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void ignoresIfMissingSecretFromRequest() throws Exception {
|
||||
loadContext("spring.developertools.remote.secret:supersecret");
|
||||
DispatcherFilter filter = this.context.getBean(DispatcherFilter.class);
|
||||
this.request.setRequestURI(DEFAULT_CONTEXT_PATH + "/restart");
|
||||
filter.doFilter(this.request, this.response, this.chain);
|
||||
assertRestartInvoked(false);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void ignoresInvalidSecretInRequest() throws Exception {
|
||||
loadContext("spring.developertools.remote.secret:supersecret");
|
||||
DispatcherFilter filter = this.context.getBean(DispatcherFilter.class);
|
||||
this.request.setRequestURI(DEFAULT_CONTEXT_PATH + "/restart");
|
||||
this.request.addHeader(DEFAULT_SECRET_HEADER_NAME, "invalid");
|
||||
filter.doFilter(this.request, this.response, this.chain);
|
||||
assertRestartInvoked(false);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void invokeRestartWithDefaultSetup() throws Exception {
|
||||
loadContext("spring.developertools.remote.secret:supersecret");
|
||||
DispatcherFilter filter = this.context.getBean(DispatcherFilter.class);
|
||||
this.request.setRequestURI(DEFAULT_CONTEXT_PATH + "/restart");
|
||||
this.request.addHeader(DEFAULT_SECRET_HEADER_NAME, "supersecret");
|
||||
filter.doFilter(this.request, this.response, this.chain);
|
||||
assertRestartInvoked(true);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void disableRestart() throws Exception {
|
||||
loadContext("spring.developertools.remote.secret:supersecret",
|
||||
"spring.developertools.remote.restart.enabled:false");
|
||||
this.thrown.expect(NoSuchBeanDefinitionException.class);
|
||||
this.context.getBean("remoteRestartHanderMapper");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void invokeTunnelWithDefaultSetup() throws Exception {
|
||||
loadContext("spring.developertools.remote.secret:supersecret");
|
||||
DispatcherFilter filter = this.context.getBean(DispatcherFilter.class);
|
||||
this.request.setRequestURI(DEFAULT_CONTEXT_PATH + "/debug");
|
||||
this.request.addHeader(DEFAULT_SECRET_HEADER_NAME, "supersecret");
|
||||
filter.doFilter(this.request, this.response, this.chain);
|
||||
assertTunnelInvoked(true);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void invokeTunnelWithCustomHeaderName() throws Exception {
|
||||
loadContext("spring.developertools.remote.secret:supersecret",
|
||||
"spring.developertools.remote.secretHeaderName:customheader");
|
||||
DispatcherFilter filter = this.context.getBean(DispatcherFilter.class);
|
||||
this.request.setRequestURI(DEFAULT_CONTEXT_PATH + "/debug");
|
||||
this.request.addHeader("customheader", "supersecret");
|
||||
filter.doFilter(this.request, this.response, this.chain);
|
||||
assertTunnelInvoked(true);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void disableRemoteDebug() throws Exception {
|
||||
loadContext("spring.developertools.remote.secret:supersecret",
|
||||
"spring.developertools.remote.debug.enabled:false");
|
||||
this.thrown.expect(NoSuchBeanDefinitionException.class);
|
||||
this.context.getBean("remoteDebugHanderMapper");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void developerToolsHealthReturns200() throws Exception {
|
||||
loadContext("spring.developertools.remote.secret:supersecret");
|
||||
DispatcherFilter filter = this.context.getBean(DispatcherFilter.class);
|
||||
this.request.setRequestURI(DEFAULT_CONTEXT_PATH);
|
||||
this.request.addHeader(DEFAULT_SECRET_HEADER_NAME, "supersecret");
|
||||
this.response.setStatus(500);
|
||||
filter.doFilter(this.request, this.response, this.chain);
|
||||
assertThat(this.response.getStatus(), equalTo(200));
|
||||
}
|
||||
|
||||
private void assertTunnelInvoked(boolean value) {
|
||||
assertThat(this.context.getBean(MockHttpTunnelServer.class).invoked,
|
||||
equalTo(value));
|
||||
}
|
||||
|
||||
private void assertRestartInvoked(boolean value) {
|
||||
assertThat(this.context.getBean(MockHttpRestartServer.class).invoked,
|
||||
equalTo(value));
|
||||
}
|
||||
|
||||
private void loadContext(String... properties) {
|
||||
this.context = new AnnotationConfigWebApplicationContext();
|
||||
this.context.setServletContext(new MockServletContext());
|
||||
this.context.register(Config.class, ServerPropertiesAutoConfiguration.class,
|
||||
PropertyPlaceholderAutoConfiguration.class);
|
||||
EnvironmentTestUtils.addEnvironment(this.context, properties);
|
||||
this.context.refresh();
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@Import(RemoteDeveloperToolsAutoConfiguration.class)
|
||||
static class Config {
|
||||
|
||||
@Bean
|
||||
public HttpTunnelServer remoteDebugHttpTunnelServer() {
|
||||
return new MockHttpTunnelServer(new SocketTargetServerConnection(
|
||||
new RemoteDebugPortProvider()));
|
||||
}
|
||||
|
||||
@Bean
|
||||
public HttpRestartServer remoteRestartHttpRestartServer() {
|
||||
SourceFolderUrlFilter sourceFolderUrlFilter = mock(SourceFolderUrlFilter.class);
|
||||
return new MockHttpRestartServer(sourceFolderUrlFilter);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Mock {@link HttpTunnelServer} implementation.
|
||||
*/
|
||||
static class MockHttpTunnelServer extends HttpTunnelServer {
|
||||
|
||||
private boolean invoked;
|
||||
|
||||
public MockHttpTunnelServer(TargetServerConnection serverConnection) {
|
||||
super(serverConnection);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle(ServerHttpRequest request, ServerHttpResponse response)
|
||||
throws IOException {
|
||||
this.invoked = true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Mock {@link HttpRestartServer} implementation.
|
||||
*/
|
||||
static class MockHttpRestartServer extends HttpRestartServer {
|
||||
|
||||
private boolean invoked;
|
||||
|
||||
public MockHttpRestartServer(SourceFolderUrlFilter sourceFolderUrlFilter) {
|
||||
super(sourceFolderUrlFilter);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle(ServerHttpRequest request, ServerHttpResponse response)
|
||||
throws IOException {
|
||||
this.invoked = true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue