/**
* Helios, OpenSource Monitoring
* Brought to you by the Helios Development Group
*
* Copyright 2007, Helios Development Group and individual contributors
* as indicated by the @author tags. See the copyright.txt file in the
* distribution for a full listing of individual contributors.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*
*/
package org.helios.apmrouter.codahale.agent;
import static org.helios.apmrouter.codahale.SimpleLogger.debug;
import static org.helios.apmrouter.codahale.SimpleLogger.info;
import static org.helios.apmrouter.codahale.SimpleLogger.trace;
import static org.helios.apmrouter.codahale.SimpleLogger.warn;
import java.lang.annotation.Annotation;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.lang.reflect.Proxy;
import java.net.URL;
import java.net.URLClassLoader;
import java.security.ProtectionDomain;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
import javassist.ByteArrayClassPath;
import javassist.CannotCompileException;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtField;
import javassist.CtMethod;
import javassist.CtNewMethod;
import javassist.LoaderClassPath;
import javassist.Modifier;
import javassist.NotFoundException;
import javassist.bytecode.annotation.AnnotationImpl;
import javassist.bytecode.annotation.NoSuchClassError;
import org.helios.apmrouter.codahale.SimpleLogger;
import org.helios.apmrouter.codahale.annotation.TimedImpl;
import com.yammer.metrics.annotation.Timed;
import com.yammer.metrics.core.Timer;
/**
* <p>Title: CodahaleClassTransformer</p>
* <p>Description: Java Agent class transformer for <a href="http://metrics.codahale.com">Codahale</a> instrumentation</p>
* <p>Company: Helios Development Group LLC</p>
* @author Whitehead (nwhitehead AT heliosdev DOT org)
* <p><code>org.helios.apmrouter.codahale.agent.CodahaleClassTransformer</code></p>
*/
public class CodahaleClassTransformer implements ClassFileTransformer {
/** A set of the package names (in binary format) to be inspected for instrumentation */
protected final Set<String> targetPackages = new CopyOnWriteArraySet<String>();
/** A set of the package names (in binary format) that should not be instrumented */
protected final Set<String> prohibitedPackages = new CopyOnWriteArraySet<String>();
/** A set of instrumented class info */
protected final Set<InstrumentedClassInfo> classInfo = new CopyOnWriteArraySet<InstrumentedClassInfo>();
/** A javassist classpool for the current thread */
protected final ThreadLocal<ClassPool> classPool = new ThreadLocal<ClassPool>() {
@Override
protected ClassPool initialValue() {
return new ClassPool(true);
}
};
protected final String jarUrl;
/**
* Creates a new CodahaleClassTransformer
* @param packageNames An array of package names to instrument
*/
public CodahaleClassTransformer(String jarUrl, String...packageNames) {
this.jarUrl = jarUrl;
for(String s: packageNames) {
if(s.indexOf('.')!=-1) {
targetPackages.add(s.replace('.', '/').trim());
continue;
}
if(s.indexOf('/')==-1) {
targetPackages.add(s.trim());
}
}
info("SimpleLogger Level:" + SimpleLogger.getLevel());
}
/**
* Creates a new CodahaleClassTransformer
* @param packageNames A collection of package names to instrument
*/
public CodahaleClassTransformer(Collection<String> packageNames, String jarUrl) {
this(jarUrl, packageNames.toArray(new String[0]));
}
/**
* Converts from a standard java class name to the binary class name
* @param className The dot separated java class name
* @return The / separated class name
*/
public static String toBinaryName(String className) {
return className.replace('.', '/');
}
/**
* Converts from a binary java class name to the java internal class name
* @param binClassName The / separated java class name
* @return The . separated class name
*/
public static String toName(String binClassName) {
return binClassName.replace('/', '.');
}
/**
* Returns the binary package name for the passed binary class name
* @param binClassName The / separated class name
* @return the / separated package name
*/
public static String getBinaryPackage(String binClassName) {
int index = binClassName.lastIndexOf('/');
if(index==0) return binClassName;
return binClassName.substring(0, index);
}
/**
* {@inheritDoc}
* @see java.lang.instrument.ClassFileTransformer#transform(java.lang.ClassLoader, java.lang.String, java.lang.Class, java.security.ProtectionDomain, byte[])
*/
@Override
public byte[] transform(ClassLoader classLoader, String className, Class<?> classToBeTransformed,
ProtectionDomain protectionDomain, byte[] classFileBuffer) throws IllegalClassFormatException {
final byte[] original = classFileBuffer;
CtClass clazz = null;
String packageName = getBinaryPackage(className);
if(!targetPackages.contains(packageName) || prohibitedPackages.contains(packageName)) {
trace("Skipped [", className,"]");
return original;
}
debug("Examining [", className, "]");
//cp.appendClassPath(new LoaderClassPath(classLoader));
ClassPool cp = classPool.get();
try {
cp.appendClassPath(new LoaderClassPath(new URLClassLoader(new URL[]{new URL(jarUrl)}, Thread.currentThread().getContextClassLoader())));
cp.appendClassPath(new LoaderClassPath(Thread.currentThread().getContextClassLoader()));
cp.appendClassPath(new LoaderClassPath(Timer.class.getClassLoader()));
cp.appendClassPath(new ByteArrayClassPath(toName(className), original));
cp.appendClassPath(new LoaderClassPath(Timed.class.getClassLoader()));
cp.get(Timer.class.getName());
//cp.appendSystemPath();
clazz = cp.get(toName(className));
debug("Loaded CtClass [", clazz.getName(), "]");
int totalInstrumentations = 0;
for(CtMethod method: clazz.getDeclaredMethods()) {
Object[] annotations = method.getAvailableAnnotations();
if(annotations.length>0) {
cp.importPackage("com.yammer.metrics.core");
debug("Attempting to instrument [", clazz.getName(), ".", method.getLongName(), "]");
totalInstrumentations += instrumentMethod(method, annotations);
debug("Instrumented [", clazz.getName(), ".", method.getName(), "]");
}
}
if(totalInstrumentations>0) {
debug("Completed [", totalInstrumentations, "] joinpoint instrumentations on [" ,clazz.getName(), "]");
byte[] instrumentedByteCode = clazz.toBytecode();
classInfo.add(new InstrumentedClassInfo(className, classLoader, protectionDomain, original, instrumentedByteCode));
clazz.writeFile("c:\\temp\\apmclasses");
info("Wrote file");
return instrumentedByteCode;
}
return original;
} catch (Exception ex) {
warn("Failed to instrument class [", className, "]", ex);
ex.printStackTrace(System.err);
return original;
} finally {
if(clazz!=null) try { clazz.detach(); } catch (Exception ex) {/* No Op */}
classPool.remove();
}
}
/**
* Instruments the passed method for each of the passed annotations
* @param method The method to instrument
* @param annotations The annotations describing the type of instrumentation to apply
* @return the number of successfully applied annotations
* @throws NoSuchClassError thrown if supporting classes cannot be found
* @throws ClassNotFoundException thrown if supporting classes cannot be found
*/
protected int instrumentMethod(CtMethod method, Object...annotations) throws ClassNotFoundException, NoSuchClassError {
int cnt = 0;
for(Object annotation: annotations) {
AnnotationImpl ai = (AnnotationImpl)Proxy.getInvocationHandler(annotation);
AnnotationType annotationType = AnnotationType.CLASS2TYPE.get(ai.getTypeName());
if(annotationType==null) continue;
//Object rebuiltAnnotation = AnnotationImpl.make(annotationType.getAnnotationClazz().getClassLoader(), annotationType.getAnnotationClazz(), classPool.get(), ai.getAnnotation());
//Annotation typedAnnotation = (Annotation) Proxy.newProxyInstance(annotationType.getAnnotationClazz().getClassLoader(), new Class[]{annotationType.getAnnotationClazz()}, ai);
final String mName = method.getName();
try {
instrumentMethodSwitch(method, annotationType);
cnt++;
} catch (Exception ex) {
warn("Failed to instrument method [", mName, "] with annotation [" ,annotationType.name() , "]", ex);
}
}
return cnt;
}
/**
* Basically a switch block to direct the method to the correct instrumentation method according to the type of annotation
* @param method The method to instrument
* @param annotationType The annotation type to switch on
* @throws CannotCompileException possibly thrown by the delegated instrumentation methods
* @throws NotFoundException thrown if supporting classes cannot be found
* @throws ClassNotFoundException thrown if supporting classes cannot be found
*/
protected void instrumentMethodSwitch(CtMethod method, AnnotationType annotationType) throws CannotCompileException, NotFoundException, ClassNotFoundException {
switch (annotationType) {
case TIMED:
instrumentMethodTimed(method);
break;
default:
break;
}
}
/**
* Instruments a method with a {@link Timed} annotation
* @param method The method to instrument
* @throws CannotCompileException thrown on javassist compilation errors
* @throws NotFoundException thrown if supporting classes cannot be found
* @throws ClassNotFoundException thrown if supporting classes cannot be found
*/
protected void instrumentMethodTimed(CtMethod method) throws CannotCompileException, NotFoundException, ClassNotFoundException {
final boolean staticMethod = Modifier.isStatic(method.getModifiers());
CtClass clazz = method.getDeclaringClass();
String fieldName = (staticMethod ? "static" : "") + "Timer_" + clazz.makeUniqueName(method.getName());
TimedImpl timedImpl = new TimedImpl(method);
String initer = timedImpl.getTimerInitializer(clazz.getName());
debug("Timer init for [", method.toString() , "]\n[" , initer , "]");
CtField timerField = new CtField(classPool.get().get(Timer.class.getName()), fieldName, clazz);
timerField.setModifiers(timerField.getModifiers() | Modifier.PRIVATE | Modifier.FINAL );
if(staticMethod) timerField.setModifiers(timerField.getModifiers() | Modifier.STATIC );
clazz.addField(timerField, CtField.Initializer.byExpr(initer));
String rename = clazz.makeUniqueName(method.getName());
String originalName = method.getName();
method.setName(rename);
method.setModifiers(method.getModifiers() & ~Modifier.PROTECTED);
method.setModifiers(method.getModifiers() & ~Modifier.PUBLIC);
method.setModifiers(method.getModifiers() | Modifier.PRIVATE);
CtMethod replacement = CtNewMethod.copy(method, originalName, method.getDeclaringClass(), null);
StringBuilder delegateCode = new StringBuilder("{ com.yammer.metrics.core.TimerContext timerContext = ").append(fieldName).append(".time(); try {");
delegateCode.append("return ").append(rename).append("($$);");
delegateCode.append("} finally { timerContext.stop(); }");
delegateCode.append("}");
replacement.setBody(delegateCode.toString());
clazz.addMethod(replacement);
// === Code using Cflow
/*
String cflowName = clazz.makeUniqueName(method.getName() + "_cflow");
replacement.useCflow(cflowName);
StringBuilder delegateCode = new StringBuilder("{ com.yammer.metrics.core.TimerContext timerContext = null; ");
delegateCode.append("boolean notRecursive = $cflow(").append(cflowName).append(")==0;");
delegateCode.append("if(notRecursive) timerContext = ").append(fieldName).append(".time();");
delegateCode.append(" try {");
delegateCode.append("return ").append(rename).append("($$);");
delegateCode.append("} finally { if(notRecursive) timerContext.stop(); }");
delegateCode.append("}");
*/
}
// protected void instrumentMethodTimed(CtMethod method, Map<String, ?> av) throws CannotCompileException {
// final boolean staticMethod = Modifier.isStatic(method.getModifiers());
// CtClass clazz = method.getDeclaringClass();
// String fieldName = (staticMethod ? "static" : "") + "Timer_" + clazz.makeUniqueName(method.getName());
//
// //CtClass timerClass = gc("com.yammer.metrics.core.Timer");
// boolean scoped = !"".equals(av.get("scope"));
// String initer = scoped ?
// String.format(SCOPED_TIMER_TEMPLATE, fieldName, clazz.getName(), av.get("name"), av.get("scope"), av.get("durationUnit"), av.get("rateUnit"))
// :
// String.format(TIMER_TEMPLATE, fieldName, clazz.getName(), av.get("name"), av.get("durationUnit"), av.get("rateUnit"))
// ;
// log("Timer init for [" + method.toString() + "]\n[" + initer + "]");
// CtField timerField = CtField.make(initer, clazz);
// timerField.setModifiers(timerField.getModifiers() | Modifier.PRIVATE | Modifier.FINAL );
// if(staticMethod) timerField.setModifiers(timerField.getModifiers() | Modifier.STATIC );
// clazz.addField(timerField);
// method.instrument(new TimerExpressionEditor(fieldName));
// }
/**
* Looks up the named class in the current thread's classpool
* @param className The class name to get the class for
* @return the named class
*/
protected CtClass gc(String className) {
try {
return classPool.get().get(className);
} catch (NotFoundException nfe) {
throw new RuntimeException("The class [" + className + "] was not found", nfe);
}
}
/**
* Returns an array of annotation instances found for the passed CtMethod.
* @param method The CtMethod to look for annotations on
* @return A possible empty array of annotations
*/
protected Object[] getAnnotations(CtMethod method) {
Set<Annotation> annotations = new HashSet<Annotation>();
try {
annotations.add((Annotation) method.getAnnotation(Timed.class));
} catch (ClassNotFoundException e) { /* No Op */ }
return annotations.toArray(new Annotation[annotations.size()]);
}
}