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

com.fitbur.mockito.bytebuddy.asm.Advice Maven / Gradle / Ivy

There is a newer version: 1.0.0
Show newest version
package com.fitbur.mockito.bytebuddy.asm;

import com.fitbur.mockito.bytebuddy.ClassFileVersion;
import com.fitbur.mockito.bytebuddy.description.annotation.AnnotationDescription;
import com.fitbur.mockito.bytebuddy.description.field.FieldDescription;
import com.fitbur.mockito.bytebuddy.description.method.MethodDescription;
import com.fitbur.mockito.bytebuddy.description.method.MethodList;
import com.fitbur.mockito.bytebuddy.description.method.ParameterDescription;
import com.fitbur.mockito.bytebuddy.description.method.ParameterList;
import com.fitbur.mockito.bytebuddy.description.type.TypeDefinition;
import com.fitbur.mockito.bytebuddy.description.type.TypeDescription;
import com.fitbur.mockito.bytebuddy.description.type.TypeList;
import com.fitbur.mockito.bytebuddy.dynamic.ClassFileLocator;
import com.fitbur.mockito.bytebuddy.dynamic.scaffold.FieldLocator;
import com.fitbur.mockito.bytebuddy.implementation.bytecode.StackSize;
import com.fitbur.mockito.bytebuddy.matcher.ElementMatcher;
import com.fitbur.mockito.bytebuddy.utility.CompoundList;
import com.fitbur.mockito.bytebuddy.utility.ExceptionTableSensitiveMethodVisitor;
import com.fitbur.mockito.bytebuddy.jar.asm.*;

import java.io.*;
import java.lang.annotation.*;
import java.util.*;

import static com.fitbur.mockito.bytebuddy.matcher.ElementMatchers.named;

