All Downloads are FREE. Search and download functionalities are using the official Maven repository.

patterntesting.runtime.monitor.ClasspathMonitor Maven / Gradle / Ivy

Go to download

PatternTesting Runtime (patterntesting-rt) is the runtime component for the PatternTesting framework. It provides the annotations and base classes for the PatternTesting testing framework (e.g. patterntesting-check, patterntesting-concurrent or patterntesting-exception) but can be also used standalone for classpath monitoring or profiling. It uses AOP and AspectJ to perform this feat.

There is a newer version: 2.5.1
Show newest version
/**
 * $Id: ClasspathMonitor.java,v 1.85 2016/03/25 13:13:17 oboehm Exp $
 *
 * Copyright (c) 2008 by Oliver Boehm
 *
 * Licensed 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 orimplied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * (c)reated 10.02.2009 by oliver ([email protected])
 */

package patterntesting.runtime.monitor;

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.io.Writer;
import java.lang.management.ManagementFactory;
import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.net.URI;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.FutureTask;
import java.util.concurrent.TimeUnit;
import java.util.jar.Attributes;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

import javax.management.JMException;
import javax.management.MBeanServer;
import javax.management.ObjectName;

import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import patterntesting.runtime.NullConstants;
import patterntesting.runtime.annotation.ProfileMe;
import patterntesting.runtime.io.FileHelper;
import patterntesting.runtime.jmx.AnnotatedStandardMBean;
import patterntesting.runtime.jmx.Description;
import patterntesting.runtime.jmx.MBeanHelper;
import patterntesting.runtime.monitor.internal.ClasspathDigger;
import patterntesting.runtime.monitor.internal.DoubletDigger;
import patterntesting.runtime.util.ArchivEntry;
import patterntesting.runtime.util.ClasspathHelper;
import patterntesting.runtime.util.Converter;
import patterntesting.runtime.util.ObjectComparator;
import patterntesting.runtime.util.ReflectionHelper;
import patterntesting.runtime.util.ThreadUtil;

