Update Restarter to support for ClassLoaderFiles
Update the `Restarter` so that class files and resources can change independently of the underlying source folders. This change will allow updates to be pushed to a remotely running application, without requiring the application to run in an exploded form. See gh-3086
This commit is contained in:
parent
fe1f344ae8
commit
bd945c7b39
|
|
@ -22,9 +22,11 @@ import java.lang.reflect.Field;
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
import java.util.Collection;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.IdentityHashMap;
|
import java.util.IdentityHashMap;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
@ -41,6 +43,7 @@ import org.apache.commons.logging.LogFactory;
|
||||||
import org.springframework.beans.CachedIntrospectionResults;
|
import org.springframework.beans.CachedIntrospectionResults;
|
||||||
import org.springframework.beans.factory.ObjectFactory;
|
import org.springframework.beans.factory.ObjectFactory;
|
||||||
import org.springframework.boot.SpringApplication;
|
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.developertools.restart.classloader.RestartClassLoader;
|
||||||
import org.springframework.boot.logging.DeferredLog;
|
import org.springframework.boot.logging.DeferredLog;
|
||||||
import org.springframework.cglib.core.ClassNameReader;
|
import org.springframework.cglib.core.ClassNameReader;
|
||||||
|
|
@ -81,7 +84,7 @@ public class Restarter {
|
||||||
|
|
||||||
private final boolean forceReferenceCleanup;
|
private final boolean forceReferenceCleanup;
|
||||||
|
|
||||||
private URL[] urls;
|
private URL[] initialUrls;
|
||||||
|
|
||||||
private final String mainClassName;
|
private final String mainClassName;
|
||||||
|
|
||||||
|
|
@ -91,6 +94,10 @@ public class Restarter {
|
||||||
|
|
||||||
private final UncaughtExceptionHandler exceptionHandler;
|
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 Map<String, Object> attributes = new HashMap<String, Object>();
|
||||||
|
|
||||||
private final BlockingDeque<LeakSafeThread> leakSafeThreads = new LinkedBlockingDeque<LeakSafeThread>();
|
private final BlockingDeque<LeakSafeThread> leakSafeThreads = new LinkedBlockingDeque<LeakSafeThread>();
|
||||||
|
|
@ -115,7 +122,7 @@ public class Restarter {
|
||||||
this.logger.debug("Creating new Restarter for thread " + thread);
|
this.logger.debug("Creating new Restarter for thread " + thread);
|
||||||
SilentExitExceptionHandler.setup(thread);
|
SilentExitExceptionHandler.setup(thread);
|
||||||
this.forceReferenceCleanup = forceReferenceCleanup;
|
this.forceReferenceCleanup = forceReferenceCleanup;
|
||||||
this.urls = initializer.getInitialUrls(thread);
|
this.initialUrls = initializer.getInitialUrls(thread);
|
||||||
this.mainClassName = getMainClassName(thread);
|
this.mainClassName = getMainClassName(thread);
|
||||||
this.applicationClassLoader = thread.getContextClassLoader();
|
this.applicationClassLoader = thread.getContextClassLoader();
|
||||||
this.args = args;
|
this.args = args;
|
||||||
|
|
@ -134,7 +141,8 @@ public class Restarter {
|
||||||
|
|
||||||
protected void initialize(boolean restartOnInitialize) {
|
protected void initialize(boolean restartOnInitialize) {
|
||||||
preInitializeLeakyClasses();
|
preInitializeLeakyClasses();
|
||||||
if (this.urls != null) {
|
if (this.initialUrls != null) {
|
||||||
|
this.urls.addAll(Arrays.asList(this.initialUrls));
|
||||||
if (restartOnInitialize) {
|
if (restartOnInitialize) {
|
||||||
this.logger.debug("Immediately restarting application");
|
this.logger.debug("Immediately restarting application");
|
||||||
immediateRestart();
|
immediateRestart();
|
||||||
|
|
@ -176,6 +184,24 @@ public class Restarter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 {@link ThreadFactory} that can be used to create leak safe threads.
|
||||||
* @return a leak safe thread factory
|
* @return a leak safe thread factory
|
||||||
|
|
@ -208,10 +234,13 @@ public class Restarter {
|
||||||
protected void start() throws Exception {
|
protected void start() throws Exception {
|
||||||
Assert.notNull(this.mainClassName, "Unable to find the main class to restart");
|
Assert.notNull(this.mainClassName, "Unable to find the main class to restart");
|
||||||
ClassLoader parent = this.applicationClassLoader;
|
ClassLoader parent = this.applicationClassLoader;
|
||||||
ClassLoader classLoader = new RestartClassLoader(parent, this.urls, this.logger);
|
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()) {
|
if (this.logger.isDebugEnabled()) {
|
||||||
this.logger.debug("Starting application " + this.mainClassName
|
this.logger.debug("Starting application " + this.mainClassName
|
||||||
+ " with URLs " + Arrays.asList(this.urls));
|
+ " with URLs " + Arrays.asList(urls));
|
||||||
}
|
}
|
||||||
relaunch(classLoader);
|
relaunch(classLoader);
|
||||||
}
|
}
|
||||||
|
|
@ -369,7 +398,7 @@ public class Restarter {
|
||||||
* @return the initial URLs or {@code null}
|
* @return the initial URLs or {@code null}
|
||||||
*/
|
*/
|
||||||
public URL[] getInitialUrls() {
|
public URL[] getInitialUrls() {
|
||||||
return this.urls;
|
return this.initialUrls;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -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());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -17,12 +17,16 @@
|
||||||
package org.springframework.boot.developertools.restart.classloader;
|
package org.springframework.boot.developertools.restart.classloader;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.net.MalformedURLException;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.net.URLClassLoader;
|
import java.net.URLClassLoader;
|
||||||
|
import java.security.AccessController;
|
||||||
|
import java.security.PrivilegedAction;
|
||||||
import java.util.Enumeration;
|
import java.util.Enumeration;
|
||||||
|
|
||||||
import org.apache.commons.logging.Log;
|
import org.apache.commons.logging.Log;
|
||||||
import org.apache.commons.logging.LogFactory;
|
import org.apache.commons.logging.LogFactory;
|
||||||
|
import org.springframework.boot.developertools.restart.classloader.ClassLoaderFile.Kind;
|
||||||
import org.springframework.core.SmartClassLoader;
|
import org.springframework.core.SmartClassLoader;
|
||||||
import org.springframework.util.Assert;
|
import org.springframework.util.Assert;
|
||||||
|
|
||||||
|
|
@ -38,25 +42,44 @@ public class RestartClassLoader extends URLClassLoader implements SmartClassLoad
|
||||||
|
|
||||||
private final Log logger;
|
private final Log logger;
|
||||||
|
|
||||||
|
private final ClassLoaderFileRepository updatedFiles;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new {@link RestartClassLoader} instance.
|
* Create a new {@link RestartClassLoader} instance.
|
||||||
* @param parent the parent classloader URLs were created.
|
* @param parent the parent classloader
|
||||||
* @param urls the urls managed by the classloader
|
* @param urls the urls managed by the classloader
|
||||||
*/
|
*/
|
||||||
public RestartClassLoader(ClassLoader parent, URL[] urls) {
|
public RestartClassLoader(ClassLoader parent, URL[] urls) {
|
||||||
this(parent, urls, LogFactory.getLog(RestartClassLoader.class));
|
this(parent, urls, ClassLoaderFileRepository.NONE);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new {@link RestartClassLoader} instance.
|
* Create a new {@link RestartClassLoader} instance.
|
||||||
* @param parent the parent classloader URLs were created.
|
* @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 urls the urls managed by the classloader
|
||||||
* @param logger the logger used for messages
|
* @param logger the logger used for messages
|
||||||
*/
|
*/
|
||||||
public RestartClassLoader(ClassLoader parent, URL[] urls, Log logger) {
|
public RestartClassLoader(ClassLoader parent, URL[] urls,
|
||||||
|
ClassLoaderFileRepository updatedFiles, Log logger) {
|
||||||
super(urls, parent);
|
super(urls, parent);
|
||||||
Assert.notNull(parent, "Parent must not be null");
|
Assert.notNull(parent, "Parent must not be null");
|
||||||
|
Assert.notNull(updatedFiles, "UpdatedFiles must not be null");
|
||||||
Assert.notNull(logger, "Logger must not be null");
|
Assert.notNull(logger, "Logger must not be null");
|
||||||
|
this.updatedFiles = updatedFiles;
|
||||||
this.logger = logger;
|
this.logger = logger;
|
||||||
if (logger.isDebugEnabled()) {
|
if (logger.isDebugEnabled()) {
|
||||||
logger.debug("Created RestartClassLoader " + toString());
|
logger.debug("Created RestartClassLoader " + toString());
|
||||||
|
|
@ -66,11 +89,26 @@ public class RestartClassLoader extends URLClassLoader implements SmartClassLoad
|
||||||
@Override
|
@Override
|
||||||
public Enumeration<URL> getResources(String name) throws IOException {
|
public Enumeration<URL> getResources(String name) throws IOException {
|
||||||
// Use the parent since we're shadowing resource and we don't want duplicates
|
// Use the parent since we're shadowing resource and we don't want duplicates
|
||||||
return getParent().getResources(name);
|
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
|
@Override
|
||||||
public URL getResource(String name) {
|
public URL getResource(String name) {
|
||||||
|
ClassLoaderFile file = this.updatedFiles.getFile(name);
|
||||||
|
if (file != null && file.getKind() == Kind.DELETED) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
URL resource = findResource(name);
|
URL resource = findResource(name);
|
||||||
if (resource != null) {
|
if (resource != null) {
|
||||||
return resource;
|
return resource;
|
||||||
|
|
@ -78,8 +116,30 @@ public class RestartClassLoader extends URLClassLoader implements SmartClassLoad
|
||||||
return getParent().getResource(name);
|
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
|
@Override
|
||||||
public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
|
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);
|
Class<?> loadedClass = findLoadedClass(name);
|
||||||
if (loadedClass == null) {
|
if (loadedClass == null) {
|
||||||
try {
|
try {
|
||||||
|
|
@ -95,6 +155,35 @@ public class RestartClassLoader extends URLClassLoader implements SmartClassLoad
|
||||||
return 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
|
@Override
|
||||||
protected void finalize() throws Throwable {
|
protected void finalize() throws Throwable {
|
||||||
if (this.logger.isDebugEnabled()) {
|
if (this.logger.isDebugEnabled()) {
|
||||||
|
|
@ -108,4 +197,35 @@ public class RestartClassLoader extends URLClassLoader implements SmartClassLoad
|
||||||
return (classType.getClassLoader() instanceof RestartClassLoader);
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,8 @@ package org.springframework.boot.developertools.restart;
|
||||||
|
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.net.URLClassLoader;
|
import java.net.URLClassLoader;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.concurrent.ThreadFactory;
|
import java.util.concurrent.ThreadFactory;
|
||||||
|
|
||||||
import org.junit.After;
|
import org.junit.After;
|
||||||
|
|
@ -27,11 +29,15 @@ import org.junit.Test;
|
||||||
import org.junit.rules.ExpectedException;
|
import org.junit.rules.ExpectedException;
|
||||||
import org.springframework.beans.BeansException;
|
import org.springframework.beans.BeansException;
|
||||||
import org.springframework.beans.factory.ObjectFactory;
|
import org.springframework.beans.factory.ObjectFactory;
|
||||||
|
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.test.OutputCapture;
|
import org.springframework.boot.test.OutputCapture;
|
||||||
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
||||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||||
import org.springframework.scheduling.annotation.Scheduled;
|
import org.springframework.scheduling.annotation.Scheduled;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.util.FileCopyUtils;
|
||||||
import org.springframework.util.StringUtils;
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
import static org.hamcrest.Matchers.equalTo;
|
import static org.hamcrest.Matchers.equalTo;
|
||||||
|
|
@ -100,6 +106,44 @@ public class RestarterTests {
|
||||||
assertThat(attribute, equalTo((Object) "abc"));
|
assertThat(attribute, equalTo((Object) "abc"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void addUrlsMustNotBeNull() throws Exception {
|
||||||
|
this.thrown.expect(IllegalArgumentException.class);
|
||||||
|
this.thrown.expectMessage("Urls must not be null");
|
||||||
|
Restarter.getInstance().addUrls(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void addUrls() throws Exception {
|
||||||
|
URL url = new URL("file:/proj/module-a.jar!/");
|
||||||
|
Collection<URL> urls = Collections.singleton(url);
|
||||||
|
Restarter restarter = Restarter.getInstance();
|
||||||
|
restarter.addUrls(urls);
|
||||||
|
restarter.restart();
|
||||||
|
ClassLoader classLoader = ((TestableRestarter) restarter)
|
||||||
|
.getRelaunchClassLoader();
|
||||||
|
assertThat(((URLClassLoader) classLoader).getURLs()[0], equalTo(url));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void addClassLoaderFilesMustNotBeNull() throws Exception {
|
||||||
|
this.thrown.expect(IllegalArgumentException.class);
|
||||||
|
this.thrown.expectMessage("ClassLoaderFiles must not be null");
|
||||||
|
Restarter.getInstance().addClassLoaderFiles(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void addClassLoaderFiles() throws Exception {
|
||||||
|
ClassLoaderFiles classLoaderFiles = new ClassLoaderFiles();
|
||||||
|
classLoaderFiles.addFile("f", new ClassLoaderFile(Kind.ADDED, "abc".getBytes()));
|
||||||
|
Restarter restarter = Restarter.getInstance();
|
||||||
|
restarter.addClassLoaderFiles(classLoaderFiles);
|
||||||
|
restarter.restart();
|
||||||
|
ClassLoader classLoader = ((TestableRestarter) restarter)
|
||||||
|
.getRelaunchClassLoader();
|
||||||
|
assertThat(FileCopyUtils.copyToByteArray(classLoader.getResourceAsStream("f")),
|
||||||
|
equalTo("abc".getBytes()));
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@SuppressWarnings("rawtypes")
|
@SuppressWarnings("rawtypes")
|
||||||
public void getOrAddAttributeWithExistingAttribute() throws Exception {
|
public void getOrAddAttributeWithExistingAttribute() throws Exception {
|
||||||
|
|
@ -191,6 +235,8 @@ public class RestarterTests {
|
||||||
|
|
||||||
private static class TestableRestarter extends Restarter {
|
private static class TestableRestarter extends Restarter {
|
||||||
|
|
||||||
|
private ClassLoader relaunchClassLoader;
|
||||||
|
|
||||||
public TestableRestarter() {
|
public TestableRestarter() {
|
||||||
this(Thread.currentThread(), new String[] {}, false,
|
this(Thread.currentThread(), new String[] {}, false,
|
||||||
new MockRestartInitializer());
|
new MockRestartInitializer());
|
||||||
|
|
@ -212,10 +258,19 @@ public class RestarterTests {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void relaunch(ClassLoader classLoader) throws Exception {
|
||||||
|
this.relaunchClassLoader = classLoader;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void stop() throws Exception {
|
protected void stop() throws Exception {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ClassLoader getRelaunchClassLoader() {
|
||||||
|
return this.relaunchClassLoader;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,90 @@
|
||||||
|
/*
|
||||||
|
* 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 org.junit.Rule;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.rules.ExpectedException;
|
||||||
|
import org.springframework.boot.developertools.restart.classloader.ClassLoaderFile;
|
||||||
|
import org.springframework.boot.developertools.restart.classloader.ClassLoaderFile.Kind;
|
||||||
|
|
||||||
|
import static org.hamcrest.Matchers.equalTo;
|
||||||
|
import static org.hamcrest.Matchers.nullValue;
|
||||||
|
import static org.junit.Assert.assertThat;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for {@link ClassLoaderFile}.
|
||||||
|
*
|
||||||
|
* @author Phillip Webb
|
||||||
|
*/
|
||||||
|
public class ClassLoaderFileTests {
|
||||||
|
|
||||||
|
public static final byte[] BYTES = "ABC".getBytes();
|
||||||
|
|
||||||
|
@Rule
|
||||||
|
public ExpectedException thrown = ExpectedException.none();
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void kindMustNotBeNull() {
|
||||||
|
this.thrown.expect(IllegalArgumentException.class);
|
||||||
|
this.thrown.expectMessage("Kind must not be null");
|
||||||
|
new ClassLoaderFile(null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void addedContentsMustNotBeNull() throws Exception {
|
||||||
|
this.thrown.expect(IllegalArgumentException.class);
|
||||||
|
this.thrown.expectMessage("Contents must not be null");
|
||||||
|
new ClassLoaderFile(Kind.ADDED, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void modifiedContentsMustNotBeNull() throws Exception {
|
||||||
|
this.thrown.expect(IllegalArgumentException.class);
|
||||||
|
this.thrown.expectMessage("Contents must not be null");
|
||||||
|
new ClassLoaderFile(Kind.MODIFIED, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void deletedContentsMustBeNull() throws Exception {
|
||||||
|
this.thrown.expect(IllegalArgumentException.class);
|
||||||
|
this.thrown.expectMessage("Contents must be null");
|
||||||
|
new ClassLoaderFile(Kind.DELETED, new byte[10]);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void added() throws Exception {
|
||||||
|
ClassLoaderFile file = new ClassLoaderFile(Kind.ADDED, BYTES);
|
||||||
|
assertThat(file.getKind(), equalTo(ClassLoaderFile.Kind.ADDED));
|
||||||
|
assertThat(file.getContents(), equalTo(BYTES));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void modified() throws Exception {
|
||||||
|
ClassLoaderFile file = new ClassLoaderFile(Kind.MODIFIED, BYTES);
|
||||||
|
assertThat(file.getKind(), equalTo(ClassLoaderFile.Kind.MODIFIED));
|
||||||
|
assertThat(file.getContents(), equalTo(BYTES));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void deleted() throws Exception {
|
||||||
|
ClassLoaderFile file = new ClassLoaderFile(Kind.DELETED, null);
|
||||||
|
assertThat(file.getKind(), equalTo(ClassLoaderFile.Kind.DELETED));
|
||||||
|
assertThat(file.getContents(), nullValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,184 @@
|
||||||
|
/*
|
||||||
|
* 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.ByteArrayOutputStream;
|
||||||
|
import java.io.ObjectInputStream;
|
||||||
|
import java.io.ObjectOutputStream;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Iterator;
|
||||||
|
|
||||||
|
import org.junit.Rule;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.rules.ExpectedException;
|
||||||
|
import org.springframework.boot.developertools.restart.classloader.ClassLoaderFile.Kind;
|
||||||
|
import org.springframework.boot.developertools.restart.classloader.ClassLoaderFiles.SourceFolder;
|
||||||
|
|
||||||
|
import static org.hamcrest.Matchers.equalTo;
|
||||||
|
import static org.hamcrest.Matchers.notNullValue;
|
||||||
|
import static org.hamcrest.Matchers.nullValue;
|
||||||
|
import static org.junit.Assert.assertThat;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for {@link ClassLoaderFiles}.
|
||||||
|
*
|
||||||
|
* @author Phillip Webb
|
||||||
|
*/
|
||||||
|
public class ClassLoaderFilesTests {
|
||||||
|
|
||||||
|
@Rule
|
||||||
|
public ExpectedException thrown = ExpectedException.none();
|
||||||
|
|
||||||
|
private ClassLoaderFiles files = new ClassLoaderFiles();
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void addFileNameMustNotBeNull() throws Exception {
|
||||||
|
this.thrown.expect(IllegalArgumentException.class);
|
||||||
|
this.thrown.expectMessage("Name must not be null");
|
||||||
|
this.files.addFile(null, mock(ClassLoaderFile.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void addFileFileMustNotBeNull() throws Exception {
|
||||||
|
this.thrown.expect(IllegalArgumentException.class);
|
||||||
|
this.thrown.expectMessage("File must not be null");
|
||||||
|
this.files.addFile("test", null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getFileWithNullName() throws Exception {
|
||||||
|
assertThat(this.files.getFile(null), nullValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void addAndGet() throws Exception {
|
||||||
|
ClassLoaderFile file = new ClassLoaderFile(Kind.ADDED, new byte[10]);
|
||||||
|
this.files.addFile("myfile", file);
|
||||||
|
assertThat(this.files.getFile("myfile"), equalTo(file));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getMissing() throws Exception {
|
||||||
|
assertThat(this.files.getFile("missing"), nullValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void addTwice() throws Exception {
|
||||||
|
ClassLoaderFile file1 = new ClassLoaderFile(Kind.ADDED, new byte[10]);
|
||||||
|
ClassLoaderFile file2 = new ClassLoaderFile(Kind.MODIFIED, new byte[10]);
|
||||||
|
this.files.addFile("myfile", file1);
|
||||||
|
this.files.addFile("myfile", file2);
|
||||||
|
assertThat(this.files.getFile("myfile"), equalTo(file2));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void addTwiceInDifferentSourceFolders() throws Exception {
|
||||||
|
ClassLoaderFile file1 = new ClassLoaderFile(Kind.ADDED, new byte[10]);
|
||||||
|
ClassLoaderFile file2 = new ClassLoaderFile(Kind.MODIFIED, new byte[10]);
|
||||||
|
this.files.addFile("a", "myfile", file1);
|
||||||
|
this.files.addFile("b", "myfile", file2);
|
||||||
|
assertThat(this.files.getFile("myfile"), equalTo(file2));
|
||||||
|
assertThat(this.files.getOrCreateSourceFolder("a").getFiles().size(), equalTo(0));
|
||||||
|
assertThat(this.files.getOrCreateSourceFolder("b").getFiles().size(), equalTo(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getSourceFolders() throws Exception {
|
||||||
|
ClassLoaderFile file1 = new ClassLoaderFile(Kind.ADDED, new byte[10]);
|
||||||
|
ClassLoaderFile file2 = new ClassLoaderFile(Kind.MODIFIED, new byte[10]);
|
||||||
|
ClassLoaderFile file3 = new ClassLoaderFile(Kind.MODIFIED, new byte[10]);
|
||||||
|
ClassLoaderFile file4 = new ClassLoaderFile(Kind.MODIFIED, new byte[10]);
|
||||||
|
this.files.addFile("a", "myfile1", file1);
|
||||||
|
this.files.addFile("a", "myfile2", file2);
|
||||||
|
this.files.addFile("b", "myfile3", file3);
|
||||||
|
this.files.addFile("b", "myfile4", file4);
|
||||||
|
Iterator<SourceFolder> sourceFolders = this.files.getSourceFolders().iterator();
|
||||||
|
SourceFolder sourceFolder1 = sourceFolders.next();
|
||||||
|
SourceFolder sourceFolder2 = sourceFolders.next();
|
||||||
|
assertThat(sourceFolders.hasNext(), equalTo(false));
|
||||||
|
assertThat(sourceFolder1.getName(), equalTo("a"));
|
||||||
|
assertThat(sourceFolder2.getName(), equalTo("b"));
|
||||||
|
assertThat(new ArrayList<ClassLoaderFile>(sourceFolder1.getFiles()),
|
||||||
|
equalTo(Arrays.asList(file1, file2)));
|
||||||
|
assertThat(new ArrayList<ClassLoaderFile>(sourceFolder2.getFiles()),
|
||||||
|
equalTo(Arrays.asList(file3, file4)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void serialzie() throws Exception {
|
||||||
|
ClassLoaderFile file = new ClassLoaderFile(Kind.ADDED, new byte[10]);
|
||||||
|
this.files.addFile("myfile", file);
|
||||||
|
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||||
|
ObjectOutputStream oos = new ObjectOutputStream(bos);
|
||||||
|
oos.writeObject(this.files);
|
||||||
|
oos.close();
|
||||||
|
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(
|
||||||
|
bos.toByteArray()));
|
||||||
|
ClassLoaderFiles readObject = (ClassLoaderFiles) ois.readObject();
|
||||||
|
assertThat(readObject.getFile("myfile"), notNullValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void addAll() throws Exception {
|
||||||
|
ClassLoaderFile file1 = new ClassLoaderFile(Kind.ADDED, new byte[10]);
|
||||||
|
this.files.addFile("a", "myfile1", file1);
|
||||||
|
ClassLoaderFiles toAdd = new ClassLoaderFiles();
|
||||||
|
ClassLoaderFile file2 = new ClassLoaderFile(Kind.MODIFIED, new byte[10]);
|
||||||
|
ClassLoaderFile file3 = new ClassLoaderFile(Kind.MODIFIED, new byte[10]);
|
||||||
|
toAdd.addFile("a", "myfile2", file2);
|
||||||
|
toAdd.addFile("b", "myfile3", file3);
|
||||||
|
this.files.addAll(toAdd);
|
||||||
|
Iterator<SourceFolder> sourceFolders = this.files.getSourceFolders().iterator();
|
||||||
|
SourceFolder sourceFolder1 = sourceFolders.next();
|
||||||
|
SourceFolder sourceFolder2 = sourceFolders.next();
|
||||||
|
assertThat(sourceFolders.hasNext(), equalTo(false));
|
||||||
|
assertThat(sourceFolder1.getName(), equalTo("a"));
|
||||||
|
assertThat(sourceFolder2.getName(), equalTo("b"));
|
||||||
|
assertThat(new ArrayList<ClassLoaderFile>(sourceFolder1.getFiles()),
|
||||||
|
equalTo(Arrays.asList(file1, file2)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getSize() throws Exception {
|
||||||
|
this.files.addFile("s1", "n1", mock(ClassLoaderFile.class));
|
||||||
|
this.files.addFile("s1", "n2", mock(ClassLoaderFile.class));
|
||||||
|
this.files.addFile("s2", "n3", mock(ClassLoaderFile.class));
|
||||||
|
this.files.addFile("s2", "n1", mock(ClassLoaderFile.class));
|
||||||
|
assertThat(this.files.size(), equalTo(3));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void classLoaderFilesMustNotBeNull() throws Exception {
|
||||||
|
this.thrown.expect(IllegalArgumentException.class);
|
||||||
|
this.thrown.expectMessage("ClassLoaderFiles must not be null");
|
||||||
|
new ClassLoaderFiles(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void constructFromExistingSet() throws Exception {
|
||||||
|
this.files.addFile("s1", "n1", mock(ClassLoaderFile.class));
|
||||||
|
this.files.addFile("s1", "n2", mock(ClassLoaderFile.class));
|
||||||
|
ClassLoaderFiles copy = new ClassLoaderFiles(this.files);
|
||||||
|
this.files.addFile("s2", "n3", mock(ClassLoaderFile.class));
|
||||||
|
assertThat(this.files.size(), equalTo(3));
|
||||||
|
assertThat(copy.size(), equalTo(2));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -34,6 +34,7 @@ import org.junit.Rule;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.rules.ExpectedException;
|
import org.junit.rules.ExpectedException;
|
||||||
import org.junit.rules.TemporaryFolder;
|
import org.junit.rules.TemporaryFolder;
|
||||||
|
import org.springframework.boot.developertools.restart.classloader.ClassLoaderFile.Kind;
|
||||||
import org.springframework.util.FileCopyUtils;
|
import org.springframework.util.FileCopyUtils;
|
||||||
import org.springframework.util.StreamUtils;
|
import org.springframework.util.StreamUtils;
|
||||||
|
|
||||||
|
|
@ -66,6 +67,8 @@ public class RestartClassLoaderTests {
|
||||||
|
|
||||||
private URLClassLoader parentClassLoader;
|
private URLClassLoader parentClassLoader;
|
||||||
|
|
||||||
|
private ClassLoaderFiles updatedFiles;
|
||||||
|
|
||||||
private RestartClassLoader reloadClassLoader;
|
private RestartClassLoader reloadClassLoader;
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
|
|
@ -75,7 +78,9 @@ public class RestartClassLoaderTests {
|
||||||
ClassLoader classLoader = getClass().getClassLoader();
|
ClassLoader classLoader = getClass().getClassLoader();
|
||||||
URL[] urls = new URL[] { url };
|
URL[] urls = new URL[] { url };
|
||||||
this.parentClassLoader = new URLClassLoader(urls, classLoader);
|
this.parentClassLoader = new URLClassLoader(urls, classLoader);
|
||||||
this.reloadClassLoader = new RestartClassLoader(this.parentClassLoader, urls);
|
this.updatedFiles = new ClassLoaderFiles();
|
||||||
|
this.reloadClassLoader = new RestartClassLoader(this.parentClassLoader, urls,
|
||||||
|
this.updatedFiles);
|
||||||
}
|
}
|
||||||
|
|
||||||
private File createSampleJarFile() throws IOException {
|
private File createSampleJarFile() throws IOException {
|
||||||
|
|
@ -98,6 +103,13 @@ public class RestartClassLoaderTests {
|
||||||
new RestartClassLoader(null, new URL[] {});
|
new RestartClassLoader(null, new URL[] {});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void updatedFilesMustNotBeNull() throws Exception {
|
||||||
|
this.thrown.expect(IllegalArgumentException.class);
|
||||||
|
this.thrown.expectMessage("UpdatedFiles must not be null");
|
||||||
|
new RestartClassLoader(this.parentClassLoader, new URL[] {}, null);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void getResourceFromReloadableUrl() throws Exception {
|
public void getResourceFromReloadableUrl() throws Exception {
|
||||||
String content = readString(this.reloadClassLoader
|
String content = readString(this.reloadClassLoader
|
||||||
|
|
@ -131,6 +143,73 @@ public class RestartClassLoaderTests {
|
||||||
assertThat(loaded.getClassLoader(), equalTo(getClass().getClassLoader()));
|
assertThat(loaded.getClassLoader(), equalTo(getClass().getClassLoader()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getDeletedResource() throws Exception {
|
||||||
|
String name = PACKAGE_PATH + "/Sample.txt";
|
||||||
|
this.updatedFiles.addFile(name, new ClassLoaderFile(Kind.DELETED, null));
|
||||||
|
assertThat(this.reloadClassLoader.getResource(name), equalTo(null));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getDeletedResourceAsStream() throws Exception {
|
||||||
|
String name = PACKAGE_PATH + "/Sample.txt";
|
||||||
|
this.updatedFiles.addFile(name, new ClassLoaderFile(Kind.DELETED, null));
|
||||||
|
assertThat(this.reloadClassLoader.getResourceAsStream(name), equalTo(null));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getUpdatedResource() throws Exception {
|
||||||
|
String name = PACKAGE_PATH + "/Sample.txt";
|
||||||
|
byte[] bytes = "abc".getBytes();
|
||||||
|
this.updatedFiles.addFile(name, new ClassLoaderFile(Kind.MODIFIED, bytes));
|
||||||
|
URL resource = this.reloadClassLoader.getResource(name);
|
||||||
|
assertThat(FileCopyUtils.copyToByteArray(resource.openStream()), equalTo(bytes));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getResourcesWithDeleted() throws Exception {
|
||||||
|
String name = PACKAGE_PATH + "/Sample.txt";
|
||||||
|
this.updatedFiles.addFile(name, new ClassLoaderFile(Kind.DELETED, null));
|
||||||
|
List<URL> resources = toList(this.reloadClassLoader.getResources(name));
|
||||||
|
assertThat(resources.size(), equalTo(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getResourcesWithUpdated() throws Exception {
|
||||||
|
String name = PACKAGE_PATH + "/Sample.txt";
|
||||||
|
byte[] bytes = "abc".getBytes();
|
||||||
|
this.updatedFiles.addFile(name, new ClassLoaderFile(Kind.MODIFIED, bytes));
|
||||||
|
List<URL> resources = toList(this.reloadClassLoader.getResources(name));
|
||||||
|
assertThat(FileCopyUtils.copyToByteArray(resources.get(0).openStream()),
|
||||||
|
equalTo(bytes));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getDeletedClass() throws Exception {
|
||||||
|
String name = PACKAGE_PATH + "/Sample.class";
|
||||||
|
this.updatedFiles.addFile(name, new ClassLoaderFile(Kind.DELETED, null));
|
||||||
|
this.thrown.expect(ClassNotFoundException.class);
|
||||||
|
this.reloadClassLoader.loadClass(PACKAGE + ".Sample");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getUpdatedClass() throws Exception {
|
||||||
|
String name = PACKAGE_PATH + "/Sample.class";
|
||||||
|
this.updatedFiles.addFile(name, new ClassLoaderFile(Kind.MODIFIED, new byte[10]));
|
||||||
|
this.thrown.expect(ClassFormatError.class);
|
||||||
|
this.reloadClassLoader.loadClass(PACKAGE + ".Sample");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getAddedClass() throws Exception {
|
||||||
|
String name = PACKAGE_PATH + "/SampleParent.class";
|
||||||
|
byte[] bytes = FileCopyUtils.copyToByteArray(getClass().getResourceAsStream(
|
||||||
|
"SampleParent.class"));
|
||||||
|
this.updatedFiles.addFile(name, new ClassLoaderFile(Kind.ADDED, bytes));
|
||||||
|
Class<?> loaded = this.reloadClassLoader.loadClass(PACKAGE + ".SampleParent");
|
||||||
|
assertThat(loaded.getClassLoader(), equalTo((ClassLoader) this.reloadClassLoader));
|
||||||
|
}
|
||||||
|
|
||||||
private String readString(InputStream in) throws IOException {
|
private String readString(InputStream in) throws IOException {
|
||||||
return new String(FileCopyUtils.copyToByteArray(in));
|
return new String(FileCopyUtils.copyToByteArray(in));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue