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}