/**
 * To avoid classpath problems like double entries of the same class or resource
 * in the classpath there are several methods available.
 * 

* To get the boot classpath the system property "sun.boot.class.path" is used * to get them. This will work of course only for the SunVM. *

*

* After an idea from * Java * Tip 105. *

*

* With v1.5 the startup time and performance was increased. To speed up * time a {@link FutureTask} is used to set up internal structurs. For a * better performance doublets are now detected with parallel threads. Also * some expensive operations are cached now. *

*

* Because some stuff are done in parallel we no longer use a WeakHashMap but * {@link ConcurrentHashMap} to avoid synchronization problems. It is expected * that the size of cached objects is small enough and we will not run in any * OutOfMemory problems. *

* * @author oliver * @version $Revision: 1.85 $ * @since 10.02.2009 */ public class ClasspathMonitor extends AbstractMonitor implements ClasspathMonitorMBean { private static final long serialVersionUID = 20090511L; private static final Logger LOG = LoggerFactory.getLogger(ClasspathMonitor.class); private static final Executor EXECUTOR = Executors.newCachedThreadPool(); private static final ClasspathMonitor INSTANCE; /** This array is used to call the corresponding getter method. */ private static final String[] DUMP_GETTERS = { "BootClasspath", "Classpath", "ClasspathClasses", "DoubletClasspath", "DoubletClasspathURIs", "Doublets", "LoadedClasses", "LoadedPackages", "IncompatibleClasses", "IncompatibleClasspath", "IncompatibleClasspathURIs", "UnusedClasses", "UnusedClasspath", "UsedClasspath", "UsedClasspathURIs" }; private static ObjectName mbeanName = MBeanHelper.getAsObjectName(ClasspathMonitor.class); private final transient ClasspathDigger classpathDigger; private final transient DoubletDigger doubletDigger; private final transient ClassLoader cloader; private final String[] classpath; private String[] loadedClasses = new String[0]; private final transient FutureTask allClasspathClasses; private final transient FutureTask> unusedClasses; private final Map, URI> usedClassCache = new ConcurrentHashMap, URI>(); private final Map, URI> usedClasspathCache = new ConcurrentHashMap, URI>(); private final List> incompatibleClassList = new ArrayList>(); static { INSTANCE = new ClasspathMonitor(); } /** * Instantiates a new classpath monitor. *

* Note: The constructor is protected because it is needed by the * benchmark subproject which compares different implementations. *

*/ protected ClasspathMonitor() { this.classpathDigger = new ClasspathDigger(); this.cloader = this.classpathDigger.getClassLoader(); this.classpath = this.classpathDigger.getClasspath(); this.allClasspathClasses = getFutureCasspathClasses(); this.unusedClasses = getFutureUnusedClasses(); this.doubletDigger = new DoubletDigger(this.classpathDigger); } private FutureTask getFutureCasspathClasses() { Callable callable = new Callable() { public String[] call() throws Exception { return getClasspathClassArray(); } }; FutureTask classes = new FutureTask(callable); EXECUTOR.execute(classes); return classes; } private FutureTask> getFutureUnusedClasses() { Callable> callable = new Callable>() { public Set call() throws Exception { return getClasspathClassSet(); } }; FutureTask> classes = new FutureTask>(callable); EXECUTOR.execute(classes); return classes; } /** * Yes, it is a Singleton because it offers only some services. So we don't * need the object twice. * * @return the only instance */ public static ClasspathMonitor getInstance() { return INSTANCE; } /** * With this method you can register the {@link ClasspathMonitor} with the * default name. *

* You can only register the {@link ClasspathMonitor} once only. If you * want to register it with anoter name you have to first unregister it. *

* * @see ClasspathMonitor#registerAsMBean(String) * @see ClasspathMonitor#unregisterAsMBean() */ public static void registerAsMBean() { registerAsMBean(mbeanName); } /** * With this method you can register the {@link ClasspathMonitor} with your * own name. This is e.g. useful if you have an application server with * several applications. *

* You can only register the {@link ClasspathMonitor} once only. If you * want to register it with anoter name you have to first unregister it. *

* * @param name e.g "my.company.ClasspathMonitor" * @see ClasspathMonitor#unregisterAsMBean() * @since 1.6 */ public static void registerAsMBean(final String name) { registerAsMBean(MBeanHelper.getAsObjectName(name)); } /** * With this method you can register the {@link ClasspathMonitor} with your * own name. This is e.g. useful if you have an application server with * several applications. *

* You can only register the {@link ClasspathMonitor} once only. If you * want to register it with anoter name you have to first unregister it. *

* * @param name the name * @see ClasspathMonitor#unregisterAsMBean() * @since 1.6 */ public static synchronized void registerAsMBean(final ObjectName name) { if (isRegisteredAsMBean()) { LOG.debug("MBean already registered - registerAsMBean(\"{}\") ignored.", name); } else { mbeanName = name; MBeanHelper.registerMBean(mbeanName, getInstance()); } } /** * Unregister ClasspathMonitor as MBean. */ public static void unregisterAsMBean() { MBeanHelper.unregisterMBean(mbeanName); } /** * If you want to ask JMX if bean is already registered you can ask the * MBeanHelper or you can ask this method. * * @return true if MBean is already registered. * @since 1.0 */ public static boolean isRegisteredAsMBean() { return MBeanHelper.isRegistered(mbeanName); } /** * Which resource. * * @param name the name * * @return the URI * * @see ClasspathMonitorMBean#whichResource(String) */ @Description("returns the URI of the given resource") public URI whichResource(final String name) { return classpathDigger.whichResource(name); } /** * Which class. * * @param name the name * * @return the URI * * @see patterntesting.runtime.monitor.ClasspathMonitorMBean#whichClass(java.lang.String) */ public URI whichClass(final String name) { String resource = Converter.classToResource(name); return whichResource(resource); } /** * Where is the given class found in the classpath? Which class was loaded * from which URI?. * * @param clazz the class * * @return file or jar path */ public URI whichClass(final Class clazz) { LOG.trace("Searching {} in classpath.", clazz); URI uri = this.usedClassCache.get(clazz); if (uri == null) { uri = whichClass(clazz.getName()); if (uri == null) { LOG.trace("{} was not found in classpath.", clazz); } else { this.usedClassCache.put(clazz, uri); } } LOG.trace("Searching {} in classpath finished, {} found.", clazz, uri); return uri; } /** * Returns the jar file or path where the given classname was found. * * @param classname e.g. java.lang.String * * @return jar or path as URI */ public URI whichClassPath(final String classname) { String resource = Converter.classToResource(classname); return whichResourcePath(resource); } /** * Returns the jar file or path where the given class was found. * * @param clazz e.g. Sting.class * * @return jar or path as URI */ public URI whichClassPath(final Class clazz) { return whichClassPath(clazz.getName()); } /** * Which class path. * * @param p the p * * @return the uRI */ public URI whichClassPath(final Package p) { String resource = Converter.toResource(p); return whichResourcePath(resource); } /** * Returns the jar file or path where the given resource was found. * * @param resource e.g. log4j.properties * * @return jar or path as URI */ public URI whichResourcePath(final String resource) { URI uri = this.whichResource(resource); if (uri == null) { return null; } return ClasspathHelper.getParent(uri, resource); } /** * Which class jar. * * @param clazz the clazz * * @return the jar file */ public JarFile whichClassJar(final Class clazz) { return whichClassJar(clazz.getName()); } /** * Which class jar. * * @param classname the classname * * @return the jar file */ public JarFile whichClassJar(final String classname) { String resource = Converter.classToResource(classname); return whichResourceJar(resource); } /** * Which resource jar. * * @param resource the resource * * @return the jar file */ public JarFile whichResourceJar(final String resource) { return whichResourceJar(this.whichResourcePath(resource)); } /** * Which resource jar. * * @param resource the resource * * @return the jar file */ public static JarFile whichResourceJar(final URI resource) { if (resource == null) { return null; } File file = Converter.toFile(resource); try { return new JarFile(file); } catch (IOException ioe) { LOG.debug("Cannot read " + file + ":", ioe); return null; } } /** * Gets the resources. * * @param name the name * @return the resources */ public Enumeration getResources(final String name) { return this.classpathDigger.getResources(name); } /** * Gets the resource. * * @param name the name * * @return the resource */ public static URL getResource(final String name) { return ClasspathMonitor.class.getResource(name); } /** * Gets the no resources. * * @param name the name of the resource * @return number of resources * @see ClasspathMonitorMBean#getNoResources(java.lang.String) */ public int getNoResources(final String name) { Enumeration resources = getResources(name); if (resources == null) { return 0; } int n = 0; while (resources.hasMoreElements()) { n++; resources.nextElement(); } if (LOG.isTraceEnabled()) { LOG.trace(n + " element(s) of " + name + " found in classpath"); } return n; } /** * Gets the no classes. * * @param cl the cl * * @return the no classes */ public int getNoClasses(final Class cl) { return getNoClasses(cl.getName()); } /** * Gets the no classes. * * @param classname the classname * @return number of classes * @see ClasspathMonitorMBean#getNoClasses(java.lang.String) */ public int getNoClasses(final String classname) { return getNoResources(Converter.classToResource(classname)); } /** * Checks if is doublet. * * @param name the name * @return true, if checks if is doublet * @see patterntesting.runtime.monitor.ClasspathMonitorMBean#isDoublet(java.lang.String) */ public boolean isDoublet(final String name) { return this.doubletDigger.isDoublet(name); } /** * Is the given class a doublet, i.e. can it be found more than once in the * classpath? *

* Note: Because this method is needed by other methods like * {@link #getIncompatibleClassList()} the result of it is cached since 1.5. * This speeds up this method about the factor 2000 and more: *

*
     * Implementation | Mode  | Score    | Unit
     * ---------------+-------+----------+-------
     * old (till 1.4) | thrpt |    35514 | ops/s
     * new (cached)   | thrpt | 92968300 | ops/s
     * 
*

* This results were calculated with the help of JMH and was executed on a * Dell Latitude e6520 with i5 processor (i5-2540M CPU @ 2.60GHz). *

*

* Since 1.6.2 this functionality is now located in the extracted * {@link DoubletDigger} class. *

* * @param clazz the class to be checked * @return true if class is found more than once in the classpath */ public boolean isDoublet(final Class clazz) { return this.doubletDigger.isDoublet(clazz); } /** * Gets the first doublet. * * @param name the name * @return the first doublet * @see ClasspathMonitorMBean#getFirstDoublet(String) */ public URL getFirstDoublet(final String name) { return getDoublet(name, 1); } /** * Gets the first doublet. * * @param clazz the class * @return the first doublet */ public URL getFirstDoublet(final Class clazz) { return getDoublet(clazz, 1); } /** * Gets the doublet. * * @param name the name * @param nr the nr * @return the doublet * @see patterntesting.runtime.monitor.ClasspathMonitorMBean#getDoublet(java.lang.String, * int) */ public URL getDoublet(final String name, final int nr) { return this.doubletDigger.getDoublet(name, nr); } /** * Gets the doublet. * * @param clazz the clazz * @param nr the nr * @return the doublet */ public URL getDoublet(final Class clazz, final int nr) { return this.doubletDigger.getDoublet(clazz, nr); } /** * Gets the doublet list. * * @return the doublet list */ @ProfileMe public synchronized List> getDoubletList() { return this.doubletDigger.getDoubletList(); } /** * Gets the doublets. * * @return the doublets * @see ClasspathMonitorMBean#getDoublets() */ public String[] getDoublets() { return this.doubletDigger.getDoublets(); } /** * Gets the doublet classpath. * * @return the doublet classpath * * @see ClasspathMonitorMBean#getDoubletClasspath() */ @ProfileMe public String[] getDoubletClasspath() { URI[] classpathURIs = this.getDoubletClasspathURIs(); return toStringArray(classpathURIs); } private URI[] getDoubletClasspathURIs() { LOG.trace("Calculating doublet-classpath."); Set classpathSet = getClasspathSet(this.getDoubletList()); return classpathSet.toArray(new URI[classpathSet.size()]); } /** * Gets the classpath set. * * @param classes the classes * * @return the classpath set */ private SortedSet getClasspathSet(final List> classes) { SortedSet clpath = new TreeSet(); for (Class cl : classes) { String resource = Converter.toResource(cl); Enumeration resources = this.getResources(resource); while (resources.hasMoreElements()) { URL url = resources.nextElement(); URI path = Converter.toURI(url); clpath.add(ClasspathHelper.getParent(path, resource)); } } return clpath; } /** * Dumps the internal fields of the classloader (after an idea from * "Java ist auch eine Insel"). * * @return the class loader details */ @ProfileMe public String getClassLoaderDetails() { StringBuilder sbuf = new StringBuilder("dump of " + cloader + ":\n"); for (Class cl = this.cloader.getClass(); cl != null; cl = cl .getSuperclass()) { sbuf.append('\t'); dumpFields(sbuf, cl, this.cloader); } return sbuf.toString().trim(); } /** * Dump fields. * * @param sbuf the sbuf * @param cl the cl * @param obj the obj */ private static void dumpFields(final StringBuilder sbuf, final Class cl, final Object obj) { Field[] fields = cl.getDeclaredFields(); AccessibleObject.setAccessible(fields, true); for (int i = 0; i < fields.length; i++) { sbuf.append(fields[i]); sbuf.append(" = "); try { sbuf.append(fields[i].get(obj)); } catch (IllegalAccessException ex) { LOG.warn("Cannot access " + fields[i] + ":", ex); sbuf.append("<" + ex + ">"); } sbuf.append('\n'); } } /** * Checks if is classloader supported. * * @return true if it is a known classloader */ public boolean isClassloaderSupported() { return this.classpathDigger.isClassloaderSupported(); } /** * Returns some information about the classloader. At least the user should * be informed if it is a unknown classloader which is not supported or not * tested. * * @return e.g. "unknown classloader xxx - classpath can be wrong" */ public String getClassloaderInfo() { String info = this.isClassloaderSupported() ? "supported" : "unsupported"; return this.cloader.getClass().getName() + " (" + info + ")"; } /** * Gets the loaded packages. * * @return the loaded packages * * @see ClasspathMonitorMBean#getLoadedPackages() */ public String[] getLoadedPackages() { Package[] packages = this.classpathDigger.getLoadedPackageArray(); String[] strings = new String[packages.length]; for (int i = 0; i < packages.length; i++) { strings[i] = packages[i].toString(); } Arrays.sort(strings); return strings; } /** * This method is used by PatternTesting Samples (in packages.jsp). So it * is left here as wrapper around ClasspathDigger. * * @return array of packages */ public Package[] getLoadedPackageArray() { return this.classpathDigger.getLoadedPackageArray(); } /** * If you want to dump all packages you can use this method. The output will * be sorted. * * @return each package in a single line */ public String getLoadedPackagesAsString() { String[] packages = getLoadedPackages(); StringBuilder sbuf = new StringBuilder(); for (int i = 0; i < packages.length; i++) { sbuf.append(packages[i]); sbuf.append('\n'); } return sbuf.toString().trim(); } /** * Returns a list of classes which were loaded by the classloader. * * @return list of classes */ public synchronized List> getLoadedClassList() { return classpathDigger.getLoadedClassList(); } /** * As MBean a string array could be displayed by the 'jconsole'. A class * array not. * * @return the classes as sorted string array */ public synchronized String[] getLoadedClasses() { List> classes = getLoadedClassList(); if (classes.size() != loadedClasses.length) { loadedClasses = new String[classes.size()]; for (int i = 0; i < loadedClasses.length; i++) { loadedClasses[i] = classes.get(i).toString(); } Arrays.sort(loadedClasses); } return loadedClasses.clone(); } /** * Gets the loaded classes as string. * * @return the loaded classes as string */ public String getLoadedClassesAsString() { List> classes = getLoadedClassList(); Collections.sort(classes, new ObjectComparator()); StringBuilder sbuf = new StringBuilder(); for (Iterator> i = classes.iterator(); i.hasNext();) { sbuf.append(i.next().toString().trim()); sbuf.append('\n'); } return sbuf.toString().trim(); } /** * Checks if the given classname is loaded. * Why does we use not Class as parameter here? If you would allow a * parameter of type "Class" this class will be problably loaded before * and this method will return always true! * * @param classname name of the class * @return true if class is loaded * @see patterntesting.runtime.monitor.ClasspathMonitorMBean#isLoaded(java.lang.String) */ public boolean isLoaded(final String classname) { return this.classpathDigger.isLoaded(classname); } /** * Unused classes are classes which are not loaded but which are found in * the classpath. * * @return unused classes * * @see #getLoadedClasses() * @see #getClasspathClasses() */ @ProfileMe public String[] getUnusedClasses() { List> classes = getLoadedClassList(); Collection used = new ArrayList(); Set unusedSet = this.getUnusedClassSet(); for (Class cl : classes) { String classname = cl.getName(); if (unusedSet.contains(classname)) { used.add(classname); } } unusedSet.removeAll(used); String[] unused = new String[unusedSet.size()]; unusedSet.toArray(unused); return unused; } private Set getUnusedClassSet() { try { return this.unusedClasses.get(); } catch (InterruptedException e) { LOG.warn("Was interrupted before got result from {}:", this.unusedClasses, e); Thread.currentThread().interrupt(); } catch (ExecutionException e) { LOG.warn("Cannot execute get of {}:", this.unusedClasses, e); } return this.getClasspathClassSet(); } /** * Scans the classpath for all classes. * For performance reason we return no longer a copy but the origin array. * Don't do changes for the returned array! * * @return all classes as String array */ @ProfileMe public String[] getClasspathClasses() { try { return this.allClasspathClasses.get(); } catch (InterruptedException e) { LOG.warn("Was interrupted before got result from {}:", this.allClasspathClasses, e); Thread.currentThread().interrupt(); } catch (ExecutionException e) { LOG.warn("Cannot execute get of {}:", this.allClasspathClasses, e); } return this.getClasspathClassArray(); } private Set getClasspathClassSet() { String[] classes = getClasspathClasses(); return new TreeSet(Arrays.asList(classes)); } /** * Gets the classes of a given package name as collection. * * @param packageName the package name * @return the classpath class list */ public Collection getClasspathClassList(final String packageName) { Collection classlist = new ArrayList(); String[] classes = this.getClasspathClasses(); for (int i = 0; i < classes.length; i++) { if (classes[i].startsWith(packageName)) { classlist.add(classes[i]); } } return classlist; } /** * Gets the classes of the given type. * * @param the generic type * @param packageName the package name * @param type the type * @return the classes */ @SuppressWarnings("unchecked") public Collection> getClassList(final String packageName, final Class type) { Collection> classes = new ArrayList>(); Collection> concreteClasses = getConcreteClassList(packageName); for (Class clazz : concreteClasses) { if (type.isAssignableFrom(clazz)) { classes.add((Class) clazz); LOG.trace("subclass of {} found: {}", type, clazz); } } return classes; } /** * Gets a list of concrete classes. These are classes which can be * instantiated, i.e. they are not abstract and have a default * constructor. * * @param packageName the package name * @return the concrete class list */ @SuppressWarnings("unchecked") public Collection> getConcreteClassList(final String packageName) { assert packageName != null; Collection classList = this.getClasspathClassList(packageName); Collection> classes = new ArrayList>(classList.size()); for (String classname : classList) { try { Class clazz = (Class) Class.forName(classname); if (!canBeInstantiated(clazz)) { LOG.trace("{} will be ignored (can't be instantiated).", clazz); continue; } classes.add(clazz); } catch (ClassNotFoundException e) { LOG.debug("Class '{}' is ignored because it was not found:", classname, e); } catch (NoClassDefFoundError e) { LOG.debug("Class '{}' is ignored because definition was not found:", classname, e); } catch (ExceptionInInitializerError ex) { LOG.debug("Class '{}' is ignored because it cannot be iniatialized:", classname, ex); } } return classes; } private static boolean canBeInstantiated(final Class clazz) { if (clazz.isInterface()) { return false; } int mod = clazz.getModifiers(); if (Modifier.isAbstract(mod)) { return false; } try { clazz.getConstructor(); return true; } catch (SecurityException ex) { LOG.info("Cannot access default ctor {}:", clazz, ex); return false; } catch (NoSuchMethodException ex) { LOG.trace("Cannot get default ctor of {}:", clazz, ex); LOG.debug("{} has no default constructor.", clazz); return false; } } /** * Gets the classpath class set (sorted). * * @return the classpath class set */ private Set createClasspathClassSet() { Set classSet = new TreeSet(); for (int i = 0; i < classpath.length; i++) { addClasses(classSet, new File(classpath[i])); } return classSet; } private String[] getClasspathClassArray() { Set classSet = createClasspathClassSet(); return classSet.toArray(new String[classSet.size()]); } /** * Adds the classes. * * @param classSet the class set * @param path the path */ private static void addClasses(final Set classSet, final File path) { LOG.trace("Adding classes from {}...", path); try { if (path.isDirectory()) { addClassesFromDir(classSet, path); } else { addClassesFromArchive(classSet, path); } } catch (IOException ioe) { LOG.warn("Cannot add classes from " + path.getAbsolutePath() + ":", ioe); } } /** * Adds the classes from dir. * * @param classSet the class set * @param dir the dir * * @throws IOException Signals that an I/O exception has occurred. */ private static void addClassesFromDir(final Set classSet, final File dir) throws IOException { ClassWalker classWalker = new ClassWalker(dir); Collection classes = classWalker.getClasses(); classSet.addAll(classes); } /** * Adds the classes from archive. * * @param classSet the class set * @param archive the archive * * @throws IOException Signals that an I/O exception has occurred. */ private static void addClassesFromArchive(final Set classSet, final File archive) throws IOException { ZipFile zipFile = new ZipFile(archive); try { Enumeration entries = zipFile.entries(); while (entries.hasMoreElements()) { ZipEntry entry = entries.nextElement(); String name = entry.getName(); if (name.endsWith(".class")) { classSet.add(Converter.resourceToClass(name)); } } } finally { zipFile.close(); } } /** * Gets the loaded classpath (without the bootclasspath) as sorted set. * Since v1.5 this method is about 5 times faster than the old * implementation by caching the result from previous calls. *

* Here the result from running some benchmarks with the old and new * implementation: Profiling with JMH gives the following result: *

*
     * Implementation | Mode  | Score             | Error (99.9%) | Unit
     * ---------------+-------+-------------------+---------------+-------
     * new (v1.5)     | thrpt | 6.495745459743192 | 0.14376149437 | ops/ms
     * old (v1.4)     | thrpt | 1.167003226917474 | 0.01126705718 | ops/ms
     * new (v1.5)     | avgt  | 0.149637883043804 | 0.00084667052 | ms/op
     * old (v1.4)     | avgt  | 0.860845377760151 | 0.02615918865 | ms/op
     * 
*

* As framework for the benchmarks JMH was used. *

* * @return the loaded classpath (excluding the bootclasspath) */ @ProfileMe public SortedSet getUsedClasspathSet() { List> loadedClassList = this.getLoadedClassList(); SortedSet usedClasspathSet = new TreeSet(); for (Class clazz : loadedClassList) { URI classpathUri = this.getClasspathOf(clazz); if (!NullConstants.NULL_URI.equals(classpathUri)) { usedClasspathSet.add(classpathUri); } } return usedClasspathSet; } private URI getClasspathOf(final Class clazz) { URI classpathUri = this.usedClasspathCache.get(clazz); if (classpathUri == null) { URI classUri = this.whichClass(clazz); if (classUri == null) { LOG.trace("URI for {} was not found (probably a proxy class).", clazz); classpathUri = NullConstants.NULL_URI; } else { classpathUri = ClasspathHelper.getParent(classUri, clazz); } this.usedClasspathCache.put(clazz, classpathUri); } return classpathUri; } /** * It might be that the used classpath changes during the calculation of it. * So it is only a good approximation how it looks at the moment of the * call. * * @return the used classpath * * @see ClasspathMonitor#getUsedClasspath() */ public String[] getUsedClasspath() { URI[] uris = this.getUsedClasspathURIs(); return toStringArray(uris); } private URI[] getUsedClasspathURIs() { LOG.debug("calculating used classpath..."); SortedSet classpathSet = this.getUsedClasspathSet(); return classpathSet.toArray(new URI[classpathSet.size()]); } /** * We remove the URI prefix from the result to be in-line with the other * classpath methods. * * * @param uris an array with URIs * @return the string array */ @ProfileMe private static String[] toStringArray(final URI[] uris) { String[] classpath = new String[uris.length]; for (int i = 0; i < classpath.length; i++) { URI uri = uris[i]; classpath[i] = uri.getPath(); if (StringUtils.isEmpty(classpath[i])) { classpath[i] = "/" + StringUtils.substringAfter(uri.toString(), ":/"); } classpath[i] = new File(classpath[i]).getAbsolutePath(); } Arrays.sort(classpath); return classpath; } /** * The unused classpath is this path which are not used by the classloader. * * @return the unused classpath (sorted) */ @ProfileMe public String[] getUnusedClasspath() { LOG.debug("calculating unused classpath..."); SortedSet unused = new TreeSet(); LOG.trace(Arrays.class + " loaded (to get corrected used classpath"); String[] used = this.getUsedClasspath(); for (int i = 0; i < classpath.length; i++) { String path = new File(classpath[i]).getAbsolutePath(); if (Arrays.binarySearch(used, path) < 0) { unused.add(path); } } String[] a = new String[unused.size()]; return unused.toArray(a); } /** * To get the boot classpath the sytem property "sun.boot.class.path" is * used to get them. This will work of course only for the SunVM. * * @return the boot classpath as String array */ public String[] getBootClasspath() { return this.classpathDigger.getBootClasspath(); } /** * The classpath is stored in the system property "java.class.path". And * this is the classpath which will be returned. * * @return the classpath as String array */ public String[] getClasspath() { return this.classpath.clone(); } /** * Gets the serial version uid. * * @param classname name of the class * @return the serialVersionUID * @throws IllegalAccessException if class can't be accessed * @see patterntesting.runtime.monitor.ClasspathMonitorMBean#getSerialVersionUID(java.lang.String) */ public Long getSerialVersionUID(final String classname) throws IllegalAccessException { try { Class clazz = Class.forName(classname); return getSerialVersionUID(clazz); } catch (ClassNotFoundException e) { LOG.debug("Class '" + classname + "' not found:", e); return null; } } /** * Gets the serial version uid. * * @param clazz the clazz * * @return the serial version uid * * @throws IllegalAccessException the illegal access exception */ public Long getSerialVersionUID(final Class clazz) throws IllegalAccessException { try { Field field = ReflectionHelper.getField(clazz, "serialVersionUID"); return (Long) field.get(null); } catch (NoSuchFieldException ex) { LOG.debug(clazz + " has no serialVersionUID:", ex); return null; } } /** * If a MANIFEST is found for a given class the attributes in this * file should be returned as a string array. * E.g. for commons-lang-2.3.jar the string array may looks like *
     * Manifest-Version: 1.0
     * Ant-Version: Apache Ant 1.6.5
     * Created-By: 1.3.1_09-85 ("Apple Computer, Inc.")
     * Package: org.apache.commons.lang
     * Extension-Name: commons-lang
     * Specification-Version: 2.3
     * Specification-Vendor: Apache Software Foundation
     * Specification-Title: Commons Lang
     * Implementation-Version: 2.3
     * Implementation-Vendor: Apache Software Foundation
     * Implementation-Title: Commons Lang
     * Implementation-Vendor-Id: org.apache
     * X-Compile-Source-JDK: 1.3
     * X-Compile-Target-JDK: 1.1
     * 
* * @param clazz the clazz * * @return the attribute entries of the Manifest * (or emtpy array if no Manifest or no attributes are found) */ public String[] getManifestEntries(final Class clazz) { return this.getManifestEntries(clazz.getName()); } /** * If a MANIFEST is found for a given class the attributes in this * file should be returned as a string array. * E.g. for commons-lang-2.3.jar the string array may look like *
     * Manifest-Version: 1.0
     * Ant-Version: Apache Ant 1.6.5
     * Created-By: 1.3.1_09-85 ("Apple Computer, Inc.")
     * Package: org.apache.commons.lang
     * Extension-Name: commons-lang
     * Specification-Version: 2.3
     * Specification-Vendor: Apache Software Foundation
     * Specification-Title: Commons Lang
     * Implementation-Version: 2.3
     * Implementation-Vendor: Apache Software Foundation
     * Implementation-Title: Commons Lang
     * Implementation-Vendor-Id: org.apache
     * X-Compile-Source-JDK: 1.3
     * X-Compile-Target-JDK: 1.1
     * 
* * @param classname (must be in the classpath, otherwise you'll get a * IllegalArgumentException) * * @return the attribute entries of the Manifest * (or emtpy array if no Manifest or no attributes are found) */ public String[] getManifestEntries(final String classname) { URI classpathURI = whichClassPath(classname); if (classpathURI == null) { throw new IllegalArgumentException(classname + " not found in classpath"); } File path = Converter.toFile(classpathURI); return getManifestEntries(path); } /** * Gets the manifest entries. * * @param path e.g. a classpath or a JAR file * * @return the attribute entries of the manifest file */ private String[] getManifestEntries(final File path) { if (path.isFile()) { try { return getManifestEntries(new JarFile(path)); } catch (IOException ioe) { LOG.info("No manifest found in " + path + ":", ioe); return new String[0]; } } File manifestFile = new File(path, "META-INF/MANIFEST.MF"); if (!manifestFile.exists()) { LOG.debug("File '{}' does not exist.", manifestFile); return new String[0]; } try { InputStream istream = new FileInputStream(manifestFile); Manifest manifest = new Manifest(istream); IOUtils.closeQuietly(istream); return getManifestEntries(manifest); } catch (IOException ioe) { LOG.info("Cannot read '" + manifestFile + "':", ioe); return new String[0]; } } /** * We look for the manifest file for the given JAR file. * * @param jarfile the jarfile * * @return the entries as string array (may be an empty array if no * manifest file was found) */ private String[] getManifestEntries(final JarFile jarfile) { Manifest manifest; try { manifest = jarfile.getManifest(); } catch (IOException ioe) { LOG.info("No manifest found in '" + jarfile + "':", ioe); return new String[0]; } if (manifest == null) { LOG.debug("No manifest found in '{}'.", jarfile); return new String[0]; } return getManifestEntries(manifest); } /** * Gets the manifest entries. * * @param manifest the manifest * * @return the manifest entries */ private String[] getManifestEntries(final Manifest manifest) { Attributes attributes = manifest.getMainAttributes(); String[] manifestEntries = new String[attributes.size()]; Set> entries = attributes.entrySet(); Iterator> iterator = entries.iterator(); for (int i = 0; i < manifestEntries.length; i++) { Map.Entry entry = iterator.next(); manifestEntries[i] = entry.getKey() + ": " + entry.getValue(); } return manifestEntries; } /** * Looks for each doublet if it has a different doublets. For the * performance reason it looks in the incompatibleClassList from the last * time if it is already found. This is done because normally the number of * incompatible classes does not decrease. * * @return a sorted list of incompatible classes */ @ProfileMe public synchronized List> getIncompatibleClassList() { List> doublets = this.getDoubletList(); for (Class clazz : doublets) { if (incompatibleClassList.contains(clazz)) { continue; } String resource = Converter.classToResource(clazz.getName()); Enumeration resources = getResources(resource); try { URL url = resources.nextElement(); ArchivEntry archivEntry = new ArchivEntry(url); while (resources.hasMoreElements()) { url = resources.nextElement(); ArchivEntry doubletEntry = new ArchivEntry(url); if (archivEntry.equals(doubletEntry)) { incompatibleClassList.add(clazz); break; } } } catch (NoSuchElementException nse) { LOG.warn("{} is not added to incompatible class list:", clazz, nse); } } return Collections.unmodifiableList(incompatibleClassList); } /** * Incompatible classes are doublets with different byte codes. * * @return doublet classes with different byte codes. */ public String[] getIncompatibleClasses() { LOG.debug("Calculating incompatible classes..."); List> classList = this.getIncompatibleClassList(); String[] classes = new String[classList.size()]; for (int i = 0; i < classes.length; i++) { classes[i] = classList.get(i).toString(); } return classes; } /** * Gets the incompatible classpath. * * @return the classpathes where incompatible classes were found */ public String[] getIncompatibleClasspath() { URI[] classpathURIs = this.getIncompatibleClasspathURIs(); return toStringArray(classpathURIs); } private URI[] getIncompatibleClasspathURIs() { LOG.debug("calculating incompatible-classpath..."); Set classpathSet = getClasspathSet(this.getIncompatibleClassList()); return classpathSet.toArray(new URI[classpathSet.size()]); } /** * You can register the instance as shutdown hook. If the VM is terminated * the attributes are logged and dumped to a text file in the tmp directory. * * @see #logMe() * @see #dumpMe() * @see #addMeAsShutdownHook() * @see #removeMeAsShutdownHook() */ public static synchronized void addAsShutdownHook() { INSTANCE.addMeAsShutdownHook(); } /** * If you have registered the instance you can now de-register it. * * @see #addAsShutdownHook() * @see #removeMeAsShutdownHook() * @since 1.0 */ public static synchronized void removeAsShutdownHook() { INSTANCE.removeMeAsShutdownHook(); } /** * This method is called when the ClasspathMonitor is registered as shutdown * hook. * * @see java.lang.Thread#run() */ @Override public void run() { try { File file = dumpMe(); LOG.info("{} was dumped to '{}'.", this, file); } catch (IOException ioe) { LOG.error("Cannot dump " + this + ":", ioe); } } /** * Logs the different array to the log output. */ public void logMe() { try { StringWriter writer = new StringWriter(); this.dumpMe(writer); LOG.info(writer.toString()); } catch (IOException cannothappen) { LOG.warn("Will only dump classloader info:", cannothappen); LOG.info(this.getClassloaderInfo()); } } /** * This operation dumps the different MBean attributes to a temporary * directory with the prefix "cpmon" (for ClasspathMonitor). The name of the * created directory is returned so that you can see it in the 'jconsole' * (if you have triggered it from there). * * @return the file * @throws IOException Signals that an I/O exception has occurred. * @see ClasspathMonitorMBean#dumpMe() */ public File dumpMe() throws IOException { File dumpDir = FileHelper.createTmpdir("cpmon", ""); this.dumpMe(dumpDir); return dumpDir; } /** * This operation dumps the different MBean attributes to the given * directory. *

* Note: Since 1.5 the different MBean attributes are dumped into its own * file. The given 'dirname' is the name of the directory where this file * is stored. *

* * @param dirname the name of the directory * @throws IOException Signals that an I/O exception has occurred. * @since 1.5 */ public void dumpMe(final String dirname) throws IOException { this.dumpMe(new File(dirname)); } /** * This operation dumps the different MBean attributes to the given * directory. *

* Note: Till 1.5 the different attributes are dumped to a single file. Now * each attribute is dumped into its own file. The given file parameter is * the directory for these files. *

*

* To dump the different attributes (which are mainly given as array) we use * the internal 'dumpArray()' method. This method uses reflection to call * the corresponding getter method for that attribute. The reason for this * approach is that we can better control exceptions if they happens in one * of the getter method. If it happens it is ignored (but logged) and the * next attribute is dumped. *

* * @param dumpDir the directory where the attributes are dumped to. * @throws IOException Signals that an I/O exception has occurred. * @since 1.5 */ public void dumpMe(final File dumpDir) throws IOException { LOG.info("Attributes will be dumped to dir '{}'.", dumpDir); if (!dumpDir.exists()) { if (dumpDir.mkdir()) { LOG.debug("Directory '{}' successful created.", dumpDir); } else { LOG.error("Cannot create directory '{}' - dump aborted.", dumpDir); return; } } for (int i = 0; i < DUMP_GETTERS.length; i++) { this.dumpArray(dumpDir, DUMP_GETTERS[i]); } dumpClassloaderInfo(dumpDir); copyResource("README.txt", dumpDir); } private void copyResource(final String name, final File dir) throws IOException { URL readmeURL = ClasspathMonitor.class.getResource(name); if (readmeURL == null) { throw new IOException("'" + name + "' not found in classpath"); } FileUtils.copyURLToFile(readmeURL, new File(dir, name)); } /** * This operation dumps the different MBean attributes. Use this method * if you want to have all relevant infos in one stream. This * is e.g. used by the {@link #logMe()} method. * * @param unbuffered the writer (can be unbuffered) * @throws IOException Signals that an I/O exception has occurred. */ private void dumpMe(final Writer unbuffered) throws IOException { BufferedWriter writer = new BufferedWriter(unbuffered); for (int i = 0; i < DUMP_GETTERS.length; i++) { this.dumpArray(writer, DUMP_GETTERS[i]); } dumpClassloaderInfo(writer); writer.flush(); } private void dumpClassloaderInfo(final File dir) throws IOException { File dumpFile = new File(dir, "ClassloaderInfo.txt"); LOG.debug("Dumping classloader info to {}...", dumpFile); OutputStreamWriter writer = new OutputStreamWriter(new FileOutputStream(dumpFile), "UTF-8"); try { dumpClassloaderInfo(new BufferedWriter(writer)); writer.flush(); } finally { writer.close(); } } private void dumpClassloaderInfo(final BufferedWriter writer) throws IOException { dumpHeadline(writer, "ClassloaderInfo"); writer.write(this.getClassloaderInfo()); writer.newLine(); writer.newLine(); dumpHeadline(writer, "=== ClassLoaderDetails ==="); writer.write(this.getClassLoaderDetails()); writer.newLine(); dumpHeadline(writer, "ClassloaderInfo (end)"); writer.flush(); } private void dumpArray(final File dir, final String title) throws IOException { File dumpFile = new File(dir, title + ".txt"); LOG.debug("Dumping '{}' to {}...", title, dumpFile); OutputStreamWriter writer = new OutputStreamWriter(new FileOutputStream(dumpFile), "UTF-8"); try { dumpArray(new BufferedWriter(writer), title); } finally { writer.close(); } } private void dumpArray(final BufferedWriter writer, final String title) throws IOException { try { Method getter = ReflectionHelper.getMethod(this.getClass(), "get" + title); Object[] array = (Object[]) getter.invoke(this); dumpArray(writer, title, array); } catch (NoSuchMethodException ex) { throw new IllegalArgumentException("no getter for '" + title + "' found", ex); } catch (IllegalAccessException ex) { throw new IllegalArgumentException("cannot access getter for '" + title + "'", ex); } catch (InvocationTargetException ex) { LOG.error("Error happens in getter for '" + title + "':", ex); dumpException(writer, ex.getTargetException()); } catch (RuntimeException ex) { LOG.error("Cannot get '" + title + "':", ex); dumpException(writer, ex); } } private static void dumpArray(final BufferedWriter writer, final String title, final Object[] array) throws IOException { dumpHeadline(writer, title + " (" + array.length + " entries)"); for (int i = 0; i < array.length; i++) { writer.write(array[i].toString()); writer.newLine(); } dumpHeadline(writer, title + " (end)"); writer.flush(); } private static void dumpHeadline(final BufferedWriter writer, final String headline) throws IOException { writer.write("----- " + headline + " -----"); writer.newLine(); } private static void dumpException(final BufferedWriter writer, final Throwable ex) throws IOException { writer.write("*** " + ex); writer.newLine(); ex.printStackTrace(new PrintWriter(writer)); writer.newLine(); writer.flush(); } /** * We don't want to use the toString() implementation of the Thread class so * we use our own one. * * @return the string * @see java.lang.Thread#toString() */ @Override public String toString() { return this.getClass().getSimpleName() + " for " + this.cloader; } /** * This main method is only for testing the ClasspathMonitor together with * the 'jconsole'. On a Java5 VM start it with the following options: * -Dcom.sun.management.jmxremote.local.only=false * -Dcom.sun.management.jmxremote * * @param args the args * @throws JMException the JM exception */ public static void main(final String[] args) throws JMException { MBeanServer mbs = ManagementFactory.getPlatformMBeanServer(); ObjectName name = new ObjectName("patterntesting.runtime.monitor:type=ClasspathMonitor"); ClasspathMonitor hello = new ClasspathMonitor(); Object mbean = new AnnotatedStandardMBean(hello, ClasspathMonitorMBean.class); mbs.registerMBean(mbean, name); ThreadUtil.sleep(300, TimeUnit.SECONDS); } /** * Is multi threading enabled? * Multi threading is automatically enabled if more than one processor * is found. Otherwise you can use set via the system property * "multiThreadingEnabled=true" to activate it. * * @return true if multi threading is enabled for this class. * @see ClasspathMonitorMBean#isMultiThreadingEnabled() * @since 0.9.7 */ public boolean isMultiThreadingEnabled() { return this.doubletDigger.isMultiThreadingEnabled(); } /** * Here you can enable or disable the (experimental) multi threading mode to * see if it is really faster on a mult-core machine. * * @param enabled the enabled * @see ClasspathMonitorMBean#setMultiThreadingEnabled(boolean) * @since 0.9.7 */ public void setMultiThreadingEnabled(final boolean enabled) { this.doubletDigger.setMultiThreadingEnabled(enabled); } }