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

com.newrelic.weave.weavepackage.WeavePackageManager Maven / Gradle / Ivy

There is a newer version: 8.17.0
Show newest version
/*
 *
 *  * Copyright 2020 New Relic Corporation. All rights reserved.
 *  * SPDX-License-Identifier: Apache-2.0
 *
 */

package com.newrelic.weave.weavepackage;

import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.google.common.collect.Sets;
import com.newrelic.weave.utils.BootstrapLoader;
import com.newrelic.weave.utils.ClassCache;
import com.newrelic.weave.utils.ClassInformation;
import com.newrelic.weave.utils.ClassLoaderFinder;
import com.newrelic.weave.utils.WeaveUtils;
import org.objectweb.asm.AnnotationVisitor;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import org.objectweb.asm.commons.Method;
import org.objectweb.asm.tree.ClassNode;

import java.io.IOException;
import java.lang.instrument.Instrumentation;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.atomic.AtomicBoolean;

/**
 * Manages a group of {@link WeavePackage}s. This class is thread safe.
 */
public class WeavePackageManager {
    /**
     * WeavePackageName -> WeavePackage
     */
    private final ConcurrentMap weavePackages = new ConcurrentHashMap<>();

    /**
     * The set of @Weave and reference classes in all registered weave packages (used as an optimization)
     */
    private final Set requiredClasses = Sets.newConcurrentHashSet();

    /**
     * The set of all method signatures in all registered @Weave classes (used as an optimization)
     */
    private final Set methodSignatures = Sets.newConcurrentHashSet();

    /**
     * The set of all annotations required in all registered @Weave pacakges
     */
    private final Set requiredAnnotationClasses = Sets.newConcurrentHashSet();

    private final Set requiredMethodAnnotationClasses = Sets.newConcurrentHashSet();

    /**
     * ClassLoader -> (WeavePackageName -> WeavePackage)
     */
    private final Cache> optimizedWeavePackages = Caffeine.newBuilder().weakKeys().executor(Runnable::run).build();

    private final WeavePackageLifetimeListener packageListener;
    private final Instrumentation instrumentation;
    private final int maxPreValidatedClassLoaders;
    private final boolean preValidateWeavePackages;
    private final boolean preMatchWeaveMethods;

    /**
     * Stores the a PackageValidationResult for the current thread. This exists to work around an issue where a `defineClass()` call
     * will cause a weave package to be validated again, generating different utility classes and breaking the instrumentation module.
     *
     * By storing this in a thread local we are able to check and re-use the result in the secondary call without requiring the package
     * to be added to the map of `validPackages` which could cause a NoClassDefFoundError if we add the utility classes in before the
     * weave classes have a chance to get added.
     */
    private static final ThreadLocal currentValidationResult = new ThreadLocal<>();

    static final int MAX_VALID_PACKAGE_CACHE = 100;
    static final int MAX_INVALID_PACKAGE_CACHE = 100;

    /**
     * classloader -> (weave package -> result of successful weaving)
     */
    Cache> validPackages = Caffeine.newBuilder().weakKeys().initialCapacity(
            8).maximumSize(MAX_VALID_PACKAGE_CACHE).executor(Runnable::run).build();
    /**
     * classloader -> (weave package -> result of successful weaving)
     */
    Cache> invalidPackages = Caffeine.newBuilder().weakKeys().initialCapacity(
            8).maximumSize(MAX_INVALID_PACKAGE_CACHE).executor(Runnable::run).build();

    WeavePackageManager() {
        this(null);
    }

    /**
     * Create a manager with the specified listener
     *
     * @param listener callback interface that's invoked when packages are registered, deregistered, or validated
     */
    public WeavePackageManager(WeavePackageLifetimeListener listener) {
        this(listener, null, 10, true, true);
    }

    /**
     * Create a manager with the specified listener and {@link Instrumentation} instance.
     *
     * @param listener callback interface that's invoked when packages are registered, deregistered, or validated
     * @param instrumentation {@link Instrumentation} instance
     */
    public WeavePackageManager(WeavePackageLifetimeListener listener, Instrumentation instrumentation,
            int maxPreValidatedClassLoaders, boolean preValidateWeavePackages, boolean preMatchWeaveMethods) {
        this.packageListener = listener;
        this.instrumentation = instrumentation;
        this.maxPreValidatedClassLoaders = maxPreValidatedClassLoaders;
        this.preValidateWeavePackages = preValidateWeavePackages;
        this.preMatchWeaveMethods = preMatchWeaveMethods;
    }

    /**
     * Indicates whether this manager can weave classes loaded by the bootstrap class loader.
     *
     * @return true if bootstrap classes can be weaved
     */
    boolean canWeaveBootstrapClassLoader() {
        return null != instrumentation;
    }

    /**
     * Indicates whether or not the specified package is currently registered by this manager.
     *
     * @param weavePackageName package name
     * @return true if registered
     */
    public boolean isRegistered(String weavePackageName) {
        return weavePackages.containsKey(weavePackageName);
    }

    /**
     * Add the specified package to this manager
     *
     * @param weavePackage package to add
     */
    public void register(WeavePackage weavePackage) {
        if (null != weavePackage && (!weavePackages.containsKey(weavePackage.getName())) && (weavePackages.putIfAbsent(
                weavePackage.getName(), weavePackage) == null)) {
            methodSignatures.addAll(weavePackage.getMethodSignatures());
            requiredClasses.addAll(weavePackage.getRequiredClasses());
            requiredAnnotationClasses.addAll(weavePackage.getAllRequiredAnnotationClasses());
            requiredMethodAnnotationClasses.addAll(weavePackage.getAllRequiredMethodAnnotationClasses());

            if (null != packageListener) {
                packageListener.registered(weavePackage);
            }
            // Just in case we've already built up the cache before an extension was registered we
            // need to clear the cache to rebuild with the new extension jar on the next call to match()
            optimizedWeavePackages.invalidateAll();
        }
    }

    /**
     * Remove the specified weave package from this manager.
     *
     * @param weavePackage package to remove
     * @return removed package, or null if nothing was removed
     */
    public WeavePackage deregister(WeavePackage weavePackage) {
        if (null == weavePackage) {
            return null;
        }
        WeavePackage remove = weavePackages.remove(weavePackage.getName());
        if (null != remove) {
            optimizedWeavePackages.invalidateAll();
            requiredClasses.removeAll(remove.getRequiredClasses());
            // Rebuild method signatures from weavePackages map
            rebuildWeavePackages();
        }
        if (null != remove && null != packageListener) {
            packageListener.deregistered(remove);
        }
        return remove;
    }

    private void rebuildWeavePackages() {
        Set updatedMethodSignatures = Sets.newConcurrentHashSet();
        for (WeavePackage weavePackage : weavePackages.values()) {
            updatedMethodSignatures.addAll(weavePackage.getMethodSignatures());
        }
        methodSignatures.clear();
        methodSignatures.addAll(updatedMethodSignatures);
    }

    /**
     * Remove the specified weave package from this manager.
     *
     * @param weavePackageName name of package to remove
     * @return removed package, or null if nothing was removed
     */
    public WeavePackage deregister(String weavePackageName) {
        return deregister(weavePackages.get(weavePackageName));
    }

    /**
     * Find the package for the specified name if registered by this manager.
     *
     * @param name package name
     * @return registered package, or null if nothing was registered
     */
    public WeavePackage getWeavePackage(String name) {
        return weavePackages.get(name);
    }

    /**
     * All of the packages registered by this class.
     */
    public Collection getRegisteredPackages() {
        return weavePackages.values();
    }

    /**
     * A hack to treat the bootsrap like a {@link ClassLoader} to clean up implementations.
     *
     * @param classloader instance of a {@link ClassLoader}, or null indicating the bootstrap
     * @return provided classloader instance, or {@link BootstrapLoader#PLACEHOLDER} if null
     */
    private ClassLoader classLoaderSub(ClassLoader classloader) {
        if (null == classloader) {
            return BootstrapLoader.PLACEHOLDER;
        } else {
            return classloader;
        }
    }

    /**
     * Determine if the specified class and class loader match any packages.
     *
     * @param classloader classloader containing classpath to validate against
     * @param className name of the class
     * @param cache {@link ClassCache} used to resolve classes without loading them
     * @return validation result set
     */
    public Set match(ClassLoader classloader, String className, ClassCache cache)
            throws IOException {
        classloader = classLoaderSub(classloader);
        ClassInformation classInformation = cache.getClassInformation(className);
        if (null == classInformation) {
            return Collections.emptySet();
        }

        String[] superNames = classInformation.getAllSuperNames(cache).toArray(new String[0]);
        String[] interfaceNames = classInformation.getAllInterfaces(cache).toArray(new String[0]);
        Set classAnnotations = classInformation.classAnnotationNames;
        Set methodAnnotations = classInformation.methodAnnotationNames;

        return match(classloader, cache, className, classAnnotations, methodAnnotations, superNames, interfaceNames);
    }

    private Set match(ClassLoader classloader, ClassCache cache, String className,
            Set classAnnotations, Set methodAnnotations, String[] superNames, String[] interfaceNames)
            throws IOException {
        Set matchedPackageResults = Sets.newConcurrentHashSet();

        Map classloaderWeavePackages;
        if (preValidateWeavePackages && optimizedWeavePackages.asMap().size() < maxPreValidatedClassLoaders) {
            classloaderWeavePackages = getOptimizedWeavePackages(classloader, cache);
        } else {
            classloaderWeavePackages = weavePackages;
        }

        for (WeavePackage weavePackage : classloaderWeavePackages.values()) {
            if (weavePackage.hasMatcher(className, superNames, interfaceNames, classAnnotations, methodAnnotations, cache)) {
                PackageValidationResult successfulValidation = this.getSuccessfulValidation(className, superNames[0], interfaceNames, classloader, cache,
                        weavePackage);
                if (null != successfulValidation) {
                    matchedPackageResults.add(successfulValidation);
                }
            }
        }
        return matchedPackageResults;
    }

    /**
     * Weave all of the matched packages with the specified target bytes and return the composite class.
     *
     * @param classloader classloader to resolve classes with
     * @param className target class name
     * @param targetBytes target class bytes
     * @return composite class bytes, or null if no weaving occurred
     */
    public byte[] weave(ClassLoader classloader, String className, byte[] targetBytes,
                        Map> skipMethods) throws IOException {
        classloader = classLoaderSub(classloader);
        ClassCache cache = new ClassCache(new ClassLoaderFinder(classloader));
        return weave(classloader, cache, className, targetBytes, skipMethods, null);
    }

    /**
     * Weave all of the matched packages with the specified target bytes using the specified cache and listener.
     *
     * @param classloader classloader to resolve classes with
     * @param cache {@link ClassCache} to find class metadata
     * @param className target class name
     * @param targetBytes target class bytes
     * @param weaveListener listener containing callback if/when the composite is created
     * @return composite class bytes, or null if no weaving occurred
     */
    public byte[] weave(ClassLoader classloader, ClassCache cache, String className,
                        byte[] targetBytes, Map> skipMethods,
            ClassWeavedListener weaveListener) throws IOException {
        classloader = classLoaderSub(classloader);

        if (preMatchWeaveMethods && !containsPossibleClassOrMethodMatch(className, targetBytes, requiredClasses, methodSignatures, cache)) {
            // No potential method match was found, we are definitely not weaving this class so we should exit now
            return null;
        }

        ClassInformation classInformation = cache.getClassInformation(className);
        if (classInformation == null) {
            return null;
        }

        String[] superNames = cache.getClassInformation(className).getAllSuperNames(cache).toArray(new String[0]);
        String[] interfaceNames = cache.getClassInformation(className).getAllInterfaces(cache).toArray(new String[0]);
        Set classAnnotations = cache.getClassInformation(className).classAnnotationNames;
        Set methodAnnotations = cache.getClassInformation(className).methodAnnotationNames;

        Set matchedPackageResults = this.match(classloader, cache, className, classAnnotations,
                methodAnnotations, superNames, interfaceNames);
        if (matchedPackageResults.isEmpty()) {
            return null;
        }

        ClassNode composite = WeaveUtils.convertToClassNode(targetBytes);
        PackageWeaveResult finalResult = null;

        List sortedMatchedPackages = new ArrayList<>(matchedPackageResults);
        sortedMatchedPackages.sort(PackageValidationResult.CONFIG_COMPARATOR);
        for (PackageValidationResult weavePackageResult : sortedMatchedPackages) {
            PackageWeaveResult result = weavePackageResult.weave(className, superNames, interfaceNames, composite,
                                                                 cache, skipMethods);
            if (null != weaveListener) {
                weaveListener.classWeaved(result, classloader, cache);
            }
            if (result.weavedClass()) {
                composite = result.getComposite();
                finalResult = result;
            }
        }
        return null == finalResult ? null : finalResult.getCompositeBytes(cache);
    }

    /**
     * Quickly checks to see if the class/super class/interfaces or any of the methods in the Class represented by the
     * "classByte" parameter match the Set of known classes/method signatures that constitute all of our weave classes.
     * If a potential match is found it means we should continue with the normal validation process. If no match was
     * found, then it's certain that this class would never match and we should end transformation of this class via the
     * weaver here.
     *
     * @param classBytes the byte[] representing the class to check
     * @param requiredClasses the set of known classes/superclasses/interfaces to compare against
     * @param methodSignatures the set of all method signatures to compare against
     * @param cache class cache to lookup interface annotations
     * @return true if this contains a possible match, false otherwise
     */
    private boolean containsPossibleClassOrMethodMatch(final String className, final byte[] classBytes, final Set requiredClasses,
                                                       final Set methodSignatures, final ClassCache cache) {
        final AtomicBoolean containsPossibleMatch = new AtomicBoolean(false);
        ClassReader originalBytesReader = new ClassReader(classBytes);
        originalBytesReader.accept(new ClassVisitor(WeaveUtils.ASM_API_LEVEL) {

            public String[] interfaces;
            private boolean isInterface = false;

            @Override
            public void visit(int version, int access, String name, String signature, String superName,
                    String[] interfaces) {

                this.interfaces = interfaces;
                isInterface = (access & Opcodes.ACC_INTERFACE) == Opcodes.ACC_INTERFACE;

                // Figure out if this exact class or the super class is a match
                if (requiredClasses.contains(name) || requiredClasses.contains(superName)) {
                    containsPossibleMatch.set(true);
                    return;
                }
                super.visit(version, access, name, signature, superName, interfaces);
            }

            @Override
            public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
                String annotationClass = Type.getType(desc).getClassName();

                if (!isInterface && requiredAnnotationClasses.contains(annotationClass)) {
                    // Adding class name here allows us to catch interfaces
                    containsPossibleMatch.set(true);
                    requiredClasses.add(className);

                    // Found a potential match. Exit early.
                    return null;
                }
                return super.visitAnnotation(desc, visible);
            }

            @Override
            public MethodVisitor visitMethod(int access, String name, String desc, String signature,
                    String[] exceptions) {
                if (containsPossibleMatch.get()) {
                    // This means we found a class/superclass/interface match so we should exit early here
                    return null;
                }

                // Default constructor and static initializer will be matched by visit() method above
                if (!(name.equals(WeaveUtils.INIT_NAME) && desc.equals(WeaveUtils.INIT_DESC)) &&
                        !(name.equals(WeaveUtils.CLASS_INIT_NAME) && desc.equals(WeaveUtils.INIT_DESC)) &&
                        methodSignatures.contains(name + desc)) {
                    // If any method matches this is a potential match so we should return "true"
                    containsPossibleMatch.set(true);

                    // Skip out of this method visitor early since we've found a match
                    return null;
                }

                MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions);
                if (!requiredMethodAnnotationClasses.isEmpty()) {
                    return new MethodVisitor(WeaveUtils.ASM_API_LEVEL, mv) {
                        @Override
                        public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
                            String annotationClass = Type.getType(desc).getClassName();
                            if (requiredMethodAnnotationClasses.contains(annotationClass)) {
                                containsPossibleMatch.set(true);

                                // Found a potential match. Exit early.
                                return null;
                            }
                            return super.visitAnnotation(desc, visible);
                        }
                    };
                }
                return mv;
            }

