Commit bd945c7b authored by Phillip Webb's avatar Phillip Webb

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
parent fe1f344a
......@@ -22,9 +22,11 @@ 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;
......@@ -41,6 +43,7 @@ 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;
......@@ -81,7 +84,7 @@ public class Restarter {
private final boolean forceReferenceCleanup;
private URL[] urls;
private URL[] initialUrls;
private final String mainClassName;
......@@ -91,6 +94,10 @@ public class Restarter {
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>();
......@@ -115,7 +122,7 @@ public class Restarter {
this.logger.debug("Creating new Restarter for thread " + thread);
SilentExitExceptionHandler.setup(thread);
this.forceReferenceCleanup = forceReferenceCleanup;
this.urls = initializer.getInitialUrls(thread);
this.initialUrls = initializer.getInitialUrls(thread);
this.mainClassName = getMainClassName(thread);
this.applicationClassLoader = thread.getContextClassLoader();
this.args = args;
......@@ -134,7 +141,8 @@ public class Restarter {
protected void initialize(boolean restartOnInitialize) {
preInitializeLeakyClasses();
if (this.urls != null) {
if (this.initialUrls != null) {
this.urls.addAll(Arrays.asList(this.initialUrls));
if (restartOnInitialize) {
this.logger.debug("Immediately restarting application");
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 leak safe thread factory
......@@ -208,10 +234,13 @@ public class Restarter {
protected void start() throws Exception {
Assert.notNull(this.mainClassName, "Unable to find the main class to restart");
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()) {
this.logger.debug("Starting application " + this.mainClassName
+ " with URLs " + Arrays.asList(this.urls));
+ " with URLs " + Arrays.asList(urls));
}
relaunch(classLoader);
}
......@@ -369,7 +398,7 @@ public class Restarter {
* @return the initial URLs or {@code null}
*/
public URL[] getInitialUrls() {
return this.urls;
return this.initialUrls;
}
/**
......
/*
* 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
}
}
/*
* 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);
}
/*
* 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();
}
}
}
/*
* 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;
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;
......@@ -38,25 +42,44 @@ public class RestartClassLoader extends URLClassLoader implements SmartClassLoad
private final Log logger;
private final ClassLoaderFileRepository updatedFiles;
/**
* 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
*/
public RestartClassLoader(ClassLoader parent, URL[] urls) {
this(parent, urls, LogFactory.getLog(RestartClassLoader.class));
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 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
* @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);
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());
......@@ -66,11 +89,26 @@ public class RestartClassLoader extends URLClassLoader implements SmartClassLoad
@Override
public Enumeration<URL> getResources(String name) throws IOException {
// 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
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;
......@@ -78,8 +116,30 @@ public class RestartClassLoader extends URLClassLoader implements SmartClassLoad
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 {
......@@ -95,6 +155,35 @@ public class RestartClassLoader extends URLClassLoader implements SmartClassLoad
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()) {
......@@ -108,4 +197,35 @@ public class RestartClassLoader extends URLClassLoader implements SmartClassLoad
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.URLClassLoader;
import java.util.Collection;
import java.util.Collections;
import java.util.concurrent.ThreadFactory;
import org.junit.After;
......@@ -27,11 +29,15 @@ import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.springframework.beans.BeansException;
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.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.springframework.util.FileCopyUtils;
import org.springframework.util.StringUtils;
import static org.hamcrest.Matchers.equalTo;
......@@ -100,6 +106,44 @@ public class RestarterTests {
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
@SuppressWarnings("rawtypes")
public void getOrAddAttributeWithExistingAttribute() throws Exception {
......@@ -191,6 +235,8 @@ public class RestarterTests {
private static class TestableRestarter extends Restarter {
private ClassLoader relaunchClassLoader;
public TestableRestarter() {
this(Thread.currentThread(), new String[] {}, false,
new MockRestartInitializer());
......@@ -212,10 +258,19 @@ public class RestarterTests {
}
}
@Override
protected void relaunch(ClassLoader classLoader) throws Exception {
this.relaunchClassLoader = classLoader;
}
@Override
protected void stop() throws Exception {
}
public ClassLoader getRelaunchClassLoader() {
return this.relaunchClassLoader;
}
}
}
/*
* 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());
}
}
/*
* 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.rules.ExpectedException;
import org.junit.rules.TemporaryFolder;
import org.springframework.boot.developertools.restart.classloader.ClassLoaderFile.Kind;
import org.springframework.util.FileCopyUtils;
import org.springframework.util.StreamUtils;
......@@ -66,6 +67,8 @@ public class RestartClassLoaderTests {
private URLClassLoader parentClassLoader;
private ClassLoaderFiles updatedFiles;
private RestartClassLoader reloadClassLoader;
@Before
......@@ -75,7 +78,9 @@ public class RestartClassLoaderTests {
ClassLoader classLoader = getClass().getClassLoader();
URL[] urls = new URL[] { url };
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 {
......@@ -98,6 +103,13 @@ public class RestartClassLoaderTests {
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
public void getResourceFromReloadableUrl() throws Exception {
String content = readString(this.reloadClassLoader
......@@ -131,6 +143,73 @@ public class RestartClassLoaderTests {
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 {
return new String(FileCopyUtils.copyToByteArray(in));
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment