Java tutorial
/* * Copyright 2013-2015 the original author or authors. * * 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 or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.glowroot.local.ui; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.lang.instrument.Instrumentation; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.net.MalformedURLException; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; import java.net.URLClassLoader; import java.util.Collection; import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.Set; import java.util.jar.JarEntry; import java.util.jar.JarInputStream; import java.util.jar.Manifest; import javax.annotation.Nullable; import javax.annotation.concurrent.GuardedBy; import com.google.common.base.Splitter; import com.google.common.collect.HashMultimap; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMultimap; import com.google.common.collect.Iterables; import com.google.common.collect.Iterators; import com.google.common.collect.Lists; import com.google.common.collect.Multimap; import com.google.common.collect.Ordering; import com.google.common.collect.Sets; import com.google.common.collect.TreeMultimap; import com.google.common.io.Closer; import com.google.common.io.Resources; import org.objectweb.asm.ClassReader; import org.objectweb.asm.ClassVisitor; import org.objectweb.asm.MethodVisitor; import org.objectweb.asm.Type; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.glowroot.common.ClassNames; import org.glowroot.common.Reflections; import org.glowroot.weaving.AnalyzedWorld; import static com.google.common.base.Preconditions.checkNotNull; import static org.objectweb.asm.Opcodes.ACC_NATIVE; import static org.objectweb.asm.Opcodes.ACC_SYNTHETIC; import static org.objectweb.asm.Opcodes.ASM5; // TODO remove items from classpathLocations and classNameLocations when class loaders are no longer // present, e.g. in wildfly after undeploying an application class ClasspathCache { private static final Logger logger = LoggerFactory.getLogger(ClasspathCache.class); private final AnalyzedWorld analyzedWorld; private final @Nullable Instrumentation instrumentation; @GuardedBy("this") private final Set<File> classpathLocations = Sets.newHashSet(); // using ImmutableMultimap because it is very space efficient // this is not updated often so trading space efficiency for copying the entire map on update @GuardedBy("this") private ImmutableMultimap<String, File> classNameLocations = ImmutableMultimap.of(); ClasspathCache(AnalyzedWorld analyzedWorld, @Nullable Instrumentation instrumentation) { this.analyzedWorld = analyzedWorld; this.instrumentation = instrumentation; } // using synchronization instead of concurrent structures in this cache to conserve memory synchronized ImmutableList<String> getMatchingClassNames(String partialClassName, int limit) { // update cache before proceeding updateCache(); PartialClassNameMatcher matcher = new PartialClassNameMatcher(partialClassName); Set<String> fullMatchingClassNames = Sets.newLinkedHashSet(); Set<String> matchingClassNames = Sets.newLinkedHashSet(); // also check loaded classes, e.g. for groovy classes Iterator<String> i = classNameLocations.keySet().iterator(); if (instrumentation != null) { List<String> loadedClassNames = Lists.newArrayList(); for (Class<?> clazz : instrumentation.getAllLoadedClasses()) { if (!clazz.getName().startsWith("[")) { loadedClassNames.add(clazz.getName()); } } i = Iterators.concat(i, loadedClassNames.iterator()); } while (i.hasNext()) { String className = i.next(); String classNameUpper = className.toUpperCase(Locale.ENGLISH); boolean potentialFullMatch = matcher.isPotentialFullMatch(classNameUpper); if (matchingClassNames.size() == limit && !potentialFullMatch) { // once limit reached, only consider full matches continue; } if (fullMatchingClassNames.size() == limit) { break; } if (matcher.isPotentialMatch(classNameUpper)) { if (potentialFullMatch) { fullMatchingClassNames.add(className); } else { matchingClassNames.add(className); } } } return combineClassNamesWithLimit(fullMatchingClassNames, matchingClassNames, limit); } // using synchronization over concurrent structures in this cache to conserve memory synchronized ImmutableList<UiAnalyzedMethod> getAnalyzedMethods(String className) { // update cache before proceeding updateCache(); Set<UiAnalyzedMethod> analyzedMethods = Sets.newHashSet(); Collection<File> locations = classNameLocations.get(className); for (File location : locations) { try { analyzedMethods.addAll(getAnalyzedMethods(location, className)); } catch (IOException e) { logger.warn(e.getMessage(), e); } } if (instrumentation != null) { // also check loaded classes, e.g. for groovy classes for (Class<?> clazz : instrumentation.getAllLoadedClasses()) { if (clazz.getName().equals(className)) { analyzedMethods.addAll(getAnalyzedMethods(clazz)); } } } return ImmutableList.copyOf(analyzedMethods); } // using synchronization over concurrent structures in this cache to conserve memory synchronized void updateCache() { Multimap<String, File> newClassNameLocations = HashMultimap.create(); for (ClassLoader loader : getKnownClassLoaders()) { updateCache(loader, newClassNameLocations); } updateCacheWithBootstrapClasses(newClassNameLocations); if (!newClassNameLocations.isEmpty()) { Multimap<String, File> newMap = TreeMultimap.create(String.CASE_INSENSITIVE_ORDER, Ordering.natural()); newMap.putAll(classNameLocations); newMap.putAll(newClassNameLocations); classNameLocations = ImmutableMultimap.copyOf(newMap); } } private ImmutableList<String> combineClassNamesWithLimit(Set<String> fullMatchingClassNames, Set<String> matchingClassNames, int limit) { if (fullMatchingClassNames.size() < limit) { int space = limit - fullMatchingClassNames.size(); int numToAdd = Math.min(space, matchingClassNames.size()); fullMatchingClassNames.addAll(ImmutableList.copyOf(Iterables.limit(matchingClassNames, numToAdd))); } return ImmutableList.copyOf(fullMatchingClassNames); } private void updateCacheWithBootstrapClasses(Multimap<String, File> newClassNameLocations) { String bootClassPath = System.getProperty("sun.boot.class.path"); if (bootClassPath == null) { return; } for (String path : Splitter.on(File.pathSeparatorChar).split(bootClassPath)) { File file = new File(path); if (!classpathLocations.contains(file)) { loadClassNames(file, newClassNameLocations); classpathLocations.add(file); } } } private List<UiAnalyzedMethod> getAnalyzedMethods(File location, String className) throws IOException { String name = className.replace('.', '/') + ".class"; if (location.isDirectory()) { URI uri = new File(location, name).toURI(); return getAnalyzedMethods(uri); } else if (location.exists() && location.getName().endsWith(".jar")) { String path = location.getPath(); try { URI uri = new URI("jar", "file:" + path + "!/" + name, ""); return getAnalyzedMethods(uri); } catch (URISyntaxException e) { logger.error(e.getMessage(), e); } } return ImmutableList.of(); } private List<UiAnalyzedMethod> getAnalyzedMethods(URI uri) throws IOException { AnalyzingClassVisitor cv = new AnalyzingClassVisitor(); byte[] bytes = Resources.toByteArray(uri.toURL()); ClassReader cr = new ClassReader(bytes); cr.accept(cv, 0); return cv.getAnalyzedMethods(); } private List<UiAnalyzedMethod> getAnalyzedMethods(Class<?> clazz) { List<UiAnalyzedMethod> analyzedMethods = Lists.newArrayList(); for (Method method : clazz.getDeclaredMethods()) { if (method.isSynthetic() || Modifier.isNative(method.getModifiers())) { // don't add synthetic or native methods to the analyzed model continue; } UiAnalyzedMethod.Builder builder = UiAnalyzedMethod.builder(); builder.name(method.getName()); for (Class<?> parameterType : method.getParameterTypes()) { // Class.getName() for arrays returns internal notation (e.g. "[B" for byte array) // so using Type.getType().getClassName() instead builder.addParameterTypes(Type.getType(parameterType).getClassName()); } // Class.getName() for arrays returns internal notation (e.g. "[B" for byte array) // so using Type.getType().getClassName() instead builder.returnType(Type.getType(method.getReturnType()).getClassName()); builder.modifiers(method.getModifiers()); for (Class<?> exceptionType : method.getExceptionTypes()) { builder.addExceptions(exceptionType.getName()); } analyzedMethods.add(builder.build()); } return analyzedMethods; } private void updateCache(ClassLoader loader, Multimap<String, File> newClassNameLocations) { List<URL> urls = getURLs(loader); List<File> locations = Lists.newArrayList(); for (URL url : urls) { File file = tryToGetFileFromURL(url, loader); if (file != null) { locations.add(file); } } for (File location : locations) { if (!classpathLocations.contains(location)) { loadClassNames(location, newClassNameLocations); classpathLocations.add(location); } } } private @Nullable File tryToGetFileFromURL(URL url, ClassLoader loader) { if (url.getProtocol().equals("vfs")) { // special case for try { return getFileFromJBossVfsURL(url, loader); } catch (Exception e) { logger.warn(e.getMessage(), e); } } else { try { URI uri = url.toURI(); if (uri.getScheme().equals("file")) { return new File(uri); } } catch (URISyntaxException e) { // log exception at debug level logger.debug(e.getMessage(), e); } } return null; } private List<URL> getURLs(ClassLoader loader) { if (loader instanceof URLClassLoader) { try { return Lists.newArrayList(((URLClassLoader) loader).getURLs()); } catch (Exception e) { // tomcat WebappClassLoader.getURLs() throws NullPointerException after stop() has // been called on the WebappClassLoader (this happens, for example, after a webapp // fails to load) // // log exception at debug level logger.debug(e.getMessage(), e); return ImmutableList.of(); } } // special case for jboss/wildfly try { return Collections.list(loader.getResources("/")); } catch (IOException e) { logger.warn(e.getMessage(), e); return ImmutableList.of(); } } private List<ClassLoader> getKnownClassLoaders() { List<ClassLoader> loaders = analyzedWorld.getClassLoaders(); if (loaders.isEmpty()) { // this is needed for testing the UI outside of javaagent ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader(); if (systemClassLoader == null) { return ImmutableList.of(); } else { return ImmutableList.of(systemClassLoader); } } return loaders; } private static void loadClassNames(File file, Multimap<String, File> newClassNameLocations) { try { if (file.isDirectory()) { loadClassNamesFromDirectory(file, "", file, newClassNameLocations); } else if (file.exists() && file.getName().endsWith(".jar")) { loadClassNamesFromJarFile(file, newClassNameLocations); } } catch (IllegalArgumentException e) { // new File(URI) constructor can throw IllegalArgumentException logger.debug(e.getMessage(), e); } catch (IOException e) { logger.debug("error reading classes from file: {}", file, e); } } private static void loadClassNamesFromDirectory(File dir, String prefix, File location, Multimap<String, File> newClassNameLocations) throws MalformedURLException { File[] files = dir.listFiles(); if (files == null) { return; } for (File file : files) { String name = file.getName(); if (file.isFile() && name.endsWith(".class")) { String className = prefix + name.substring(0, name.lastIndexOf('.')); newClassNameLocations.put(className, location); } else if (file.isDirectory()) { loadClassNamesFromDirectory(file, prefix + name + ".", location, newClassNameLocations); } } } private static void loadClassNamesFromJarFile(File jarFile, Multimap<String, File> newClassNameLocations) throws IOException { Closer closer = Closer.create(); InputStream s = new FileInputStream(jarFile); JarInputStream jarIn = closer.register(new JarInputStream(s)); try { loadClassNamesFromManifestClassPath(jarIn, jarFile, newClassNameLocations); loadClassNamesFromJarInputStream(jarIn, jarFile, newClassNameLocations); } catch (Throwable t) { throw closer.rethrow(t); } finally { closer.close(); } } private static void loadClassNamesFromManifestClassPath(JarInputStream jarIn, File jarFile, Multimap<String, File> newClassNameLocations) { Manifest manifest = jarIn.getManifest(); if (manifest == null) { return; } String classpath = manifest.getMainAttributes().getValue("Class-Path"); if (classpath == null) { return; } URI baseUri = jarFile.toURI(); for (String path : Splitter.on(' ').omitEmptyStrings().split(classpath)) { File file = new File(baseUri.resolve(path)); loadClassNames(file, newClassNameLocations); } } private static void loadClassNamesFromJarInputStream(JarInputStream jarIn, File jarFile, Multimap<String, File> newClassNameLocations) throws IOException { JarEntry jarEntry; while ((jarEntry = jarIn.getNextJarEntry()) != null) { if (jarEntry.isDirectory()) { continue; } String name = jarEntry.getName(); if (!name.endsWith(".class")) { continue; } String className = name.substring(0, name.lastIndexOf('.')).replace('/', '.'); newClassNameLocations.put(className, jarFile); } } private static File getFileFromJBossVfsURL(URL url, ClassLoader loader) throws Exception { Object virtualFile = url.openConnection().getContent(); Class<?> virtualFileClass = loader.loadClass("org.jboss.vfs.VirtualFile"); Method getPhysicalFileMethod = Reflections.getMethod(virtualFileClass, "getPhysicalFile"); Method getNameMethod = Reflections.getMethod(virtualFileClass, "getName"); File physicalFile = (File) Reflections.invoke(getPhysicalFileMethod, virtualFile); checkNotNull(physicalFile, "org.jboss.vfs.VirtualFile.getPhysicalFile() returned null"); String name = (String) Reflections.invoke(getNameMethod, virtualFile); checkNotNull(name, "org.jboss.vfs.VirtualFile.getName() returned null"); return new File(physicalFile.getParentFile(), name); } private static class PartialClassNameMatcher { private final String partialClassNameUpper; private final String prefixedPartialClassNameUpper1; private final String prefixedPartialClassNameUpper2; private PartialClassNameMatcher(String partialClassName) { partialClassNameUpper = partialClassName.toUpperCase(Locale.ENGLISH); prefixedPartialClassNameUpper1 = '.' + partialClassNameUpper; prefixedPartialClassNameUpper2 = '$' + partialClassNameUpper; } private boolean isPotentialFullMatch(String classNameUpper) { return classNameUpper.equals(partialClassNameUpper) || classNameUpper.endsWith(prefixedPartialClassNameUpper1) || classNameUpper.endsWith(prefixedPartialClassNameUpper2); } private boolean isPotentialMatch(String classNameUpper) { return classNameUpper.startsWith(partialClassNameUpper) || classNameUpper.contains(prefixedPartialClassNameUpper1) || classNameUpper.contains(prefixedPartialClassNameUpper2); } } private static class AnalyzingClassVisitor extends ClassVisitor { private final List<UiAnalyzedMethod> analyzedMethods = Lists.newArrayList(); private AnalyzingClassVisitor() { super(ASM5); } @Override public @Nullable MethodVisitor visitMethod(int access, String name, String desc, @Nullable String signature, String /*@Nullable*/[] exceptions) { if ((access & ACC_SYNTHETIC) != 0 || (access & ACC_NATIVE) != 0) { // don't add synthetic or native methods to the analyzed model return null; } if (name.equals("<init>")) { // don't add constructors to the analyzed model return null; } UiAnalyzedMethod.Builder builder = UiAnalyzedMethod.builder(); builder.name(name); for (Type parameterType : Type.getArgumentTypes(desc)) { builder.addParameterTypes(parameterType.getClassName()); } builder.returnType(Type.getReturnType(desc).getClassName()); builder.modifiers(access); if (exceptions != null) { for (String exception : exceptions) { builder.addExceptions(ClassNames.fromInternalName(exception)); } } analyzedMethods.add(builder.build()); return null; } private List<UiAnalyzedMethod> getAnalyzedMethods() { return analyzedMethods; } } }