            @Override
            public void visitEnd() {
                // If nothing has matched, check interface annotations
                if (!isInterface && !requiredAnnotationClasses.isEmpty()) {

                    try {
                        for (String interfaceName : interfaces) {
                            ClassInformation interfaceInfo = cache.getClassInformation(interfaceName);
                            if (interfaceInfo == null) {
                                continue;
                            }

                            for (String interfaceAnnotationName : interfaceInfo.classAnnotationNames) {
                                if (requiredAnnotationClasses.contains(WeaveUtils.getClassBinaryName(interfaceAnnotationName))) {
                                    containsPossibleMatch.set(true);
                                    return;
                                }
                            }
                        }
                    } catch (IOException ignored) {
                    }
                }
            }
        }, ClassReader.SKIP_FRAMES | ClassReader.SKIP_DEBUG | ClassReader.SKIP_CODE);

        return containsPossibleMatch.get();
    }

    /**
     * Returns the successful validation result for the weave package against the classloader, or null if
     * there is no successful result.
     */
    private PackageValidationResult getSuccessfulValidation(String className, String superName, String[] interfaceNames, ClassLoader classloader,
            ClassCache cache, WeavePackage weavePackage) throws IOException {
        // If we already have a completed result on this thread we need to use it rather than attempting to re-validate for the same package
        PackageValidationResult localValidationResult = currentValidationResult.get();
        if (localValidationResult != null) {
            return localValidationResult;
        }

        if (validateAgainstClassLoader(className, superName, interfaceNames, classloader, cache, weavePackage)) {
            final ClassLoader classloaderToUse = weavePackage.weavesBootstrap() ? BootstrapLoader.PLACEHOLDER : classloader;
            ConcurrentMap valid = validPackages.getIfPresent(classloaderToUse);
            return valid == null ? null : valid.get(weavePackage);
        }
        return null;
    }

    /**
     * Determine if the weavePackage is valid against classloader (using cache to check for resources).
     */
    private boolean validateAgainstClassLoader(String className, String superName, String[] interfaceNames, ClassLoader classloader, ClassCache cache,
            WeavePackage weavePackage) throws IOException {
        if (classloader != BootstrapLoader.PLACEHOLDER && weavePackage.weavesBootstrap()) {
            classloader = BootstrapLoader.PLACEHOLDER;
            cache = new ClassCache(BootstrapLoader.get());
        }

        try {
            // this is the first time we've validated this package against this classloader.
            if (!hasValidated(classloader, weavePackage)) {
                PackageValidationResult verificationResult = weavePackage.validate(cache);
                currentValidationResult.set(verificationResult);

                if (null != packageListener) {
                    packageListener.validated(verificationResult, classloader);
                }

                if ((classloader == BootstrapLoader.PLACEHOLDER && !this.canWeaveBootstrapClassLoader())
                        || (!verificationResult.succeeded())) {
                    ConcurrentMap result = invalidPackages.asMap().putIfAbsent(
                            classloader, new ConcurrentHashMap());
                    if (result == null) {
                        result = invalidPackages.asMap().get(classloader);
                    }
                    result.put(weavePackage, verificationResult);
                    return false;
                } else {
                    // We need to add this to the valid packages list before appending new classes to prevent a circular class load
                    ConcurrentMap result = validPackages.asMap().putIfAbsent(
                            classloader, new ConcurrentHashMap());
                    if (result == null) {
                        result = validPackages.asMap().get(classloader);
                    }

                    try {
                        if (BootstrapLoader.PLACEHOLDER == classloader) {
                            NewClassAppender.appendClassesToBootstrapClassLoader(instrumentation,
                                    verificationResult.computeUtilityClassBytes(cache));
                        } else {
                            NewClassAppender.appendClasses(className, superName, interfaceNames, classloader,
                                    verificationResult.computeUtilityClassBytes(cache));
                        }
                        result.put(weavePackage, verificationResult);
                    } catch (Throwable t) {
                        // If the new class appending above throws an exception we need to remove the validated package (since it's no longer valid)
                        result.remove(weavePackage);
                    }
                }
            }

            ConcurrentMap result = validPackages.getIfPresent(classloader);
            return result != null && result.containsKey(weavePackage);
        } finally {
            currentValidationResult.remove();
        }
    }

    /**
     * Determine whether the specified package has been validated by the specified class loader.
     */
    private boolean hasValidated(ClassLoader classloader, WeavePackage weavePackage) {
        ConcurrentMap invalidResult = invalidPackages.getIfPresent(classloader);
        ConcurrentMap validResult = validPackages.getIfPresent(classloader);
        return (invalidResult != null && invalidResult.containsKey(weavePackage)) || (validResult != null
                && validResult.containsKey(weavePackage));
    }

    /**
     * This method reduces the list of weave packages down to only those that could potentially match based on the
     * classes that currently exist on the classpath and the classes that the weave package requires.
     *
     * After determining which packages could match, we cache this value for use with all future calls to match() for a
     * big performance increase. If a new package is registered we invalidate the cache and the next call to match()
     * will take the hit to rebuild the cache to potentially include the newly added package.
     *
     * @param classloader the classloader to check for required classes
     * @param cache the class cache (backed by the classloader) to check if the resource exists
     * @return an optimized map of weave packages for faster matching
     */
    private Map getOptimizedWeavePackages(ClassLoader classloader, ClassCache cache) {
        ConcurrentMap classloaderWeavePackages = optimizedWeavePackages.getIfPresent(classloader);
        if (classloaderWeavePackages == null) {
            classloaderWeavePackages = new ConcurrentHashMap<>();

            // Loop over each weave package and verify that all required classes can be found by the classloader
            Collection unmatchedWeavePackages = new ArrayList<>();
            for (Map.Entry entry : this.weavePackages.entrySet()) {
                boolean packageExists = true;
                boolean hasAtLeastOneMatch = false;
                for (String weaveClass : entry.getValue().getRequiredClasses()) {
                    if (!cache.hasClassResource(weaveClass)) {
                        // The class can't be found via this classloader so we'll set the flag
                        // to skip this package but we want to continue this loop to see if we
                        // can find any matching class resources for debugging purposes
                        packageExists = false;
                    } else {
                        // If at least one required class is found lets attempt to validate
                        // the package so we can send the correct debug logging to the user
                        hasAtLeastOneMatch = true;

                        // Optimization to break out of this loop early
                        if (!packageExists) {
                            break;
                        }
                    }
                }

                if (packageExists) {
                    classloaderWeavePackages.put(entry.getKey(), entry.getValue());
                } else if (hasAtLeastOneMatch) {
                    unmatchedWeavePackages.add(entry.getValue());
                }
            }

            ConcurrentMap optimizedMap = optimizedWeavePackages.asMap().putIfAbsent(classloader,
                    classloaderWeavePackages);
            if (optimizedMap == null) {
                // Run verifications against unmatched packages since this is
                // the thread that added the packages to the optimized map
                for (WeavePackage unmatchedWeavePackage : unmatchedWeavePackages) {
                    try {
                        PackageValidationResult verificationResult = unmatchedWeavePackage.validate(cache);
                        if (null != packageListener) {
                            packageListener.validated(verificationResult, classloader);
                        }
                    } catch (Exception e) {
                        // Ignore
                    }
                }
            }
        }
        return classloaderWeavePackages;
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy