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

org.glowroot.agent.weaving.AnalyzedWorld Maven / Gradle / Ivy

There is a newer version: 0.14.0-beta.3
Show newest version
/*
 * Copyright 2012-2018 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.agent.weaving;

import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.net.URL;
import java.security.CodeSource;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.WeakHashMap;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

import org.glowroot.agent.shaded.org.glowroot.agent.it.harness.shaded.com.google.common.base.Supplier;
import org.glowroot.agent.shaded.org.glowroot.agent.it.harness.shaded.com.google.common.collect.ImmutableList;
import org.glowroot.agent.shaded.org.glowroot.agent.it.harness.shaded.com.google.common.collect.ImmutableMap;
import org.glowroot.agent.shaded.org.glowroot.agent.it.harness.shaded.com.google.common.collect.Lists;
import org.glowroot.agent.shaded.org.glowroot.agent.it.harness.shaded.com.google.common.collect.Maps;
import org.glowroot.agent.shaded.org.glowroot.agent.it.harness.shaded.com.google.common.io.Resources;
import org.glowroot.agent.shaded.org.glowroot.agent.it.harness.shaded.com.google.common.primitives.Bytes;
import org.glowroot.agent.shaded.org.checkerframework.checker.nullness.qual.Nullable;
import org.immutables.value.Value;
import org.glowroot.agent.shaded.org.objectweb.asm.ClassReader;
import org.glowroot.agent.shaded.org.objectweb.asm.Type;
import org.glowroot.agent.shaded.org.glowroot.agent.shaded.org.slf4j.Logger;
import org.glowroot.agent.shaded.org.glowroot.agent.shaded.org.slf4j.LoggerFactory;

import org.glowroot.agent.weaving.ClassLoaders.LazyDefinedClass;
import org.glowroot.agent.shaded.org.glowroot.common.config.InstrumentationConfig;
import org.glowroot.agent.shaded.org.glowroot.common.util.Styles;

import static org.glowroot.agent.shaded.org.glowroot.agent.it.harness.shaded.com.google.common.base.Charsets.UTF_8;

public class AnalyzedWorld {

    private static final Logger logger = LoggerFactory.getLogger(AnalyzedWorld.class);

    private static final Method findLoadedClassMethod;

    static {
        try {
            findLoadedClassMethod =
                    ClassLoader.class.getDeclaredMethod("findLoadedClass", String.class);
            findLoadedClassMethod.setAccessible(true);
        } catch (Exception e) {
            // unrecoverable error
            throw new AssertionError(e);
        }
    }

    // weak keys to prevent retention of class loaders
    // it's important that the weak keys point directly to the class loaders themselves (as opposed
    // to through another instance, e.g. Optional) so that the keys won't be cleared
    // while their associated class loaders are still being used
    //
    // not using the much more convenient (and concurrent) guava CacheBuilder since it uses many
    // additional classes that must then be pre-initialized since this is called from inside
    // ClassFileTransformer.transform() (see PreInitializeClasses)
    private final Map> world = Collections
            .synchronizedMap(new WeakHashMap>());

    // the analyzed classes for the bootstrap class loader (null) have to be stored separately since
    // LoadingCache doesn't accept null keys, and using an Optional for the key makes
    // the weakness on the Optional instance which is not strongly referenced from anywhere and
    // therefore the keys will most likely be cleared while their class loaders are still being used
    //
    // intentionally avoiding Maps.newConcurrentMap() for the same reason as above
    private final ConcurrentMap bootstrapLoaderWorld =
            new ConcurrentHashMap();

    private final Supplier> advisors;
    private final ImmutableList shimTypes;
    private final ImmutableList mixinTypes;

    public AnalyzedWorld(Supplier> advisors, List shimTypes,
            List mixinTypes) {
        this.advisors = advisors;
        this.shimTypes = ImmutableList.copyOf(shimTypes);
        this.mixinTypes = ImmutableList.copyOf(mixinTypes);
    }

    public List> getClassesWithReweavableAdvice(boolean remove) {
        List> classes = Lists.newArrayList();
        for (ClassLoader loader : getClassLoaders()) {
            classes.addAll(getClassesWithReweavableAdvice(loader, remove));
        }
        classes.addAll(getClassesWithReweavableAdvice(null, remove));
        return classes;
    }

    public void removeClasses(Iterable> classes) {
        for (Map map : getWorldValues()) {
            for (Class clazz : classes) {
                map.remove(clazz.getName());
            }
        }
        for (Class clazz : classes) {
            bootstrapLoaderWorld.remove(clazz.getName());
        }
    }

    public ImmutableList getClassLoaders() {
        synchronized (world) {
            return ImmutableList.copyOf(world.keySet());
        }
    }

    void add(AnalyzedClass analyzedClass, @Nullable ClassLoader loader) {
        ConcurrentMap loaderAnalyzedClasses = getAnalyzedClasses(loader);
        loaderAnalyzedClasses.put(analyzedClass.name(), analyzedClass);
    }

    // it's ok if there are duplicates in the returned list (e.g. an interface that appears twice
    // in a type hierarchy), it's rare, dups don't cause an issue for callers, and so it doesn't
    // seem worth the (minor) performance hit to de-dup every time
    List getAnalyzedHierarchy(@Nullable String className,
            @Nullable ClassLoader loader, ParseContext parseContext) {
        if (className == null || className.equals("java.lang.Object")) {
            return ImmutableList.of();
        }
        return getSuperClasses(className, loader, parseContext);
    }

    static List mergeInstrumentationAnnotations(List advisors, byte[] classBytes,
            @Nullable ClassLoader loader, String className) {
        byte[] marker = "Lorg/glowroot/agent/api/Instrumentation$".getBytes(UTF_8);
        if (Bytes.indexOf(classBytes, marker) == -1) {
            return advisors;
        }
        InstrumentationSeekerClassVisitor cv = new InstrumentationSeekerClassVisitor();
        ClassReader cr = new ClassReader(classBytes);
        cr.accept(cv, ClassReader.SKIP_CODE);
        List instrumentationConfigs = cv.getInstrumentationConfigs();
        if (instrumentationConfigs.isEmpty()) {
            return advisors;
        }
        if (loader == null) {
            logger.warn("@Instrumentation annotations not currently supported in bootstrap class"
                    + " loader: {}", className);
            return advisors;
        }
        for (InstrumentationConfig instrumentationConfig : instrumentationConfigs) {
            instrumentationConfig.logValidationErrorsIfAny();
        }
        ImmutableMap newAdvisors =
                AdviceGenerator.createAdvisors(instrumentationConfigs, null, false);
        try {
            ClassLoaders.defineClasses(newAdvisors.values(), loader);
        } catch (Exception e) {
            logger.error(e.getMessage(), e);
        }
        List mergedAdvisors = Lists.newArrayList(advisors);
        mergedAdvisors.addAll(newAdvisors.keySet());
        return mergedAdvisors;
    }

    // it's ok if there are duplicates in the returned list (e.g. an interface that appears twice
    // in a type hierarchy), it's rare, dups don't cause an issue for callers, and so it doesn't
    // seem worth the (minor) performance hit to de-dup every time
    private List getSuperClasses(String className, @Nullable ClassLoader loader,
            ParseContext parseContext) {
        AnalyzedClass analyzedClass;
        try {
            analyzedClass = getOrCreateAnalyzedClass(className, loader);
        } catch (IOException e) {
            logger.error(e.getMessage(), e);
            return ImmutableList.of();
        } catch (ClassNotFoundException e) {
            // log at debug level only since the code referencing the class must not be getting used
            // anyways, as it would fail on execution since the type doesn't exist
            logger.debug("type {} not found while parsing type {}", className, parseContext, e);
            return ImmutableList.of();
        }
        List superTypes = Lists.newArrayList();
        superTypes.add(analyzedClass);
        String superName = analyzedClass.superName();
        if (superName != null && !superName.equals("java.lang.Object")) {
            superTypes.addAll(getSuperClasses(superName, loader, parseContext));
        }
        for (String interfaceName : analyzedClass.interfaceNames()) {
            superTypes.addAll(getSuperClasses(interfaceName, loader, parseContext));
        }
        return superTypes;
    }

    private AnalyzedClass getOrCreateAnalyzedClass(String className, @Nullable ClassLoader loader)
            throws ClassNotFoundException, IOException {
        ClassLoader analyzedClassLoader = getAnalyzedLoader(className, loader);
        ConcurrentMap loaderAnalyzedClasses =
                getAnalyzedClasses(analyzedClassLoader);
        AnalyzedClass analyzedClass = loaderAnalyzedClasses.get(className);
        if (analyzedClass == null) {
            if (loader != analyzedClassLoader) {
                // this class may have been looked up and stored previously in loader's map, and
                // then subsequently loaded into it's true class loader (analyzedClassLoader)
                ConcurrentMap currLoaderAnalyzedClasses =
                        getAnalyzedClasses(loader);
                analyzedClass = currLoaderAnalyzedClasses.get(className);
                if (analyzedClass != null) {
                    analyzedClass = putAnalyzedClass(loaderAnalyzedClasses, analyzedClass);
                    // remove it from the "incorrect" class loader
                    currLoaderAnalyzedClasses.remove(className);
                    // this
                    return analyzedClass;
                }
            }
            analyzedClass = createAnalyzedClass(className, analyzedClassLoader);
            analyzedClass = putAnalyzedClass(loaderAnalyzedClasses, analyzedClass);
        }
        return analyzedClass;
    }

    private List> getClassesWithReweavableAdvice(@Nullable ClassLoader loader,
            boolean remove) {
        List> classes = Lists.newArrayList();
        ConcurrentMap loaderAnalyzedClasses = getAnalyzedClasses(loader);
        for (Map.Entry innerEntry : loaderAnalyzedClasses.entrySet()) {
            if (innerEntry.getValue().hasReweavableAdvice()) {
                try {
                    classes.add(Class.forName(innerEntry.getKey(), false, loader));
                } catch (ClassNotFoundException e) {
                    logger.warn(e.getMessage(), e);
                }
            }
        }
        if (remove) {
            for (Class clazz : classes) {
                loaderAnalyzedClasses.remove(clazz.getName());
            }
        }
        return classes;
    }

    private AnalyzedClass createAnalyzedClass(String className, @Nullable ClassLoader loader)
            throws ClassNotFoundException, IOException {
        String path = ClassNames.toInternalName(className) + ".class";
        URL url;
        if (loader == null) {
            // null loader means the bootstrap class loader
            url = ClassLoader.getSystemResource(path);
        } else {
            url = loader.getResource(path);
            if (url != null) {
                AnalyzedClass parentLoaderAnalyzedClass =
                        tryToReuseFromParentLoader(className, loader, path, url);
                if (parentLoaderAnalyzedClass != null) {
                    return parentLoaderAnalyzedClass;
                }
            }
        }
        if (url == null) {
            // what follows is just a best attempt in the sort-of-rare case when a custom class
            // loader does not expose .class file contents via getResource(), e.g.
            // org.codehaus.groovy.runtime.callsite.CallSiteClassLoader
            return createAnalyzedClassPlanB(className, loader);
        }
        byte[] bytes = Resources.toByteArray(url);
        List advisors =
                mergeInstrumentationAnnotations(this.advisors.get(), bytes, loader, className);
        ThinClassVisitor accv = new ThinClassVisitor();
        new ClassReader(bytes).accept(accv, ClassReader.SKIP_FRAMES + ClassReader.SKIP_CODE);
        // passing noLongerNeedToWeaveMainMethods=true since not really weaving bytecode here
        ClassAnalyzer classAnalyzer = new ClassAnalyzer(accv.getThinClass(), advisors, shimTypes,
                mixinTypes, loader, this, null, bytes, null, true);
        classAnalyzer.analyzeMethods();
        return classAnalyzer.getAnalyzedClass();
    }

    private @Nullable AnalyzedClass tryToReuseFromParentLoader(String className,
            ClassLoader originalLoader, String path, URL url) {
        ClassLoader loader = originalLoader;
        while (loader != null) {
            ClassLoader parentLoader = loader.getParent();
            URL parentLoaderUrl;
            if (parentLoader == null) {
                parentLoaderUrl = ClassLoader.getSystemResource(path);
            } else {
                parentLoaderUrl = parentLoader.getResource(path);
            }
            // comparing results of URL.toExternalForm() since using URL.equals() directly
            // performs name resolution and is a blocking operation (from the javadoc)
            if (parentLoaderUrl != null
                    && parentLoaderUrl.toExternalForm().equals(url.toExternalForm())) {
                // reuse parent loader's AnalyzedClass if available
                // this saves time here, and reduces memory footprint of AnalyzedWorld
                // which can be very noticeable when lots of ClassLoaders, e.g. groovy
                AnalyzedClass parentLoaderAnalyzedClass =
                        getAnalyzedClasses(parentLoader).get(className);
                if (parentLoaderAnalyzedClass != null) {
                    return parentLoaderAnalyzedClass;
                }
            }
            loader = parentLoader;
        }
        return null;
    }

    // plan B covers some class loaders like
    // org.codehaus.groovy.runtime.callsite.CallSiteClassLoader that delegate loadClass() to some
    // other loader where the type may have already been loaded
    private AnalyzedClass createAnalyzedClassPlanB(String className, @Nullable ClassLoader loader)
            throws ClassNotFoundException {
        Class clazz = Class.forName(className, false, loader);
        AnalyzedClass analyzedClass = getAnalyzedClasses(clazz.getClassLoader()).get(className);
        if (analyzedClass != null) {
            return analyzedClass;
        }
        // the class loaded by Class.forName() above was not previously loaded which means
        // weaving was bypassed since ClassFileTransformer.transform() is not re-entrant
        analyzedClass = createAnalyzedClassPlanC(clazz, advisors.get());
        if (analyzedClass.isInterface()) {
            // FIXME log warning if any default methods have advice
            return analyzedClass;
        }
        if (!analyzedClass.analyzedMethods().isEmpty()) {
            logger.warn(
                    "{} was not woven with requested advice (it was first encountered during the"
                            + " weaving of one of its {} and the resource {}.class could not be"
                            + " found in class loader {}, so {} had to be explicitly loaded using"
                            + " Class.forName() in the middle of weaving the {}, which means it was"
                            + " not woven itself since weaving is not re-entrant)",
                    clazz.getName(), analyzedClass.isInterface() ? "implementations" : "subclasses",
                    ClassNames.toInternalName(clazz.getName()), loader, clazz.getName(),
                    analyzedClass.isInterface() ? "implementation" : "subclass");
        }
        return analyzedClass;
    }

    private ConcurrentMap getAnalyzedClasses(@Nullable ClassLoader loader) {
        if (loader == null) {
            return bootstrapLoaderWorld;
        } else {
            // this synchronization is for atomicity of get/put
            synchronized (world) {
                ConcurrentMap map = world.get(loader);
                if (map == null) {
                    map = new ConcurrentHashMap();
                    world.put(loader, map);
                }
                return map;
            }
        }
    }

    private ImmutableList> getWorldValues() {
        synchronized (world) {
            return ImmutableList.copyOf(world.values());
        }
    }

    private static AnalyzedClass putAnalyzedClass(
            ConcurrentMap loaderAnalyzedClasses,
            AnalyzedClass analyzedClass) {
        AnalyzedClass existingAnalyzedClass =
                loaderAnalyzedClasses.putIfAbsent(analyzedClass.name(), analyzedClass);
        if (existingAnalyzedClass != null) {
            // (rare) concurrent AnalyzedClass creation, use the one that made it into the map
            return existingAnalyzedClass;
        }
        return analyzedClass;
    }

    private static @Nullable ClassLoader getAnalyzedLoader(String className,
            @Nullable ClassLoader loader) {
        if (loader == null) {
            return null;
        }
        // can't call Class.forName() since that bypasses ClassFileTransformer.transform() if the
        // class hasn't already been loaded, so instead, call the package protected
        // ClassLoader.findLoadClass()
        Class clazz = null;
        try {
            clazz = (Class) findLoadedClassMethod.invoke(loader, className);
        } catch (Exception e) {
            logger.error(e.getMessage(), e);
        }
        ClassLoader analyzedLoader = loader;
        if (clazz != null) {
            // this class has already been loaded, so the corresponding analyzedClass should already
            // be in the cache under its class loader
            //
            // this helps in cases where the .class files are not available via
            // ClassLoader.getResource(), as well as being a good optimization in other cases
            analyzedLoader = clazz.getClassLoader();
        }
        return analyzedLoader;
    }

    // now that the type has been loaded anyways, build the analyzed class via reflection
    private static AnalyzedClass createAnalyzedClassPlanC(Class clazz, List advisors) {
        ImmutableAnalyzedClass.Builder classBuilder = ImmutableAnalyzedClass.builder();
        classBuilder.modifiers(clazz.getModifiers());
        classBuilder.name(clazz.getName());
        Class superClass = clazz.getSuperclass();
        String superName = superClass == null ? null : superClass.getName();
        classBuilder.superName(superName);
        List superClassNames = Lists.newArrayList();
        if (superName != null) {
            superClassNames.add(superName);
        }
        for (Class interfaceClass : clazz.getInterfaces()) {
            String interfaceClassName = interfaceClass.getName();
            classBuilder.addInterfaceNames(interfaceClassName);
            superClassNames.add(interfaceClassName);
        }
        // FIXME handle @Instrumentation.*
        List classAnnotations = Lists.newArrayList();
        for (Annotation annotation : clazz.getAnnotations()) {
            classAnnotations.add(annotation.annotationType().getName());
        }
        // TODO document limitations of superClassNames only containing first level super classes
        // (e.g. doesn't include super class's super class)
        List adviceMatchers = AdviceMatcher.getAdviceMatchers(clazz.getName(),
                classAnnotations, superClassNames, advisors);
        Map> bridgeTargetAdvisors = Maps.newHashMap();
        for (Method method : clazz.getDeclaredMethods()) {
            if (!method.isBridge()) {
                continue;
            }
            List methodAnnotations = Lists.newArrayList();
            for (Annotation annotation : method.getAnnotations()) {
                methodAnnotations.add(annotation.annotationType().getName());
            }
            List parameterTypes = Lists.newArrayList();
            for (Class parameterType : method.getParameterTypes()) {
                parameterTypes.add(Type.getType(parameterType));
            }
            Type returnType = Type.getType(method.getReturnType());
            List matchingAdvisors =
                    getMatchingAdvisors(method.getModifiers(), method.getName(), methodAnnotations,
                            parameterTypes, returnType, adviceMatchers);
            if (!matchingAdvisors.isEmpty()) {
                Method targetMethod = getTargetMethod(method, clazz);
                if (targetMethod != null) {
                    bridgeTargetAdvisors.put(targetMethod, matchingAdvisors);
                }
            }
        }
        boolean intf = clazz.isInterface();
        for (Method method : clazz.getDeclaredMethods()) {
            if (method.isSynthetic()) {
                // don't add synthetic methods to the analyzed model
                continue;
            }
            int modifiers = method.getModifiers();
            List methodAnnotations = Lists.newArrayList();
            for (Annotation annotation : method.getAnnotations()) {
                methodAnnotations.add(annotation.annotationType().getName());
            }
            List parameterTypes = Lists.newArrayList();
            for (Class parameterType : method.getParameterTypes()) {
                parameterTypes.add(Type.getType(parameterType));
            }
            Type returnType = Type.getType(method.getReturnType());
            List matchingAdvisors = getMatchingAdvisors(modifiers, method.getName(),
                    methodAnnotations, parameterTypes, returnType, adviceMatchers);
            List extraAdvisors = bridgeTargetAdvisors.get(method);
            if (extraAdvisors != null) {
                matchingAdvisors.addAll(extraAdvisors);
            }
            ClassAnalyzer.sortAdvisors(matchingAdvisors);
            boolean intfMethod = intf && !Modifier.isStatic(modifiers);
            if (!matchingAdvisors.isEmpty() || intfMethod) {
                ImmutableAnalyzedMethod.Builder methodBuilder = ImmutableAnalyzedMethod.builder();
                methodBuilder.name(method.getName());
                for (Type parameterType : parameterTypes) {
                    methodBuilder.addParameterTypes(parameterType.getClassName());
                }
                methodBuilder.returnType(returnType.getClassName());
                methodBuilder.modifiers(modifiers);
                // FIXME re-build signature and set in AnalyzedMethod.signature()
                for (Class exceptionType : method.getExceptionTypes()) {
                    methodBuilder.addExceptions(exceptionType.getName());
                }
                methodBuilder.addAllAdvisors(matchingAdvisors);
                classBuilder.addAnalyzedMethods(methodBuilder.build());
            }
            if (Modifier.isFinal(modifiers) && Modifier.isPublic(modifiers)) {
                ImmutablePublicFinalMethod.Builder publicFinalMethodBuilder =
                        ImmutablePublicFinalMethod.builder()
                                .name(method.getName());
                for (Type parameterType : parameterTypes) {
                    publicFinalMethodBuilder.addParameterTypes(parameterType.getClassName());
                }
                classBuilder.addPublicFinalMethods(publicFinalMethodBuilder.build());
            }
        }
        boolean ejbRemote = false;
        for (Annotation annotation : clazz.getDeclaredAnnotations()) {
            if (annotation.annotationType().getName().equals("javax.ejb.Remote")) {
                ejbRemote = true;
                break;
            }
        }
        return classBuilder.ejbRemote(ejbRemote)
                .build();
    }

    private static @Nullable Method getTargetMethod(Method bridgeMethod, Class clazz) {
        List possibleTargetMethods = getPossibleTargetMethods(bridgeMethod, clazz);
        if (possibleTargetMethods.isEmpty()) {
            logger.warn("could not find any target for bridge method: {}", bridgeMethod);
        }
        if (possibleTargetMethods.size() == 1) {
            return possibleTargetMethods.get(0);
        }
        // FIXME what now, look at generic signatures?
        logger.warn("found more than one possible target for bridge method: {}", bridgeMethod);
        return null;
    }

    private static List getPossibleTargetMethods(Method bridgeMethod, Class clazz) {
        List possibleTargetMethods = Lists.newArrayList();
        for (Method method : clazz.getDeclaredMethods()) {
            if (!method.getName().equals(bridgeMethod.getName())) {
                continue;
            }
            if (method.getParameterTypes().length != bridgeMethod.getParameterTypes().length) {
                continue;
            }
            possibleTargetMethods.add(method);
        }
        return possibleTargetMethods;
    }

    // important that this returns a mutable list
    private static List getMatchingAdvisors(int access, String name,
            List methodAnnotations, List parameterTypes, Type returnType,
            List adviceMatchers) {
        List matchingAdvisors = Lists.newArrayList();
        for (AdviceMatcher adviceMatcher : adviceMatchers) {
            if (adviceMatcher.isMethodLevelMatch(name, methodAnnotations, parameterTypes,
                    returnType, access)) {
                matchingAdvisors.add(adviceMatcher.advice());
            }
        }
        return matchingAdvisors;
    }

    @Value.Immutable
    @Styles.AllParameters
    abstract static class ParseContext {
        abstract String className();
        abstract @Nullable CodeSource codeSource();
        // toString() is used in logger warning construction
        @Override
        public String toString() {
            CodeSource codeSource = codeSource();
            if (codeSource == null) {
                return className();
            } else {
                return className() + " (" + codeSource.getLocation() + ")";
            }
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy