| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you 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.apache.myfaces.extensions.scripting.loader; |
| |
| import org.apache.commons.logging.Log; |
| import org.apache.commons.logging.LogFactory; |
| import org.apache.myfaces.extensions.scripting.loader.support.ClassFileLoader; |
| import org.apache.myfaces.extensions.scripting.loader.support.ThrowAwayClassLoader; |
| import org.apache.myfaces.extensions.scripting.loader.support.OverridingClassLoader; |
| |
| import java.io.File; |
| import java.io.IOException; |
| import java.net.URL; |
| import java.net.URLClassLoader; |
| import java.util.ArrayList; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.Map; |
| |
| /** |
| * <p>A class loader implementation that enables you to reload certain classes. It automatically |
| * reloads classes if there's a newer version of a .class file available in a specified compilation |
| * target path. However, it's also possible to explicitly reload other classes.</p> |
| * <p/> |
| * <p>This enables you to do both modify and reload various classes that you've used for Spring |
| * bean definitions, but it also enables you to reload for example classes depending on those |
| * dynamically compiled classes, like factory bean classes. By explicitly reloading a factory |
| * bean class the newly loaded factory bean will return updated bean instances as well!</p> |
| * <p/> |
| * <p>Note that even though this class extends the class URLClassLoader it doesn't use any |
| * of its functionalities. This class loader just works similar and provides a similar interface |
| * so it's useful to extend the class URLClassLoader as you can treat it like one (especially |
| * when it comes to resolving the classpath of a class loader).</p> |
| */ |
| public class ReloadingClassLoader extends URLClassLoader { |
| |
| /** |
| * The system-dependent default name-separator character. Note that it's safe to |
| * use this version of the file separator in regex methods, like replaceAll(). |
| */ |
| private static String FILE_SEPARATOR = File.separator; |
| |
| static { |
| if ("\\".equals(FILE_SEPARATOR)) { |
| FILE_SEPARATOR = "\\\\"; |
| } |
| } |
| |
| /** |
| * The logger instance for this class. |
| */ |
| private static final Log logger = LogFactory.getLog(ReloadingClassLoader.class); |
| |
| /** |
| * A table of class names and the according class loaders. It's basically like |
| * a list of classes that this class loader has already loaded. However, the |
| * thing is that this class loader isn't actually going to load them as we |
| * would loose the possibility to override them then, which is the reason why |
| * each class has got its own class loader. |
| */ |
| private Map<String, ThrowAwayClassLoader> classLoaders = |
| new HashMap<String, ThrowAwayClassLoader>(); |
| |
| /** |
| * A list of reloading listeners that this class loader notifies once a class |
| * has been reloaded. |
| */ |
| private List<ClassReloadingListener> reloadingListeners = new ArrayList<ClassReloadingListener>(); |
| |
| /** |
| * The target directory for the compiler, i.e. the directory that contains the |
| * dynamically compiled .class files. |
| */ |
| private File compilationDirectory; |
| |
| // ------------------------------------------ Constructors |
| |
| /** |
| * <p>Constructs a new reloading class loader for the specified compilation |
| * directory using the default delegation parent class loader. Note that this |
| * class loader will only delegate to the parent class loader if there's no |
| * dynamically compiled class available.</p> |
| * |
| * @param compilationDirectory the compilation directory |
| */ |
| public ReloadingClassLoader(File compilationDirectory) { |
| super(new URL[0]); |
| this.compilationDirectory = compilationDirectory; |
| |
| // Register a default logging listener |
| registerReloadingListener(new LoggingClassReloadingListener()); |
| } |
| |
| /** |
| * <p>Constructs a new reloading class loader for the specified compilation |
| * directory using the given delegation parent class loader. Note that this |
| * class loader will only delegate to the parent class loader if there's no |
| * dynamically compiled class available.</p> |
| * |
| * @param parentClassLoader the parent class loader |
| * @param compilationDirectory the compilation directory |
| */ |
| public ReloadingClassLoader(ClassLoader parentClassLoader, File compilationDirectory) { |
| super(new URL[0], parentClassLoader); |
| this.compilationDirectory = compilationDirectory; |
| |
| // Register a default logging listener |
| registerReloadingListener(new LoggingClassReloadingListener()); |
| } |
| |
| // ------------------------------------------ URLClassLoader methods |
| |
| /** |
| * <p>Loads the class with the specified binary name. This method searches for classes in the |
| * compilation directory that you've specified previously. Note that this class loader recognizes |
| * whether the class files have changed, that means, if you recompile and reload a class, you'll |
| * get a Class object that represents the recompiled class.</p> |
| * |
| * @param className the binary name of the class you want to load |
| * @param resolve <tt>true</tt>, if the class is to be resolved |
| * @return The resulting <tt>Class</tt> object |
| * @throws ClassNotFoundException if the class could not be found |
| */ |
| protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException { |
| // First of all, check if there's a class file available in the compilation target path. |
| // It doesn't matter which class we're dealing with at the moment as there's always the |
| // possibility that the user is either trying to override a statically compiled class |
| // (i.e. a class that has been compiled before deploying the application) or he/she is |
| // trying to modify a dynamically compiled class, in which case we should compare |
| // timestamps, etc. |
| File classFile = resolveClassFile(className); |
| if (classFile != null && classFile.exists()) { |
| if (classLoaders.containsKey(className)) { |
| // Check if the class loader is already outdated, i.e. there is a newer class file available |
| // for the class we want to load than the class file we've already loaded. If that's the case |
| // we're going to throw away this ClassLoader and create a new one for linkage reasons. |
| ThrowAwayClassLoader classLoader = classLoaders.get(className); |
| if (classLoader.isOutdated(classFile.lastModified())) { |
| // If the class loader is outdated, create a new one. Otherwise the same class loader |
| // would have to load the same class twice or more often which would cause severe |
| // linkage errors. Actually the JVM wouldn't permit that anyway and throw some |
| // linkage errors / exceptions. |
| reloadClass(className); |
| } |
| } else { |
| if (logger.isTraceEnabled()) { |
| logger.trace("A new dynamic class '" |
| + className + "' has been found by this class loader '" + this + "'."); |
| } |
| |
| // We haven't loaded this class so far, but there is a .class file available, |
| // so we have to reload the given class. |
| reloadClass(className); |
| } |
| |
| ThrowAwayClassLoader classLoader = classLoaders.get(className); |
| return classLoader.loadClass(className, resolve); |
| } else { |
| // Even though there is no class file available, there's still a chance that this |
| // class loader has forcefully reloaded a statically compiled class. |
| if (classLoaders.containsKey(className)) { |
| ThrowAwayClassLoader classLoader = classLoaders.get(className); |
| return classLoader.loadClass(className, resolve); |
| } else { |
| // However, if there's neither a .class file nor a reloadable class loader |
| // available, just delegate to the parent class loader. |
| return super.loadClass(className, resolve); |
| } |
| } |
| } |
| |
| /** |
| * <p>Returns the search path of URLs for loading classes, i.e. the |
| * given compilation target directory, the directory that contains the |
| * dynamically compiled .class files.</p> |
| * |
| * @return the search path of URLs for loading classes |
| */ |
| public URL[] getURLs() { |
| try { |
| return new URL[]{ compilationDirectory.toURI().toURL() }; |
| } catch (IOException ex) { |
| logger.error("Couldn't resolve the URL to the compilation directory '" + compilationDirectory + "'.", ex); |
| return new URL[0]; |
| } |
| } |
| |
| |
| // ------------------------------------------ Public methods |
| |
| /** |
| * <p>Determines whether the given class has been loaded by a class |
| * loader that is already outdated.</p> |
| * |
| * @param classObj the class you want to check |
| * @return <code>true</code, if there is a newer class file available for the given object |
| */ |
| public boolean isOutdated(Class classObj) { |
| // Is there even a dynamically compiled class file available for the given class? |
| File classFile = resolveClassFile(classObj.getName()); |
| if (classFile.exists()) { |
| // If so, check if we the Class reference has been loaded by a ThrowAwayClassLoader. |
| // Otherwise it's definitely outdated and we don't have to compare timestamps. |
| if (classObj.getClassLoader() instanceof ThrowAwayClassLoader) { |
| // Compare the timestamps in order to determine whether the given Class |
| // reference is already outdated. |
| ThrowAwayClassLoader classLoader = (ThrowAwayClassLoader) classObj.getClassLoader(); |
| return classLoader.isOutdated(classFile.lastModified()); |
| } else { |
| return true; |
| } |
| } |
| |
| return false; |
| } |
| |
| /** |
| * <p>Reloads the given class internally explicitly. Note that this class loader usually |
| * reloads classes automatically, i.e. this class loader detects if there is a newer |
| * version of a class file available in the compilation directory. However, by using |
| * this method you tell this class loader to forcefully reload the given class. For |
| * example, if you've got a newer version of a dynamically recompiled class and a |
| * statically compiled class depending on this one, you can tell this class loader to |
| * reload the statically compiled class as well so that it references the correct |
| * version of the Class object.</p> |
| * |
| * @param className the class you want to reload |
| */ |
| public void reloadClass(String className) { |
| ThrowAwayClassLoader classLoader; |
| |
| File classFile = resolveClassFile(className); |
| if (classFile != null && classFile.exists()) { |
| classLoader = new ClassFileLoader(className, classFile, this); |
| } else { |
| classLoader = new OverridingClassLoader(className, this); |
| } |
| |
| // Replace the class loader in the table and fire the according event. |
| ThrowAwayClassLoader oldClassLoader = classLoaders.put(className, classLoader); |
| synchronized (reloadingListeners) { |
| for (ClassReloadingListener reloadingListener : reloadingListeners) { |
| reloadingListener.classReloaded(oldClassLoader, classLoader, className); |
| } |
| } |
| } |
| |
| /** |
| * <p>Registers a new reloading listener. Afterwards the given listener |
| * will be notified if this class loader reloads or loads a class.</p> |
| * |
| * @param reloadingListener the reloading listener you want to register |
| */ |
| public void registerReloadingListener(ClassReloadingListener reloadingListener) { |
| synchronized (reloadingListeners) { |
| if (reloadingListener != null) { |
| reloadingListeners.add(reloadingListener); |
| } |
| } |
| } |
| |
| /** |
| * <p>Returns a copy of the current reloading class loader with the only difference |
| * being the parent class loader to use. Use this method if you just want to replace |
| * the parent class loader (obviously you can't do that after a ClassLoader has been |
| * created, hence a copy is created).</p> |
| * |
| * @param parentClassLoader the parent ClassLoader to use |
| * @return a copy of the current reloading class loader |
| */ |
| public ReloadingClassLoader cloneWithParentClassLoader(ClassLoader parentClassLoader) { |
| ReloadingClassLoader classLoader = |
| cloneClassLoader(parentClassLoader, compilationDirectory); |
| |
| // Note that we don't have to create "deep copies" as the class loaders in the map |
| // are immutable anyway (they are only supposed to load a single class) and additionally |
| // this map doesn't contain classes that have been loaded using the current parent |
| // class loader! |
| classLoader.classLoaders = new HashMap<String, ThrowAwayClassLoader>(classLoaders); |
| classLoader.reloadingListeners = new ArrayList<ClassReloadingListener>(reloadingListeners); |
| |
| return classLoader; |
| } |
| |
| // ------------------------------------------ Utility methods |
| |
| /** |
| * <p>Creates and returns new instance of a reloading class loader which is basically a clone of this one.</p> |
| * |
| */ |
| protected ReloadingClassLoader cloneClassLoader(ClassLoader parentClassLoader, File compilationDirectory) { |
| return new ReloadingClassLoader(parentClassLoader, compilationDirectory); |
| } |
| |
| /** |
| * <p>Resolves and returns a File handle that represents the class file of |
| * the given class on the file system. However, note that this method only |
| * returns <code>null</code> if an error occured while resolving the class |
| * file. A non-null valuee doesn't necessarily mean that the class file |
| * actually exists. In oder to check the existence call the according |
| * method on the returned object.</p> |
| * |
| * @param className the name of the class that you want to resolve |
| * @return a File handle that represents the class file of the given class |
| * on the file system |
| * @see java.io.File#exists() |
| */ |
| protected File resolveClassFile(String className) { |
| // This method just has to look in the specified compilation directory. The |
| // relative class file path can be computed from the class name. |
| return new File(compilationDirectory, |
| className.replaceAll("\\.", FILE_SEPARATOR).concat(".class")); |
| } |
| |
| // ------------------------------------------ Private classes |
| |
| private static class LoggingClassReloadingListener implements ClassReloadingListener { |
| |
| public void classReloaded(ThrowAwayClassLoader oldClassLoader, ThrowAwayClassLoader newClassLoader, String className) { |
| if (logger.isInfoEnabled()) { |
| if (oldClassLoader != null) { |
| logger.info("Replaced the class loader '" + oldClassLoader + "' with the class loader '" |
| + newClassLoader + "' as this class loader is supposed to reload the class '" + className + "'."); |
| } else { |
| logger.info("Installed a new class loader '" + newClassLoader + "' for the class '" |
| + className + "' as this class loader is supposed to reload it."); |
| } |
| } |
| } |
| } |
| |
| } |