package rocks.inspectit.agent.java.instrumentation.asm;
import info.novatec.inspectit.org.objectweb.asm.ClassVisitor;
import info.novatec.inspectit.org.objectweb.asm.MethodVisitor;
import info.novatec.inspectit.org.objectweb.asm.Opcodes;
import info.novatec.inspectit.org.objectweb.asm.Type;
import info.novatec.inspectit.org.objectweb.asm.commons.JSRInlinerAdapter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang.ArrayUtils;
import rocks.inspectit.agent.java.instrumentation.InstrumenterFactory;
import rocks.inspectit.shared.all.instrumentation.config.IMethodInstrumentationPoint;
import rocks.inspectit.shared.all.instrumentation.config.impl.MethodInstrumentationConfig;
/**
* Used to instrument classes via ASM.
*
* @author Ivan Senic
*
*/
public class ClassInstrumenter extends ClassVisitor {
/**
* All instrumentation configurations that should be added as instrumentation points.
*/
private final Collection<MethodInstrumentationConfig> instrumentationConfigs;
/**
* Collection of the applied instrumentation configurations.
*/
private final Collection<MethodInstrumentationConfig> appliedInstrumentationConfigs = new ArrayList<MethodInstrumentationConfig>(0);
/**
* Loader aware class writer we delegate calls to.
*/
private final LoaderAwareClassWriter loaderAwareClassWriter;
/**
* {@link InstrumenterFactory} for providing correct method visitors.
*/
private final InstrumenterFactory instrumenterFactory;
/**
* If enhanced exception sensor is active.
*/
private final boolean enhancedExceptionSensor;
/**
* Default constructor.
*
* @param instrumenterFactory
* {@link InstrumenterFactory} for providing correct method visitors.
* @param loaderAwareClassWriter
* LoaderAwareClassWriter
* @param methodInstrumentationConfigs
* Config holding instrumentation points.
* @param enhancedExceptionSensor
* If enhanced exception sensor is active.
*/
public ClassInstrumenter(InstrumenterFactory instrumenterFactory, LoaderAwareClassWriter loaderAwareClassWriter, Collection<MethodInstrumentationConfig> methodInstrumentationConfigs,
boolean enhancedExceptionSensor) {
super(Opcodes.ASM5, loaderAwareClassWriter);
this.instrumenterFactory = instrumenterFactory;
this.instrumentationConfigs = new ArrayList<MethodInstrumentationConfig>(methodInstrumentationConfigs);
this.enhancedExceptionSensor = enhancedExceptionSensor;
this.loaderAwareClassWriter = loaderAwareClassWriter;
}
/**
* {@inheritDoc}
*/
@Override
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
// calling super to ensure the visitor pattern
super.visit(version, access, name, signature, superName, interfaces);
loaderAwareClassWriter.setClassName(name);
loaderAwareClassWriter.setSuperClassName(superName);
}
/**
* {@inheritDoc}
*/
@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
// calling super to ensure the visitor pattern
MethodVisitor methodVisitor = super.visitMethod(access, name, desc, signature, exceptions);
MethodInstrumentationConfig instrumentationConfig = shouldInstrument(name, desc);
if (null != instrumentationConfig) {
// using JSR inliner adapter in order to remove JSR/RET instructions
// see http://mail-archive.ow2.org/asm/2008-11/msg00008.html
// using only if we add byte code
methodVisitor = new JSRInlinerAdapter(methodVisitor, access, name, desc, signature, exceptions);
// go over all instrumentation points and create the method visitor
for (IMethodInstrumentationPoint instrumentationPoint : instrumentationConfig.getAllInstrumentationPoints()) {
// note that here we create a chain of method visitor by passing the current one to
// the one being created, thus following the visitor pattern of ASM
MethodVisitor mv = instrumenterFactory.getMethodVisitor(instrumentationPoint, methodVisitor, access, name, desc, enhancedExceptionSensor);
// safety for the null returned method visitor
if (null != mv) {
methodVisitor = mv;
}
}
// add to list so that we know which are applied
appliedInstrumentationConfigs.add(instrumentationConfig);
}
return methodVisitor;
}
/**
* If method should be instrumented. If there is appropriate {@link MethodInstrumentationConfig}
* that denotes that method should be instrumented this will be removed from the
* {@link #instrumentationConfigs} and returned as a result.
*
* @param name
* Name of the method.
* @param desc
* ASM description of the method.
* @return {@link MethodInstrumentationConfig} if method should be instrumented, otherwise
* <code>null</code>
*/
private MethodInstrumentationConfig shouldInstrument(String name, String desc) {
for (Iterator<MethodInstrumentationConfig> it = instrumentationConfigs.iterator(); it.hasNext();) {
MethodInstrumentationConfig config = it.next();
if (matches(name, desc, config)) {
it.remove();
return config;
}
}
return null;
}
/**
* If method name and description matches the {@link MethodInstrumentationConfig}.
*
* @param name
* method name
* @param desc
* method ASM description
* @param instrumentationConfig
* {@link MethodInstrumentationConfig}
* @return <code>true</code> if name and desc matches the instrumentation config
*/
private boolean matches(String name, String desc, MethodInstrumentationConfig instrumentationConfig) {
if (!name.equals(instrumentationConfig.getTargetMethodName())) {
return false;
}
Type methodType = Type.getMethodType(desc);
if (!methodType.getReturnType().getClassName().equals(instrumentationConfig.getReturnType())) {
return false;
}
Type[] argumentTypes = methodType.getArgumentTypes();
List<String> parameterTypes = instrumentationConfig.getParameterTypes();
// if both are empty return true, if only one is empty return false (null safety)
if (CollectionUtils.isEmpty(parameterTypes) && ArrayUtils.isEmpty(argumentTypes)) {
return true;
} else if (CollectionUtils.isEmpty(parameterTypes) || ArrayUtils.isEmpty(argumentTypes)) {
return false;
}
// if not same size return false
if (argumentTypes.length != parameterTypes.size()) {
return false;
}
// check then one by one
for (int i = 0; i < argumentTypes.length; i++) {
if (!argumentTypes[i].getClassName().equals(parameterTypes.get(i))) {
return false;
}
}
return true;
}
/**
* Returns if the byte code was added. This effectively checks if the
* {@link #appliedInstrumentationConfigs} is not empty.
*
* @return Returns if any byte code was added.
*/
public boolean isByteCodeAdded() {
return CollectionUtils.isNotEmpty(appliedInstrumentationConfigs);
}
/**
* Gets {@link #appliedInstrumentationConfigs}.
*
* @return {@link #appliedInstrumentationConfigs}
*/
public Collection<MethodInstrumentationConfig> getAppliedInstrumentationConfigs() {
return appliedInstrumentationConfigs;
}
}