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

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

There is a newer version: 0.9.24
Show newest version
/*
 * Copyright 2015-2016 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.lang.reflect.Modifier;
import java.security.CodeSource;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.regex.Pattern;

import javax.annotation.Nullable;

import org.glowroot.agent.shaded.google.common.collect.ImmutableList;
import org.glowroot.agent.shaded.google.common.collect.ImmutableMap;
import org.glowroot.agent.shaded.google.common.collect.ImmutableSet;
import org.glowroot.agent.shaded.google.common.collect.Lists;
import org.glowroot.agent.shaded.google.common.collect.Maps;
import org.glowroot.agent.shaded.google.common.collect.Sets;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
import org.checkerframework.checker.nullness.qual.RequiresNonNull;
import org.immutables.value.Value;
import org.glowroot.agent.shaded.objectweb.asm.ClassReader;
import org.glowroot.agent.shaded.objectweb.asm.Type;
import org.glowroot.agent.shaded.slf4j.Logger;
import org.glowroot.agent.shaded.slf4j.LoggerFactory;

import org.glowroot.agent.weaving.AnalyzedWorld.ParseContext;
import org.glowroot.agent.weaving.ThinClassVisitor.ThinClass;
import org.glowroot.agent.weaving.ThinClassVisitor.ThinMethod;

import static org.glowroot.agent.shaded.google.common.base.Preconditions.checkNotNull;

class ClassAnalyzer {

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

    private final ThinClass thinClass;
    private final String className;

    private final ImmutableAnalyzedClass.Builder analyzedClassBuilder;
    private final ImmutableList adviceMatchers;
    private final ImmutableList superAnalyzedClasses;
    private final ImmutableList matchedShimTypes;
    private final ImmutableList matchedMixinTypes;

    private final ImmutableSet superClassNames;

    private final boolean shortCircuitBeforeAnalyzeMethods;

    private final byte[] classBytes;

    private @MonotonicNonNull Map> methodAdvisors;
    private @MonotonicNonNull List methodsThatOnlyNowFulfillAdvice;

    // this is used to propagate bridge method advice to its target
    private @MonotonicNonNull Map> bridgeTargetAdvisors;

    ClassAnalyzer(ThinClass thinClass, List advisors, List shimTypes,
            List mixinTypes, @Nullable ClassLoader loader, AnalyzedWorld analyzedWorld,
            @Nullable CodeSource codeSource, byte[] classBytes) {
        this.thinClass = thinClass;
        ImmutableList interfaceNames = ClassNames.fromInternalNames(thinClass.interfaces());
        className = ClassNames.fromInternalName(thinClass.name());
        String superClassName = ClassNames.fromInternalName(thinClass.superName());
        analyzedClassBuilder = ImmutableAnalyzedClass.builder()
                .modifiers(thinClass.access())
                .name(className)
                .superName(superClassName)
                .addAllInterfaceNames(interfaceNames);
        adviceMatchers =
                AdviceMatcher.getAdviceMatchers(className, thinClass.annotations(), advisors);
        if (Modifier.isInterface(thinClass.access())) {
            superAnalyzedClasses = ImmutableList.of();
            matchedShimTypes = getMatchedShimTypes(shimTypes, className,
                    ImmutableList.of(), ImmutableList.of());
            analyzedClassBuilder.addAllShimTypes(matchedShimTypes);
            matchedMixinTypes = getMatchedMixinTypes(mixinTypes, className,
                    ImmutableList.of(), ImmutableList.of());
            analyzedClassBuilder.addAllMixinTypes(matchedMixinTypes);
            shortCircuitBeforeAnalyzeMethods = adviceMatchers.isEmpty();
        } else {
            ParseContext parseContext = ImmutableParseContext.of(className, codeSource);
            List superAnalyzedHierarchy =
                    analyzedWorld.getAnalyzedHierarchy(superClassName, loader, parseContext);
            List interfaceAnalyzedHierarchy = Lists.newArrayList();
            for (String interfaceName : interfaceNames) {
                interfaceAnalyzedHierarchy.addAll(
                        analyzedWorld.getAnalyzedHierarchy(interfaceName, loader, parseContext));
            }
            // it's ok if there are duplicates in the superAnalyzedClasses 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 superAnalyzedClasses = Lists.newArrayList();
            superAnalyzedClasses.addAll(superAnalyzedHierarchy);
            superAnalyzedClasses.addAll(interfaceAnalyzedHierarchy);
            this.superAnalyzedClasses = ImmutableList.copyOf(superAnalyzedClasses);
            matchedShimTypes = getMatchedShimTypes(shimTypes, className, superAnalyzedHierarchy,
                    interfaceAnalyzedHierarchy);
            analyzedClassBuilder.addAllShimTypes(matchedShimTypes);
            matchedMixinTypes = getMatchedMixinTypes(mixinTypes, className, superAnalyzedHierarchy,
                    interfaceAnalyzedHierarchy);
            analyzedClassBuilder.addAllMixinTypes(matchedMixinTypes);
            shortCircuitBeforeAnalyzeMethods =
                    !hasSuperAdvice(superAnalyzedClasses) && matchedShimTypes.isEmpty()
                            && matchedMixinTypes.isEmpty() && adviceMatchers.isEmpty();
        }
        Set superClassNames = Sets.newHashSet();
        superClassNames.add(className);
        for (AnalyzedClass analyzedClass : superAnalyzedClasses) {
            superClassNames.add(analyzedClass.name());
        }
        this.superClassNames = ImmutableSet.copyOf(superClassNames);
        this.classBytes = classBytes;
    }

    boolean isShortCircuitBeforeAnalyzeMethods() {
        return shortCircuitBeforeAnalyzeMethods;
    }

    void analyzeMethods() {
        methodAdvisors = Maps.newHashMap();
        bridgeTargetAdvisors = Maps.newHashMap();
        for (ThinMethod bridgeMethod : thinClass.bridgeMethods()) {
            List advisors = analyzeMethod(bridgeMethod);
            removeSuperseded(advisors);
            if (!advisors.isEmpty()) {
                // don't add advisors to bridge method
                // instead propagate bridge method advice to its target
                ThinMethod targetMethod = getTargetMethod(bridgeMethod);
                if (targetMethod != null) {
                    bridgeTargetAdvisors.put(targetMethod, advisors);
                }
            }
        }
        for (ThinMethod nonBridgeMethod : thinClass.nonBridgeMethods()) {
            List advisors = analyzeMethod(nonBridgeMethod);
            removeSuperseded(advisors);
            if (!advisors.isEmpty()) {
                methodAdvisors.put(nonBridgeMethod.name() + nonBridgeMethod.desc(), advisors);
            }
        }
        AnalyzedClass mostlyAnalyzedClass = analyzedClassBuilder.build();
        methodsThatOnlyNowFulfillAdvice = getMethodsThatOnlyNowFulfillAdvice(mostlyAnalyzedClass);
        analyzedClassBuilder.addAllAnalyzedMethods(methodsThatOnlyNowFulfillAdvice);
    }

    boolean isWeavingRequired() {
        checkNotNull(methodAdvisors);
        checkNotNull(methodsThatOnlyNowFulfillAdvice);
        if (Modifier.isInterface(thinClass.access())) {
            return false;
        }
        return !methodAdvisors.isEmpty() || !methodsThatOnlyNowFulfillAdvice.isEmpty()
                || !matchedShimTypes.isEmpty() || !matchedMixinTypes.isEmpty();
    }

    ImmutableList getMatchedShimTypes() {
        return matchedShimTypes;
    }

    ImmutableList getMatchedMixinTypes() {
        return matchedMixinTypes;
    }

    List getSuperAnalyzedClasses() {
        return superAnalyzedClasses;
    }

    Set getSuperClassNames() {
        return superClassNames;
    }

    Map> getMethodAdvisors() {
        return checkNotNull(methodAdvisors);
    }

    AnalyzedClass getAnalyzedClass() {
        return analyzedClassBuilder.build();
    }

    List getMethodsThatOnlyNowFulfillAdvice() {
        return checkNotNull(methodsThatOnlyNowFulfillAdvice);
    }

    @RequiresNonNull("bridgeTargetAdvisors")
    private List analyzeMethod(ThinMethod thinMethod) {
        List parameterTypes = Arrays.asList(Type.getArgumentTypes(thinMethod.desc()));
        Type returnType = Type.getReturnType(thinMethod.desc());
        List methodAnnotations = thinMethod.annotations();
        List matchingAdvisors = getMatchingAdvisors(thinMethod, methodAnnotations,
                parameterTypes, returnType);
        if (matchingAdvisors.isEmpty()) {
            return ImmutableList.of();
        }
        ImmutableAnalyzedMethod.Builder builder = ImmutableAnalyzedMethod.builder();
        builder.name(thinMethod.name());
        for (Type parameterType : parameterTypes) {
            builder.addParameterTypes(parameterType.getClassName());
        }
        builder.returnType(returnType.getClassName())
                .modifiers(thinMethod.access())
                .signature(thinMethod.signature());
        for (String exception : thinMethod.exceptions()) {
            builder.addExceptions(ClassNames.fromInternalName(exception));
        }
        List declaredOnlyMatchingAdvisors = Lists.newArrayList();
        for (Iterator i = matchingAdvisors.iterator(); i.hasNext();) {
            Advice advice = i.next();
            if (advice.pointcutClassName().equals(advice.pointcutMethodDeclaringClassName())) {
                continue;
            } else if (!isTargetClassNameMatch(advice, superClassNames)) {
                declaredOnlyMatchingAdvisors.add(advice);
                i.remove();
            }
        }
        builder.addAllAdvisors(matchingAdvisors);
        builder.addAllDeclaredOnlyAdvisors(declaredOnlyMatchingAdvisors);
        analyzedClassBuilder.addAnalyzedMethods(builder.build());
        return matchingAdvisors;
    }

    private void removeSuperseded(List advisors) {
        Set superseded = Sets.newHashSet();
        for (Advice advice : advisors) {
            String supersedes = advice.pointcut().supersedes();
            if (!supersedes.isEmpty()) {
                superseded.add(supersedes);
            }
        }
        Iterator i = advisors.iterator();
        while (i.hasNext()) {
            String timerName = i.next().pointcut().timerName();
            if (superseded.contains(timerName)) {
                i.remove();
            }
        }
    }

    // returns mutable list if non-empty
    @RequiresNonNull("bridgeTargetAdvisors")
    private List getMatchingAdvisors(ThinMethod thinMethod, List methodAnnotations,
            List parameterTypes, Type returnType) {
        Set matchingAdvisors = Sets.newHashSet();
        for (AdviceMatcher adviceMatcher : adviceMatchers) {
            if (adviceMatcher.isMethodLevelMatch(thinMethod.name(), methodAnnotations,
                    parameterTypes, returnType, thinMethod.access())) {
                matchingAdvisors.add(adviceMatcher.advice());
            }
        }
        // look at super types
        for (AnalyzedClass superAnalyzedClass : superAnalyzedClasses) {
            for (AnalyzedMethod analyzedMethod : superAnalyzedClass.analyzedMethods()) {
                if (analyzedMethod.isOverriddenBy(thinMethod.name(), parameterTypes)) {
                    matchingAdvisors.addAll(analyzedMethod.advisors());
                    matchingAdvisors.addAll(analyzedMethod.declaredOnlyAdvisors());
                }
            }
        }
        List extraAdvisors = bridgeTargetAdvisors.get(thinMethod);
        if (extraAdvisors != null) {
            matchingAdvisors.addAll(extraAdvisors);
        }
        // sort since the order affects advice nesting
        return sortAdvisors(matchingAdvisors);
    }

    private @Nullable ThinMethod getTargetMethod(ThinMethod bridgeMethod) {
        List possibleTargetMethods = getPossibleTargetMethods(bridgeMethod);
        if (possibleTargetMethods.isEmpty()) {
            // probably a visibility bridge for public method in package-private super class
            return null;
        }
        // more than one match, need to drop down to bytecode
        BridgeMethodClassVisitor bmcv = new BridgeMethodClassVisitor();
        new ClassReader(classBytes).accept(bmcv, ClassReader.SKIP_FRAMES);
        Map bridgeMethodMap = bmcv.getBridgeTargetMethods();
        String targetMethod = bridgeMethodMap.get(bridgeMethod.name() + bridgeMethod.desc());
        if (targetMethod == null) {
            // probably a visibility bridge for public method in package-private super class
            return null;
        }
        for (ThinMethod possibleTargetMethod : possibleTargetMethods) {
            if (targetMethod.equals(possibleTargetMethod.name() + possibleTargetMethod.desc())) {
                return possibleTargetMethod;
            }
        }
        logger.warn("could not find match for bridge method: {}", bridgeMethod);
        return null;
    }

    private List getPossibleTargetMethods(ThinMethod bridgeMethod) {
        List possibleTargetMethods = Lists.newArrayList();
        for (ThinMethod possibleTargetMethod : thinClass.nonBridgeMethods()) {
            if (!possibleTargetMethod.name().equals(bridgeMethod.name())) {
                continue;
            }
            Type[] bridgeMethodParamTypes = Type.getArgumentTypes(bridgeMethod.desc());
            Type[] possibleTargetMethodParamTypes =
                    Type.getArgumentTypes(possibleTargetMethod.desc());
            if (possibleTargetMethodParamTypes.length != bridgeMethodParamTypes.length) {
                continue;
            }
            possibleTargetMethods.add(possibleTargetMethod);
        }
        return possibleTargetMethods;
    }

    private List getMethodsThatOnlyNowFulfillAdvice(AnalyzedClass analyzedClass) {
        if (analyzedClass.isInterface() || analyzedClass.isAbstract()) {
            ImmutableMap.of();
        }
        Map> matchingAdvisorSets =
                getInheritedInterfaceMethodsWithAdvice();
        for (AnalyzedMethod analyzedMethod : analyzedClass.analyzedMethods()) {
            matchingAdvisorSets.remove(AnalyzedMethodKey.wrap(analyzedMethod));
        }
        removeAdviceAlreadyWovenIntoSuperClass(matchingAdvisorSets);
        List methodsThatOnlyNowFulfillAdvice = Lists.newArrayList();
        for (Entry> entry : matchingAdvisorSets.entrySet()) {
            AnalyzedMethod inheritedMethod = entry.getKey().analyzedMethod();
            Set advisors = entry.getValue();
            if (!advisors.isEmpty()) {
                methodsThatOnlyNowFulfillAdvice.add(ImmutableAnalyzedMethod.builder()
                        .copyFrom(inheritedMethod)
                        .advisors(advisors)
                        .declaredOnlyAdvisors(ImmutableList.of())
                        .build());
            }
        }
        return methodsThatOnlyNowFulfillAdvice;
    }

    private Map> getInheritedInterfaceMethodsWithAdvice() {
        Map> matchingAdvisorSets = Maps.newHashMap();
        for (AnalyzedClass superAnalyzedClass : superAnalyzedClasses) {
            for (AnalyzedMethod superAnalyzedMethod : superAnalyzedClass.analyzedMethods()) {
                AnalyzedMethodKey key = AnalyzedMethodKey.wrap(superAnalyzedMethod);
                Set matchingAdvisorSet = matchingAdvisorSets.get(key);
                if (matchingAdvisorSet == null) {
                    matchingAdvisorSet = Sets.newHashSet();
                    matchingAdvisorSets.put(key, matchingAdvisorSet);
                }
                if (superAnalyzedClass.isInterface()) {
                    addToMatchingAdvisorsIfTargetClassNameMatch(matchingAdvisorSet,
                            superAnalyzedMethod.advisors(), superClassNames);
                } else {
                    addToMatchingAdvisorsIfTargetClassNameMatch(matchingAdvisorSet,
                            superAnalyzedMethod.declaredOnlyAdvisors(), superClassNames);
                }
            }
        }
        return matchingAdvisorSets;
    }

    private void removeAdviceAlreadyWovenIntoSuperClass(
            Map> matchingAdvisorSets) {
        for (AnalyzedClass superAnalyzedClass : superAnalyzedClasses) {
            if (superAnalyzedClass.isInterface()) {
                continue;
            }
            for (AnalyzedMethod superAnalyzedMethod : superAnalyzedClass.analyzedMethods()) {
                Set matchingAdvisorSet =
                        matchingAdvisorSets.get(AnalyzedMethodKey.wrap(superAnalyzedMethod));
                if (matchingAdvisorSet == null) {
                    continue;
                }
                matchingAdvisorSet.removeAll(superAnalyzedMethod.advisors());
            }
        }
    }

    private static void addToMatchingAdvisorsIfTargetClassNameMatch(Set matchingAdvisors,
            List advisors, Set superClassNames) {
        for (Advice advice : advisors) {
            if (isTargetClassNameMatch(advice, superClassNames)) {
                matchingAdvisors.add(advice);
            }
        }
    }

    private static ImmutableList getMatchedShimTypes(List shimTypes,
            String className, Iterable superAnalyzedClasses,
            List newInterfaceAnalyzedClasses) {
        Set matchedShimTypes = Sets.newHashSet();
        for (ShimType shimType : shimTypes) {
            Pattern targetPattern = shimType.targetPattern();
            if (targetPattern == null && shimType.target().equals(className)
                    || targetPattern != null && targetPattern.matcher(className).matches()) {
                matchedShimTypes.add(shimType);
            }
        }
        for (AnalyzedClass newInterfaceAnalyzedClass : newInterfaceAnalyzedClasses) {
            matchedShimTypes.addAll(newInterfaceAnalyzedClass.shimTypes());
        }
        // remove shims that were already implemented in a super class
        for (AnalyzedClass superAnalyzedClass : superAnalyzedClasses) {
            if (!superAnalyzedClass.isInterface()) {
                matchedShimTypes.removeAll(superAnalyzedClass.shimTypes());
            }
        }
        return ImmutableList.copyOf(matchedShimTypes);
    }

    private static ImmutableList getMatchedMixinTypes(List mixinTypes,
            String className, Iterable superAnalyzedClasses,
            List newInterfaceAnalyzedClasses) {
        Set matchedMixinTypes = Sets.newHashSet();
        for (MixinType mixinType : mixinTypes) {
            // currently only exact matching is supported
            if (mixinType.targets().contains(className)) {
                matchedMixinTypes.add(mixinType);
            }
        }
        for (AnalyzedClass newInterfaceAnalyzedClass : newInterfaceAnalyzedClasses) {
            matchedMixinTypes.addAll(newInterfaceAnalyzedClass.mixinTypes());
        }
        // remove mixins that were already implemented in a super class
        for (AnalyzedClass superAnalyzedClass : superAnalyzedClasses) {
            if (!superAnalyzedClass.isInterface()) {
                matchedMixinTypes.removeAll(superAnalyzedClass.mixinTypes());
            }
        }
        return ImmutableList.copyOf(matchedMixinTypes);
    }

    private static boolean hasSuperAdvice(List superAnalyzedClasses) {
        for (AnalyzedClass superAnalyzedClass : superAnalyzedClasses) {
            if (!superAnalyzedClass.analyzedMethods().isEmpty()) {
                return true;
            }
        }
        return false;
    }

    private static boolean isTargetClassNameMatch(Advice advice, Set superClassNames) {
        Pattern classNamePattern = advice.pointcutClassNamePattern();
        if (classNamePattern == null) {
            String className = advice.pointcutClassName();
            return className.isEmpty() || superClassNames.contains(className);
        }
        for (String superClassName : superClassNames) {
            if (classNamePattern.matcher(superClassName).matches()) {
                return true;
            }
        }
        return false;
    }

    static List sortAdvisors(Collection matchingAdvisors) {
        switch (matchingAdvisors.size()) {
            case 0:
                return ImmutableList.of();
            case 1:
                return Lists.newArrayList(matchingAdvisors);
            default:
                return Advice.ordering.sortedCopy(matchingAdvisors);
        }
    }

    // AnalyzedMethod equivalence defined only in terms of method name and parameter types
    // so that overridden methods will be equivalent
    @Value.Immutable
    abstract static class AnalyzedMethodKey {

        abstract String name();
        abstract ImmutableList parameterTypes();
        @Value.Auxiliary
        abstract AnalyzedMethod analyzedMethod();

        private static AnalyzedMethodKey wrap(AnalyzedMethod analyzedMethod) {
            return ImmutableAnalyzedMethodKey.builder()
                    .name(analyzedMethod.name())
                    .addAllParameterTypes(analyzedMethod.parameterTypes())
                    .analyzedMethod(analyzedMethod)
                    .build();
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy