/* * ALMA - Atacama Large Millimiter Array * (c) European Southern Observatory, 2004 * Copyright by ESO (in the framework of the ALMA collaboration), * All rights reserved * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, * MA 02111-1307 USA */ package alma.acs.classloading; import java.io.File; import java.net.MalformedURLException; import java.net.URISyntaxException; import java.net.URL; import java.net.URLClassLoader; import java.util.ArrayList; import java.util.Arrays; import java.util.Iterator; import java.util.List; import java.util.StringTokenizer; import alma.acs.makesupport.AcsJarFileFinder; /** * Custom class loader which correctly handles multiple occurrences of identical jar files in * "overlayed" directories. These directories would typically be ../lib, INTROOT/lib, ACSROOT/lib and ORB dirs. * <p> * The main purpose is to cut down the very long classpath produced otherwise by the ACS start scripts. * Only the jar file that contains this class loader must be supplied to the JVM using the "-classpath" option. * The JVM must be started with property definitions similar to: * <ul> * <li><code>-Djava.system.class.loader=alma.acs.classloading.AcsSystemClassLoader</code>. * <li><code>-Dacs.system.classpath.jardirs=../lib/:$INTROOT/lib:$ACSROOT/lib</code>. * If an operating system has a native style path separator different from "<code>:</code>", it can optionally be used instead. * </ul> * Notes on the classloader hierarchies: * <ul> * <li>Don't confuse the inheritance hierarchy and the delegation hierarchy. * Both this CL as well as its delegation-parent inherit from <code>URLClassLoader</code>. * <li>This classloader is meant to be registered with the JVM using the property <code>java.system.class.loader</code>. * <li>Instead of using the real system classloader (<code>sun.misc.Launcher$AppClassLoader</code>) as the parent, * since ACS 6.0 this CL takes over the functionality of the direct parent, and uses the original grandparent CL for parent-delegation. * Circumventing the original parent CL after copying its CLASSPATH entries, removes the distinction between the CLASSPATH entries * and the jar files from the <code>acs.system.classpath.jardirs</code> directories. * <ul> * <li>The effect is that we can add jar files to the CLASSPATH, which use classes from the normal pool of jar files. * One use case for this is testing component implementation classes outside of containers: the jar files under * <code>../lib/ACScomponents</code> are not picked up by the classloader unless we add them to the CLASSPATH, * e.g. using <code>acsStartJava -addToClasspath</code>. * <li>The CL hierarchy becomes (1) Bootstrap CL, (2) Extention CL, (3) AcsSystemClassLoader, [(4) optional component CL etc] * </ul> * <li>The method {@link java.lang.ClassLoader#getSystemClassLoader()} "tricks" the caller and returns this class loader, * even though it thinks that the real system CL is our circumvented sibling, the <code>sun.misc.Launcher$AppClassLoader</code>. * This allows JUnit tests run, since the JUnit framework sets up their own classloader to be a child * of the system classloader, shortcutting any "regular" child class loaders in between. * </ul> * * @author hsommer created Sep 13, 2004 6:31:32 PM */ public class AcsSystemClassLoader extends URLClassLoader { public static final String PROPERTY_JARDIRS = "acs.system.classpath.jardirs"; public static final String PROPERTY_TOPJARSONLY = "acs.system.classpath.topjarsonly"; public static final String PROPERTY_CLASSLOADERVERBOSE = "acs.system.classloader.verbose"; private boolean verbose; private final JarOrderOptimizer optimizer; private final URLClassLoader appClassLoader; /** * Constructor will be called by the JVM. * The standard system class loader (instance of <code>sun.misc.Launcher$AppClassLoader</code> * is expected to be the <code>parent</code> parameter, although no such assumption is made in the code. * <p> * In the ctor, the directories given in <code>acs.system.classpath.jardirs</code> are searched for JAR files, * which are fed as URLs into the classpath maintained by the <code>URLClassLoader</code> base class. * * @see AcsJarFileFinder */ public AcsSystemClassLoader(ClassLoader parent) { super(new URL[0], parent.getParent()); if (!(parent instanceof URLClassLoader)) { throw new IllegalStateException("AcsSystemClassLoader expects parent CL of type URLClassLoader!"); } appClassLoader = (URLClassLoader) parent; verbose = Boolean.getBoolean(PROPERTY_CLASSLOADERVERBOSE); List<File> jarFileList = new ArrayList<File>(); optimizer = new JarOrderOptimizer(verbose); String jarDirPath = System.getProperty(PROPERTY_JARDIRS); if (jarDirPath != null) { File[] jarDirs = parseJarDirs(jarDirPath); // extract jar files from directories AcsJarFileFinder jarFinder = new AcsJarFileFinder(jarDirs, null); jarFileList.addAll(Arrays.asList(jarFinder.getAllFiles())); // test mode if (Boolean.getBoolean(PROPERTY_TOPJARSONLY)) { System.out.println("AcsSystemClassLoader running in test mode: will only use jar files from priority list."); jarFileList = optimizer.getTopJarsOnly(jarFileList); } // sort the jar files optimizer.sortJars(jarFileList); } else { System.err.println("No jar path given in property " + PROPERTY_JARDIRS + "! If that is intended, you should not use AcsSystemClassLoader at all!"); // Still we go on, acting just like the normal system class loader. } // we prepend the CLASSPATH entries so that this CL can replace the normal JVM application CL prependCLASSPATHJars(jarFileList); // add the jars and directories for (Iterator<File> iter = jarFileList.iterator(); iter.hasNext();) { File cpEntry = iter.next(); try { addURL(cpEntry.toURI().toURL()); if (verbose) { System.out.println("added " + cpEntry.getAbsolutePath() + " to the custom system class loader's path."); } } catch (Exception e) { System.err.println("failed to add " + cpEntry.getAbsolutePath() + " to the custom class loader's path." + e.toString()); } } } private File[] parseJarDirs(String jarDirPath) { StringTokenizer tok = new StringTokenizer(jarDirPath, File.pathSeparator); File[] jarDirs = new File[tok.countTokens()]; for (int i = 0; tok.hasMoreTokens(); i++) { jarDirs[i] = new File(tok.nextToken()); } return jarDirs; } /** * Extracts the CLASSPATH jar files (or directories) from the parent class loader * and adds them at the beginning of the given list. * <p> * It is assumed that thanks to the <code>java.system.class.loader</code> property, * this AcsSystemClassLoader is a direct child of the application system class loader * which normally would handle the CLASSPATH. */ private void prependCLASSPATHJars(List<File> jarFileList) { if (verbose) { System.out.print("Will prepend the following entries from the CLASSPATH to AcsSystemClassLoader's entries: "); } URL[] sysCP = appClassLoader.getURLs(); List<File> sysCPList = new ArrayList<File>(); for (int i = 0; i < sysCP.length; i++) { try { File entry = new File(sysCP[i].toURI()); sysCPList.add(entry); if (verbose) { System.out.print(entry.getAbsolutePath() + ", "); } } catch (URISyntaxException e) { System.err.println("Failed to add CLASSPATH entry '" + sysCP[i] + "' to AcsSystemClassLoader: " + e.toString()); } } if (verbose) { System.out.println(); } jarFileList.addAll(0, sysCPList); } protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { if (verbose) { if (ClassLoader.getSystemClassLoader() != this) { System.err.println("Warning: AcsSystemClassLoader is not registered as the JVM's system class loader!"); } } // First, check if the class has already been loaded by this classloader Class c = findLoadedClass(name); if (c == null) { // don't delegate to parent class loader for classes from this module (this jar must be on the system class path!) // The shortcutting will also be a tiny performance improvement since all "alma.acs.xyz" classes even from other jar files // are supposed to be loaded through this class loader, in which case the parent would only delegate back to us anyway. if (name.startsWith("alma.acs.") || name.startsWith("com.cosylab.util") ) { try { c = findClass(name); } catch (ClassNotFoundException e) { // fallthrough: try parent class loader after all; // perhaps some jar files with ACS classes were put on the system class path } } if (c == null) { // The super implementation will delegate to the parent class loader, and only call findClass(name) if required. // This is the default for all well-behaved class loaders: first try parent, then self c = super.loadClass(name, false); } } if (resolve) { resolveClass(c); } return c; } /** * Delegates to base class implementation. * <p> * Discards requests to load classes from certain sub-packages of <code>sun.</code> or <code>com.sun.</code> * by throwing a <code>ClassNotFoundException</code>. * The JDK libs try to search (e.g. during ACS container startup) for some optional system classes such as * <code>sun.text.resources.DateFormatZoneData_en_US</code> or <code>sun.util.logging.resources.logging_en</code>. * We know that ALMA jar files do not contain any classes in those packages, and can thus speed up the applications, * in cases where those classes have already been searched unsuccessfully by any parent class loaders, * which as the last step desparately would ask this class loader, which would cause a search through <b>all</b> jar files. * sun.awt.resources. * com.sun.swing.internal.plaf. */ protected Class<?> findClass(String name) throws ClassNotFoundException { if (optimizer.isClassKnownToBeUnavailable(name)) { throw new ClassNotFoundException("ACS and application software are not supposed to deliver a class like '" + name + "' and therefore AcsSystemClassLoader won't attempt to load it (for IO optimization)."); } Class<?> clazz = null; try { // System.out.println("****AcsSystemClassLoader will try to find class " + name); clazz = super.findClass(name); } catch (ClassNotFoundException e) { if (verbose) { System.err.println(AcsSystemClassLoader.class.getName() + " failed to load class " + name); e.printStackTrace(); } throw e; } return clazz; } /** * This method, even though private, allows jconsole and possibly similar profilers and other tools * to add their required jar files, * see {@linkplain java.lang.instrument.Instrumentation#appendToSystemClassLoaderSearch(java.util.jar.JarFile)}. * <p> * For example, jconsole would currently call this method with argument * <code>/usr/java/jdk1.6.0_02/jre/lib/management-agent.jar </code>. * * @since ACS 7.0 */ private void appendToClassPathForInstrumentation(String jarPathName) { File jarFile = new File(jarPathName); try { addURL(jarFile.toURI().toURL()); System.out.println("AcsSystemClassLoader#appendToClassPathForInstrumentation called with jar file " + jarPathName); } catch (MalformedURLException ex) { System.err.println("AcsSystemClassLoader#appendToClassPathForInstrumentation failed with MalformedURLException for jar file " + jarPathName); } } }