/* * Copyright 2012-2017 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.annotation.Annotation; import java.net.URL; import java.net.URLClassLoader; import java.security.AccessController; import java.security.PrivilegedAction; import java.util.List; import java.util.regex.Pattern; import javax.annotation.Nullable; import com.google.common.base.Joiner; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Lists; import org.objectweb.asm.Type; import org.objectweb.asm.commons.Method; import org.glowroot.agent.plugin.api.OptionalThreadContext; import org.glowroot.agent.plugin.api.ThreadContext; import org.glowroot.agent.plugin.api.weaving.BindClassMeta; import org.glowroot.agent.plugin.api.weaving.BindMethodMeta; import org.glowroot.agent.plugin.api.weaving.BindMethodName; import org.glowroot.agent.plugin.api.weaving.BindOptionalReturn; import org.glowroot.agent.plugin.api.weaving.BindParameter; import org.glowroot.agent.plugin.api.weaving.BindParameterArray; import org.glowroot.agent.plugin.api.weaving.BindReceiver; import org.glowroot.agent.plugin.api.weaving.BindReturn; import org.glowroot.agent.plugin.api.weaving.BindThrowable; import org.glowroot.agent.plugin.api.weaving.BindTraveler; import org.glowroot.agent.plugin.api.weaving.IsEnabled; import org.glowroot.agent.plugin.api.weaving.OnAfter; import org.glowroot.agent.plugin.api.weaving.OnBefore; import org.glowroot.agent.plugin.api.weaving.OnReturn; import org.glowroot.agent.plugin.api.weaving.OnThrow; import org.glowroot.agent.plugin.api.weaving.Pointcut; import org.glowroot.agent.weaving.Advice.AdviceParameter; import org.glowroot.agent.weaving.Advice.ParameterKind; import org.glowroot.agent.weaving.ClassLoaders.LazyDefinedClass; import org.glowroot.common.util.Patterns; import static com.google.common.base.Preconditions.checkNotNull; class AdviceBuilder { private static final ImmutableList<Class<? extends Annotation>> isEnabledBindAnnotationTypes = ImmutableList.of(BindReceiver.class, BindParameter.class, BindParameterArray.class, BindMethodName.class, BindClassMeta.class, BindMethodMeta.class); private static final ImmutableList<Class<? extends Annotation>> onBeforeBindAnnotationTypes = ImmutableList.of(BindReceiver.class, BindParameter.class, BindParameterArray.class, BindMethodName.class, BindClassMeta.class, BindMethodMeta.class); private static final ImmutableList<Class<? extends Annotation>> onReturnBindAnnotationTypes = ImmutableList.of(BindReceiver.class, BindParameter.class, BindParameterArray.class, BindMethodName.class, BindReturn.class, BindOptionalReturn.class, BindTraveler.class, BindClassMeta.class, BindMethodMeta.class); private static final ImmutableList<Class<? extends Annotation>> onThrowBindAnnotationTypes = ImmutableList.of(BindReceiver.class, BindParameter.class, BindParameterArray.class, BindMethodName.class, BindThrowable.class, BindTraveler.class, BindClassMeta.class, BindMethodMeta.class); private static final ImmutableList<Class<? extends Annotation>> onAfterBindAnnotationTypes = ImmutableList.of(BindReceiver.class, BindParameter.class, BindParameterArray.class, BindMethodName.class, BindTraveler.class, BindClassMeta.class, BindMethodMeta.class); private static final ImmutableMap<Class<? extends Annotation>, ParameterKind> parameterKindMap = new ImmutableMap.Builder<Class<? extends Annotation>, ParameterKind>() .put(BindReceiver.class, ParameterKind.RECEIVER) .put(BindParameter.class, ParameterKind.METHOD_ARG) .put(BindParameterArray.class, ParameterKind.METHOD_ARG_ARRAY) .put(BindMethodName.class, ParameterKind.METHOD_NAME) .put(BindReturn.class, ParameterKind.RETURN) .put(BindOptionalReturn.class, ParameterKind.OPTIONAL_RETURN) .put(BindThrowable.class, ParameterKind.THROWABLE) .put(BindTraveler.class, ParameterKind.TRAVELER) .put(BindClassMeta.class, ParameterKind.CLASS_META) .put(BindMethodMeta.class, ParameterKind.METHOD_META) .build(); private final ImmutableAdvice.Builder builder = ImmutableAdvice.builder(); private final @Nullable Class<?> adviceClass; private final @Nullable LazyDefinedClass lazyAdviceClass; private boolean hasIsEnabledAdvice; private boolean hasOnBeforeAdvice; private boolean hasOnReturnAdvice; private boolean hasOnThrowAdvice; private boolean hasOnAfterAdvice; AdviceBuilder(Class<?> adviceClass) { this.adviceClass = adviceClass; this.lazyAdviceClass = null; builder.reweavable(false); } AdviceBuilder(LazyDefinedClass lazyAdviceClass, boolean reweavable) { this.adviceClass = null; this.lazyAdviceClass = lazyAdviceClass; builder.reweavable(reweavable); } @SuppressWarnings("deprecation") Advice build() throws Exception { Class<?> adviceClass = this.adviceClass; if (adviceClass == null) { // safe check, if adviceClass is null then lazyAdviceClass is non-null checkNotNull(lazyAdviceClass); ClassLoader tempClassLoader = AccessController.doPrivileged(new PrivilegedAction<ClassLoader>() { @Override public ClassLoader run() { return new URLClassLoader(new URL[0], AdviceBuilder.class.getClassLoader()); } }); adviceClass = ClassLoaders.defineClass(lazyAdviceClass, tempClassLoader); } Pointcut pointcut = adviceClass.getAnnotation(Pointcut.class); checkNotNull(pointcut, "Class has no @Pointcut annotation"); if (!pointcut.methodDeclaringClassName().isEmpty()) { throw new AdviceConstructionException("Pointcuts with methodDeclaringClassName are no" + " longer supported, use subTypeRestriction or superTypeRestriction depending" + " on the need"); } builder.pointcut(pointcut); builder.adviceType(Type.getType(adviceClass)); builder.pointcutClassNamePattern(buildPattern(pointcut.className())); builder.pointcutClassAnnotationPattern(buildPattern(pointcut.classAnnotation())); builder.pointcutSubTypeRestrictionPattern(buildPattern(pointcut.subTypeRestriction())); builder.pointcutSuperTypeRestrictionPattern(buildPattern(pointcut.superTypeRestriction())); builder.pointcutMethodNamePattern(buildPattern(pointcut.methodName())); builder.pointcutMethodAnnotationPattern(buildPattern(pointcut.methodAnnotation())); builder.pointcutMethodParameterTypes(buildPatterns(pointcut.methodParameterTypes())); // hasBindThreadContext will be overridden below if needed builder.hasBindThreadContext(false); // hasBindOptionalThreadContext will be overridden below if needed builder.hasBindOptionalThreadContext(false); for (java.lang.reflect.Method method : adviceClass.getMethods()) { if (method.isAnnotationPresent(IsEnabled.class)) { initIsEnabledAdvice(adviceClass, method); } else if (method.isAnnotationPresent(OnBefore.class)) { initOnBeforeAdvice(adviceClass, method); } else if (method.isAnnotationPresent(OnReturn.class)) { initOnReturnAdvice(adviceClass, method); } else if (method.isAnnotationPresent(OnThrow.class)) { initOnThrowAdvice(adviceClass, method); } else if (method.isAnnotationPresent(OnAfter.class)) { initOnAfterAdvice(adviceClass, method); } } return builder.build(); } private void initIsEnabledAdvice(Class<?> adviceClass, java.lang.reflect.Method method) throws AdviceConstructionException { checkState(!hasIsEnabledAdvice, "@Pointcut '" + adviceClass.getName() + "' has more than one @IsEnabled method"); Method asmMethod = Method.getMethod(method); checkState(asmMethod.getReturnType().getSort() == Type.BOOLEAN, "@IsEnabled method must return boolean"); builder.isEnabledAdvice(asmMethod); List<AdviceParameter> parameters = getAdviceParameters(method.getParameterAnnotations(), method.getParameterTypes(), isEnabledBindAnnotationTypes, IsEnabled.class); builder.addAllIsEnabledParameters(parameters); hasIsEnabledAdvice = true; } private void initOnBeforeAdvice(Class<?> adviceClass, java.lang.reflect.Method method) throws AdviceConstructionException { checkState(!hasOnBeforeAdvice, "@Pointcut '" + adviceClass.getName() + "' has more than one @OnBefore method"); Method onBeforeAdvice = Method.getMethod(method); builder.onBeforeAdvice(onBeforeAdvice); List<AdviceParameter> parameters = getAdviceParameters(method.getParameterAnnotations(), method.getParameterTypes(), onBeforeBindAnnotationTypes, OnBefore.class); builder.addAllOnBeforeParameters(parameters); if (onBeforeAdvice.getReturnType().getSort() != Type.VOID) { builder.travelerType(onBeforeAdvice.getReturnType()); } checkForBindThreadContext(parameters); checkForBindOptionalThreadContext(parameters); hasOnBeforeAdvice = true; } private void initOnReturnAdvice(Class<?> adviceClass, java.lang.reflect.Method method) throws AdviceConstructionException { checkState(!hasOnReturnAdvice, "@Pointcut '" + adviceClass.getName() + "' has more than one @OnReturn method"); List<AdviceParameter> parameters = getAdviceParameters(method.getParameterAnnotations(), method.getParameterTypes(), onReturnBindAnnotationTypes, OnReturn.class); for (int i = 1; i < parameters.size(); i++) { checkState(parameters.get(i).kind() != ParameterKind.RETURN, "@BindReturn must be the first argument to @OnReturn"); checkState(parameters.get(i).kind() != ParameterKind.OPTIONAL_RETURN, "@BindOptionalReturn must be the first argument to @OnReturn"); } builder.onReturnAdvice(Method.getMethod(method)); builder.addAllOnReturnParameters(parameters); checkForBindThreadContext(parameters); checkForBindOptionalThreadContext(parameters); hasOnReturnAdvice = true; } private void initOnThrowAdvice(Class<?> adviceClass, java.lang.reflect.Method method) throws AdviceConstructionException { checkState(!hasOnThrowAdvice, "@Pointcut '" + adviceClass.getName() + "' has more than one @OnThrow method"); List<AdviceParameter> parameters = getAdviceParameters(method.getParameterAnnotations(), method.getParameterTypes(), onThrowBindAnnotationTypes, OnThrow.class); for (int i = 1; i < parameters.size(); i++) { checkState(parameters.get(i).kind() != ParameterKind.THROWABLE, "@BindThrowable must be the first argument to @OnThrow"); } Method asmMethod = Method.getMethod(method); checkState(asmMethod.getReturnType().getSort() == Type.VOID, "@OnThrow method must return void (for now)"); builder.onThrowAdvice(asmMethod); builder.addAllOnThrowParameters(parameters); checkForBindThreadContext(parameters); checkForBindOptionalThreadContext(parameters); hasOnThrowAdvice = true; } private void initOnAfterAdvice(Class<?> adviceClass, java.lang.reflect.Method method) throws AdviceConstructionException { checkState(!hasOnAfterAdvice, "@Pointcut '" + adviceClass.getName() + "' has more than one @OnAfter method"); Method asmMethod = Method.getMethod(method); checkState(asmMethod.getReturnType().getSort() == Type.VOID, "@OnAfter method must return void"); builder.onAfterAdvice(asmMethod); List<AdviceParameter> parameters = getAdviceParameters(method.getParameterAnnotations(), method.getParameterTypes(), onAfterBindAnnotationTypes, OnAfter.class); builder.addAllOnAfterParameters(parameters); checkForBindThreadContext(parameters); checkForBindOptionalThreadContext(parameters); hasOnAfterAdvice = true; } private void checkForBindThreadContext(List<AdviceParameter> parameters) { for (AdviceParameter parameter : parameters) { if (parameter.kind() == ParameterKind.THREAD_CONTEXT) { builder.hasBindThreadContext(true); return; } } } private void checkForBindOptionalThreadContext(List<AdviceParameter> parameters) { for (AdviceParameter parameter : parameters) { if (parameter.kind() == ParameterKind.OPTIONAL_THREAD_CONTEXT) { builder.hasBindOptionalThreadContext(true); break; } } } private static void checkState(boolean condition, String message) throws AdviceConstructionException { if (!condition) { throw new AdviceConstructionException(message); } } private List<Object> buildPatterns(String[] maybePatterns) { List<Object> patterns = Lists.newArrayList(); for (String maybePattern : maybePatterns) { Pattern pattern = buildPattern(maybePattern); if (pattern == null) { patterns.add(maybePattern); } else { patterns.add(pattern); } } return patterns; } static @Nullable Pattern buildPattern(String maybePattern) { if (maybePattern.startsWith("/") && maybePattern.endsWith("/")) { // full regex power return Pattern.compile(maybePattern.substring(1, maybePattern.length() - 1)); } // limited regex, | and *, should be used whenever possible over full regex since // . and $ are common in class names if (maybePattern.contains("|")) { String[] parts = maybePattern.split("\\|"); for (int i = 0; i < parts.length; i++) { parts[i] = Patterns.buildSimplePattern(parts[i]); } return Pattern.compile(Joiner.on('|').join(parts)); } if (maybePattern.contains("*")) { return Pattern.compile(Patterns.buildSimplePattern(maybePattern)); } return null; } private static List<AdviceParameter> getAdviceParameters(Annotation[][] parameterAnnotations, Class<?>[] parameterTypes, List<Class<? extends Annotation>> validBindAnnotationTypes, Class<? extends Annotation> adviceAnnotationType) throws AdviceConstructionException { List<AdviceParameter> parameters = Lists.newArrayList(); for (int i = 0; i < parameterAnnotations.length; i++) { if (parameterTypes[i] == ThreadContext.class) { parameters.add(ImmutableAdviceParameter.builder() .kind(ParameterKind.THREAD_CONTEXT) .type(Type.getType(ThreadContext.class)) .build()); continue; } if (parameterTypes[i] == OptionalThreadContext.class) { parameters.add(ImmutableAdviceParameter.builder() .kind(ParameterKind.OPTIONAL_THREAD_CONTEXT) .type(Type.getType(OptionalThreadContext.class)) .build()); continue; } Class<? extends Annotation> validBindAnnotationType = getValidBindAnnotationType(parameterAnnotations[i], validBindAnnotationTypes); if (validBindAnnotationType == null) { // no valid bind annotations found, provide a good error message List<String> validBindAnnotationNames = Lists.newArrayList(); for (Class<? extends Annotation> annotationType : validBindAnnotationTypes) { validBindAnnotationNames.add("@" + annotationType.getSimpleName()); } throw new AdviceConstructionException("All parameters to @" + adviceAnnotationType.getSimpleName() + " must be annotated with one" + " of " + Joiner.on(", ").join(validBindAnnotationNames)); } parameters.add(getAdviceParameter(validBindAnnotationType, parameterTypes[i])); } return parameters; } private static @Nullable Class<? extends Annotation> getValidBindAnnotationType( Annotation[] parameterAnnotations, List<Class<? extends Annotation>> validBindAnnotationTypes) throws AdviceConstructionException { Class<? extends Annotation> foundBindAnnotationType = null; for (Annotation annotation : parameterAnnotations) { Class<? extends Annotation> annotationType = annotation.annotationType(); if (!parameterKindMap.containsKey(annotationType)) { continue; } checkState(foundBindAnnotationType == null, "Multiple annotations found on a single parameter"); checkState(validBindAnnotationTypes.contains(annotationType), "Annotation '" + annotationType.getName() + "' found in an invalid location"); foundBindAnnotationType = annotationType; } return foundBindAnnotationType; } private static AdviceParameter getAdviceParameter( Class<? extends Annotation> validBindAnnotationType, Class<?> parameterType) throws AdviceConstructionException { checkState( validBindAnnotationType != BindMethodName.class || parameterType.isAssignableFrom(String.class), "@BindMethodName parameter type must be" + " java.lang.String (or super type of java.lang.String)"); checkState( validBindAnnotationType != BindThrowable.class || parameterType.isAssignableFrom(Throwable.class), "@BindMethodName parameter type must be" + " java.lang.Throwable (or super type of java.lang.Throwable)"); ParameterKind parameterKind = parameterKindMap.get(validBindAnnotationType); // parameterKind should never be null since all bind annotations have a mapping in // parameterKindMap checkNotNull(parameterKind, "Annotation not found in parameterKindMap: " + validBindAnnotationType.getName()); return ImmutableAdviceParameter.builder() .kind(parameterKind) .type(Type.getType(parameterType)) .build(); } @SuppressWarnings("serial") private static class AdviceConstructionException extends Exception { private AdviceConstructionException(@Nullable String message) { super(message); } } }