diff --git a/spring-boot-developer-tools/src/main/java/org/springframework/boot/developertools/restart/classloader/RestartClassLoader.java b/spring-boot-developer-tools/src/main/java/org/springframework/boot/developertools/restart/classloader/RestartClassLoader.java new file mode 100644 index 0000000000..7be6917d1b --- /dev/null +++ b/spring-boot-developer-tools/src/main/java/org/springframework/boot/developertools/restart/classloader/RestartClassLoader.java @@ -0,0 +1,111 @@ +/* + * Copyright 2012-2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.developertools.restart.classloader; + +import java.io.IOException; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.Enumeration; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.core.SmartClassLoader; +import org.springframework.util.Assert; + +/** + * Disposable {@link ClassLoader} used to support application restarting. Provides parent + * last loading for the specified URLs. + * + * @author Andy Clement + * @author Phillip Webb + * @since 1.3.0 + */ +public class RestartClassLoader extends URLClassLoader implements SmartClassLoader { + + private final Log logger; + + /** + * Create a new {@link RestartClassLoader} instance. + * @param parent the parent classloader URLs were created. + * @param urls the urls managed by the classloader + */ + public RestartClassLoader(ClassLoader parent, URL[] urls) { + this(parent, urls, LogFactory.getLog(RestartClassLoader.class)); + } + + /** + * Create a new {@link RestartClassLoader} instance. + * @param parent the parent classloader 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) { + super(urls, parent); + Assert.notNull(parent, "Parent must not be null"); + Assert.notNull(logger, "Logger must not be null"); + this.logger = logger; + if (logger.isDebugEnabled()) { + logger.debug("Created RestartClassLoader " + toString()); + } + } + + @Override + public Enumeration getResources(String name) throws IOException { + // Use the parent since we're shadowing resource and we don't want duplicates + return getParent().getResources(name); + } + + @Override + public URL getResource(String name) { + URL resource = findResource(name); + if (resource != null) { + return resource; + } + return getParent().getResource(name); + } + + @Override + public Class loadClass(String name, boolean resolve) throws ClassNotFoundException { + Class loadedClass = findLoadedClass(name); + if (loadedClass == null) { + try { + loadedClass = findClass(name); + } + catch (ClassNotFoundException ex) { + loadedClass = getParent().loadClass(name); + } + } + if (resolve) { + resolveClass(loadedClass); + } + return loadedClass; + } + + @Override + protected void finalize() throws Throwable { + if (this.logger.isDebugEnabled()) { + this.logger.debug("Finalized classloader " + toString()); + } + super.finalize(); + } + + @Override + public boolean isClassReloadable(Class classType) { + return (classType.getClassLoader() instanceof RestartClassLoader); + } + +} diff --git a/spring-boot-developer-tools/src/main/java/org/springframework/boot/developertools/restart/classloader/package-info.java b/spring-boot-developer-tools/src/main/java/org/springframework/boot/developertools/restart/classloader/package-info.java new file mode 100644 index 0000000000..126699d341 --- /dev/null +++ b/spring-boot-developer-tools/src/main/java/org/springframework/boot/developertools/restart/classloader/package-info.java @@ -0,0 +1,21 @@ +/* + * Copyright 2012-2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Classloaders used for reload support + */ +package org.springframework.boot.developertools.restart.classloader; + diff --git a/spring-boot-developer-tools/src/test/java/org/springframework/boot/developertools/restart/classloader/RestartClassLoaderTests.java b/spring-boot-developer-tools/src/test/java/org/springframework/boot/developertools/restart/classloader/RestartClassLoaderTests.java new file mode 100644 index 0000000000..8f152ea388 --- /dev/null +++ b/spring-boot-developer-tools/src/test/java/org/springframework/boot/developertools/restart/classloader/RestartClassLoaderTests.java @@ -0,0 +1,148 @@ +/* + * 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.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.List; +import java.util.jar.JarOutputStream; +import java.util.zip.ZipEntry; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.rules.TemporaryFolder; +import org.springframework.util.FileCopyUtils; +import org.springframework.util.StreamUtils; + +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.startsWith; +import static org.junit.Assert.assertThat; + +/** + * Tests for {@link RestartClassLoader}. + * + * @author Phillip Webb + */ +@SuppressWarnings("resource") +public class RestartClassLoaderTests { + + private static final String PACKAGE = RestartClassLoaderTests.class.getPackage() + .getName(); + + private static final String PACKAGE_PATH = PACKAGE.replace(".", "/"); + + private static final Charset UTF_8 = Charset.forName("UTF-8"); + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Rule + public TemporaryFolder temp = new TemporaryFolder(); + + private File sampleJarFile; + + private URLClassLoader parentClassLoader; + + private RestartClassLoader reloadClassLoader; + + @Before + public void setup() throws Exception { + this.sampleJarFile = createSampleJarFile(); + URL url = this.sampleJarFile.toURI().toURL(); + ClassLoader classLoader = getClass().getClassLoader(); + URL[] urls = new URL[] { url }; + this.parentClassLoader = new URLClassLoader(urls, classLoader); + this.reloadClassLoader = new RestartClassLoader(this.parentClassLoader, urls); + } + + private File createSampleJarFile() throws IOException { + File file = this.temp.newFile("sample.jar"); + JarOutputStream jarOutputStream = new JarOutputStream(new FileOutputStream(file)); + jarOutputStream.putNextEntry(new ZipEntry(PACKAGE_PATH + "/Sample.class")); + StreamUtils.copy(getClass().getResourceAsStream("Sample.class"), jarOutputStream); + jarOutputStream.closeEntry(); + jarOutputStream.putNextEntry(new ZipEntry(PACKAGE_PATH + "/Sample.txt")); + StreamUtils.copy("fromchild", UTF_8, jarOutputStream); + jarOutputStream.closeEntry(); + jarOutputStream.close(); + return file; + } + + @Test + public void parentMustNotBeNull() throws Exception { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("Parent must not be null"); + new RestartClassLoader(null, new URL[] {}); + } + + @Test + public void getResourceFromReloadableUrl() throws Exception { + String content = readString(this.reloadClassLoader + .getResourceAsStream(PACKAGE_PATH + "/Sample.txt")); + assertThat(content, startsWith("fromchild")); + } + + @Test + public void getResourceFromParent() throws Exception { + String content = readString(this.reloadClassLoader + .getResourceAsStream(PACKAGE_PATH + "/Parent.txt")); + assertThat(content, startsWith("fromparent")); + } + + @Test + public void getResourcesFiltersDuplicates() throws Exception { + List resources = toList(this.reloadClassLoader.getResources(PACKAGE_PATH + + "/Sample.txt")); + assertThat(resources.size(), equalTo(1)); + } + + @Test + public void loadClassFromReloadableUrl() throws Exception { + Class loaded = this.reloadClassLoader.loadClass(PACKAGE + ".Sample"); + assertThat(loaded.getClassLoader(), equalTo((ClassLoader) this.reloadClassLoader)); + } + + @Test + public void loadClassFromParent() throws Exception { + Class loaded = this.reloadClassLoader.loadClass(PACKAGE + ".SampleParent"); + assertThat(loaded.getClassLoader(), equalTo(getClass().getClassLoader())); + } + + private String readString(InputStream in) throws IOException { + return new String(FileCopyUtils.copyToByteArray(in)); + } + + private List toList(Enumeration enumeration) { + List list = new ArrayList(); + if (enumeration != null) { + while (enumeration.hasMoreElements()) { + list.add(enumeration.nextElement()); + } + } + return list; + } + +} diff --git a/spring-boot-developer-tools/src/test/java/org/springframework/boot/developertools/restart/classloader/Sample.java b/spring-boot-developer-tools/src/test/java/org/springframework/boot/developertools/restart/classloader/Sample.java new file mode 100644 index 0000000000..ba80dea846 --- /dev/null +++ b/spring-boot-developer-tools/src/test/java/org/springframework/boot/developertools/restart/classloader/Sample.java @@ -0,0 +1,26 @@ +/* + * 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 sample class used to test reloading. + * + * @author Phillip Webb + */ +public class Sample { + +} diff --git a/spring-boot-developer-tools/src/test/java/org/springframework/boot/developertools/restart/classloader/SampleParent.java b/spring-boot-developer-tools/src/test/java/org/springframework/boot/developertools/restart/classloader/SampleParent.java new file mode 100644 index 0000000000..7d53252954 --- /dev/null +++ b/spring-boot-developer-tools/src/test/java/org/springframework/boot/developertools/restart/classloader/SampleParent.java @@ -0,0 +1,26 @@ +/* + * 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 sample class used to test reloading. + * + * @author Phillip Webb + */ +public class SampleParent { + +}