/**
 * 

* Advice wrappers copy the code of blueprint methods to be executed before and/or after a matched method. To achieve this, a {@code static} * method of a class is annotated by {@link OnMethodEnter} and/or {@link OnMethodExit} and provided to an instance of this class. *

*

* A method that is annotated with {@link OnMethodEnter} can annotate its parameters with {@link Argument} where field access to this parameter * is substituted with access to the specified argument of the instrumented method. Alternatively, a parameter can be annotated by {@link This} * where the {@code this} reference of the instrumented method is read when the parameter is accessed. This mechanism can also be used to assign a * new value to the {@code this} reference of an instrumented method. If no annotation is used on a parameter, it is assigned the {@code n}-th * parameter of the instrumented method for the {@code n}-th parameter of the advice method. All parameters must declare the exact same type as * the parameters of the instrumented type or the method's declaring type for the {@link This} reference respectively if they are not marked as * read-only. In the latter case, it suffices that a parameter type is a super type of the corresponding type of the instrumented method. *

*

* A method that is annotated with {@link OnMethodExit} can equally annotate its parameters with {@link Argument} and {@link This}. Additionally, * it can annotate a parameter with {@link Return} to receive the original method's return value. By reassigning the return value, it is possible * to replace the returned value. If an instrumented method does not return a value, this annotation must not be used. If a method returns * exceptionally, the parameter is set to its default value, i.e. to {@code 0} for primitive types and to {@code null} for reference types. The * parameter's type must be equal to the instrumented method's return type if it is not set to read-only where it suffices to declare the * parameter type to be of any super type to the instrumented method's return type. An exception can be read by annotating a parameter of type * {@link Throwable} annotated with {@link Thrown} which is assigned the thrown {@link Throwable} or {@code null} if a method returns normally. * Doing so, it is possible to exchange a thrown exception with any checked or unchecked exception.Finally, if a method annotated with * {@link OnMethodEnter} exists and this method returns a value, this value can be accessed by a parameter annotated with {@link Enter}. * This parameter must declare the same type as type being returned by the method annotated with {@link OnMethodEnter}. If the parameter is marked * to be read-only, it suffices that the annotated parameter is of a super type of the return type of the method annotated by * {@link OnMethodEnter}. If no such method exists or this method returns {@code void}, no such parameter must be declared. Any return value * of a method that is annotated by {@link OnMethodExit} is discarded. *

*

* If any advice method throws an exception, the method is terminated prematurely. If the method annotated by {@link OnMethodEnter} throws an exception, * the method annotated by {@link OnMethodExit} method is not invoked. If the instrumented method throws an exception, the method that is annotated by * {@link OnMethodExit} is only invoked if the {@link OnMethodExit#onThrowable()} property is set to {@code true} what is the default. If this property * is set to {@code false}, the {@link Thrown} annotation must not be used on any parameter. *

*

* Byte Buddy does not assert the visibility of any types that are referenced within an inlined advice method. It is the responsibility of * the user of this class to assure that all types referenced within the advice methods are visible to the instrumented class. Failing to * do so results in a {@link IllegalAccessError} at the instrumented class's runtime. *

*

* Advice cannot be applied to native or abstract methods which are silently skipped when advice matches such a method. *

*

* Important: Since Java 6, class files contain stack map frames embedded into a method's byte code. When advice methods are compiled * with a class file version less then Java 6 but are used for a class file that was compiled to Java 6 or newer, these stack map frames must be * computed by ASM by using the {@link ClassWriter#COMPUTE_FRAMES} option. If the advice methods do not contain any branching instructions, this is * not required. No action is required if the advice methods are at least compiled with Java 6 but are used on classes older than Java 6. This * limitation only applies to advice methods that are inlined. Also, it is the responsibility of this class's user to assure that the advice method * does not contain byte code constructs that are not supported by the class containing the instrumented method. In particular, pre Java-5 * try-finally blocks cannot be inlined into classes with newer byte code levels as the jsr instruction was deprecated. Also, classes prior * to Java 7 do not support the invokedynamic command which must not be contained by an advice method if the instrumented method targets an * older class file format version. *

*

* Note: For the purpose of inlining, Java 5 and Java 6 byte code can be seen as the best candidate for advice methods. These versions do * no longer allow subroutines, neither do they already allow invokedynamic instructions or method handles. This way, Java 5 and Java 6 byte * code is compatible to both older and newer versions. One exception for backwards-incompatible byte code is the possibility to load type references * from the constant pool onto the operand stack. These instructions can however easily be transformerd for classes compiled to Java 4 and older * by registering a {@link TypeConstantAdjustment} before the advice visitor. *

*

* Note: It is not possible to trigger break points in inlined advice methods as the debugging information of the inlined advice is not * preserved. It is not possible in Java to reference more than one source file per class what makes translating such debugging information * impossible. It is however possible to set break points in advice methods when invoking the original advice target. This allows debugging * of advice code within unit tests that invoke the advice method without instrumentation. As a conequence of not transferring debugging information, * the names of the parameters of an advice method do not matter when inlining, neither does any meta information on the advice method's body * such as annotations or parameter modifiers. *

* * @see Argument * @see BoxedArguments * @see BoxedReturn * @see DynamicValue * @see Enter * @see FieldValue * @see Ignored * @see OnMethodEnter * @see OnMethodExit * @see Origin * @see Return * @see This * @see Thrown */ public class Advice implements AsmVisitorWrapper.ForDeclaredMethods.MethodVisitorWrapper { /** * A reference to the {@link OnMethodEnter#inline()} method. */ private static final MethodDescription.InDefinedShape INLINE_ENTER; /** * A reference to the {@link OnMethodEnter#suppress()} method. */ private static final MethodDescription.InDefinedShape SUPPRESS_ENTER; /** * A reference to the {@link OnMethodExit#inline()} method. */ private static final MethodDescription.InDefinedShape INLINE_EXIT; /** * A reference to the {@link OnMethodExit#suppress()} method. */ private static final MethodDescription.InDefinedShape SUPPRESS_EXIT; /** * A reference to the {@link OnMethodExit#onThrowable()} method. */ private static final MethodDescription.InDefinedShape ON_THROWABLE; /* * Extracts the annotation values for the enter and exit advice annotations. */ static { MethodList enter = new TypeDescription.ForLoadedType(OnMethodEnter.class).getDeclaredMethods(); INLINE_ENTER = enter.filter(named("inline")).getOnly(); SUPPRESS_ENTER = enter.filter(named("suppress")).getOnly(); MethodList exit = new TypeDescription.ForLoadedType(OnMethodExit.class).getDeclaredMethods(); INLINE_EXIT = exit.filter(named("inline")).getOnly(); SUPPRESS_EXIT = exit.filter(named("suppress")).getOnly(); ON_THROWABLE = exit.filter(named("onThrowable")).getOnly(); } /** * The dispatcher for instrumenting the instrumented method upon entering. */ private final Dispatcher.Resolved.ForMethodEnter methodEnter; /** * The dispatcher for instrumenting the instrumented method upon exiting. */ private final Dispatcher.Resolved.ForMethodExit methodExit; /** * Creates a new advice. * * @param methodEnter The dispatcher for instrumenting the instrumented method upon entering. * @param methodExit The dispatcher for instrumenting the instrumented method upon exiting. */ protected Advice(Dispatcher.Resolved.ForMethodEnter methodEnter, Dispatcher.Resolved.ForMethodExit methodExit) { this.methodEnter = methodEnter; this.methodExit = methodExit; } /** * Implements advice where every matched method is advised by the given type's advisory methods. The advices binary representation is * accessed by querying the class loader of the supplied class for a class file. * * @param type The type declaring the advice. * @return A method visitor wrapper representing the supplied advice. */ public static Advice to(Class type) { return to(type, ClassFileLocator.ForClassLoader.of(type.getClassLoader())); } /** * Implements advice where every matched method is advised by the given type's advisory methods. * * @param type The type declaring the advice. * @param classFileLocator The class file locator for locating the advisory class's class file. * @return A method visitor wrapper representing the supplied advice. */ public static Advice to(Class type, ClassFileLocator classFileLocator) { return to(new TypeDescription.ForLoadedType(type), classFileLocator); } /** * Implements advice where every matched method is advised by the given type's advisory methods. Using this method, a non-operational * class file locator is specified for the advice target. This implies that only advice targets with the inline target set * to {@code false} are resolvable by the returned instance. * * @param typeDescription The type declaring the advice. * @return A method visitor wrapper representing the supplied advice. */ public static Advice to(TypeDescription typeDescription) { return to(typeDescription, ClassFileLocator.NoOp.INSTANCE); } /** * Implements advice where every matched method is advised by the given type's advisory methods. * * @param typeDescription A description of the type declaring the advice. * @param classFileLocator The class file locator for locating the advisory class's class file. * @return A method visitor wrapper representing the supplied advice. */ public static Advice to(TypeDescription typeDescription, ClassFileLocator classFileLocator) { return to(typeDescription, classFileLocator, Collections.emptyList()); } /** * Creates a new advice. * * @param typeDescription A description of the type declaring the advice. * @param classFileLocator The class file locator for locating the advisory class's class file. * @param userFactories A list of custom factories for user generated offset mappings. * @return A method visitor wrapper representing the supplied advice. */ protected static Advice to(TypeDescription typeDescription, ClassFileLocator classFileLocator, List userFactories) { try { Dispatcher.Unresolved methodEnter = Dispatcher.Inactive.INSTANCE, methodExit = Dispatcher.Inactive.INSTANCE; for (MethodDescription.InDefinedShape methodDescription : typeDescription.getDeclaredMethods()) { methodEnter = locate(OnMethodEnter.class, INLINE_ENTER, methodEnter, methodDescription); methodExit = locate(OnMethodExit.class, INLINE_EXIT, methodExit, methodDescription); } if (!methodEnter.isAlive() && !methodExit.isAlive()) { throw new IllegalArgumentException("No advice defined by " + typeDescription); } ClassFileLocator.Resolution binaryRepresentation = methodEnter.isBinary() || methodExit.isBinary() ? classFileLocator.locate(typeDescription.getName()) : new ClassFileLocator.Resolution.Illegal(typeDescription.getName()); Dispatcher.Resolved.ForMethodEnter resolved = methodEnter.asMethodEnter(userFactories, binaryRepresentation); return new Advice(resolved, methodExit.asMethodExitTo(userFactories, binaryRepresentation, resolved)); } catch (IOException exception) { throw new IllegalStateException("Error reading class file of " + typeDescription, exception); } } /** * Locates a dispatcher for the method if available. * * @param type The annotation type that indicates a given form of advice that is currently resolved. * @param property An annotation property that indicates if the advice method should be inlined. * @param dispatcher Any previous dispatcher that was discovered or {@code null} if no such dispatcher was yet found. * @param methodDescription The method description that is considered as an advice method. * @return A resolved dispatcher or {@code null} if no dispatcher was resolved. */ private static Dispatcher.Unresolved locate(Class type, MethodDescription.InDefinedShape property, Dispatcher.Unresolved dispatcher, MethodDescription.InDefinedShape methodDescription) { AnnotationDescription annotation = methodDescription.getDeclaredAnnotations().ofType(type); if (annotation == null) { return dispatcher; } else if (dispatcher.isAlive()) { throw new IllegalStateException("Duplicate advice for " + dispatcher + " and " + methodDescription); } else if (!methodDescription.isStatic()) { throw new IllegalStateException("Advice for " + methodDescription + " is not static"); } else { return annotation.getValue(property, Boolean.class) ? new Dispatcher.Inlining(methodDescription) : new Dispatcher.Delegating(methodDescription); } } /** * Allows for the configuration of custom annotations that are then bound to a dynamically computed, constant value. * * @return A builder for an {@link Advice} instrumentation with custom values. * @see DynamicValue */ public static WithCustomMapping withCustomMapping() { return new WithCustomMapping(); } /** * Returns an ASM visitor wrapper that matches the given matcher and applies this advice to the matched methods. * * @param matcher The matcher identifying methods to apply the advice to. * @return A suitable ASM visitor wrapper with the compute frames option enabled. */ public AsmVisitorWrapper.ForDeclaredMethods on(ElementMatcher matcher) { return new AsmVisitorWrapper.ForDeclaredMethods().method(matcher, this); } @Override public MethodVisitor wrap(TypeDescription instrumentedType, MethodDescription.InDefinedShape methodDescription, MethodVisitor methodVisitor, ClassFileVersion classFileVersion, int writerFlags, int readerFlags) { if (methodDescription.isAbstract() || methodDescription.isNative()) { return methodVisitor; } else if (!methodExit.isAlive()) { return new AdviceVisitor.WithoutExitAdvice(methodVisitor, methodDescription, methodEnter, classFileVersion, writerFlags, readerFlags); } else if (methodExit.getTriggeringThrowable().represents(NoExceptionHandler.class)) { return new AdviceVisitor.WithExitAdvice.WithoutExceptionHandling(methodVisitor, methodDescription, methodEnter, methodExit, classFileVersion, writerFlags, readerFlags); } else if (methodDescription.isConstructor()) { throw new IllegalStateException("Cannot catch exception during constructor call for " + methodDescription); } else { return new AdviceVisitor.WithExitAdvice.WithExceptionHandling(methodVisitor, methodDescription, methodEnter, methodExit, classFileVersion, writerFlags, readerFlags, methodExit.getTriggeringThrowable()); } } @Override public boolean equals(Object other) { if (this == other) return true; if (other == null || getClass() != other.getClass()) return false; Advice advice = (Advice) other; return methodEnter.equals(advice.methodEnter) && methodExit.equals(advice.methodExit); } @Override public int hashCode() { int result = methodEnter.hashCode(); result = 31 * result + methodExit.hashCode(); return result; } @Override public String toString() { return "Advice{" + "methodEnter=" + methodEnter + ", methodExit=" + methodExit + '}'; } /** * A handler for computing the instrumented method's size. */ protected interface MethodSizeHandler { /** * Indicates that a size is not computed but handled directly by ASM. */ int UNDEFINED_SIZE = Short.MAX_VALUE; /** * Binds a method size handler for the entry advice. * * @param adviceMethod The method representing the entry advice. * @return A method size handler for the entry advice. */ ForAdvice bindEntry(MethodDescription.InDefinedShape adviceMethod); /** * Binds the method size handler for the exit advice. * * @param adviceMethod The method representing the exit advice. * @param skipThrowable {@code true} if the exit advice is not invoked on an exception. * @return A method size handler for the exit advice. */ ForAdvice bindExit(MethodDescription.InDefinedShape adviceMethod, boolean skipThrowable); /** * Computes a compound stack size for the advice and the translated instrumented method. * * @param stackSize The required stack size of the instrumented method before translation. * @return The stack size required by the instrumented method and its advice methods. */ int compoundStackSize(int stackSize); /** * Computes a compound local variable array length for the advice and the translated instrumented method. * * @param localVariableLength The required local variable array length of the instrumented method before translation. * @return The local variable length required by the instrumented method and its advice methods. */ int compoundLocalVariableLength(int localVariableLength); /** * A method size handler for an advice method. */ interface ForAdvice { /** * Records a minimum stack size required by the represented advice method. * * @param stackSize The minimum size required by the represented advice method. */ void recordMinimum(int stackSize); /** * Records the maximum values for stack size and local variable array which are required by the advice method * for its individual execution without translation. * * @param stackSize The minimum required stack size. * @param localVariableLength The minimum required length of the local variable array. */ void recordMaxima(int stackSize, int localVariableLength); /** * Records a minimum padding additionally to the computed stack size that is required for implementing this advice method. * * @param padding The minimum required padding. */ void recordPadding(int padding); } /** * A non-operational method size handler. */ enum NoOp implements MethodSizeHandler, ForAdvice { /** * The singleton instance. */ INSTANCE; @Override public ForAdvice bindEntry(MethodDescription.InDefinedShape adviceMethod) { return this; } @Override public ForAdvice bindExit(MethodDescription.InDefinedShape adviceMethod, boolean skipThrowable) { return this; } @Override public int compoundStackSize(int stackSize) { return UNDEFINED_SIZE; } @Override public int compoundLocalVariableLength(int localVariableLength) { return UNDEFINED_SIZE; } @Override public void recordMinimum(int stackSize) { /* do nothing */ } @Override public void recordMaxima(int stackSize, int localVariableLength) { /* do nothing */ } @Override public void recordPadding(int padding) { /* do nothing */ } @Override public String toString() { return "Advice.MethodSizeHandler.NoOp." + name(); } } /** * A default implementation for a method size handler. */ class Default implements MethodSizeHandler { /** * The instrumented method. */ private final MethodDescription.InDefinedShape instrumentedMethod; /** * The list of types that the instrumented method requires in addition to the method parameters. */ private final TypeList requiredTypes; /** * A list of types that are yielded by the instrumented method and available to the exit advice. */ private final TypeList yieldedTypes; /** * The maximum stack size required by a visited advice method. */ private int stackSize; /** * The maximum length of the local variable array required by a visited advice method. */ private int localVariableLength; /** * Creates a new default meta data handler that recomputes the space requirements of an instrumented method. * * @param instrumentedMethod The instrumented method. * @param requiredTypes The types this meta data handler expects to be available additionally to the instrumented method's parameters. * @param yieldedTypes The types that are expected to be added after the instrumented method returns. */ protected Default(MethodDescription.InDefinedShape instrumentedMethod, TypeList requiredTypes, TypeList yieldedTypes) { this.instrumentedMethod = instrumentedMethod; this.requiredTypes = requiredTypes; this.yieldedTypes = yieldedTypes; } /** * Creates a method size handler applicable for the given instrumented method. * * @param instrumentedMethod The instrumented method. * @param requiredTypes The list of types that the instrumented method requires in addition to the method parameters. * @param yieldedTypes A list of types that are yielded by the instrumented method and available to the exit advice. * @param writerFlags The flags supplied to the ASM class writer. * @return An appropriate method size handler. */ protected static MethodSizeHandler of(MethodDescription.InDefinedShape instrumentedMethod, List requiredTypes, List yieldedTypes, int writerFlags) { return (writerFlags & (ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES)) != 0 ? NoOp.INSTANCE : new Default(instrumentedMethod, new TypeList.Explicit(requiredTypes), new TypeList.Explicit(yieldedTypes)); } @Override public MethodSizeHandler.ForAdvice bindEntry(MethodDescription.InDefinedShape adviceMethod) { stackSize = Math.max(stackSize, adviceMethod.getReturnType().getStackSize().getSize()); return new ForAdvice(adviceMethod, new TypeList.Empty(), new TypeList.Explicit(requiredTypes)); } @Override public MethodSizeHandler.ForAdvice bindExit(MethodDescription.InDefinedShape adviceMethod, boolean skipThrowable) { stackSize = Math.max(stackSize, adviceMethod.getReturnType().getStackSize().maximum(skipThrowable ? StackSize.ZERO : StackSize.SINGLE).getSize()); return new ForAdvice(adviceMethod, new TypeList.Explicit(CompoundList.of(requiredTypes, yieldedTypes)), new TypeList.Empty()); } @Override public int compoundStackSize(int stackSize) { return Math.max(this.stackSize, stackSize); } @Override public int compoundLocalVariableLength(int localVariableLength) { return Math.max(this.localVariableLength, localVariableLength + requiredTypes.getStackSize() + yieldedTypes.getStackSize()); } @Override public String toString() { return "Advice.MethodSizeHandler.Default{" + "instrumentedMethod=" + instrumentedMethod + ", requiredTypes=" + requiredTypes + ", yieldedTypes=" + yieldedTypes + ", stackSize=" + stackSize + ", localVariableLength=" + localVariableLength + '}'; } /** * A method size handler for an advice method. */ protected class ForAdvice implements MethodSizeHandler.ForAdvice { /** * The advice method. */ private final MethodDescription.InDefinedShape adviceMethod; /** * A list of types required by this advice method. */ private final TypeList requiredTypes; /** * A list of types yielded by this advice method. */ private final TypeList yieldedTypes; /** * The padding that this advice method requires additionally to its computed size. */ private int padding; /** * Creates a new method size handler for an advice method. * * @param adviceMethod The advice method. * @param requiredTypes A list of types required by this advice method. * @param yieldedTypes A list of types yielded by this advice method. */ protected ForAdvice(MethodDescription.InDefinedShape adviceMethod, TypeList requiredTypes, TypeList yieldedTypes) { this.adviceMethod = adviceMethod; this.requiredTypes = requiredTypes; this.yieldedTypes = yieldedTypes; stackSize = Math.max(stackSize, adviceMethod.getReturnType().getStackSize().getSize()); } @Override public void recordMinimum(int stackSize) { Default.this.stackSize = Math.max(Default.this.stackSize, stackSize); } @Override public void recordMaxima(int stackSize, int localVariableLength) { Default.this.stackSize = Math.max(Default.this.stackSize, stackSize) + padding; Default.this.localVariableLength = Math.max(Default.this.localVariableLength, localVariableLength - adviceMethod.getStackSize() + instrumentedMethod.getStackSize() + requiredTypes.getStackSize() + yieldedTypes.getStackSize()); } @Override public void recordPadding(int padding) { this.padding = Math.max(this.padding, padding); } @Override public String toString() { return "Advice.MethodSizeHandler.Default.ForAdvice{" + "adviceMethod=" + adviceMethod + ", requiredTypes=" + requiredTypes + ", yieldedTypes=" + yieldedTypes + ", padding=" + padding + '}'; } } } } /** * A handler for computing and translating stack map frames. */ protected interface StackMapFrameHandler { /** * Translates a frame. * * @param methodVisitor The method visitor to write the frame to. * @param frameType The frame's type. * @param localVariableLength The local variable length. * @param localVariable An array containing the types of the current local variables. * @param stackSize The size of the operand stack. * @param stack An array containing the types of the current operand stack. */ void translateFrame(MethodVisitor methodVisitor, int frameType, int localVariableLength, Object[] localVariable, int stackSize, Object[] stack); /** * Injects a frame indicating the beginning of a return value handler for the currently handled method. * * @param methodVisitor The method visitor onto which to apply the stack map frame. */ void injectReturnFrame(MethodVisitor methodVisitor); /** * Injects a frame indicating the beginning of an exception handler for the currently handled method. * * @param methodVisitor The method visitor onto which to apply the stack map frame. */ void injectExceptionFrame(MethodVisitor methodVisitor); /** * Injects a frame indicating the completion of the currently handled method, i.e. all yielded types were added. * * @param methodVisitor The method visitor onto which to apply the stack map frame. * @param secondary {@code true} if another completion frame for this method was written previously. */ void injectCompletionFrame(MethodVisitor methodVisitor, boolean secondary); /** * A stack map frame handler for an instrumented method. */ interface ForInstrumentedMethod extends StackMapFrameHandler { /** * Binds this meta data handler for the entry advice. * * @param adviceMethod The entry advice method. * @return An appropriate meta data handler for the enter method. */ ForAdvice bindEntry(MethodDescription.InDefinedShape adviceMethod); /** * Binds this meta data handler for the exit advice. * * @param adviceMethod The exit advice method. * @return An appropriate meta data handler for the enter method. */ ForAdvice bindExit(MethodDescription.InDefinedShape adviceMethod); /** * Returns a hint to supply to a {@link ClassReader} when parsing an advice method. * * @return The reader hint to supply to an ASM class reader. */ int getReaderHint(); } /** * A stack map frame handler for an advice method. */ interface ForAdvice extends StackMapFrameHandler { /* marker interface */ } /** * A non-operational stack map frame handler. */ enum NoOp implements ForInstrumentedMethod, ForAdvice { /** * The singleton instance. */ INSTANCE; @Override public StackMapFrameHandler.ForAdvice bindEntry(MethodDescription.InDefinedShape adviceMethod) { return this; } @Override public StackMapFrameHandler.ForAdvice bindExit(MethodDescription.InDefinedShape adviceMethod) { return this; } @Override public int getReaderHint() { return ClassReader.SKIP_FRAMES; } @Override public void translateFrame(MethodVisitor methodVisitor, int frameType, int localVariableLength, Object[] localVariable, int stackSize, Object[] stack) { /* do nothing */ } @Override public void injectReturnFrame(MethodVisitor methodVisitor) { /* do nothing */ } @Override public void injectExceptionFrame(MethodVisitor methodVisitor) { /* do nothing */ } @Override public void injectCompletionFrame(MethodVisitor methodVisitor, boolean secondary) { /* do nothing */ } @Override public String toString() { return "Advice.StackMapFrameHandler.NoOp." + name(); } } /** * A default implementation of a stack map frame handler for an instrumented method. */ class Default implements ForInstrumentedMethod { /** * An empty array indicating an empty frame. */ private static final Object[] EMPTY = new Object[0]; /** * The instrumented method. */ protected final MethodDescription.InDefinedShape instrumentedMethod; /** * A list of intermediate types to be considered as part of the instrumented method's steady signature. */ protected final TypeList requiredTypes; /** * The types that are expected to be added after the instrumented method returns. */ protected final TypeList yieldedTypes; /** * {@code true} if the meta data handler is expected to expand its frames. */ private final boolean expandFrames; /** * The current frame's size divergence from the original local variable array. */ private int currentFrameDivergence; /** * Creates a new default meta data handler. * * @param instrumentedMethod The instrumented method. * @param requiredTypes A list of intermediate types to be considered as part of the instrumented method's steady signature. * @param yieldedTypes The types that are expected to be added after the instrumented method returns. * @param expandFrames {@code true} if the meta data handler is expected to expand its frames. */ protected Default(MethodDescription.InDefinedShape instrumentedMethod, TypeList requiredTypes, TypeList yieldedTypes, boolean expandFrames) { this.instrumentedMethod = instrumentedMethod; this.requiredTypes = requiredTypes; this.yieldedTypes = yieldedTypes; this.expandFrames = expandFrames; } /** * Creates an appropriate stack map frame handler for an instrumented method. * * @param instrumentedMethod The instrumented method. * @param requiredTypes A list of intermediate types to be considered as part of the instrumented method's steady signature. * @param yieldedTypes The types that are expected to be added after the instrumented method returns. * @param classFileVersion The instrumented type's class file version. * @param writerFlags The flags supplied to the ASM writier. * @param readerFlags The reader flags supplied to the ASM reader. * @return An approrpiate stack map frame handler for an instrumented method. */ protected static ForInstrumentedMethod of(MethodDescription.InDefinedShape instrumentedMethod, List requiredTypes, List yieldedTypes, ClassFileVersion classFileVersion, int writerFlags, int readerFlags) { return (writerFlags & ClassWriter.COMPUTE_FRAMES) != 0 || classFileVersion.isLessThan(ClassFileVersion.JAVA_V6) ? NoOp.INSTANCE : new Default(instrumentedMethod, new TypeList.Explicit(requiredTypes), new TypeList.Explicit(yieldedTypes), (readerFlags & ClassReader.EXPAND_FRAMES) != 0); } /** * Translates a type into a representation of its form inside a stack map frame. * * @param typeDescription The type to translate. * @return A stack entry representation of the supplied type. */ protected static Object toFrame(TypeDescription typeDescription) { if (typeDescription.represents(boolean.class) || typeDescription.represents(byte.class) || typeDescription.represents(short.class) || typeDescription.represents(char.class) || typeDescription.represents(int.class)) { return Opcodes.INTEGER; } else if (typeDescription.represents(long.class)) { return Opcodes.LONG; } else if (typeDescription.represents(float.class)) { return Opcodes.FLOAT; } else if (typeDescription.represents(double.class)) { return Opcodes.DOUBLE; } else { return typeDescription.getInternalName(); } } @Override public StackMapFrameHandler.ForAdvice bindEntry(MethodDescription.InDefinedShape adviceMethod) { return new ForAdvice(adviceMethod, new TypeList.Empty(), requiredTypes, TranslationMode.ENTRY); } @Override public StackMapFrameHandler.ForAdvice bindExit(MethodDescription.InDefinedShape adviceMethod) { return new ForAdvice(adviceMethod, new TypeList.Explicit(CompoundList.of(requiredTypes, yieldedTypes)), new TypeList.Empty(), TranslationMode.EXIT); } @Override public int getReaderHint() { return expandFrames ? ClassReader.EXPAND_FRAMES : AsmVisitorWrapper.NO_FLAGS; } @Override public void translateFrame(MethodVisitor methodVisitor, int type, int localVariableLength, Object[] localVariable, int stackSize, Object[] stack) { translateFrame(methodVisitor, TranslationMode.COPY, instrumentedMethod, requiredTypes, type, localVariableLength, localVariable, stackSize, stack); } /** * Translates a frame. * * @param methodVisitor The method visitor to write the frame to. * @param translationMode The translation mode to apply. * @param methodDescription The method description for which the frame is written. * @param additionalTypes The additional types to consider part of the instrumented method's parameters. * @param frameType The frame's type. * @param localVariableLength The local variable length. * @param localVariable An array containing the types of the current local variables. * @param stackSize The size of the operand stack. * @param stack An array containing the types of the current operand stack. */ protected void translateFrame(MethodVisitor methodVisitor, TranslationMode translationMode, MethodDescription.InDefinedShape methodDescription, TypeList additionalTypes, int frameType, int localVariableLength, Object[] localVariable, int stackSize, Object[] stack) { switch (frameType) { case Opcodes.F_SAME: case Opcodes.F_SAME1: break; case Opcodes.F_APPEND: currentFrameDivergence += localVariableLength; break; case Opcodes.F_CHOP: currentFrameDivergence -= localVariableLength; break; case Opcodes.F_FULL: case Opcodes.F_NEW: Object[] translated = new Object[localVariableLength - methodDescription.getParameters().size() - (methodDescription.isStatic() ? 0 : 1) + instrumentedMethod.getParameters().size() + (instrumentedMethod.isStatic() ? 0 : 1) + additionalTypes.size()]; int index = translationMode.copy(instrumentedMethod, methodDescription, localVariable, translated); for (TypeDescription typeDescription : additionalTypes) { translated[index++] = toFrame(typeDescription); } System.arraycopy(localVariable, methodDescription.getParameters().size() + (methodDescription.isStatic() ? 0 : 1), translated, index, translated.length - index); localVariableLength = translated.length; localVariable = translated; currentFrameDivergence = translated.length - index; break; default: throw new IllegalArgumentException("Unexpected frame frameType: " + frameType); } methodVisitor.visitFrame(frameType, localVariableLength, localVariable, stackSize, stack); } @Override public void injectReturnFrame(MethodVisitor methodVisitor) { if (!expandFrames && currentFrameDivergence == 0 && !instrumentedMethod.isConstructor()) { if (instrumentedMethod.getReturnType().represents(void.class)) { methodVisitor.visitFrame(Opcodes.F_SAME, 0, EMPTY, 0, EMPTY); } else { methodVisitor.visitFrame(Opcodes.F_SAME1, 0, EMPTY, 1, new Object[]{toFrame(instrumentedMethod.getReturnType().asErasure())}); } } else { injectFullFrame(methodVisitor, requiredTypes, instrumentedMethod.getReturnType().represents(void.class) ? Collections.emptyList() : Collections.singletonList(instrumentedMethod.getReturnType().asErasure())); } } @Override public void injectExceptionFrame(MethodVisitor methodVisitor) { if (!expandFrames && currentFrameDivergence == 0) { methodVisitor.visitFrame(Opcodes.F_SAME1, 0, EMPTY, 1, new Object[]{Type.getInternalName(Throwable.class)}); } else { injectFullFrame(methodVisitor, requiredTypes, Collections.singletonList(TypeDescription.THROWABLE)); } } @Override public void injectCompletionFrame(MethodVisitor methodVisitor, boolean secondary) { if (!expandFrames && currentFrameDivergence == 0 && (secondary || !instrumentedMethod.isConstructor())) { if (secondary) { methodVisitor.visitFrame(Opcodes.F_SAME, 0, EMPTY, 0, EMPTY); } else { Object[] local = new Object[yieldedTypes.size()]; int index = 0; for (TypeDescription typeDescription : yieldedTypes) { local[index++] = toFrame(typeDescription); } methodVisitor.visitFrame(Opcodes.F_APPEND, local.length, local, 0, EMPTY); } } else { injectFullFrame(methodVisitor, CompoundList.of(requiredTypes, yieldedTypes), Collections.emptyList()); } } /** * Injects a full stack map frame. * * @param methodVisitor The method visitor onto which to write the stack map frame. * @param typesInArray The types that were added to the local variable array additionally to the values of the instrumented method. * @param typesOnStack The types currently on the operand stack. */ protected void injectFullFrame(MethodVisitor methodVisitor, List typesInArray, List typesOnStack) { Object[] localVariable = new Object[instrumentedMethod.getParameters().size() + (instrumentedMethod.isStatic() ? 0 : 1) + typesInArray.size()]; int index = 0; if (!instrumentedMethod.isStatic()) { localVariable[index++] = toFrame(instrumentedMethod.getDeclaringType()); } for (TypeDescription typeDescription : instrumentedMethod.getParameters().asTypeList().asErasures()) { localVariable[index++] = toFrame(typeDescription); } for (TypeDescription typeDescription : typesInArray) { localVariable[index++] = toFrame(typeDescription); } index = 0; Object[] stackType = new Object[typesOnStack.size()]; for (TypeDescription typeDescription : typesOnStack) { stackType[index++] = toFrame(typeDescription); } methodVisitor.visitFrame(expandFrames ? Opcodes.F_NEW : Opcodes.F_FULL, localVariable.length, localVariable, stackType.length, stackType); currentFrameDivergence = 0; } @Override public String toString() { return "Advice.StackMapFrameHandler.Default{" + "instrumentedMethod=" + instrumentedMethod + ", requiredTypes=" + requiredTypes + ", yieldedTypes=" + yieldedTypes + ", expandFrames=" + expandFrames + ", currentFrameDivergence=" + currentFrameDivergence + '}'; } /** * A translation mode that determines how the fixed frames of the instrumented method are written. */ protected enum TranslationMode { /** * A translation mode that simply copies the original frames which are available when translating frames of the instrumented method. */ COPY { @Override protected int copy(MethodDescription.InDefinedShape instrumentedMethod, MethodDescription.InDefinedShape methodDescription, Object[] localVariable, Object[] translated) { int length = instrumentedMethod.getParameters().size() + (instrumentedMethod.isStatic() ? 0 : 1); System.arraycopy(localVariable, 0, translated, 0, length); return length; } }, /** * A translation mode for the entry advice that considers that the {@code this} reference might not be initialized for a constructor. */ ENTRY { @Override protected int copy(MethodDescription.InDefinedShape instrumentedMethod, MethodDescription.InDefinedShape methodDescription, Object[] localVariable, Object[] translated) { int index = 0; if (!instrumentedMethod.isStatic()) { translated[index++] = instrumentedMethod.isConstructor() ? Opcodes.UNINITIALIZED_THIS : toFrame(instrumentedMethod.getDeclaringType()); } for (TypeDescription typeDescription : instrumentedMethod.getParameters().asTypeList().asErasures()) { translated[index++] = toFrame(typeDescription); } return index; } }, /** * A translation mode for an exit advice where the {@code this} reference is always initialized. */ EXIT { @Override protected int copy(MethodDescription.InDefinedShape instrumentedMethod, MethodDescription.InDefinedShape methodDescription, Object[] localVariable, Object[] translated) { int index = 0; if (!instrumentedMethod.isStatic()) { translated[index++] = toFrame(instrumentedMethod.getDeclaringType()); } for (TypeDescription typeDescription : instrumentedMethod.getParameters().asTypeList().asErasures()) { translated[index++] = toFrame(typeDescription); } return index; } }; /** * Copies the fixed parameters of the instrumented method onto the operand stack. * * @param instrumentedMethod The instrumented method. * @param methodDescription The method for which a frame is created. * @param localVariable The original local variable array. * @param translated The array containing the translated frames. * @return The amount of frames added to the translated frame array. */ protected abstract int copy(MethodDescription.InDefinedShape instrumentedMethod, MethodDescription.InDefinedShape methodDescription, Object[] localVariable, Object[] translated); @Override public String toString() { return "Advice.StackMapFrameHandler.Default.TranslationMode." + name(); } } /** * A stack map frame handler for an advice method. */ protected class ForAdvice implements StackMapFrameHandler.ForAdvice { /** * The method description for which frames are translated. */ protected final MethodDescription.InDefinedShape adviceMethod; /** * A list of intermediate types to be considered as part of the instrumented method's steady signature. */ protected final TypeList requiredTypes; /** * The types that this method yields as a result. */ private final TypeList yieldedTypes; /** * The translation mode to apply for this advice method. Should be either {@link TranslationMode#ENTRY} or {@link TranslationMode#EXIT}. */ protected final TranslationMode translationMode; /** * Creates a new meta data handler for an advice method. * * @param adviceMethod The method description for which frames are translated. * @param requiredTypes A list of expected types to be considered as part of the instrumented method's steady signature. * @param yieldedTypes The types that this method yields as a result. * @param translationMode The translation mode to apply for this advice method. Should be * either {@link TranslationMode#ENTRY} or {@link TranslationMode#EXIT}. */ protected ForAdvice(MethodDescription.InDefinedShape adviceMethod, TypeList requiredTypes, TypeList yieldedTypes, TranslationMode translationMode) { this.adviceMethod = adviceMethod; this.requiredTypes = requiredTypes; this.yieldedTypes = yieldedTypes; this.translationMode = translationMode; } @Override public void translateFrame(MethodVisitor methodVisitor, int type, int localVariableLength, Object[] localVariable, int stackSize, Object[] stack) { Default.this.translateFrame(methodVisitor, translationMode, adviceMethod, requiredTypes, type, localVariableLength, localVariable, stackSize, stack); } @Override public void injectReturnFrame(MethodVisitor methodVisitor) { if (!expandFrames && currentFrameDivergence == 0) { if (yieldedTypes.isEmpty() || adviceMethod.getReturnType().represents(void.class)) { methodVisitor.visitFrame(Opcodes.F_SAME, 0, EMPTY, 0, EMPTY); } else { methodVisitor.visitFrame(Opcodes.F_SAME1, 0, EMPTY, 1, new Object[]{toFrame(adviceMethod.getReturnType().asErasure())}); } } else { injectFullFrame(methodVisitor, requiredTypes, yieldedTypes.isEmpty() || adviceMethod.getReturnType().represents(void.class) ? Collections.emptyList() : Collections.singletonList(adviceMethod.getReturnType().asErasure())); } } @Override public void injectExceptionFrame(MethodVisitor methodVisitor) { if (!expandFrames && currentFrameDivergence == 0) { methodVisitor.visitFrame(Opcodes.F_SAME1, 0, EMPTY, 1, new Object[]{Type.getInternalName(Throwable.class)}); } else { injectFullFrame(methodVisitor, requiredTypes, Collections.singletonList(TypeDescription.THROWABLE)); } } @Override public void injectCompletionFrame(MethodVisitor methodVisitor, boolean secondary) { if ((!expandFrames && currentFrameDivergence == 0 && yieldedTypes.size() < 4)) { if (secondary || yieldedTypes.isEmpty()) { methodVisitor.visitFrame(Opcodes.F_SAME, 0, EMPTY, 0, EMPTY); } else { Object[] local = new Object[yieldedTypes.size()]; int index = 0; for (TypeDescription typeDescription : yieldedTypes) { local[index++] = toFrame(typeDescription); } methodVisitor.visitFrame(Opcodes.F_APPEND, local.length, local, 0, EMPTY); } } else { injectFullFrame(methodVisitor, CompoundList.of(requiredTypes, yieldedTypes), Collections.emptyList()); } } @Override public String toString() { return "Advice.StackMapFrameHandler.Default.ForAdvice{" + "adviceMethod=" + adviceMethod + ", requiredTypes=" + requiredTypes + ", yieldedTypes=" + yieldedTypes + ", translationMode=" + translationMode + '}'; } } } } /** * A method visitor that weaves the advice methods' byte codes. */ protected abstract static class AdviceVisitor extends ExceptionTableSensitiveMethodVisitor { /** * Indicates a zero offset. */ private static final int NO_OFFSET = 0; /** * A description of the instrumented method. */ protected final MethodDescription.InDefinedShape instrumentedMethod; /** * The required padding before using local variables after the instrumented method's arguments. */ private final int padding; /** * The dispatcher to be used for method entry. */ private final Dispatcher.Bound methodEnter; /** * The dispatcher to be used for method exit. */ protected final Dispatcher.Bound methodExit; /** * A handler for computing the method size requirements. */ protected final MethodSizeHandler methodSizeHandler; /** * A handler for translating and injecting stack map frames. */ protected final StackMapFrameHandler.ForInstrumentedMethod stackMapFrameHandler; /** * Creates a new advice visitor. * * @param methodVisitor The method visitor to which all instructions are written. * @param instrumentedMethod The instrumented method. * @param methodEnter The method enter advice. * @param methodExit The method exit advice. * @param yieldedTypes The types that are expected to be added after the instrumented method returns. * @param classFileVersion The instrumented type's class file version. * @param writerFlags The ASM writer flags that were set. * @param readerFlags The ASM reader flags that were set. */ protected AdviceVisitor(MethodVisitor methodVisitor, MethodDescription.InDefinedShape instrumentedMethod, Dispatcher.Resolved.ForMethodEnter methodEnter, Dispatcher.Resolved.ForMethodExit methodExit, List yieldedTypes, ClassFileVersion classFileVersion, int writerFlags, int readerFlags) { super(Opcodes.ASM5, methodVisitor); this.instrumentedMethod = instrumentedMethod; padding = methodEnter.getEnterType().getStackSize().getSize(); List requiredTypes = methodEnter.getEnterType().represents(void.class) ? Collections.emptyList() : Collections.singletonList(methodEnter.getEnterType()); methodSizeHandler = MethodSizeHandler.Default.of(instrumentedMethod, requiredTypes, yieldedTypes, writerFlags); stackMapFrameHandler = StackMapFrameHandler.Default.of(instrumentedMethod, requiredTypes, yieldedTypes, classFileVersion, writerFlags, readerFlags); this.methodEnter = methodEnter.bind(instrumentedMethod, methodVisitor, methodSizeHandler, stackMapFrameHandler); this.methodExit = methodExit.bind(instrumentedMethod, methodVisitor, methodSizeHandler, stackMapFrameHandler); } @Override protected void onAfterExceptionTable() { methodEnter.prepare(); onUserPrepare(); methodExit.prepare(); methodEnter.apply(); onUserStart(); } /** * Invoked when the user method's exception handler (if any) is supposed to be prepared. */ protected abstract void onUserPrepare(); /** * Writes the advice for entering the instrumented method. */ protected abstract void onUserStart(); @Override protected void onVisitVarInsn(int opcode, int offset) { mv.visitVarInsn(opcode, offset < instrumentedMethod.getStackSize() ? offset : padding + offset); } @Override protected void onVisitIincInsn(int offset, int increment) { mv.visitIincInsn(offset < instrumentedMethod.getStackSize() ? offset : padding + offset, increment); } /** * Access the first variable after the instrumented variables and return type are stored. * * @param opcode The opcode for accessing the variable. */ protected void variable(int opcode) { variable(opcode, NO_OFFSET); } /** * Access the first variable after the instrumented variables and return type are stored. * * @param opcode The opcode for accessing the variable. * @param offset The additional offset of the variable. */ protected void variable(int opcode, int offset) { mv.visitVarInsn(opcode, instrumentedMethod.getStackSize() + padding + offset); } @Override public void visitFrame(int frameType, int localVariableLength, Object[] localVariable, int stackSize, Object[] stack) { stackMapFrameHandler.translateFrame(mv, frameType, localVariableLength, localVariable, stackSize, stack); } @Override public void visitMaxs(int stackSize, int localVariableLength) { onUserEnd(); mv.visitMaxs(methodSizeHandler.compoundStackSize(stackSize), methodSizeHandler.compoundLocalVariableLength(localVariableLength)); } /** * Writes the advice for completing the instrumented method. */ protected abstract void onUserEnd(); /** * An advice visitor that does not apply exit advice. */ protected static class WithoutExitAdvice extends AdviceVisitor { /** * Creates an advice visitor that does not apply exit advice. * * @param methodVisitor The method visitor for the instrumented method. * @param instrumentedMethod A description of the instrumented method. * @param methodEnter The dispatcher to be used for method entry. * @param classFileVersion The instrumented type's class file version. * @param writerFlags The ASM writer flags that were set. * @param readerFlags The ASM reader flags that were set. */ protected WithoutExitAdvice(MethodVisitor methodVisitor, MethodDescription.InDefinedShape instrumentedMethod, Dispatcher.Resolved.ForMethodEnter methodEnter, ClassFileVersion classFileVersion, int writerFlags, int readerFlags) { super(methodVisitor, instrumentedMethod, methodEnter, Dispatcher.Inactive.INSTANCE, Collections.emptyList(), classFileVersion, writerFlags, readerFlags); } @Override protected void onUserPrepare() { /* do nothing */ } @Override protected void onUserStart() { /* do nothing */ } @Override protected void onUserEnd() { /* do nothing */ } @Override public String toString() { return "Advice.AdviceVisitor.WithoutExitAdvice{" + ", instrumentedMethod=" + instrumentedMethod + "}"; } } /** * An advice visitor that applies exit advice. */ protected abstract static class WithExitAdvice extends AdviceVisitor { /** * Indicates the handler for the value returned by the advice method. */ protected final Label returnHandler; /** * Creates an advice visitor that applies exit advice. * * @param methodVisitor The method visitor for the instrumented method. * @param instrumentedMethod A description of the instrumented method. * @param methodEnter The dispatcher to be used for method entry. * @param methodExit The dispatcher to be used for method exit. * @param yieldedTypes The types that are expected to be added after the instrumented method returns. * @param classFileVersion The instrumented type's class file version. * @param writerFlags The ASM writer flags that were set. * @param readerFlags The ASM reader flags that were set. */ protected WithExitAdvice(MethodVisitor methodVisitor, MethodDescription.InDefinedShape instrumentedMethod, Dispatcher.Resolved.ForMethodEnter methodEnter, Dispatcher.Resolved.ForMethodExit methodExit, List yieldedTypes, ClassFileVersion classFileVersion, int writerFlags, int readerFlags) { super(methodVisitor, instrumentedMethod, methodEnter, methodExit, yieldedTypes, classFileVersion, writerFlags, readerFlags); returnHandler = new Label(); } @Override protected void onVisitInsn(int opcode) { switch (opcode) { case Opcodes.RETURN: case Opcodes.IRETURN: case Opcodes.FRETURN: case Opcodes.DRETURN: case Opcodes.LRETURN: case Opcodes.ARETURN: mv.visitJumpInsn(Opcodes.GOTO, returnHandler); break; default: mv.visitInsn(opcode); } } @Override protected void onUserEnd() { mv.visitLabel(returnHandler); stackMapFrameHandler.injectReturnFrame(mv); Type returnType = Type.getType(instrumentedMethod.getReturnType().asErasure().getDescriptor()); if (!returnType.equals(Type.VOID_TYPE)) { variable(returnType.getOpcode(Opcodes.ISTORE)); } onUserReturn(); methodExit.apply(); onExitAdviceReturn(); if (returnType.equals(Type.VOID_TYPE)) { mv.visitInsn(Opcodes.RETURN); } else { variable(returnType.getOpcode(Opcodes.ILOAD)); mv.visitInsn(returnType.getOpcode(Opcodes.IRETURN)); } } /** * Invoked after the user method has returned. */ protected abstract void onUserReturn(); /** * Invoked after the exit advice method has returned. */ protected abstract void onExitAdviceReturn(); /** * An advice visitor that does not capture exceptions. */ protected static class WithoutExceptionHandling extends WithExitAdvice { /** * Creates a new advice visitor that does not capture exceptions. * * @param methodVisitor The method visitor for the instrumented method. * @param instrumentedMethod A description of the instrumented method. * @param methodEnter The dispatcher to be used for method entry. * @param methodExit The dispatcher to be used for method exit. * @param classFileVersion The instrumented type's class file version. * @param writerFlags The ASM writer flags that were set. * @param readerFlags The ASM reader flags that were set. */ protected WithoutExceptionHandling(MethodVisitor methodVisitor, MethodDescription.InDefinedShape instrumentedMethod, Dispatcher.Resolved.ForMethodEnter methodEnter, Dispatcher.Resolved.ForMethodExit methodExit, ClassFileVersion classFileVersion, int writerFlags, int readerFlags) { super(methodVisitor, instrumentedMethod, methodEnter, methodExit, instrumentedMethod.getReturnType().represents(void.class) ? Collections.emptyList() : Collections.singletonList(instrumentedMethod.getReturnType().asErasure()), classFileVersion, writerFlags, readerFlags); } @Override protected void onUserPrepare() { /* empty */ } @Override protected void onUserStart() { /* empty */ } @Override protected void onUserReturn() { if (!instrumentedMethod.getReturnType().represents(void.class)) { stackMapFrameHandler.injectCompletionFrame(mv, false); } } @Override protected void onExitAdviceReturn() { /* empty */ } @Override public String toString() { return "Advice.AdviceVisitor.WithExitAdvice.WithoutExceptionHandling{" + "instrumentedMethod=" + instrumentedMethod + "}"; } } /** * An advice visitor that captures exceptions by weaving try-catch blocks around user code. */ protected static class WithExceptionHandling extends WithExitAdvice { /** * The type of the handled throwable type for which this advice is invoked. */ private final TypeDescription triggeringThrowable; /** * Indicates the start of the user method. */ private final Label userStart; /** * Indicates the exception handler. */ private final Label exceptionHandler; /** * Creates a new advice visitor that captures exception by weaving try-catch blocks around user code. * * @param methodVisitor The method visitor for the instrumented method. * @param instrumentedMethod A description of the instrumented method. * @param methodEnter The dispatcher to be used for method entry. * @param methodExit The dispatcher to be used for method exit. * @param classFileVersion The instrumented type's class file version. * @param writerFlags The ASM writer flags that were set. * @param readerFlags The ASM reader flags that were set. * @param triggeringThrowable The type of the handled throwable type for which this advice is invoked. */ protected WithExceptionHandling(MethodVisitor methodVisitor, MethodDescription.InDefinedShape instrumentedMethod, Dispatcher.Resolved.ForMethodEnter methodEnter, Dispatcher.Resolved.ForMethodExit methodExit, ClassFileVersion classFileVersion, int writerFlags, int readerFlags, TypeDescription triggeringThrowable) { super(methodVisitor, instrumentedMethod, methodEnter, methodExit, instrumentedMethod.getReturnType().represents(void.class) ? Collections.singletonList(TypeDescription.THROWABLE) : Arrays.asList(instrumentedMethod.getReturnType().asErasure(), TypeDescription.THROWABLE), classFileVersion, writerFlags, readerFlags); this.triggeringThrowable = triggeringThrowable; userStart = new Label(); exceptionHandler = new Label(); } @Override protected void onUserPrepare() { mv.visitTryCatchBlock(userStart, returnHandler, exceptionHandler, triggeringThrowable.getInternalName()); } @Override protected void onUserStart() { mv.visitLabel(userStart); } @Override protected void onUserReturn() { mv.visitInsn(Opcodes.ACONST_NULL); variable(Opcodes.ASTORE, instrumentedMethod.getReturnType().getStackSize().getSize()); Label endOfHandler = new Label(); mv.visitJumpInsn(Opcodes.GOTO, endOfHandler); mv.visitLabel(exceptionHandler); stackMapFrameHandler.injectExceptionFrame(mv); variable(Opcodes.ASTORE, instrumentedMethod.getReturnType().getStackSize().getSize()); storeDefaultReturn(); mv.visitLabel(endOfHandler); stackMapFrameHandler.injectCompletionFrame(mv, false); } @Override protected void onExitAdviceReturn() { variable(Opcodes.ALOAD, instrumentedMethod.getReturnType().getStackSize().getSize()); Label endOfHandler = new Label(); mv.visitJumpInsn(Opcodes.IFNULL, endOfHandler); variable(Opcodes.ALOAD, instrumentedMethod.getReturnType().getStackSize().getSize()); mv.visitInsn(Opcodes.ATHROW); mv.visitLabel(endOfHandler); stackMapFrameHandler.injectCompletionFrame(mv, true); } /** * Stores a default return value in the designated slot of the local variable array. */ private void storeDefaultReturn() { if (instrumentedMethod.getReturnType().represents(boolean.class) || instrumentedMethod.getReturnType().represents(byte.class) || instrumentedMethod.getReturnType().represents(short.class) || instrumentedMethod.getReturnType().represents(char.class) || instrumentedMethod.getReturnType().represents(int.class)) { mv.visitInsn(Opcodes.ICONST_0); variable(Opcodes.ISTORE); } else if (instrumentedMethod.getReturnType().represents(long.class)) { mv.visitInsn(Opcodes.LCONST_0); variable(Opcodes.LSTORE); } else if (instrumentedMethod.getReturnType().represents(float.class)) { mv.visitInsn(Opcodes.FCONST_0); variable(Opcodes.FSTORE); } else if (instrumentedMethod.getReturnType().represents(double.class)) { mv.visitInsn(Opcodes.DCONST_0); variable(Opcodes.DSTORE); } else if (!instrumentedMethod.getReturnType().represents(void.class)) { mv.visitInsn(Opcodes.ACONST_NULL); variable(Opcodes.ASTORE); } } @Override public String toString() { return "Advice.AdviceVisitor.WithExitAdvice.WithExceptionHandling{" + "instrumentedMethod=" + instrumentedMethod + ", triggeringThrowable=" + triggeringThrowable + "}"; } } } } /** * A dispatcher for implementing advice. */ protected interface Dispatcher { /** * Indicates that a method does not represent advice and does not need to be visited. */ MethodVisitor IGNORE_METHOD = null; /** * Expresses that an annotation should not be visited. */ AnnotationVisitor IGNORE_ANNOTATION = null; /** * Returns {@code true} if this dispatcher is alive. * * @return {@code true} if this dispatcher is alive. */ boolean isAlive(); /** * A dispatcher that is not yet resolved. */ interface Unresolved extends Dispatcher { /** * Indicates that this dispatcher requires access to the class file declaring the advice method. * * @return {@code true} if this dispatcher requires access to the advice method's class file. */ boolean isBinary(); /** * Resolves this dispatcher as a dispatcher for entering a method. * * @param userFactories A list of custom factories for binding parameters of an advice method. * @param binaryRepresentation An unresolved binary representation of the type containing the advice method. * @return This dispatcher as a dispatcher for entering a method. */ Resolved.ForMethodEnter asMethodEnter(List userFactories, ClassFileLocator.Resolution binaryRepresentation); /** * Resolves this dispatcher as a dispatcher for exiting a method. * * @param userFactories A list of custom factories for binding parameters of an advice method. * @param binaryRepresentation An unresolved binary representation of the type containing the advice method. * @param dispatcher The dispatcher for entering a method. * @return This dispatcher as a dispatcher for exiting a method. */ Resolved.ForMethodExit asMethodExitTo(List userFactories, ClassFileLocator.Resolution binaryRepresentation, Resolved.ForMethodEnter dispatcher); } /** * Represents an offset mapping for an advice method to an alternative offset. */ interface OffsetMapping { /** * Resolves an offset mapping to a given target offset. * * @param instrumentedMethod The instrumented method for which the mapping is to be resolved. * @param context The context in which the offset mapping is applied. * @return A suitable target mapping. */ Target resolve(MethodDescription.InDefinedShape instrumentedMethod, Context context); /** * A context for applying an {@link OffsetMapping}. */ interface Context { /** * Returns {@code true} if the advice is applied on a fully initialized instance, i.e. describes if the {@code this} * instance is available or still uninitialized during calling the advice. * * @return {@code true} if the advice is applied onto a fully initialized method. */ boolean isInitialized(); /** * Returns the padding before writing additional values that this context applies. * * @return The required padding for this context. */ int getPadding(); /** * A context for an offset mapping describing a method entry. */ enum ForMethodEntry implements Context { /** * Describes a context for a method entry that is not a constructor. */ INITIALIZED(true), /** * Describes a context for a method entry that is a constructor. */ NON_INITIALIZED(false); /** * Resolves an appropriate method entry context for the supplied instrumented method. * * @param instrumentedMethod The instrumented method. * @return An appropriate context. */ protected static Context of(MethodDescription.InDefinedShape instrumentedMethod) { return instrumentedMethod.isConstructor() ? NON_INITIALIZED : INITIALIZED; } /** * {@code true} if the method is no constructor, i.e. is invoked for an initialized instance upon entry. */ private final boolean initialized; /** * Creates a new context for a method entry. * * @param initialized {@code true} if the method is no constructor, i.e. is invoked for an initialized instance upon entry. */ ForMethodEntry(boolean initialized) { this.initialized = initialized; } @Override public boolean isInitialized() { return initialized; } @Override public int getPadding() { return StackSize.ZERO.getSize(); } @Override public String toString() { return "Advice.Dispatcher.OffsetMapping.Context.ForMethodEntry." + name(); } } /** * A context for an offset mapping describing a method exit. */ enum ForMethodExit implements Context { /** * A method exit with a zero sized padding. */ ZERO(StackSize.ZERO), /** * A method exit with a single slot padding. */ SINGLE(StackSize.SINGLE), /** * A method exit with a double slot padding. */ DOUBLE(StackSize.DOUBLE); /** * The padding implied by this method exit. */ private final StackSize stackSize; /** * Creates a new context for a method exit. * * @param stackSize The padding implied by this method exit. */ ForMethodExit(StackSize stackSize) { this.stackSize = stackSize; } /** * Resolves an appropriate method exit context for the supplied entry method type. * * @param typeDescription The type that is returned by the enter method. * @return An appropriate context for the supplied entry method type. */ protected static Context of(TypeDescription typeDescription) { switch (typeDescription.getStackSize()) { case ZERO: return ZERO; case SINGLE: return SINGLE; case DOUBLE: return DOUBLE; default: throw new IllegalStateException("Unknown stack size: " + typeDescription); } } @Override public boolean isInitialized() { return true; } @Override public int getPadding() { return stackSize.getSize(); } @Override public String toString() { return "Advice.Dispatcher.OffsetMapping.Context.ForMethodExit." + name(); } } } /** * A target offset of an offset mapping. */ interface Target { /** * Indicates that applying this target does not require any additional padding. */ int NO_PADDING = 0; /** * Applies this offset mapping for a {@link MethodVisitor#visitVarInsn(int, int)} instruction. * * @param methodVisitor The method visitor onto which this offset mapping is to be applied. * @param opcode The opcode of the original instruction. * @return The required padding to the advice's total stack size. */ int resolveAccess(MethodVisitor methodVisitor, int opcode); /** * Applies this offset mapping for a {@link MethodVisitor#visitIincInsn(int, int)} instruction. * * @param methodVisitor The method visitor onto which this offset mapping is to be applied. * @param increment The value with which to increment the targeted value. * @return The required padding to the advice's total stack size. */ int resolveIncrement(MethodVisitor methodVisitor, int increment); /** * Loads a default value onto the stack or pops the accessed value off it. */ enum ForDefaultValue implements Target { /** * The singleton instance. */ INSTANCE; @Override public int resolveAccess(MethodVisitor methodVisitor, int opcode) { switch (opcode) { case Opcodes.ALOAD: methodVisitor.visitInsn(Opcodes.ACONST_NULL); break; case Opcodes.ILOAD: methodVisitor.visitInsn(Opcodes.ICONST_0); break; case Opcodes.LLOAD: methodVisitor.visitInsn(Opcodes.LCONST_0); break; case Opcodes.FLOAD: methodVisitor.visitInsn(Opcodes.FCONST_0); break; case Opcodes.DLOAD: methodVisitor.visitInsn(Opcodes.DCONST_0); break; case Opcodes.ISTORE: case Opcodes.FSTORE: case Opcodes.ASTORE: methodVisitor.visitInsn(Opcodes.POP); break; case Opcodes.LSTORE: case Opcodes.DSTORE: methodVisitor.visitInsn(Opcodes.POP2); break; default: throw new IllegalStateException("Unexpected opcode: " + opcode); } return NO_PADDING; } @Override public int resolveIncrement(MethodVisitor methodVisitor, int increment) { return NO_PADDING; } @Override public String toString() { return "Advice.Dispatcher.OffsetMapping.Target.ForDefaultValue." + name(); } } /** * A read-only target mapping. */ abstract class ForParameter implements Target { /** * The mapped offset. */ protected final int offset; /** * Creates a new read-only target mapping. * * @param offset The mapped offset. */ protected ForParameter(int offset) { this.offset = offset; } @Override public int resolveAccess(MethodVisitor methodVisitor, int opcode) { switch (opcode) { case Opcodes.ISTORE: case Opcodes.LSTORE: case Opcodes.FSTORE: case Opcodes.DSTORE: case Opcodes.ASTORE: onWrite(methodVisitor, opcode); break; case Opcodes.ILOAD: case Opcodes.LLOAD: case Opcodes.FLOAD: case Opcodes.DLOAD: case Opcodes.ALOAD: methodVisitor.visitVarInsn(opcode, offset); break; default: throw new IllegalArgumentException("Did not expect opcode: " + opcode); } return NO_PADDING; } /** * Invoked upon attempting to write to a parameter. * * @param methodVisitor The method visitor onto which this offset mapping is to be applied. * @param opcode The applied opcode. */ protected abstract void onWrite(MethodVisitor methodVisitor, int opcode); @Override public boolean equals(Object object) { if (this == object) return true; if (object == null || getClass() != object.getClass()) return false; ForParameter forParameter = (ForParameter) object; return offset == forParameter.offset; } @Override public int hashCode() { return offset; } /** * A read-only parameter mapping. */ protected static class ReadOnly extends ForParameter { /** * Creates a new parameter mapping that is only readable. * * @param offset The mapped offset. */ protected ReadOnly(int offset) { super(offset); } @Override protected void onWrite(MethodVisitor methodVisitor, int opcode) { throw new IllegalStateException("Cannot write to read-only value"); } @Override public int resolveIncrement(MethodVisitor methodVisitor, int increment) { throw new IllegalStateException("Cannot write to read-only parameter at offset " + offset); } @Override public String toString() { return "Advice.Dispatcher.OffsetMapping.Target.ForParameter.ReadOnly{" + "offset=" + offset + "}"; } } /** * A parameter mapping that is both readable and writable. */ protected static class ReadWrite extends ForParameter { /** * Creates a new parameter mapping that is both readable and writable. * * @param offset The mapped offset. */ protected ReadWrite(int offset) { super(offset); } @Override protected void onWrite(MethodVisitor methodVisitor, int opcode) { methodVisitor.visitVarInsn(opcode, offset); } /** * Resolves a parameter mapping where the value is casted to the given type prior to assignment. * * @param targetType The type to which the target value is cased. * @return An appropriate target mapping. */ protected Target casted(TypeDescription targetType) { return targetType.represents(Object.class) ? this : new WithCasting(offset, targetType); } @Override public int resolveIncrement(MethodVisitor methodVisitor, int increment) { methodVisitor.visitIincInsn(offset, increment); return NO_PADDING; } @Override public String toString() { return "Advice.Dispatcher.OffsetMapping.Target.ForParameter.ReadWrite{" + "offset=" + offset + "}"; } /** * A readable and writable parameter mapping where the assigned value is casted to another type prior to assignment. */ protected static class WithCasting extends ReadWrite { /** * The type to which the written value is casted prior to assignment. */ private final TypeDescription targetType; /** * Creates a new parameter mapping with casting prior to assignment. * * @param offset The mapped offset. * @param targetType The type to which the written value is casted prior to assignment. */ protected WithCasting(int offset, TypeDescription targetType) { super(offset); this.targetType = targetType; } @Override protected void onWrite(MethodVisitor methodVisitor, int opcode) { methodVisitor.visitTypeInsn(Opcodes.CHECKCAST, targetType.getInternalName()); super.onWrite(methodVisitor, opcode); } @Override public boolean equals(Object object) { if (this == object) return true; if (object == null || getClass() != object.getClass()) return false; if (!super.equals(object)) return false; WithCasting that = (WithCasting) object; return targetType.equals(that.targetType); } @Override public int hashCode() { int result = super.hashCode(); result = 31 * result + targetType.hashCode(); return result; } @Override public String toString() { return "Advice.Dispatcher.OffsetMapping.Target.ForParameter.ReadWrite.WithCasting{" + "offset=" + offset + ", targetType=" + targetType + "}"; } } } } /** * An offset mapping for a field. */ abstract class ForField implements Target { /** * The field being read. */ protected final FieldDescription.InDefinedShape fieldDescription; /** * Creates a new offset mapping for a field. * * @param fieldDescription The field being read. */ protected ForField(FieldDescription.InDefinedShape fieldDescription) { this.fieldDescription = fieldDescription; } @Override public int resolveAccess(MethodVisitor methodVisitor, int opcode) { switch (opcode) { case Opcodes.ISTORE: case Opcodes.ASTORE: case Opcodes.FSTORE: return onWriteSingle(methodVisitor); case Opcodes.LSTORE: case Opcodes.DSTORE: return onWriteDouble(methodVisitor); case Opcodes.ILOAD: case Opcodes.FLOAD: case Opcodes.ALOAD: case Opcodes.LLOAD: case Opcodes.DLOAD: if (fieldDescription.isStatic()) { accessField(methodVisitor, Opcodes.GETSTATIC); } else { methodVisitor.visitVarInsn(Opcodes.ALOAD, 0); accessField(methodVisitor, Opcodes.GETFIELD); } return NO_PADDING; default: throw new IllegalArgumentException("Did not expect opcode: " + opcode); } } /** * Writes a value to a field which type occupies a single slot on the operand stack. * * @param methodVisitor The method visitor onto which this offset mapping is to be applied. * @return The required padding to the advice's total stack size. */ protected abstract int onWriteSingle(MethodVisitor methodVisitor); /** * Writes a value to a field which type occupies two slots on the operand stack. * * @param methodVisitor The method visitor onto which this offset mapping is to be applied. * @return The required padding to the advice's total stack size. */ protected abstract int onWriteDouble(MethodVisitor methodVisitor); /** * Accesses a field. * * @param methodVisitor The method visitor for which to access the field. * @param opcode The opcode for accessing the field. */ protected void accessField(MethodVisitor methodVisitor, int opcode) { methodVisitor.visitFieldInsn(opcode, fieldDescription.getDeclaringType().asErasure().getInternalName(), fieldDescription.getInternalName(), fieldDescription.getDescriptor()); } @Override public boolean equals(Object object) { if (this == object) return true; if (object == null || getClass() != object.getClass()) return false; ForField forField = (ForField) object; return fieldDescription.equals(forField.fieldDescription); } @Override public int hashCode() { return fieldDescription.hashCode(); } /** * A target mapping for a field that is only readable. */ protected static class ReadOnly extends ForField { /** * Creates a new field mapping for a field that is only readable. * * @param fieldDescription The field which is mapped by this target mapping. */ protected ReadOnly(FieldDescription.InDefinedShape fieldDescription) { super(fieldDescription); } @Override protected int onWriteSingle(MethodVisitor methodVisitor) { throw new IllegalStateException("Cannot write to read-only field " + fieldDescription); } @Override protected int onWriteDouble(MethodVisitor methodVisitor) { throw new IllegalStateException("Cannot write to read-only field " + fieldDescription); } @Override public int resolveIncrement(MethodVisitor methodVisitor, int increment) { throw new IllegalStateException("Cannot write to read-only field " + fieldDescription); } @Override public String toString() { return "Advice.Dispatcher.OffsetMapping.Target.ForField.ReadOnly{" + "fieldDescription=" + fieldDescription + "}"; } } /** * A target mapping for a field that is both readable and writable. */ protected static class ReadWrite extends ForField { /** * Creates a new field mapping for a field that is readable and writable. * * @param fieldDescription The field which is mapped by this target mapping. */ protected ReadWrite(FieldDescription.InDefinedShape fieldDescription) { super(fieldDescription); } @Override protected int onWriteSingle(MethodVisitor methodVisitor) { if (!fieldDescription.isStatic()) { methodVisitor.visitVarInsn(Opcodes.ALOAD, 0); methodVisitor.visitInsn(Opcodes.DUP_X1); methodVisitor.visitInsn(Opcodes.POP); accessField(methodVisitor, Opcodes.PUTFIELD); return 2; } else { accessField(methodVisitor, Opcodes.PUTSTATIC); return NO_PADDING; } } @Override protected int onWriteDouble(MethodVisitor methodVisitor) { if (!fieldDescription.isStatic()) { methodVisitor.visitVarInsn(Opcodes.ALOAD, 0); methodVisitor.visitInsn(Opcodes.DUP_X2); methodVisitor.visitInsn(Opcodes.POP); accessField(methodVisitor, Opcodes.PUTFIELD); return 2; } else { accessField(methodVisitor, Opcodes.PUTSTATIC); return NO_PADDING; } } @Override public int resolveIncrement(MethodVisitor methodVisitor, int increment) { if (fieldDescription.isStatic()) { accessField(methodVisitor, Opcodes.GETSTATIC); methodVisitor.visitInsn(Opcodes.ICONST_1); methodVisitor.visitInsn(Opcodes.IADD); accessField(methodVisitor, Opcodes.PUTSTATIC); return NO_PADDING; } else { methodVisitor.visitVarInsn(Opcodes.ALOAD, 0); methodVisitor.visitInsn(Opcodes.DUP); accessField(methodVisitor, Opcodes.GETFIELD); methodVisitor.visitInsn(Opcodes.ICONST_1); methodVisitor.visitInsn(Opcodes.IADD); accessField(methodVisitor, Opcodes.PUTFIELD); return 2; } } @Override public String toString() { return "Advice.Dispatcher.OffsetMapping.Target.ForField.ReadWrite{" + "fieldDescription=" + fieldDescription + "}"; } } } /** * An offset mapping for a constant pool value. */ class ForConstantPoolValue implements Target { /** * The constant pool value. */ private final Object value; /** * Creates a mapping for a constant pool value. * * @param value The constant pool value. */ protected ForConstantPoolValue(Object value) { this.value = value; } @Override public int resolveAccess(MethodVisitor methodVisitor, int opcode) { switch (opcode) { case Opcodes.ISTORE: case Opcodes.ASTORE: case Opcodes.FSTORE: case Opcodes.LSTORE: case Opcodes.DSTORE: throw new IllegalStateException("Cannot write to fixed value: " + value); case Opcodes.ILOAD: case Opcodes.FLOAD: case Opcodes.ALOAD: case Opcodes.LLOAD: case Opcodes.DLOAD: methodVisitor.visitLdcInsn(value); return NO_PADDING; default: throw new IllegalArgumentException("Did not expect opcode: " + opcode); } } @Override public int resolveIncrement(MethodVisitor methodVisitor, int increment) { throw new IllegalStateException("Cannot write to fixed value: " + value); } @Override public boolean equals(Object object) { if (this == object) return true; if (object == null || getClass() != object.getClass()) return false; ForConstantPoolValue that = (ForConstantPoolValue) object; return value.equals(that.value); } @Override public int hashCode() { return value.hashCode(); } @Override public String toString() { return "Advice.Dispatcher.OffsetMapping.Target.ForConstantPoolValue{" + "value=" + value + '}'; } } /** * A target for an offset mapping that boxes a primitive parameter value. */ abstract class ForBoxedParameter implements Target { /** * The parameters offset. */ protected final int offset; /** * A dispatcher for boxing the primitive value. */ protected final BoxingDispatcher boxingDispatcher; /** * Creates a new offset mapping for boxing a primitive parameter value. * * @param offset The parameters offset. * @param boxingDispatcher A dispatcher for boxing the primitive value. */ protected ForBoxedParameter(int offset, BoxingDispatcher boxingDispatcher) { this.offset = offset; this.boxingDispatcher = boxingDispatcher; } @Override public int resolveAccess(MethodVisitor methodVisitor, int opcode) { switch (opcode) { case Opcodes.ALOAD: boxingDispatcher.loadBoxed(methodVisitor, offset); return boxingDispatcher.getStackSize().getSize() - 1; case Opcodes.ASTORE: onStore(methodVisitor); return NO_PADDING; default: throw new IllegalStateException("Unexpected opcode: " + opcode); } } /** * Handles writing the boxed value if applicable. * * @param methodVisitor The method visitor for which to apply the writing. */ protected abstract void onStore(MethodVisitor methodVisitor); @Override public int resolveIncrement(MethodVisitor methodVisitor, int increment) { throw new IllegalStateException("Cannot increment a boxed parameter"); } @Override public boolean equals(Object object) { if (this == object) return true; if (object == null || getClass() != object.getClass()) return false; ForBoxedParameter that = (ForBoxedParameter) object; return offset == that.offset && boxingDispatcher == that.boxingDispatcher; } @Override public int hashCode() { int result = offset; result = 31 * result + boxingDispatcher.hashCode(); return result; } /** * A target mapping for a boxed parameter that only allows reading the boxed value. */ protected static class ReadOnly extends ForBoxedParameter { /** * Creates a new read-only target offset mapping for a boxed parameter. * * @param offset The parameters offset. * @param boxingDispatcher A dispatcher for boxing the primitive value. */ protected ReadOnly(int offset, BoxingDispatcher boxingDispatcher) { super(offset, boxingDispatcher); } /** * Creates an appropriate target mapping. * * @param offset The parameters offset. * @param type The primitive type that is boxed or unboxed. * @return An appropriate target mapping. */ protected static Target of(int offset, TypeDefinition type) { return new ReadOnly(offset, BoxingDispatcher.of(type)); } @Override protected void onStore(MethodVisitor methodVisitor) { throw new IllegalStateException("Cannot write to read-only boxed parameter"); } @Override public String toString() { return "Advice.Dispatcher.OffsetMapping.Target.ForBoxedParameter.ReadOnly{" + "offset=" + offset + ", boxingDispatcher=" + boxingDispatcher + '}'; } } /** * A target mapping for a boxed parameter that allows reading and writing the boxed value. */ protected static class ReadWrite extends ForBoxedParameter { /** * Creates a new read-write target offset mapping for a boxed parameter. * * @param offset The parameters offset. * @param boxingDispatcher A dispatcher for boxing the primitive value. */ protected ReadWrite(int offset, BoxingDispatcher boxingDispatcher) { super(offset, boxingDispatcher); } /** * Creates an appropriate target mapping. * * @param offset The parameters offset. * @param type The primitive type that is boxed or unboxed. * @return An appropriate target mapping. */ protected static Target of(int offset, TypeDefinition type) { return new ReadWrite(offset, BoxingDispatcher.of(type)); } @Override protected void onStore(MethodVisitor methodVisitor) { boxingDispatcher.storeUnboxed(methodVisitor, offset); } @Override public String toString() { return "Advice.Dispatcher.OffsetMapping.Target.ForBoxedParameter.ReadWrite{" + "offset=" + offset + ", boxingDispatcher=" + boxingDispatcher + '}'; } } /** * A dispatcher for boxing a primitive value. */ protected enum BoxingDispatcher { /** * A boxing dispatcher for the {@code boolean} type. */ BOOLEAN(Opcodes.ILOAD, Opcodes.ISTORE, Boolean.class, boolean.class, "booleanValue"), /** * A boxing dispatcher for the {@code byte} type. */ BYTE(Opcodes.ILOAD, Opcodes.ISTORE, Byte.class, byte.class, "byteValue"), /** * A boxing dispatcher for the {@code short} type. */ SHORT(Opcodes.ILOAD, Opcodes.ISTORE, Short.class, short.class, "shortValue"), /** * A boxing dispatcher for the {@code char} type. */ CHARACTER(Opcodes.ILOAD, Opcodes.ISTORE, Character.class, char.class, "charValue"), /** * A boxing dispatcher for the {@code int} type. */ INTEGER(Opcodes.ILOAD, Opcodes.ISTORE, Integer.class, int.class, "intValue"), /** * A boxing dispatcher for the {@code long} type. */ LONG(Opcodes.LLOAD, Opcodes.LSTORE, Long.class, long.class, "longValue"), /** * A boxing dispatcher for the {@code float} type. */ FLOAT(Opcodes.FLOAD, Opcodes.FSTORE, Float.class, float.class, "floatValue"), /** * A boxing dispatcher for the {@code double} type. */ DOUBLE(Opcodes.DLOAD, Opcodes.DSTORE, Double.class, double.class, "doubleValue"); /** * The name of the boxing method of a wrapper type. */ private static final String VALUE_OF = "valueOf"; /** * The opcode to use for loading a value of this type. */ private final int load; /** * The opcode to use for loading a value of this type. */ private final int store; /** * The name of the method used for unboxing a primitive type. */ private final String unboxingMethod; /** * The name of the wrapper type. */ private final String owner; /** * The descriptor of the boxing method. */ private final String boxingDescriptor; /** * The descriptor of the unboxing method. */ private final String unboxingDescriptor; /** * The required stack size of the unboxed value. */ private final StackSize stackSize; /** * Creates a new boxing dispatcher. * * @param load The opcode to use for loading a value of this type. * @param store The opcode to use for storing a value of this type. * @param wrapperType The represented wrapper type. * @param primitiveType The represented primitive type. * @param unboxingMethod The name of the method used for unboxing a primitive type. */ BoxingDispatcher(int load, int store, Class wrapperType, Class primitiveType, String unboxingMethod) { this.load = load; this.store = store; this.unboxingMethod = unboxingMethod; owner = Type.getInternalName(wrapperType); boxingDescriptor = Type.getMethodDescriptor(Type.getType(wrapperType), Type.getType(primitiveType)); unboxingDescriptor = Type.getMethodDescriptor(Type.getType(primitiveType)); stackSize = StackSize.of(primitiveType); } /** * Resolves a boxing dispatcher for the supplied primitive type. * * @param typeDefinition A description of a primitive type. * @return An appropriate boxing dispatcher. */ protected static BoxingDispatcher of(TypeDefinition typeDefinition) { if (typeDefinition.represents(boolean.class)) { return BOOLEAN; } else if (typeDefinition.represents(byte.class)) { return BYTE; } else if (typeDefinition.represents(short.class)) { return SHORT; } else if (typeDefinition.represents(char.class)) { return CHARACTER; } else if (typeDefinition.represents(int.class)) { return INTEGER; } else if (typeDefinition.represents(long.class)) { return LONG; } else if (typeDefinition.represents(float.class)) { return FLOAT; } else if (typeDefinition.represents(double.class)) { return DOUBLE; } else { throw new IllegalArgumentException("Cannot box: " + typeDefinition); } } /** * Loads the value as a boxed version onto the stack. * * @param methodVisitor the method visitor for which to load the value. * @param offset The offset of the primitive value. */ protected void loadBoxed(MethodVisitor methodVisitor, int offset) { methodVisitor.visitVarInsn(load, offset); methodVisitor.visitMethodInsn(Opcodes.INVOKESTATIC, owner, VALUE_OF, boxingDescriptor, false); } /** * Stores the value as a unboxed version onto the stack. * * @param methodVisitor the method visitor for which to store the value. * @param offset The offset of the primitive value. */ protected void storeUnboxed(MethodVisitor methodVisitor, int offset) { methodVisitor.visitTypeInsn(Opcodes.CHECKCAST, owner); methodVisitor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, owner, unboxingMethod, unboxingDescriptor, false); methodVisitor.visitVarInsn(store, offset); } /** * Returns the stack size of the primitive value. * * @return The stack size of the primitive value. */ protected StackSize getStackSize() { return stackSize; } @Override public String toString() { return "Advice.Dispatcher.OffsetMapping.Target.ForBoxedParameter.BoxingDispatcher." + name(); } } } /** * A target for an offset mapping of an array containing all (boxed) arguments of the instrumented method. */ class ForBoxedArguments implements Target { /** * The parameters of the instrumented method. */ private final List parameters; /** * Creates a mapping for a boxed array containing all arguments of the instrumented method. * * @param parameters The parameters of the instrumented method. */ protected ForBoxedArguments(List parameters) { this.parameters = parameters; } @Override public int resolveAccess(MethodVisitor methodVisitor, int opcode) { switch (opcode) { case Opcodes.ALOAD: loadInteger(methodVisitor, parameters.size()); methodVisitor.visitTypeInsn(Opcodes.ANEWARRAY, TypeDescription.OBJECT.getInternalName()); StackSize stackSize = StackSize.ZERO; for (ParameterDescription parameter : parameters) { methodVisitor.visitInsn(Opcodes.DUP); loadInteger(methodVisitor, parameter.getIndex()); if (parameter.getType().isPrimitive()) { ForBoxedParameter.BoxingDispatcher.of(parameter.getType()).loadBoxed(methodVisitor, parameter.getOffset()); } else { methodVisitor.visitVarInsn(Opcodes.ALOAD, parameter.getOffset()); } methodVisitor.visitInsn(Opcodes.AASTORE); stackSize = stackSize.maximum(parameter.getType().getStackSize()); } return stackSize.getSize() + 2; default: throw new IllegalStateException("Unexpected opcode: " + opcode); } } /** * Loads an integer onto the operand stack. * * @param methodVisitor The method visitor for which the integer is loaded. * @param value The integer value to load onto the stack. */ private static void loadInteger(MethodVisitor methodVisitor, int value) { switch (value) { case 0: methodVisitor.visitInsn(Opcodes.ICONST_0); break; case 1: methodVisitor.visitInsn(Opcodes.ICONST_1); break; case 2: methodVisitor.visitInsn(Opcodes.ICONST_2); break; case 3: methodVisitor.visitInsn(Opcodes.ICONST_3); break; case 4: methodVisitor.visitInsn(Opcodes.ICONST_4); break; case 5: methodVisitor.visitInsn(Opcodes.ICONST_5); break; default: if (value < Byte.MAX_VALUE) { methodVisitor.visitIntInsn(Opcodes.BIPUSH, value); } else if (value < Short.MAX_VALUE) { methodVisitor.visitIntInsn(Opcodes.SIPUSH, value); } else { methodVisitor.visitLdcInsn(value); } } } @Override public int resolveIncrement(MethodVisitor methodVisitor, int increment) { throw new IllegalStateException("Cannot increment a boxed argument"); } @Override public boolean equals(Object object) { if (this == object) return true; if (object == null || getClass() != object.getClass()) return false; ForBoxedArguments that = (ForBoxedArguments) object; return parameters.equals(that.parameters); } @Override public int hashCode() { return parameters.hashCode(); } @Override public String toString() { return "Advice.Dispatcher.OffsetMapping.Target.ForBoxedArguments{" + "parameters=" + parameters + '}'; } } /** * Binds a null constant to the target parameter. */ enum ForNullConstant implements Target { /** * A null constant that can only be put onto the stack. */ READ_ONLY { @Override protected void onWrite(MethodVisitor methodVisitor) { throw new IllegalStateException("Cannot write to read-only value"); } }, /** * A null constant that also allows virtual writes where the result is simply popped. */ READ_WRITE { @Override protected void onWrite(MethodVisitor methodVisitor) { methodVisitor.visitInsn(Opcodes.POP); } }; @Override public int resolveAccess(MethodVisitor methodVisitor, int opcode) { switch (opcode) { case Opcodes.ALOAD: methodVisitor.visitInsn(Opcodes.ACONST_NULL); break; case Opcodes.ASTORE: onWrite(methodVisitor); break; default: throw new IllegalStateException("Unexpected opcode: " + opcode); } return NO_PADDING; } /** * Determines the behavior when writing to the target. * * @param methodVisitor The method visitor to which to write the result of the mapping. */ protected abstract void onWrite(MethodVisitor methodVisitor); @Override public int resolveIncrement(MethodVisitor methodVisitor, int increment) { throw new IllegalStateException("Cannot increment a null constant"); } @Override public String toString() { return "Advice.Dispatcher.OffsetMapping.Target.ForNullConstant." + name(); } } /** * Creates a target that represents a value in form of a serialized field. */ class ForSerializedObject implements Target { /** * A charset that does not change the supplied byte array upon encoding or decoding. */ private static final String CHARSET = "ISO-8859-1"; /** * The target type. */ private final TypeDescription target; /** * The serialized form of the supplied form encoded as a string to be stored in the constant pool. */ private final String serialized; /** * Creates a target for an offset mapping that references a serialized value. * * @param target The target type. * @param serialized The serialized form of the supplied form encoded as a string to be stored in the constant pool. */ protected ForSerializedObject(TypeDescription target, String serialized) { this.target = target; this.serialized = serialized; } /** * Resolves a serializable value to a target that reads a value from reconstructing a serializable string representation. * * @param target The target type of the serializable value. * @param value The value that the mapped field should represent. * @return A target for deserializing the supplied value on access. */ protected static Target of(TypeDescription target, Serializable value) { try { ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream); try { objectOutputStream.writeObject(value); } finally { objectOutputStream.close(); } return new ForSerializedObject(target, byteArrayOutputStream.toString(CHARSET)); } catch (IOException exception) { throw new IllegalStateException("Cannot serialize " + value, exception); } } @Override public int resolveAccess(MethodVisitor methodVisitor, int opcode) { switch (opcode) { case Opcodes.ALOAD: methodVisitor.visitTypeInsn(Opcodes.NEW, Type.getInternalName(ObjectInputStream.class)); methodVisitor.visitInsn(Opcodes.DUP); methodVisitor.visitTypeInsn(Opcodes.NEW, Type.getInternalName(ByteArrayInputStream.class)); methodVisitor.visitInsn(Opcodes.DUP); methodVisitor.visitLdcInsn(serialized); methodVisitor.visitLdcInsn(CHARSET); methodVisitor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, Type.getInternalName(String.class), "getBytes", Type.getMethodType(Type.getType(byte[].class), Type.getType(String.class)).toString(), false); methodVisitor.visitMethodInsn(Opcodes.INVOKESPECIAL, Type.getInternalName(ByteArrayInputStream.class), MethodDescription.CONSTRUCTOR_INTERNAL_NAME, Type.getMethodType(Type.VOID_TYPE, Type.getType(byte[].class)).toString(), false); methodVisitor.visitMethodInsn(Opcodes.INVOKESPECIAL, Type.getInternalName(ObjectInputStream.class), MethodDescription.CONSTRUCTOR_INTERNAL_NAME, Type.getMethodType(Type.VOID_TYPE, Type.getType(InputStream.class)).toString(), false); methodVisitor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, Type.getInternalName(ObjectInputStream.class), "readObject", Type.getMethodType(Type.getType(Object.class)).toString(), false); methodVisitor.visitTypeInsn(Opcodes.CHECKCAST, target.getInternalName()); return 5; default: throw new IllegalStateException("Unexpected opcode: " + opcode); } } @Override public int resolveIncrement(MethodVisitor methodVisitor, int increment) { throw new IllegalStateException("Cannot increment serialized object"); } @Override public boolean equals(Object object) { if (this == object) return true; if (object == null || getClass() != object.getClass()) return false; ForSerializedObject that = (ForSerializedObject) object; return target.equals(that.target) && serialized.equals(that.serialized); } @Override public int hashCode() { int result = target.hashCode(); result = 31 * result + serialized.hashCode(); return result; } @Override public String toString() { return "Advice.Dispatcher.OffsetMapping.Target.ForSerializedObject{" + "target=" + target + ", serialized='" + serialized + '\'' + '}'; } } } /** * Represents a factory for creating a {@link OffsetMapping} for a given parameter. */ interface Factory { /** * Indicates that an offset mapping is undefined. */ OffsetMapping UNDEFINED = null; /** * Creates a new offset mapping for the supplied parameter if possible. * * @param parameterDescription The parameter description for which to resolve an offset mapping. * @return A resolved offset mapping or {@code null} if no mapping can be resolved for this parameter. */ OffsetMapping make(ParameterDescription.InDefinedShape parameterDescription); } /** * An offset mapping for a given parameter of the instrumented method. */ class ForParameter implements OffsetMapping { /** * The index of the parameter. */ private final int index; /** * Determines if the parameter is to be treated as read-only. */ private final boolean readOnly; /** * The type expected by the advice method. */ private final TypeDescription targetType; /** * Creates a new offset mapping for a parameter. * * @param argument The annotation for which the mapping is to be created. * @param targetType Determines if the parameter is to be treated as read-only. */ protected ForParameter(Argument argument, TypeDescription targetType) { this(argument.value(), argument.readOnly(), targetType); } /** * Creates a new offset mapping for a parameter of the instrumented method. * * @param index The index of the parameter. * @param readOnly Determines if the parameter is to be treated as read-only. * @param targetType The type expected by the advice method. */ protected ForParameter(int index, boolean readOnly, TypeDescription targetType) { this.index = index; this.readOnly = readOnly; this.targetType = targetType; } @Override public Target resolve(MethodDescription.InDefinedShape instrumentedMethod, Context context) { ParameterList parameters = instrumentedMethod.getParameters(); if (parameters.size() <= index) { throw new IllegalStateException(instrumentedMethod + " does not define an index " + index); } else if (!readOnly && !parameters.get(index).getType().asErasure().equals(targetType)) { throw new IllegalStateException("read-only " + targetType + " is not equal to type of " + parameters.get(index)); } else if (readOnly && !parameters.get(index).getType().asErasure().isAssignableTo(targetType)) { throw new IllegalStateException(targetType + " is not assignable to " + parameters.get(index)); } return readOnly ? new Target.ForParameter.ReadOnly(parameters.get(index).getOffset()) : new Target.ForParameter.ReadWrite(parameters.get(index).getOffset()); } @Override public boolean equals(Object object) { if (this == object) return true; if (object == null || getClass() != object.getClass()) return false; ForParameter that = (ForParameter) object; return index == that.index && readOnly == that.readOnly && targetType.equals(that.targetType); } @Override public int hashCode() { int result = index; result = 31 * result + (readOnly ? 1 : 0); result = 31 * result + targetType.hashCode(); return result; } @Override public String toString() { return "Advice.Dispatcher.OffsetMapping.ForParameter{" + "index=" + index + ", readOnly=" + readOnly + ", targetType=" + targetType + '}'; } /** * A factory for creating a {@link ForParameter} offset mapping. */ protected enum Factory implements OffsetMapping.Factory { /** * A factory that does not allow writing to the mapped parameter. */ READ_ONLY(true), /** * A factory that allows writing to the mapped parameter. */ READ_WRITE(false); /** * {@code true} if the parameter is read-only. */ private final boolean readOnly; /** * Creates a new factory. * * @param readOnly {@code true} if the parameter is read-only. */ Factory(boolean readOnly) { this.readOnly = readOnly; } @Override public OffsetMapping make(ParameterDescription.InDefinedShape parameterDescription) { AnnotationDescription.Loadable annotation = parameterDescription.getDeclaredAnnotations().ofType(Argument.class); if (annotation == null) { return UNDEFINED; } else if (readOnly && !annotation.loadSilent().readOnly()) { throw new IllegalStateException("Cannot define writable field access for " + parameterDescription); } else { return new ForParameter(annotation.loadSilent(), parameterDescription.getType().asErasure()); } } @Override public String toString() { return "Advice.Dispatcher.OffsetMapping.ForParameter.Factory." + name(); } } } /** * An offset mapping that provides access to the {@code this} reference of the instrumented method. */ class ForThisReference implements OffsetMapping { /** * The offset of the this reference in a Java method. */ private static final int THIS_REFERENCE = 0; /** * Determines if the parameter is to be treated as read-only. */ private final boolean readOnly; /** * {@code true} if the parameter should be bound to {@code null} if the instrumented method is static. */ private final boolean optional; /** * The type that the advice method expects for the {@code this} reference. */ private final TypeDescription targetType; /** * Creates a new offset mapping for a {@code this} reference. * * @param readOnly Determines if the parameter is to be treated as read-only. * @param optional {@code true} if the parameter should be bound to {@code null} if the instrumented method is static. * @param targetType The type that the advice method expects for the {@code this} reference. */ protected ForThisReference(boolean readOnly, boolean optional, TypeDescription targetType) { this.readOnly = readOnly; this.optional = optional; this.targetType = targetType; } @Override public Target resolve(MethodDescription.InDefinedShape instrumentedMethod, Context context) { if (!readOnly && !instrumentedMethod.getDeclaringType().equals(targetType)) { throw new IllegalStateException("Declaring type of " + instrumentedMethod + " is not equal to read-only " + targetType); } else if (readOnly && !instrumentedMethod.getDeclaringType().isAssignableTo(targetType)) { throw new IllegalStateException("Declaring type of " + instrumentedMethod + " is not assignable to " + targetType); } else if (instrumentedMethod.isStatic() && optional) { return readOnly ? Target.ForNullConstant.READ_ONLY : Target.ForNullConstant.READ_WRITE; } else if (instrumentedMethod.isStatic() && !optional) { throw new IllegalStateException("Cannot map this reference for static method " + instrumentedMethod); } else if (!context.isInitialized()) { throw new IllegalStateException("Cannot access this reference before calling constructor: " + instrumentedMethod); } return readOnly ? new Target.ForParameter.ReadOnly(THIS_REFERENCE) : new Target.ForParameter.ReadWrite(THIS_REFERENCE); } @Override public boolean equals(Object object) { if (this == object) return true; if (object == null || getClass() != object.getClass()) return false; ForThisReference that = (ForThisReference) object; return readOnly == that.readOnly && optional == that.optional && targetType.equals(that.targetType); } @Override public int hashCode() { int result = (readOnly ? 1 : 0); result = 31 * result + (readOnly ? 1 : 0); result = 31 * result + targetType.hashCode(); return result; } @Override public String toString() { return "Advice.Dispatcher.OffsetMapping.ForThisReference{" + "readOnly=" + readOnly + ", optional=" + optional + ", targetType=" + targetType + '}'; } /** * A factory for creating a {@link ForThisReference} offset mapping. */ protected enum Factory implements OffsetMapping.Factory { /** * A factory that does not allow writing to the mapped parameter. */ READ_ONLY(true), /** * A factory that allows writing to the mapped parameter. */ READ_WRITE(false); /** * {@code true} if the parameter is read-only. */ private final boolean readOnly; /** * Creates a new factory. * * @param readOnly {@code true} if the parameter is read-only. */ Factory(boolean readOnly) { this.readOnly = readOnly; } @Override public OffsetMapping make(ParameterDescription.InDefinedShape parameterDescription) { AnnotationDescription.Loadable annotation = parameterDescription.getDeclaredAnnotations().ofType(This.class); if (annotation == null) { return UNDEFINED; } else if (readOnly && !annotation.loadSilent().readOnly()) { throw new IllegalStateException("Cannot write to this reference for " + parameterDescription + " in read-only context"); } else { return new ForThisReference(annotation.loadSilent().readOnly(), annotation.loadSilent().optional(), parameterDescription.getType().asErasure()); } } @Override public String toString() { return "Advice.Dispatcher.OffsetMapping.ForThisReference.Factory." + name(); } } } /** * Maps the declaring type of the instrumented method. */ enum ForInstrumentedType implements OffsetMapping { /** * The singleton instance. */ INSTANCE; @Override public Target resolve(MethodDescription.InDefinedShape instrumentedMethod, Context context) { return new Target.ForConstantPoolValue(Type.getType(instrumentedMethod.getDeclaringType().getDescriptor())); } @Override public String toString() { return "Advice.Dispatcher.OffsetMapping.ForInstrumentedType." + name(); } } /** * An offset mapping for a field. */ abstract class ForField implements OffsetMapping { /** * The {@link FieldValue#value()} method. */ private static final MethodDescription.InDefinedShape VALUE; /** * The {@link FieldValue#declaringType()}} method. */ private static final MethodDescription.InDefinedShape DECLARING_TYPE; /** * The {@link FieldValue#readOnly()}} method. */ private static final MethodDescription.InDefinedShape READ_ONLY; static { MethodList methods = new TypeDescription.ForLoadedType(FieldValue.class).getDeclaredMethods(); VALUE = methods.filter(named("value")).getOnly(); DECLARING_TYPE = methods.filter(named("declaringType")).getOnly(); READ_ONLY = methods.filter(named("readOnly")).getOnly(); } /** * The name of the field. */ protected final String name; /** * The expected type that the field can be assigned to. */ protected final TypeDescription targetType; /** * {@code true} if this mapping is read-only. */ protected final boolean readOnly; /** * Creates an offset mapping for a field. * * @param name The name of the field. * @param targetType The expected type that the field can be assigned to. * @param readOnly {@code true} if this mapping is read-only. */ protected ForField(String name, TypeDescription targetType, boolean readOnly) { this.name = name; this.targetType = targetType; this.readOnly = readOnly; } @Override public Target resolve(MethodDescription.InDefinedShape instrumentedMethod, Context context) { FieldLocator.Resolution resolution = fieldLocator(instrumentedMethod.getDeclaringType()).locate(name); if (!resolution.isResolved()) { throw new IllegalStateException("Cannot locate field named " + name + " for " + instrumentedMethod); } else if (readOnly && !resolution.getField().asDefined().getType().asErasure().isAssignableTo(targetType)) { throw new IllegalStateException("Cannot assign type of read-only field " + resolution.getField() + " to " + targetType); } else if (!readOnly && !resolution.getField().asDefined().getType().asErasure().equals(targetType)) { throw new IllegalStateException("Type of field " + resolution.getField() + " is not equal to " + targetType); } else if (!resolution.getField().isStatic() && instrumentedMethod.isStatic()) { throw new IllegalStateException("Cannot read non-static field " + resolution.getField() + " from static method " + instrumentedMethod); } else if (!context.isInitialized() && !resolution.getField().isStatic()) { throw new IllegalStateException("Cannot access non-static field before calling constructor: " + instrumentedMethod); } return readOnly ? new Target.ForField.ReadOnly(resolution.getField().asDefined()) : new Target.ForField.ReadWrite(resolution.getField().asDefined()); } @Override public boolean equals(Object object) { if (this == object) return true; if (object == null || getClass() != object.getClass()) return false; ForField forField = (ForField) object; return name.equals(forField.name) && targetType.equals(forField.targetType) && readOnly == forField.readOnly; } @Override public int hashCode() { int result = name.hashCode(); result = 31 * result + targetType.hashCode(); result = 31 * result + (readOnly ? 1 : 0); return result; } /** * Returns a field locator for this instance. * * @param instrumentedType The instrumented type. * @return An appropriate field locator. */ protected abstract FieldLocator fieldLocator(TypeDescription instrumentedType); /** * An offset mapping for a field with an implicit declaring type. */ protected static class WithImplicitType extends ForField { /** * Creates an offset mapping for a field with an implicit declaring type. * * @param name The name of the field. * @param targetType The expected type that the field can be assigned to. * @param readOnly {@code true} if the field is read-only. */ protected WithImplicitType(String name, TypeDescription targetType, boolean readOnly) { super(name, targetType, readOnly); } @Override protected FieldLocator fieldLocator(TypeDescription instrumentedType) { return new FieldLocator.ForClassHierarchy(instrumentedType); } @Override public String toString() { return "Advice.Dispatcher.OffsetMapping.ForField.WithImplicitType{" + "name=" + name + ", targetType=" + targetType + '}'; } } /** * An offset mapping for a field with an explicit declaring type. */ protected static class WithExplicitType extends ForField { /** * The type declaring the field. */ private final TypeDescription explicitType; /** * Creates an offset mapping for a field with an explicit declaring type. * * @param name The name of the field. * @param targetType The expected type that the field can be assigned to. * @param locatedType The type declaring the field. * @param readOnly {@code true} if the field is read-only. */ protected WithExplicitType(String name, TypeDescription targetType, TypeDescription locatedType, boolean readOnly) { super(name, targetType, readOnly); this.explicitType = locatedType; } @Override protected FieldLocator fieldLocator(TypeDescription instrumentedType) { if (!instrumentedType.isAssignableTo(explicitType)) { throw new IllegalStateException(explicitType + " is no super type of " + instrumentedType); } return new FieldLocator.ForExactType(explicitType); } @Override public boolean equals(Object object) { if (this == object) return true; if (object == null || getClass() != object.getClass()) return false; if (!super.equals(object)) return false; WithExplicitType that = (WithExplicitType) object; return explicitType.equals(that.explicitType); } @Override public int hashCode() { int result = super.hashCode(); result = 31 * result + explicitType.hashCode(); return result; } @Override public String toString() { return "Advice.Dispatcher.OffsetMapping.ForField.WithExplicitType{" + "name=" + name + ", targetType=" + targetType + ", explicitType=" + explicitType + '}'; } } /** * A factory for a {@link ForField} offset mapping. */ protected enum Factory implements OffsetMapping.Factory { /** * A factory that does not allow writing to the mapped parameter. */ READ_ONLY(true), /** * A factory that allows writing to the mapped parameter. */ READ_WRITE(false); /** * {@code true} if the parameter is read-only. */ private final boolean readOnly; /** * Creates a new factory. * * @param readOnly {@code true} if the parameter is read-only. */ Factory(boolean readOnly) { this.readOnly = readOnly; } @Override public OffsetMapping make(ParameterDescription.InDefinedShape parameterDescription) { AnnotationDescription annotation = parameterDescription.getDeclaredAnnotations().ofType(FieldValue.class); if (annotation == null) { return UNDEFINED; } else if (readOnly && !annotation.getValue(ForField.READ_ONLY, Boolean.class)) { throw new IllegalStateException("Cannot write to field for " + parameterDescription + " in read-only context"); } else { TypeDescription declaringType = annotation.getValue(DECLARING_TYPE, TypeDescription.class); String name = annotation.getValue(VALUE, String.class); TypeDescription targetType = parameterDescription.getType().asErasure(); return declaringType.represents(void.class) ? new WithImplicitType(name, targetType, annotation.getValue(ForField.READ_ONLY, Boolean.class)) : new WithExplicitType(name, targetType, declaringType, annotation.getValue(ForField.READ_ONLY, Boolean.class)); } } @Override public String toString() { return "Advice.Dispatcher.OffsetMapping.ForField.Factory." + name(); } } } /** * An offset mapping for the {@link Advice.Origin} annotation. */ class ForOrigin implements OffsetMapping { /** * The delimiter character. */ private static final char DELIMITER = '#'; /** * The escape character. */ private static final char ESCAPE = '\\'; /** * The renderers to apply. */ private final List renderers; /** * Creates a new offset mapping for an origin value. * * @param renderers The renderers to apply. */ protected ForOrigin(List renderers) { this.renderers = renderers; } /** * Parses a pattern of an origin annotation. * * @param pattern The supplied pattern. * @return An appropriate offset mapping. */ protected static OffsetMapping parse(String pattern) { if (pattern.equals(Origin.DEFAULT)) { return new ForOrigin(Collections.singletonList(Renderer.ForStringRepresentation.INSTANCE)); } else { List renderers = new ArrayList(pattern.length()); int from = 0; for (int to = pattern.indexOf(DELIMITER); to != -1; to = pattern.indexOf(DELIMITER, from)) { if (to != 0 && pattern.charAt(to - 1) == ESCAPE && (to == 1 || pattern.charAt(to - 2) != ESCAPE)) { renderers.add(new Renderer.ForConstantValue(pattern.substring(from, Math.max(0, to - 1)) + DELIMITER)); from = to + 1; continue; } else if (pattern.length() == to + 1) { throw new IllegalStateException("Missing sort descriptor for " + pattern + " at index " + to); } renderers.add(new Renderer.ForConstantValue(pattern.substring(from, to).replace("" + ESCAPE + ESCAPE, "" + ESCAPE))); switch (pattern.charAt(to + 1)) { case Renderer.ForMethodName.SYMBOL: renderers.add(Renderer.ForMethodName.INSTANCE); break; case Renderer.ForTypeName.SYMBOL: renderers.add(Renderer.ForTypeName.INSTANCE); break; case Renderer.ForDescriptor.SYMBOL: renderers.add(Renderer.ForDescriptor.INSTANCE); break; case Renderer.ForReturnTypeName.SYMBOL: renderers.add(Renderer.ForReturnTypeName.INSTANCE); break; case Renderer.ForJavaSignature.SYMBOL: renderers.add(Renderer.ForJavaSignature.INSTANCE); break; default: throw new IllegalStateException("Illegal sort descriptor " + pattern.charAt(to + 1) + " for " + pattern); } from = to + 2; } renderers.add(new Renderer.ForConstantValue(pattern.substring(from))); return new ForOrigin(renderers); } } @Override public Target resolve(MethodDescription.InDefinedShape instrumentedMethod, Context context) { StringBuilder stringBuilder = new StringBuilder(); for (Renderer renderer : renderers) { stringBuilder.append(renderer.apply(instrumentedMethod)); } return new Target.ForConstantPoolValue(stringBuilder.toString()); } @Override public boolean equals(Object object) { if (this == object) return true; if (object == null || getClass() != object.getClass()) return false; ForOrigin forOrigin = (ForOrigin) object; return renderers.equals(forOrigin.renderers); } @Override public int hashCode() { return renderers.hashCode(); } @Override public String toString() { return "Advice.Dispatcher.OffsetMapping.ForOrigin{" + "renderers=" + renderers + '}'; } /** * A renderer for an origin pattern element. */ protected interface Renderer { /** * Returns a string representation for this renderer. * * @param instrumentedMethod The method being rendered. * @return The string representation. */ String apply(MethodDescription.InDefinedShape instrumentedMethod); /** * A renderer for a method's internal name. */ enum ForMethodName implements Renderer { /** * The singleton instance. */ INSTANCE; /** * The method name symbol. */ public static final char SYMBOL = 'm'; @Override public String apply(MethodDescription.InDefinedShape instrumentedMethod) { return instrumentedMethod.getInternalName(); } @Override public String toString() { return "Advice.Dispatcher.OffsetMapping.ForOrigin.Renderer.ForMethodName." + name(); } } /** * A renderer for a method declaring type's binary name. */ enum ForTypeName implements Renderer { /** * The singleton instance. */ INSTANCE; /** * The type name symbol. */ public static final char SYMBOL = 't'; @Override public String apply(MethodDescription.InDefinedShape instrumentedMethod) { return instrumentedMethod.getDeclaringType().getName(); } @Override public String toString() { return "Advice.Dispatcher.OffsetMapping.ForOrigin.Renderer.ForTypeName." + name(); } } /** * A renderer for a method descriptor. */ enum ForDescriptor implements Renderer { /** * The singleton instance. */ INSTANCE; /** * The descriptor symbol. */ public static final char SYMBOL = 'd'; @Override public String apply(MethodDescription.InDefinedShape instrumentedMethod) { return instrumentedMethod.getDescriptor(); } @Override public String toString() { return "Advice.Dispatcher.OffsetMapping.ForOrigin.Renderer.ForDescriptor." + name(); } } /** * A renderer for a method's Java signature in binary form. */ enum ForJavaSignature implements Renderer { /** * The singleton instance. */ INSTANCE; /** * The signature symbol. */ public static final char SYMBOL = 's'; @Override public String apply(MethodDescription.InDefinedShape instrumentedMethod) { StringBuilder stringBuilder = new StringBuilder("("); boolean comma = false; for (TypeDescription typeDescription : instrumentedMethod.getParameters().asTypeList().asErasures()) { if (comma) { stringBuilder.append(','); } else { comma = true; } stringBuilder.append(typeDescription.getName()); } return stringBuilder.append(')').toString(); } @Override public String toString() { return "Advice.Dispatcher.OffsetMapping.ForOrigin.Renderer.ForJavaSignature." + name(); } } /** * A renderer for a method's return type in binary form. */ enum ForReturnTypeName implements Renderer { /** * The singleton instance. */ INSTANCE; /** * The return type symbol. */ public static final char SYMBOL = 'r'; @Override public String apply(MethodDescription.InDefinedShape instrumentedMethod) { return instrumentedMethod.getReturnType().asErasure().getName(); } @Override public String toString() { return "Advice.Dispatcher.OffsetMapping.ForOrigin.Renderer.ForReturnTypeName." + name(); } } /** * A renderer for a method's {@link Object#toString()} representation. */ enum ForStringRepresentation implements Renderer { /** * The singleton instance. */ INSTANCE; @Override public String apply(MethodDescription.InDefinedShape instrumentedMethod) { return instrumentedMethod.toString(); } @Override public String toString() { return "Advice.Dispatcher.OffsetMapping.ForOrigin.Renderer.ForStringRepresentation." + name(); } } /** * A renderer for a constant value. */ class ForConstantValue implements Renderer { /** * The constant value. */ private final String value; /** * Creates a new renderer for a constant value. * * @param value The constant value. */ protected ForConstantValue(String value) { this.value = value; } @Override public String apply(MethodDescription.InDefinedShape instrumentedMethod) { return value; } @Override public boolean equals(Object object) { if (this == object) return true; if (object == null || getClass() != object.getClass()) return false; ForConstantValue that = (ForConstantValue) object; return value.equals(that.value); } @Override public int hashCode() { return value.hashCode(); } @Override public String toString() { return "Advice.Dispatcher.OffsetMapping.ForOrigin.Renderer.ForConstantValue{" + "value='" + value + '\'' + '}'; } } } /** * A factory for a method origin. */ protected enum Factory implements OffsetMapping.Factory { /** * The singleton instance. */ INSTANCE; @Override public OffsetMapping make(ParameterDescription.InDefinedShape parameterDescription) { AnnotationDescription.Loadable origin = parameterDescription.getDeclaredAnnotations().ofType(Origin.class); if (origin == null) { return UNDEFINED; } else if (parameterDescription.getType().asErasure().represents(Class.class)) { return OffsetMapping.ForInstrumentedType.INSTANCE; } else if (parameterDescription.getType().asErasure().isAssignableFrom(String.class)) { return ForOrigin.parse(origin.loadSilent().value()); } else { throw new IllegalStateException("Non-String type " + parameterDescription + " for origin annotation"); } } @Override public String toString() { return "Advice.Dispatcher.OffsetMapping.ForOrigin.Factory." + name(); } } } /** * An offset mapping for a parameter where assignments are fully ignored and that always return the parameter type's default value. */ enum ForIgnored implements OffsetMapping, Factory { /** * The singleton instance. */ INSTANCE; @Override public Target resolve(MethodDescription.InDefinedShape instrumentedMethod, Context context) { return Target.ForDefaultValue.INSTANCE; } @Override public OffsetMapping make(ParameterDescription.InDefinedShape parameterDescription) { return parameterDescription.getDeclaredAnnotations().isAnnotationPresent(Ignored.class) ? this : UNDEFINED; } @Override public String toString() { return "Advice.Dispatcher.OffsetMapping.ForIgnored." + name(); } } /** * An offset mapping that provides access to the value that is returned by the enter advice. */ enum ForEnterValue implements OffsetMapping { /** * Enables writing to the mapped offset. */ WRITABLE(false), /** * Only allows for reading the mapped offset. */ READ_ONLY(true); /** * Determines if the parameter is to be treated as read-only. */ private final boolean readOnly; /** * Creates a new offset mapping for an enter value. * * @param readOnly Determines if the parameter is to be treated as read-only. */ ForEnterValue(boolean readOnly) { this.readOnly = readOnly; } /** * Resolves an offset mapping for an enter value. * * @param readOnly {@code true} if the value is to be treated as read-only. * @return An appropriate offset mapping. */ public static OffsetMapping of(boolean readOnly) { return readOnly ? READ_ONLY : WRITABLE; } @Override public Target resolve(MethodDescription.InDefinedShape instrumentedMethod, Context context) { return readOnly ? new Target.ForParameter.ReadOnly(instrumentedMethod.getStackSize()) : new Target.ForParameter.ReadWrite(instrumentedMethod.getStackSize()); } @Override public String toString() { return "Advice.Dispatcher.OffsetMapping.ForEnterValue." + name(); } /** * A factory for creating a {@link ForEnterValue} offset mapping. */ protected static class Factory implements OffsetMapping.Factory { /** * The supplied type of the enter method. */ private final TypeDescription enterType; /** * Indicates that the mapped parameter is read-only. */ private final boolean readOnly; /** * Creates a new factory for creating a {@link ForEnterValue} offset mapping. * * @param enterType The supplied type of the enter method. * @param readOnly Indicates that the mapped parameter is read-only. */ protected Factory(TypeDescription enterType, boolean readOnly) { this.enterType = enterType; this.readOnly = readOnly; } @Override public OffsetMapping make(ParameterDescription.InDefinedShape parameterDescription) { AnnotationDescription.Loadable annotation = parameterDescription.getDeclaredAnnotations().ofType(Enter.class); if (annotation != null) { boolean readOnly = annotation.loadSilent().readOnly(); if (!readOnly && !enterType.equals(parameterDescription.getType().asErasure())) { throw new IllegalStateException("read-only type of " + parameterDescription + " does not equal " + enterType); } else if (readOnly && !enterType.isAssignableTo(parameterDescription.getType().asErasure())) { throw new IllegalStateException("Cannot assign the type of " + parameterDescription + " to supplied type " + enterType); } else if (this.readOnly && !readOnly) { throw new IllegalStateException("Cannot write to enter value field for " + parameterDescription + " in read only context"); } return ForEnterValue.of(readOnly); } else { return UNDEFINED; } } @Override public boolean equals(Object object) { if (this == object) return true; if (object == null || getClass() != object.getClass()) return false; Factory factory = (Factory) object; return readOnly == factory.readOnly && enterType.equals(factory.enterType); } @Override public int hashCode() { int result = enterType.hashCode(); result = 31 * result + (readOnly ? 1 : 0); return result; } @Override public String toString() { return "Advice.Dispatcher.OffsetMapping.ForEnterValue.Factory{" + "enterType=" + enterType + "m readOnly=" + readOnly + '}'; } } } /** * An offset mapping that provides access to the value that is returned by the instrumented method. */ class ForReturnValue implements OffsetMapping { /** * Determines if the parameter is to be treated as read-only. */ private final boolean readOnly; /** * The type that the advice method expects for the {@code this} reference. */ private final TypeDescription targetType; /** * Creates an offset mapping for accessing the return type of the instrumented method. * * @param readOnly Determines if the parameter is to be treated as read-only. * @param targetType The expected target type of the return type. */ protected ForReturnValue(boolean readOnly, TypeDescription targetType) { this.readOnly = readOnly; this.targetType = targetType; } @Override public Target resolve(MethodDescription.InDefinedShape instrumentedMethod, Context context) { if (!readOnly && !instrumentedMethod.getReturnType().asErasure().equals(targetType)) { throw new IllegalStateException("read-only return type of " + instrumentedMethod + " is not equal to " + targetType); } else if (readOnly && !instrumentedMethod.getReturnType().asErasure().isAssignableTo(targetType)) { throw new IllegalStateException("Cannot assign return type of " + instrumentedMethod + " to " + targetType); } return readOnly ? new Target.ForParameter.ReadOnly(instrumentedMethod.getStackSize() + context.getPadding()) : new Target.ForParameter.ReadWrite(instrumentedMethod.getStackSize() + context.getPadding()); } @Override public boolean equals(Object other) { if (this == other) return true; if (other == null || getClass() != other.getClass()) return false; ForReturnValue that = (ForReturnValue) other; return readOnly == that.readOnly && targetType.equals(that.targetType); } @Override public int hashCode() { return (readOnly ? 1 : 0) + 31 * targetType.hashCode(); } @Override public String toString() { return "Advice.Dispatcher.OffsetMapping.ForReturnValue{" + "readOnly=" + readOnly + ", targetType=" + targetType + '}'; } /** * A factory for creating a {@link ForReturnValue} offset mapping. */ protected enum Factory implements OffsetMapping.Factory { /** * A factory that does not allow writing to the mapped parameter. */ READ_ONLY(true), /** * A factory that allows writing to the mapped parameter. */ READ_WRITE(false); /** * {@code true} if the parameter is read-only. */ private final boolean readOnly; /** * Creates a new factory. * * @param readOnly {@code true} if the parameter is read-only. */ Factory(boolean readOnly) { this.readOnly = readOnly; } @Override public OffsetMapping make(ParameterDescription.InDefinedShape parameterDescription) { AnnotationDescription.Loadable annotation = parameterDescription.getDeclaredAnnotations().ofType(Return.class); if (annotation == null) { return UNDEFINED; } else if (readOnly && !annotation.loadSilent().readOnly()) { throw new IllegalStateException("Cannot write return value for " + parameterDescription + " in read-only context"); } else { return new ForReturnValue(annotation.loadSilent().readOnly(), parameterDescription.getType().asErasure()); } } @Override public String toString() { return "Advice.Dispatcher.OffsetMapping.ForReturnValue.Factory." + name(); } } } /** * An offset mapping for the method's (boxed) return value. */ enum ForBoxedReturnValue implements OffsetMapping { /** * Indicates that it is only legal to read the boxed return value. */ READ_ONLY(true), /** * A mapping that also allows writing to the boxed return value via a type casting and potential unboxing. */ READ_WRITE(false); /** * {@code true} if the factory is read-only. */ private final boolean readOnly; /** * Creates a new offset mapping. * * @param readOnly {@code true} if the mapping is read-only. */ ForBoxedReturnValue(boolean readOnly) { this.readOnly = readOnly; } @Override public Target resolve(MethodDescription.InDefinedShape instrumentedMethod, Context context) { if (instrumentedMethod.getReturnType().represents(void.class)) { return readOnly ? Target.ForNullConstant.READ_ONLY : Target.ForNullConstant.READ_WRITE; } else if (instrumentedMethod.getReturnType().isPrimitive()) { return readOnly ? Target.ForBoxedParameter.ReadOnly.of(instrumentedMethod.getStackSize() + context.getPadding(), instrumentedMethod.getReturnType()) : Target.ForBoxedParameter.ReadWrite.of(instrumentedMethod.getStackSize() + context.getPadding(), instrumentedMethod.getReturnType()); } else { return readOnly ? new Target.ForParameter.ReadOnly(instrumentedMethod.getStackSize() + context.getPadding()) : new Target.ForParameter.ReadWrite(instrumentedMethod.getStackSize() + context.getPadding()).casted(instrumentedMethod.getReturnType().asErasure()); } } @Override public String toString() { return "Advice.Dispatcher.OffsetMapping.ForBoxedReturnValue." + name(); } /** * A factory for an offset mapping the method's (boxed) return value. */ protected enum Factory implements OffsetMapping.Factory { /** * Indicates that it is only legal to read the boxed return value. */ READ_ONLY(true), /** * A factory that also allows writing to the boxed return value via a type casting and potential unboxing. */ READ_WRITE(false); /** * {@code true} if the factory is read-only. */ private final boolean readOnly; /** * Creates a new factory. * * @param readOnly {@code true} if the factory is read-only. */ Factory(boolean readOnly) { this.readOnly = readOnly; } @Override public OffsetMapping make(ParameterDescription.InDefinedShape parameterDescription) { AnnotationDescription.Loadable annotation = parameterDescription.getDeclaredAnnotations().ofType(BoxedReturn.class); if (annotation == null) { return UNDEFINED; } else if (readOnly && !annotation.loadSilent().readOnly()) { throw new IllegalStateException("Cannot write return value from a non-writable context for " + parameterDescription); } else if (parameterDescription.getType().represents(Object.class)) { return annotation.loadSilent().readOnly() ? ForBoxedReturnValue.READ_ONLY : ForBoxedReturnValue.READ_WRITE; } else { throw new IllegalStateException("Can only assign a boxed return value to an Object type for " + parameterDescription); } } @Override public String toString() { return "Advice.Dispatcher.OffsetMapping.ForBoxedReturnValue.Factory." + name(); } } } /** * An offset mapping for an array containing the (boxed) method arguments. */ enum ForBoxedArguments implements OffsetMapping, Factory { /** * The singleton instance. */ INSTANCE; @Override public Target resolve(MethodDescription.InDefinedShape instrumentedMethod, Context context) { return new Target.ForBoxedArguments(instrumentedMethod.getParameters()); } @Override public OffsetMapping make(ParameterDescription.InDefinedShape parameterDescription) { if (!parameterDescription.getDeclaredAnnotations().isAnnotationPresent(BoxedArguments.class)) { return UNDEFINED; } else if (parameterDescription.getType().represents(Object[].class)) { return this; } else { throw new IllegalStateException("Can only assign an array of boxed arguments to an Object[] array"); } } @Override public String toString() { return "Advice.Dispatcher.OffsetMapping.ForBoxedArguments." + name(); } } /** * An offset mapping for accessing a {@link Throwable} of the instrumented method. */ class ForThrowable implements OffsetMapping { /** * The type of parameter that is being accessed. */ private final TypeDescription targetType; /** * The type of the {@link Throwable} being catched if thrown from the instrumented method. */ private final TypeDescription triggeringThrowable; /** * {@code true} if the parameter is read-only. */ private final boolean readOnly; /** * Creates a new offset mapping for access of the exception that is thrown by the instrumented method.. * * @param targetType The type of parameter that is being accessed. * @param triggeringThrowable The type of the {@link Throwable} being catched if thrown from the instrumented method. * @param readOnly {@code true} if the parameter is read-only. */ protected ForThrowable(TypeDescription targetType, TypeDescription triggeringThrowable, boolean readOnly) { this.targetType = targetType; this.triggeringThrowable = triggeringThrowable; this.readOnly = readOnly; } @Override public Target resolve(MethodDescription.InDefinedShape instrumentedMethod, Context context) { int offset = instrumentedMethod.getStackSize() + context.getPadding() + instrumentedMethod.getReturnType().getStackSize().getSize(); return readOnly ? new Target.ForParameter.ReadOnly(offset) : new Target.ForParameter.ReadWrite(offset); } @Override public boolean equals(Object object) { if (this == object) return true; if (object == null || getClass() != object.getClass()) return false; ForThrowable forThrowable = (ForThrowable) object; return readOnly == forThrowable.readOnly && targetType.equals(forThrowable.targetType) && triggeringThrowable.equals(forThrowable.triggeringThrowable); } @Override public int hashCode() { int result = triggeringThrowable.hashCode(); result = 31 * result + targetType.hashCode(); result = 31 * result + (readOnly ? 1 : 0); return result; } @Override public String toString() { return "Advice.Dispatcher.OffsetMapping.ForThrowable{" + "targetType=" + targetType + ", triggeringThrowable=" + triggeringThrowable + ", readOnly=" + readOnly + '}'; } /** * A factory for accessing an exception that was thrown by the instrumented method. */ protected static class Factory implements OffsetMapping.Factory { /** * The type of the {@link Throwable} being catched if thrown from the instrumented method. */ private final TypeDescription triggeringThrowable; /** * {@code true} if the parameter is read-only. */ private final boolean readOnly; /** * Creates a new factory for access of the exception that is thrown by the instrumented method.. * * @param triggeringThrowable The type of the {@link Throwable} being catched if thrown from the instrumented method. * @param readOnly {@code true} if the parameter is read-only. */ protected Factory(TypeDescription triggeringThrowable, boolean readOnly) { this.triggeringThrowable = triggeringThrowable; this.readOnly = readOnly; } /** * Resolves an appropriate offset mapping factory for the {@link Thrown} parameter annotation. * * @param adviceMethod The exit advice method, annotated with {@link OnMethodExit}. * @param readOnly {@code true} if the parameter is read-only. * @return An appropriate offset mapping factory. */ @SuppressWarnings("all") // In absence of @SafeVarargs for Java 6 protected static OffsetMapping.Factory of(MethodDescription.InDefinedShape adviceMethod, boolean readOnly) { TypeDescription triggeringThrowable = adviceMethod.getDeclaredAnnotations() .ofType(OnMethodExit.class) .getValue(ON_THROWABLE, TypeDescription.class); return triggeringThrowable.represents(NoExceptionHandler.class) ? new OffsetMapping.Illegal(Thrown.class) : new Factory(triggeringThrowable, readOnly); } @Override public OffsetMapping make(ParameterDescription.InDefinedShape parameterDescription) { AnnotationDescription.Loadable annotation = parameterDescription.getDeclaredAnnotations().ofType(Thrown.class); if (annotation == null) { return UNDEFINED; } else if (!parameterDescription.getType().represents(Throwable.class)) { throw new IllegalStateException("Parameter must be a throwable type for " + parameterDescription); } else if (readOnly && !annotation.loadSilent().readOnly()) { throw new IllegalStateException("Cannot write exception value for " + parameterDescription + " in read-only context"); } else { return new ForThrowable(parameterDescription.getType().asErasure(), triggeringThrowable, annotation.loadSilent().readOnly()); } } @Override public boolean equals(Object object) { if (this == object) return true; if (object == null || getClass() != object.getClass()) return false; Factory factory = (Factory) object; return readOnly == factory.readOnly && triggeringThrowable.equals(factory.triggeringThrowable); } @Override public int hashCode() { int result = triggeringThrowable.hashCode(); result = 31 * result + (readOnly ? 1 : 0); return result; } @Override public String toString() { return "Advice.Dispatcher.OffsetMapping.ForThrowable.Factory{" + "triggeringThrowable=" + triggeringThrowable + ", readOnly=" + readOnly + '}'; } } } /** * Represents an offset mapping for a user-defined value. * * @param The mapped annotation type. */ class ForUserValue implements OffsetMapping { /** * The target parameter that is bound. */ private final ParameterDescription.InDefinedShape target; /** * The annotation value that triggered the binding. */ private final AnnotationDescription.Loadable annotation; /** * The dynamic value that is bound. */ private final DynamicValue dynamicValue; /** * Creates a new offset mapping for a user-defined value. * * @param target The target parameter that is bound. * @param annotation The annotation value that triggered the binding. * @param dynamicValue The dynamic value that is bound. */ protected ForUserValue(ParameterDescription.InDefinedShape target, AnnotationDescription.Loadable annotation, DynamicValue dynamicValue) { this.target = target; this.annotation = annotation; this.dynamicValue = dynamicValue; } @Override public Target resolve(MethodDescription.InDefinedShape instrumentedMethod, Context context) { Object value = dynamicValue.resolve(instrumentedMethod, target, annotation, context.isInitialized()); if (value == null) { if (target.getType().isPrimitive()) { throw new IllegalStateException("Cannot map null to primitive type of " + target); } return Target.ForNullConstant.READ_ONLY; } else if ((target.getType().asErasure().isAssignableFrom(String.class) && value instanceof String) || (target.getType().isPrimitive() && target.getType().asErasure().isInstanceOrWrapper(value))) { if (value instanceof Boolean) { value = (Boolean) value ? 1 : 0; } else if (value instanceof Byte) { value = ((Byte) value).intValue(); } else if (value instanceof Short) { value = ((Short) value).intValue(); } else if (value instanceof Character) { value = (int) ((Character) value).charValue(); } return new Target.ForConstantPoolValue(value); } else if (target.getType().asErasure().isAssignableFrom(Class.class) && value instanceof Class) { return new Target.ForConstantPoolValue(Type.getType((Class) value)); } else if (target.getType().asErasure().isAssignableFrom(Class.class) && value instanceof TypeDescription) { return new Target.ForConstantPoolValue(Type.getType(((TypeDescription) value).getDescriptor())); } else if (!target.getType().isPrimitive() && !target.getType().isArray() && value instanceof Serializable && target.getType().asErasure().isInstance(value)) { return Target.ForSerializedObject.of(target.getType().asErasure(), (Serializable) value); } else { throw new IllegalStateException("Cannot map " + value + " as constant value of " + target.getType()); } } @Override public boolean equals(Object object) { if (this == object) return true; if (object == null || getClass() != object.getClass()) return false; ForUserValue that = (ForUserValue) object; return target.equals(that.target) && annotation.equals(that.annotation) && dynamicValue.equals(that.dynamicValue); } @Override public int hashCode() { int result = target.hashCode(); result = 31 * result + annotation.hashCode(); result = 31 * result + dynamicValue.hashCode(); return result; } @Override public String toString() { return "Advice.Dispatcher.OffsetMapping.ForUserValue{" + "target=" + target + ", annotation=" + annotation + ", dynamicValue=" + dynamicValue + '}'; } /** * A factory for mapping a user-defined dynamic value. * * @param The mapped annotation type. */ protected static class Factory implements OffsetMapping.Factory { /** * The mapped annotation type. */ private final Class type; /** * The dynamic value instance used for resolving a binding. */ private final DynamicValue dynamicValue; /** * Creates a new factory for a user-defined dynamic value. * * @param type The mapped annotation type. * @param dynamicValue The dynamic value instance used for resolving a binding. */ protected Factory(Class type, DynamicValue dynamicValue) { this.type = type; this.dynamicValue = dynamicValue; } /** * Creates a new factory for mapping a user value. * * @param type The mapped annotation type. * @param dynamicValue The dynamic value instance used for resolving a binding. * @return An appropriate factory for such a offset mapping. */ @SuppressWarnings("unchecked") protected static OffsetMapping.Factory of(Class type, DynamicValue dynamicValue) { return new Factory(type, dynamicValue); } @Override public OffsetMapping make(ParameterDescription.InDefinedShape parameterDescription) { AnnotationDescription.Loadable annotation = parameterDescription.getDeclaredAnnotations().ofType(type); return annotation == null ? UNDEFINED : new ForUserValue(parameterDescription, annotation, dynamicValue); } @Override public boolean equals(Object object) { if (this == object) return true; if (object == null || getClass() != object.getClass()) return false; Factory factory = (Factory) object; return type.equals(factory.type) && dynamicValue.equals(factory.dynamicValue); } @Override public int hashCode() { int result = type.hashCode(); result = 31 * result + dynamicValue.hashCode(); return result; } @Override public String toString() { return "Advice.Dispatcher.OffsetMapping.ForUserValue.Factory{" + "type=" + type + ", dynamicValue=" + dynamicValue + '}'; } } } /** * Represents a factory that throws an exception for a given set of illegal parameter annotations. */ class Illegal implements Factory { /** * The set of illegal annotations. */ private final List> annotations; /** * Creates a new factory for restricting the use of illegal annotation types. * * @param annotation The set of illegal annotations. */ //@SafeVarargs protected Illegal(Class... annotation) { this(Arrays.asList(annotation)); } /** * Creates a new factory for restricting the use of illegal annotation types. * * @param annotations The set of illegal annotations. */ protected Illegal(List> annotations) { this.annotations = annotations; } @Override public OffsetMapping make(ParameterDescription.InDefinedShape parameterDescription) { for (Class annotation : annotations) { if (parameterDescription.getDeclaredAnnotations().isAnnotationPresent(annotation)) { throw new IllegalStateException("Illegal annotation " + annotation + " for " + parameterDescription); } } return UNDEFINED; } @Override public boolean equals(Object other) { if (this == other) return true; if (other == null || getClass() != other.getClass()) return false; Illegal illegal = (Illegal) other; return annotations.equals(illegal.annotations); } @Override public int hashCode() { return annotations.hashCode(); } @Override public String toString() { return "Advice.Dispatcher.OffsetMapping.Illegal{" + "annotations=" + annotations + '}'; } } } /** * A suppression handler for optionally suppressing exceptions. */ interface SuppressionHandler { /** * Binds the suppression handler for instrumenting a specific method. * * @return A bound version of the suppression handler. */ Bound bind(); /** * A producer for a default return value if this is applicable. */ interface ReturnValueProducer { /** * Instructs this return value producer to assure the production of a default value for the return type of the currently handled method. */ void onDefaultValue(); } /** * A bound version of a suppression handler that must not be reused. */ interface Bound { /** * Invoked to prepare the suppression handler, i.e. to write an exception handler entry if appropriate. * * @param methodVisitor The method visitor to apply the preparation to. */ void onPrepare(MethodVisitor methodVisitor); /** * Invoked at the start of a method. * * @param methodVisitor The method visitor of the instrumented method. * @param methodSizeHandler A handler for computing the method size requirements. */ void onStart(MethodVisitor methodVisitor, MethodSizeHandler.ForAdvice methodSizeHandler); /** * Invoked at the end of a method. * * @param methodVisitor The method visitor of the instrumented method. * @param stackMapFrameHandler A handler for translating and injecting stack map frames. * @param returnValueProducer A producer for defining a default return value of the advised method. */ void onEnd(MethodVisitor methodVisitor, StackMapFrameHandler.ForAdvice stackMapFrameHandler, ReturnValueProducer returnValueProducer); /** * Invoked at the end of a method. Additionally indicates that the handler block should be surrounding by a skipping instruction. This method * is always followed by a stack map frame (if it is required for the class level and class writer setting). * * @param methodVisitor The method visitor of the instrumented method. * @param stackMapFrameHandler A handler for translating and injecting stack map frames. * @param returnValueProducer A producer for defining a default return value of the advised method. */ void onEndSkipped(MethodVisitor methodVisitor, StackMapFrameHandler.ForAdvice stackMapFrameHandler, ReturnValueProducer returnValueProducer); } /** * A non-operational suppression handler that does not suppress any method. */ enum NoOp implements SuppressionHandler, Bound { /** * The singleton instance. */ INSTANCE; @Override public Bound bind() { return this; } @Override public void onPrepare(MethodVisitor methodVisitor) { /* do nothing */ } @Override public void onStart(MethodVisitor methodVisitor, MethodSizeHandler.ForAdvice methodSizeHandler) { /* do nothing */ } @Override public void onEnd(MethodVisitor methodVisitor, StackMapFrameHandler.ForAdvice stackMapFrameHandler, ReturnValueProducer returnValueProducer) { /* do nothing */ } @Override public void onEndSkipped(MethodVisitor methodVisitor, StackMapFrameHandler.ForAdvice stackMapFrameHandler, ReturnValueProducer returnValueProducer) { /* do nothing */ } @Override public String toString() { return "Advice.Dispatcher.SuppressionHandler.NoOp." + name(); } } /** * A suppression handler that suppresses a given throwable type. */ class Suppressing implements SuppressionHandler { /** * The suppressed throwable type. */ private final TypeDescription suppressedType; /** * Creates a new suppressing suppression handler. * * @param suppressedType The suppressed throwable type. */ protected Suppressing(TypeDescription suppressedType) { this.suppressedType = suppressedType; } /** * Resolves an appropriate suppression handler. * * @param suppressedType The suppressed type or {@link NoExceptionHandler} if no type should be suppressed. * @return An appropriate suppression handler. */ protected static SuppressionHandler of(TypeDescription suppressedType) { return suppressedType.represents(NoExceptionHandler.class) ? NoOp.INSTANCE : new Suppressing(suppressedType); } @Override public SuppressionHandler.Bound bind() { return new Bound(suppressedType); } @Override public boolean equals(Object object) { if (this == object) return true; if (object == null || getClass() != object.getClass()) return false; Suppressing that = (Suppressing) object; return suppressedType.equals(that.suppressedType); } @Override public int hashCode() { return suppressedType.hashCode(); } @Override public String toString() { return "Advice.Dispatcher.SuppressionHandler.Suppressing{" + "suppressedType=" + suppressedType + '}'; } /** * An active, bound suppression handler. */ protected static class Bound implements SuppressionHandler.Bound { /** * The suppressed throwable type. */ private final TypeDescription suppressedType; /** * A label indicating the start of the method. */ private final Label startOfMethod; /** * A label indicating the end of the method. */ private final Label endOfMethod; /** * Creates a new active, bound suppression handler. * * @param suppressedType The suppressed throwable type. */ protected Bound(TypeDescription suppressedType) { this.suppressedType = suppressedType; startOfMethod = new Label(); endOfMethod = new Label(); } @Override public void onPrepare(MethodVisitor methodVisitor) { methodVisitor.visitTryCatchBlock(startOfMethod, endOfMethod, endOfMethod, suppressedType.getInternalName()); } @Override public void onStart(MethodVisitor methodVisitor, MethodSizeHandler.ForAdvice methodSizeHandler) { methodVisitor.visitLabel(startOfMethod); methodSizeHandler.recordMinimum(StackSize.SINGLE.getSize()); } @Override public void onEnd(MethodVisitor methodVisitor, StackMapFrameHandler.ForAdvice stackMapFrameHandler, ReturnValueProducer returnValueProducer) { methodVisitor.visitLabel(endOfMethod); stackMapFrameHandler.injectExceptionFrame(methodVisitor); methodVisitor.visitInsn(Opcodes.POP); returnValueProducer.onDefaultValue(); } @Override public void onEndSkipped(MethodVisitor methodVisitor, StackMapFrameHandler.ForAdvice stackMapFrameHandler, ReturnValueProducer returnValueProducer) { Label endOfHandler = new Label(); methodVisitor.visitJumpInsn(Opcodes.GOTO, endOfHandler); onEnd(methodVisitor, stackMapFrameHandler, returnValueProducer); methodVisitor.visitLabel(endOfHandler); } @Override public String toString() { return "Advice.Dispatcher.SuppressionHandler.Suppressing.Bound{" + "suppressedType=" + suppressedType + ", startOfMethod=" + startOfMethod + ", endOfMethod=" + endOfMethod + '}'; } } } } /** * Represents a resolved dispatcher. */ interface Resolved extends Dispatcher { /** * Binds this dispatcher for resolution to a specific method. * * @param instrumentedMethod The instrumented method. * @param methodVisitor The method visitor for writing the instrumented method. * @param methodSizeHandler A handler for computing the method size requirements. * @param stackMapFrameHandler A handler for translating and injecting stack map frames. * @return A dispatcher that is bound to the instrumented method. */ Bound bind(MethodDescription.InDefinedShape instrumentedMethod, MethodVisitor methodVisitor, MethodSizeHandler methodSizeHandler, StackMapFrameHandler.ForInstrumentedMethod stackMapFrameHandler); /** * Represents a resolved dispatcher for entering a method. */ interface ForMethodEnter extends Resolved { /** * Returns the type that this dispatcher supplies as a result of its advice or a description of {@code void} if * no type is supplied as a result of the enter advice. * * @return The type that this dispatcher supplies as a result of its advice or a description of {@code void}. */ TypeDescription getEnterType(); } /** * Represents a resolved dispatcher for exiting a method. */ interface ForMethodExit extends Resolved { /** * Returns the type of throwable for which this exit advice is supposed to be invoked. * * @return The {@link Throwable} type for which to invoke this exit advice or a description of {@link NoExceptionHandler} * if this exit advice does not expect to be invoked upon any throwable. */ TypeDescription getTriggeringThrowable(); } } /** * A bound resolution of an advice method. */ interface Bound { /** * Prepares the advice method's exception handlers. */ void prepare(); /** * Writes the advice method's code. */ void apply(); } /** * An implementation for inactive devise that does not write any byte code. */ enum Inactive implements Dispatcher.Unresolved, Resolved.ForMethodEnter, Resolved.ForMethodExit, Bound { /** * The singleton instance. */ INSTANCE; @Override public boolean isAlive() { return false; } @Override public boolean isBinary() { return false; } @Override public TypeDescription getTriggeringThrowable() { return NoExceptionHandler.DESCRIPTION; } @Override public TypeDescription getEnterType() { return TypeDescription.VOID; } @Override public Resolved.ForMethodEnter asMethodEnter(List userFactories, ClassFileLocator.Resolution binaryRepresentation) { return this; } @Override public Resolved.ForMethodExit asMethodExitTo(List userFactories, ClassFileLocator.Resolution binaryRepresentation, ForMethodEnter dispatcher) { return this; } @Override public void prepare() { /* do nothing */ } @Override public void apply() { /* do nothing */ } @Override public Bound bind(MethodDescription.InDefinedShape instrumentedMethod, MethodVisitor methodVisitor, MethodSizeHandler methodSizeHandler, StackMapFrameHandler.ForInstrumentedMethod stackMapFrameHandler) { return this; } @Override public String toString() { return "Advice.Dispatcher.Inactive." + name(); } } /** * A dispatcher for an advice method that is being inlined into the instrumented method. */ class Inlining implements Unresolved { /** * The advice method. */ protected final MethodDescription.InDefinedShape adviceMethod; /** * Creates a dispatcher for inlined advice method. * * @param adviceMethod The advice method. */ protected Inlining(MethodDescription.InDefinedShape adviceMethod) { this.adviceMethod = adviceMethod; } @Override public boolean isAlive() { return true; } @Override public boolean isBinary() { return true; } @Override public Dispatcher.Resolved.ForMethodEnter asMethodEnter(List userFactories, ClassFileLocator.Resolution binaryRepresentation) { return new Resolved.ForMethodEnter(adviceMethod, userFactories, binaryRepresentation.resolve()); } @Override public Dispatcher.Resolved.ForMethodExit asMethodExitTo(List userFactories, ClassFileLocator.Resolution binaryRepresentation, Dispatcher.Resolved.ForMethodEnter dispatcher) { return Resolved.ForMethodExit.of(adviceMethod, userFactories, binaryRepresentation.resolve(), dispatcher.getEnterType()); } @Override public boolean equals(Object other) { return this == other || !(other == null || getClass() != other.getClass()) && adviceMethod.equals(((Inlining) other).adviceMethod); } @Override public int hashCode() { return adviceMethod.hashCode(); } @Override public String toString() { return "Advice.Dispatcher.Inlining{" + "adviceMethod=" + adviceMethod + '}'; } /** * A resolved version of a dispatcher. */ protected abstract static class Resolved implements Dispatcher.Resolved { /** * Indicates a read-only mapping for an offset. */ private static final boolean READ_ONLY = true; /** * The represented advice method. */ protected final MethodDescription.InDefinedShape adviceMethod; /** * The binary representation of the advice method. */ private final byte[] binaryRepresentation; /** * An unresolved mapping of offsets of the advice method based on the annotations discovered on each method parameter. */ protected final Map offsetMappings; /** * The suppression handler to use. */ protected final SuppressionHandler suppressionHandler; /** * Creates a new resolved version of a dispatcher. * * @param adviceMethod The represented advice method. * @param factories A list of factories to resolve for the parameters of the advice method. * @param binaryRepresentation The binary representation of the advice method. * @param throwableType The type to handle by a suppression handler or {@link NoExceptionHandler} to not handle any exceptions. */ protected Resolved(MethodDescription.InDefinedShape adviceMethod, List factories, byte[] binaryRepresentation, TypeDescription throwableType) { this.adviceMethod = adviceMethod; offsetMappings = new HashMap(); for (ParameterDescription.InDefinedShape parameterDescription : adviceMethod.getParameters()) { OffsetMapping offsetMapping = OffsetMapping.Factory.UNDEFINED; for (OffsetMapping.Factory factory : factories) { OffsetMapping possible = factory.make(parameterDescription); if (possible != null) { if (offsetMapping == null) { offsetMapping = possible; } else { throw new IllegalStateException(parameterDescription + " is bound to both " + possible + " and " + offsetMapping); } } } offsetMappings.put(parameterDescription.getOffset(), offsetMapping == null ? new OffsetMapping.ForParameter(parameterDescription.getIndex(), READ_ONLY, parameterDescription.getType().asErasure()) : offsetMapping); } this.binaryRepresentation = binaryRepresentation; suppressionHandler = SuppressionHandler.Suppressing.of(throwableType); } @Override public boolean isAlive() { return true; } @Override public Bound bind(MethodDescription.InDefinedShape instrumentedMethod, MethodVisitor methodVisitor, MethodSizeHandler methodSizeHandler, StackMapFrameHandler.ForInstrumentedMethod stackMapFrameHandler) { return new AdviceMethodInliner(instrumentedMethod, methodVisitor, methodSizeHandler, stackMapFrameHandler, suppressionHandler.bind(), new ClassReader(binaryRepresentation)); } /** * Applies a resolution for a given instrumented method. * * @param methodVisitor A method visitor for writing byte code to the instrumented method. * @param methodSizeHandler A handler for computing the method size requirements. * @param stackMapFrameHandler A handler for translating and injecting stack map frames. * @param instrumentedMethod A description of the instrumented method. * @param suppressionHandler The bound suppression handler that is used for suppressing exceptions of this advice method. * @return A method visitor for visiting the advice method's byte code. */ protected abstract MethodVisitor apply(MethodVisitor methodVisitor, MethodSizeHandler methodSizeHandler, StackMapFrameHandler.ForInstrumentedMethod stackMapFrameHandler, MethodDescription.InDefinedShape instrumentedMethod, SuppressionHandler.Bound suppressionHandler); @Override public boolean equals(Object other) { if (this == other) return true; if (other == null || getClass() != other.getClass()) return false; Inlining.Resolved resolved = (Inlining.Resolved) other; return adviceMethod.equals(resolved.adviceMethod) && offsetMappings.equals(resolved.offsetMappings); } @Override public int hashCode() { int result = adviceMethod.hashCode(); result = 31 * result + offsetMappings.hashCode(); return result; } /** * A bound advice method that copies the code by first extracting the exception table and later appending the * code of the method without copying any meta data. */ protected class AdviceMethodInliner extends ClassVisitor implements Bound { /** * The instrumented method. */ private final MethodDescription.InDefinedShape instrumentedMethod; /** * The method visitor for writing the instrumented method. */ private final MethodVisitor methodVisitor; /** * A handler for computing the method size requirements. */ private final MethodSizeHandler methodSizeHandler; /** * A handler for translating and injecting stack map frames. */ private final StackMapFrameHandler.ForInstrumentedMethod stackMapFrameHandler; /** * A bound suppression handler that is used for suppressing exceptions of this advice method. */ private final SuppressionHandler.Bound suppressionHandler; /** * A class reader for parsing the class file containing the represented advice method. */ private final ClassReader classReader; /** * The labels that were found during parsing the method's exception handler in the order of their discovery. */ private List