Add Restarter server support
Add server side component to allow remote updates and restarts to a running application. See gh-3086
This commit is contained in:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user