org.glowroot.agent.live.LiveWeavingServiceImpl Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of glowroot-agent-it-harness Show documentation
Show all versions of glowroot-agent-it-harness Show documentation
Glowroot Agent Integration Test Harness
/*
* Copyright 2015-2019 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.live;
import java.lang.instrument.Instrumentation;
import java.lang.instrument.UnmodifiableClassException;
import java.lang.reflect.Modifier;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.regex.Pattern;
import org.glowroot.agent.shaded.org.glowroot.agent.it.harness.shaded.com.google.common.annotations.VisibleForTesting;
import org.glowroot.agent.shaded.org.glowroot.agent.it.harness.shaded.com.google.common.base.Splitter;
import org.glowroot.agent.shaded.org.glowroot.agent.it.harness.shaded.com.google.common.cache.CacheBuilder;
import org.glowroot.agent.shaded.org.glowroot.agent.it.harness.shaded.com.google.common.cache.CacheLoader;
import org.glowroot.agent.shaded.org.glowroot.agent.it.harness.shaded.com.google.common.cache.LoadingCache;
import org.glowroot.agent.shaded.org.glowroot.agent.it.harness.shaded.com.google.common.collect.ArrayListMultimap;
import org.glowroot.agent.shaded.org.glowroot.agent.it.harness.shaded.com.google.common.collect.ComparisonChain;
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.Iterables;
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.Multimap;
import org.glowroot.agent.shaded.org.glowroot.agent.it.harness.shaded.com.google.common.collect.Ordering;
import org.glowroot.agent.shaded.org.glowroot.agent.it.harness.shaded.com.google.common.collect.Sets;
import org.glowroot.agent.shaded.org.checkerframework.checker.nullness.qual.Nullable;
import org.glowroot.agent.shaded.org.checkerframework.checker.nullness.qual.RequiresNonNull;
import org.immutables.value.Value;
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.config.ConfigService;
import org.glowroot.agent.live.ClasspathCache.UiAnalyzedMethod;
import org.glowroot.agent.util.MaybePatterns;
import org.glowroot.agent.weaving.AdviceCache;
import org.glowroot.agent.weaving.AnalyzedWorld;
import org.glowroot.agent.shaded.org.glowroot.common.config.InstrumentationConfig;
import org.glowroot.agent.shaded.org.glowroot.common.live.LiveWeavingService;
import org.glowroot.agent.shaded.org.glowroot.wire.api.model.DownstreamServiceOuterClass.GlobalMeta;
import org.glowroot.agent.shaded.org.glowroot.wire.api.model.DownstreamServiceOuterClass.MethodSignature;
import static org.glowroot.agent.shaded.org.glowroot.agent.it.harness.shaded.com.google.common.base.Preconditions.checkNotNull;
import static org.glowroot.agent.shaded.org.glowroot.agent.it.harness.shaded.com.google.common.base.Preconditions.checkState;
import static org.glowroot.agent.shaded.org.objectweb.asm.Opcodes.ACC_FINAL;
import static org.glowroot.agent.shaded.org.objectweb.asm.Opcodes.ACC_SYNCHRONIZED;
public class LiveWeavingServiceImpl implements LiveWeavingService {
private static final Logger logger = LoggerFactory.getLogger(LiveWeavingServiceImpl.class);
private static final String THE_SINGLE_KEY = "THE_SINGLE_KEY";
private static final Splitter splitter = Splitter.on(' ').omitEmptyStrings();
private final AnalyzedWorld analyzedWorld;
private final @Nullable Instrumentation instrumentation;
private final ConfigService configService;
private final AdviceCache adviceCache;
private final boolean jvmRetransformClassesSupported;
// hopefully can simplify someday https://github.org.glowroot.agent.shaded.org.glowroot.agent.it.harness.shaded.com.google/guava/issues/872
private final LoadingCache classpathCache = CacheBuilder.newBuilder()
.softValues()
.maximumSize(1)
.build(new CacheLoader() {
@Override
public ClasspathCache load(String key) throws Exception {
return new ClasspathCache(analyzedWorld, instrumentation);
}
});
public LiveWeavingServiceImpl(AnalyzedWorld analyzedWorld,
@Nullable Instrumentation instrumentation, ConfigService configService,
AdviceCache adviceCache, boolean jvmRetransformClassesSupported) {
this.analyzedWorld = analyzedWorld;
this.instrumentation = instrumentation;
this.configService = configService;
this.adviceCache = adviceCache;
this.jvmRetransformClassesSupported = jvmRetransformClassesSupported;
}
@Override
public GlobalMeta getGlobalMeta(String agentId) {
return GlobalMeta.newBuilder()
.setJvmOutOfSync(adviceCache.isOutOfSync(configService.getInstrumentationConfigs()))
.setJvmRetransformClassesSupported(jvmRetransformClassesSupported)
.build();
}
@Override
public void preloadClasspathCache(String agentId) {
// run in background and return immediate so as not to block single UI thread (when running
// embedded) or single gRPC thread (when reporting to central collector)
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
getClasspathCache().updateCache();
}
});
thread.setName("Glowroot-Preload-Classpath-Cache");
thread.setDaemon(true);
thread.start();
}
@Override
public List getMatchingClassNames(String agentId, String partialClassName, int limit) {
return getClasspathCache().getMatchingClassNames(partialClassName, limit);
}
// returns the first matching method names, ordered alphabetically (case-insensitive)
@Override
public List getMatchingMethodNames(String agentId, String className,
String partialMethodName, int limit) {
String partialMethodNameUpper = partialMethodName.toUpperCase(Locale.ENGLISH);
Set methodNames = Sets.newHashSet();
for (UiAnalyzedMethod analyzedMethod : getClasspathCache().getAnalyzedMethods(className)) {
String methodName = analyzedMethod.name();
if (methodName.equals("") || methodName.equals("")) {
// static initializers are not supported by weaver
// (see AdviceMatcher.isMethodNameMatch())
// and constructors do not support @OnBefore advice at this time
continue;
}
if (methodName.toUpperCase(Locale.ENGLISH).contains(partialMethodNameUpper)) {
methodNames.add(methodName);
}
}
ImmutableList sortedMethodNames =
Ordering.natural().immutableSortedCopy(methodNames);
if (methodNames.size() > limit) {
return sortedMethodNames.subList(0, limit);
} else {
return sortedMethodNames;
}
}
@Override
public List getMethodSignatures(String agentId, String className,
String methodName) {
if (methodName.contains("*") || methodName.contains("|")) {
return ImmutableList.of();
}
List analyzedMethods = getAnalyzedMethods(className, methodName);
List methodSignatures = Lists.newArrayList();
for (UiAnalyzedMethod analyzedMethod : analyzedMethods) {
MethodSignature.Builder builder = MethodSignature.newBuilder();
builder.setName(analyzedMethod.name());
builder.addAllParameterType(analyzedMethod.parameterTypes());
builder.setReturnType(analyzedMethod.returnType());
// strip final and synchronized from displayed modifiers since they have no impact on
// the weaver's method matching
int reducedModifiers = analyzedMethod.modifiers() & ~ACC_FINAL & ~ACC_SYNCHRONIZED;
String modifierNames = Modifier.toString(reducedModifiers);
for (String modifier : splitter.split(modifierNames)) {
builder.addModifier(modifier.toLowerCase(Locale.ENGLISH));
}
methodSignatures.add(builder.build());
}
return methodSignatures;
}
@Override
public int reweave(String agentId) throws Exception {
if (instrumentation == null) {
// this method is called from GlowrootAgentInit.resetConfigForTests() when
// instrumentation is null
return 0;
}
// this command is filtered out of the UI when retransform classes is not supported
checkState(instrumentation.isRetransformClassesSupported(),
"Retransform classes is not supported");
return reweaveInternal();
}
private List getAnalyzedMethods(String className, String methodName) {
// use set to remove duplicate methods (e.g. same class loaded by multiple class loaders)
Set analyzedMethods = Sets.newHashSet();
for (UiAnalyzedMethod analyzedMethod : getClasspathCache().getAnalyzedMethods(className)) {
if (analyzedMethod.name().equals(methodName)) {
analyzedMethods.add(analyzedMethod);
}
}
// order methods by accessibility, then by name, then by number of args
return new UiAnalyzedMethodOrdering().sortedCopy(analyzedMethods);
}
private ClasspathCache getClasspathCache() {
return classpathCache.getUnchecked(THE_SINGLE_KEY);
}
@RequiresNonNull("instrumentation")
private int reweaveInternal() throws Exception {
List configs = configService.getInstrumentationConfigs();
adviceCache.updateAdvisors(configs);
Set pointcutClassNames = Sets.newHashSet();
for (InstrumentationConfig config : configs) {
PointcutClassName subTypeRestrictionPointClassName = null;
String subTypeRestriction = config.subTypeRestriction();
if (!subTypeRestriction.isEmpty()) {
subTypeRestrictionPointClassName =
PointcutClassName.fromMaybePattern(subTypeRestriction, null, false);
}
String className = config.className();
if (!className.isEmpty()) {
pointcutClassNames.add(PointcutClassName.fromMaybePattern(className,
subTypeRestrictionPointClassName, config.methodName().equals("")));
}
}
Set> classes = Sets.newHashSet();
Set> possibleNewReweavableClasses = getExistingModifiableSubClasses(
pointcutClassNames, instrumentation.getAllLoadedClasses(), instrumentation);
// need to remove these classes from AnalyzedWorld, otherwise if a subclass and its parent
// class are both in the list and the subclass is re-transformed first, it will use the
// old cached AnalyzedClass for its parent which will have the old AnalyzedMethod advisors
List> existingReweavableClasses =
analyzedWorld.getClassesWithReweavableAdvice(true);
analyzedWorld.removeClasses(possibleNewReweavableClasses);
classes.addAll(existingReweavableClasses);
classes.addAll(possibleNewReweavableClasses);
if (classes.isEmpty()) {
return 0;
}
instrumentation.retransformClasses(Iterables.toArray(classes, Class.class));
List> updatedReweavableClasses =
analyzedWorld.getClassesWithReweavableAdvice(false);
// all existing reweavable classes were woven
int count = existingReweavableClasses.size();
// now add newly reweavable classes
for (Class> possibleNewReweavableClass : possibleNewReweavableClasses) {
if (updatedReweavableClasses.contains(possibleNewReweavableClass)
&& !existingReweavableClasses.contains(possibleNewReweavableClass)) {
count++;
}
}
return count;
}
public static void initialReweave(Set pointcutClassNames,
Class>[] initialLoadedClasses, Instrumentation instrumentation) {
if (!instrumentation.isRetransformClassesSupported()) {
return;
}
Set> classes = getExistingModifiableSubClasses(pointcutClassNames,
initialLoadedClasses, instrumentation);
for (Class> clazz : classes) {
if (clazz.isInterface()) {
continue;
}
try {
instrumentation.retransformClasses(clazz);
} catch (UnmodifiableClassException e) {
// IBM J9 VM Java 6 throws UnmodifiableClassException even though call to
// isModifiableClass() in getExistingModifiableSubClasses() returns true
logger.debug(e.getMessage(), e);
}
}
}
private static Set> getExistingModifiableSubClasses(
Set pointcutClassNames, Class>[] classes,
Instrumentation instrumentation) {
List> matchingClasses = Lists.newArrayList();
Multimap, Class>> subClasses = ArrayListMultimap.create();
for (Class> clazz : classes) {
if (!instrumentation.isModifiableClass(clazz)) {
continue;
}
Class> superclass = clazz.getSuperclass();
if (superclass != null) {
subClasses.put(superclass, clazz);
}
for (Class> iface : clazz.getInterfaces()) {
subClasses.put(iface, clazz);
}
for (PointcutClassName pointcutClassName : pointcutClassNames) {
if (pointcutClassName.appliesTo(clazz.getName())) {
matchingClasses.add(clazz);
break;
}
}
}
Set> matchingSubClasses = Sets.newHashSet();
for (Class> matchingClass : matchingClasses) {
addToMatchingSubClasses(matchingClass, matchingSubClasses, subClasses);
}
return matchingSubClasses;
}
private static void addToMatchingSubClasses(Class> clazz, Set> matchingSubClasses,
Multimap, Class>> subClasses) {
matchingSubClasses.add(clazz);
for (Class> subClass : subClasses.get(clazz)) {
addToMatchingSubClasses(subClass, matchingSubClasses, subClasses);
}
}
@VisibleForTesting
static class UiAnalyzedMethodOrdering extends Ordering {
@Override
public int compare(UiAnalyzedMethod left, UiAnalyzedMethod right) {
return ComparisonChain.start()
.compare(getAccessibility(left), getAccessibility(right))
.compare(left.name(), right.name())
.compare(left.parameterTypes().size(), right.parameterTypes().size())
.result();
}
private static int getAccessibility(UiAnalyzedMethod analyzedMethod) {
int modifiers = analyzedMethod.modifiers();
if (Modifier.isPublic(modifiers)) {
return 1;
} else if (Modifier.isProtected(modifiers)) {
return 2;
} else if (Modifier.isPrivate(modifiers)) {
return 4;
} else {
// package-private
return 3;
}
}
}
@Value.Immutable
public abstract static class PointcutClassName {
abstract @Nullable Pattern pattern();
abstract @Nullable String nonPattern();
abstract @Nullable PointcutClassName subTypeRestriction();
abstract boolean doNotMatchSubClasses();
public static PointcutClassName fromMaybePattern(String maybePattern,
@Nullable PointcutClassName subTypeRestriction, boolean doNotMatchSubClasses) {
Pattern pattern = MaybePatterns.buildPattern(maybePattern);
if (pattern == null) {
return fromNonPattern(maybePattern, subTypeRestriction, doNotMatchSubClasses);
} else {
return fromPattern(pattern, subTypeRestriction, doNotMatchSubClasses);
}
}
public static PointcutClassName fromPattern(Pattern pattern,
@Nullable PointcutClassName subTypeRestrictionPointcutClassName,
boolean doNotMatchSubClasses) {
return ImmutablePointcutClassName.builder()
.pattern(pattern)
.nonPattern(null)
.subTypeRestriction(subTypeRestrictionPointcutClassName)
.doNotMatchSubClasses(doNotMatchSubClasses)
.build();
}
public static PointcutClassName fromNonPattern(String nonPattern,
@Nullable PointcutClassName subTypeRestrictionPointcutClassName,
boolean doNotMatchSubClasses) {
return ImmutablePointcutClassName.builder()
.pattern(null)
.nonPattern(nonPattern)
.subTypeRestriction(subTypeRestrictionPointcutClassName)
.doNotMatchSubClasses(doNotMatchSubClasses)
.build();
}
private boolean appliesTo(String className) {
PointcutClassName subTypeRestriction = subTypeRestriction();
if (subTypeRestriction != null && !subTypeRestriction.appliesTo(className)) {
return false;
}
Pattern pattern = pattern();
if (pattern != null) {
return pattern.matcher(className).matches();
} else {
return checkNotNull(nonPattern()).equals(className);
}
}
}
}