
patterntesting.runtime.monitor.ClasspathMonitor Maven / Gradle / Ivy
Show all versions of patterntesting-rt Show documentation
/**
* $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