001/* 002 * Copyright 2012-2018 the original author or authors. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package org.springframework.boot.devtools.restart.classloader; 018 019import java.io.IOException; 020import java.net.MalformedURLException; 021import java.net.URL; 022import java.net.URLClassLoader; 023import java.security.AccessController; 024import java.security.PrivilegedAction; 025import java.util.Enumeration; 026 027import org.apache.commons.logging.Log; 028import org.apache.commons.logging.LogFactory; 029 030import org.springframework.boot.devtools.restart.classloader.ClassLoaderFile.Kind; 031import org.springframework.core.SmartClassLoader; 032import org.springframework.util.Assert; 033 034/** 035 * Disposable {@link ClassLoader} used to support application restarting. Provides parent 036 * last loading for the specified URLs. 037 * 038 * @author Andy Clement 039 * @author Phillip Webb 040 * @since 1.3.0 041 */ 042public class RestartClassLoader extends URLClassLoader implements SmartClassLoader { 043 044 private final Log logger; 045 046 private final ClassLoaderFileRepository updatedFiles; 047 048 /** 049 * Create a new {@link RestartClassLoader} instance. 050 * @param parent the parent classloader 051 * @param urls the urls managed by the classloader 052 */ 053 public RestartClassLoader(ClassLoader parent, URL[] urls) { 054 this(parent, urls, ClassLoaderFileRepository.NONE); 055 } 056 057 /** 058 * Create a new {@link RestartClassLoader} instance. 059 * @param parent the parent classloader 060 * @param updatedFiles any files that have been updated since the JARs referenced in 061 * URLs were created. 062 * @param urls the urls managed by the classloader 063 */ 064 public RestartClassLoader(ClassLoader parent, URL[] urls, 065 ClassLoaderFileRepository updatedFiles) { 066 this(parent, urls, updatedFiles, LogFactory.getLog(RestartClassLoader.class)); 067 } 068 069 /** 070 * Create a new {@link RestartClassLoader} instance. 071 * @param parent the parent classloader 072 * @param updatedFiles any files that have been updated since the JARs referenced in 073 * URLs were created. 074 * @param urls the urls managed by the classloader 075 * @param logger the logger used for messages 076 */ 077 public RestartClassLoader(ClassLoader parent, URL[] urls, 078 ClassLoaderFileRepository updatedFiles, Log logger) { 079 super(urls, parent); 080 Assert.notNull(parent, "Parent must not be null"); 081 Assert.notNull(updatedFiles, "UpdatedFiles must not be null"); 082 Assert.notNull(logger, "Logger must not be null"); 083 this.updatedFiles = updatedFiles; 084 this.logger = logger; 085 if (logger.isDebugEnabled()) { 086 logger.debug("Created RestartClassLoader " + toString()); 087 } 088 } 089 090 @Override 091 public Enumeration<URL> getResources(String name) throws IOException { 092 // Use the parent since we're shadowing resource and we don't want duplicates 093 Enumeration<URL> resources = getParent().getResources(name); 094 ClassLoaderFile file = this.updatedFiles.getFile(name); 095 if (file != null) { 096 // Assume that we're replacing just the first item 097 if (resources.hasMoreElements()) { 098 resources.nextElement(); 099 } 100 if (file.getKind() != Kind.DELETED) { 101 return new CompoundEnumeration<>(createFileUrl(name, file), resources); 102 } 103 } 104 return resources; 105 } 106 107 @Override 108 public URL getResource(String name) { 109 ClassLoaderFile file = this.updatedFiles.getFile(name); 110 if (file != null && file.getKind() == Kind.DELETED) { 111 return null; 112 } 113 URL resource = findResource(name); 114 if (resource != null) { 115 return resource; 116 } 117 return getParent().getResource(name); 118 } 119 120 @Override 121 public URL findResource(String name) { 122 final ClassLoaderFile file = this.updatedFiles.getFile(name); 123 if (file == null) { 124 return super.findResource(name); 125 } 126 if (file.getKind() == Kind.DELETED) { 127 return null; 128 } 129 return AccessController 130 .doPrivileged((PrivilegedAction<URL>) () -> createFileUrl(name, file)); 131 } 132 133 @Override 134 public Class<?> loadClass(String name, boolean resolve) 135 throws ClassNotFoundException { 136 String path = name.replace('.', '/').concat(".class"); 137 ClassLoaderFile file = this.updatedFiles.getFile(path); 138 if (file != null && file.getKind() == Kind.DELETED) { 139 throw new ClassNotFoundException(name); 140 } 141 synchronized (getClassLoadingLock(name)) { 142 Class<?> loadedClass = findLoadedClass(name); 143 if (loadedClass == null) { 144 try { 145 loadedClass = findClass(name); 146 } 147 catch (ClassNotFoundException ex) { 148 loadedClass = getParent().loadClass(name); 149 } 150 } 151 if (resolve) { 152 resolveClass(loadedClass); 153 } 154 return loadedClass; 155 } 156 } 157 158 @Override 159 protected Class<?> findClass(String name) throws ClassNotFoundException { 160 String path = name.replace('.', '/').concat(".class"); 161 final ClassLoaderFile file = this.updatedFiles.getFile(path); 162 if (file == null) { 163 return super.findClass(name); 164 } 165 if (file.getKind() == Kind.DELETED) { 166 throw new ClassNotFoundException(name); 167 } 168 return AccessController.doPrivileged((PrivilegedAction<Class<?>>) () -> { 169 byte[] bytes = file.getContents(); 170 return defineClass(name, bytes, 0, bytes.length); 171 }); 172 } 173 174 private URL createFileUrl(String name, ClassLoaderFile file) { 175 try { 176 return new URL("reloaded", null, -1, "/" + name, 177 new ClassLoaderFileURLStreamHandler(file)); 178 } 179 catch (MalformedURLException ex) { 180 throw new IllegalStateException(ex); 181 } 182 } 183 184 @Override 185 protected void finalize() throws Throwable { 186 if (this.logger.isDebugEnabled()) { 187 this.logger.debug("Finalized classloader " + toString()); 188 } 189 super.finalize(); 190 } 191 192 @Override 193 public boolean isClassReloadable(Class<?> classType) { 194 return (classType.getClassLoader() instanceof RestartClassLoader); 195 } 196 197 /** 198 * Compound {@link Enumeration} that adds an additional item to the front. 199 */ 200 private static class CompoundEnumeration<E> implements Enumeration<E> { 201 202 private E firstElement; 203 204 private final Enumeration<E> enumeration; 205 206 CompoundEnumeration(E firstElement, Enumeration<E> enumeration) { 207 this.firstElement = firstElement; 208 this.enumeration = enumeration; 209 } 210 211 @Override 212 public boolean hasMoreElements() { 213 return (this.firstElement != null || this.enumeration.hasMoreElements()); 214 } 215 216 @Override 217 public E nextElement() { 218 if (this.firstElement == null) { 219 return this.enumeration.nextElement(); 220 } 221 E element = this.firstElement; 222 this.firstElement = null; 223 return element; 224 } 225 226 } 227 228}