package com.laytonsmith.PureUtilities.ClassLoading; import com.laytonsmith.PureUtilities.Common.ReflectionUtils; import java.io.IOException; import java.net.URL; import java.net.URLClassLoader; import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; /** * This class extends ClassLoader, but allows for new jars/classpath elements * to be added at runtime. Note that already loaded classes won't be affected * if a new jar is added, but new requests for classes will be. */ public class DynamicClassLoader extends ClassLoader { private final Map<URL, URLClassLoader> classLoaders = new LinkedHashMap<URL, URLClassLoader>(); private final Set<URL> urls = new HashSet<URL>(); private boolean destroyed = false; /** * Adds a jar to this class loader instance. This can be done at runtime, * and if a jar already defines a class (or the class has already been loaded) * it cannot be changed inside this instance. Adding the same URL twice has * no effect. * @param url The url to the jar. */ public synchronized void addJar(URL url){ checkDestroy(); if(urls.contains(url)){ return; } urls.add(url); classLoaders.put(url, new URLClassLoader(new URL[]{url}, DynamicClassLoader.class.getClassLoader())); } /** * Remove a jar from this class loader instance. Adding the same URL twice has * no effect. * * @param url */ public synchronized void removeJar(URL url) { checkDestroy(); if(!urls.contains(url)){ return; } urls.remove(url); URLClassLoader l = classLoaders.remove(url); try { l.close(); } catch (IOException ex) { // Whatever. } } @Override protected synchronized Package getPackage(String name) { for(ClassLoader c : classLoaders.values()){ Package p = (Package) ReflectionUtils.invokeMethod(c.getClass(), c, "getPackage", new Class[]{String.class}, new Object[]{name}); if(p != null){ return p; } } return null; } @Override protected synchronized Package[] getPackages() { List<Package> packages = new ArrayList<Package>(); for(ClassLoader c : classLoaders.values()){ packages.addAll(Arrays.asList((Package[])ReflectionUtils.invokeMethod(c.getClass(), c, "getPackages"))); } return packages.toArray(new Package[packages.size()]); } @Override protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { checkDestroy(); try{ //If the parent class knows about it, we're done. Class c = Class.forName(name, resolve, DynamicClassLoader.class.getClassLoader()); return c; } catch(ClassNotFoundException ex){ //Otherwise we need to find the class ourselves. for(URLClassLoader url : classLoaders.values()){ try{ Class c = url.loadClass(name); if(resolve){ resolveClass(c); } return c; } catch(ClassNotFoundException ex1){ //Don't care, move on to the next class loader } } throw new ClassNotFoundException(name); } } /** * When this class is no longer needed, this method can be called * to destroy all the internal references and "lock" the class for * future use. If any method (other than destroy) is attempted to be * called in this instance after this method is called, it will throw * a runtime exception, which should hopefully help catch errors faster. * TODO: Use this in the code! */ public synchronized void destroy(){ destroyed = true; classLoaders.clear(); urls.clear(); } private void checkDestroy(){ if(destroyed){ throw new RuntimeException("Cannot access this instance of " + DynamicClassLoader.class.getSimpleName() + ", as it has already been destroyed."); } } @Override @SuppressWarnings("FinalizeDeclaration") protected void finalize() throws Throwable { super.finalize(); destroy(); } }