From d6d06d7174779f6e054411f63b2642fb8fef621d Mon Sep 17 00:00:00 2001 From: Rafael Winterhalter Date: Wed, 2 Nov 2016 01:56:38 +0100 Subject: [PATCH] Added possibility to map field value dynamically and added tests. --- .../main/java/net/bytebuddy/asm/Advice.java | 144 +++++++++++++-- .../bytebuddy/asm/AdviceBoxedFieldTest.java | 170 ++++++++++++++++++ .../java/net/bytebuddy/asm/AdviceTest.java | 24 +++ 3 files changed, 320 insertions(+), 18 deletions(-) create mode 100644 byte-buddy-dep/src/test/java/net/bytebuddy/asm/AdviceBoxedFieldTest.java diff --git a/byte-buddy-dep/src/main/java/net/bytebuddy/asm/Advice.java b/byte-buddy-dep/src/main/java/net/bytebuddy/asm/Advice.java index c27447cbd4a..37f2a72d445 100644 --- a/byte-buddy-dep/src/main/java/net/bytebuddy/asm/Advice.java +++ b/byte-buddy-dep/src/main/java/net/bytebuddy/asm/Advice.java @@ -28,6 +28,7 @@ import java.io.*; import java.lang.annotation.*; import java.lang.reflect.Constructor; +import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.*; @@ -1911,10 +1912,22 @@ protected static void loadInteger(MethodVisitor methodVisitor, int value) { * * @param methodVisitor the method visitor for which to load the value. * @param offset The offset of the primitive value. + * @return The additional padding required on the operand stack. */ - protected void loadBoxed(MethodVisitor methodVisitor, int offset) { + protected int loadBoxed(MethodVisitor methodVisitor, int offset) { methodVisitor.visitVarInsn(load, offset); + return box(methodVisitor); + } + + /** + * Boxes the current value on top of the operand stack. + * + * @param methodVisitor The method visitor to apply the boxing to. + * @return The additional padding required on the operand stack. + */ + protected int box(MethodVisitor methodVisitor) { methodVisitor.visitMethodInsn(Opcodes.INVOKESTATIC, owner, VALUE_OF, boxingDescriptor, false); + return stackSize.getSize() - 1; } /** @@ -1922,21 +1935,25 @@ protected void loadBoxed(MethodVisitor methodVisitor, int offset) { * * @param methodVisitor the method visitor for which to store the value. * @param offset The offset of the primitive value. + * @return The additional padding required on the operand stack. */ - protected void storeUnboxed(MethodVisitor methodVisitor, int offset) { + protected int storeUnboxed(MethodVisitor methodVisitor, int offset) { methodVisitor.visitTypeInsn(Opcodes.CHECKCAST, owner); methodVisitor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, owner, unboxingMethod, unboxingDescriptor, false); methodVisitor.visitVarInsn(store, offset); + return stackSize.getSize() - 1; } /** * Pushes the represented default value as a boxed value onto the operand stack. * * @param methodVisitor The method visitor to apply the changes to. + * @return The additional padding required on the operand stack. */ - protected void pushBoxedDefault(MethodVisitor methodVisitor) { + protected int pushBoxedDefault(MethodVisitor methodVisitor) { methodVisitor.visitInsn(defaultValue); methodVisitor.visitMethodInsn(Opcodes.INVOKESTATIC, owner, VALUE_OF, boxingDescriptor, false); + return stackSize.getSize() - 1; } /** @@ -2050,8 +2067,7 @@ protected static Target of(TypeDefinition typeDefinition) { public int resolveAccess(MethodVisitor methodVisitor, int opcode) { switch (opcode) { case Opcodes.ALOAD: - primitiveDispatcher.pushBoxedDefault(methodVisitor); - return primitiveDispatcher.getStackSize().getSize() - 1; + return primitiveDispatcher.pushBoxedDefault(methodVisitor); case Opcodes.ASTORE: methodVisitor.visitInsn(Opcodes.POP); return NO_PADDING; @@ -2490,6 +2506,66 @@ public String toString() { "}"; } } + + /** + * A target for reading a field where the final value is boxed after reading. + */ + protected static class ReadBoxed extends ForField { + + /** + * Creates a new field mapping for a field that is readable and gets boxed. + * + * @param fieldDescription The field which is mapped by this target mapping. + */ + protected ReadBoxed(FieldDescription.InDefinedShape fieldDescription) { + super(fieldDescription); + } + + /** + * Resolves a read-only target for the field description. + * + * @param instrumentedType The instrumented type. + * @param fieldDescription The field which is mapped by this target mapping. + * @return A target for reading a field where the value is boxed, if required. + */ + protected static Target of(TypeDescription instrumentedType, FieldDescription.InDefinedShape fieldDescription) { + if (!fieldDescription.isStatic() && !instrumentedType.isAssignableTo(fieldDescription.getDeclaringType())) { + throw new IllegalStateException("Cannot access " + fieldDescription + " from " + instrumentedType); + } else if (!fieldDescription.isVisibleTo(instrumentedType)) { + throw new IllegalStateException(fieldDescription + " is not visible from " + instrumentedType); + } + return fieldDescription.getType().isPrimitive() + ? new ReadBoxed(fieldDescription) + : new ReadOnly(fieldDescription); + } + + @Override + public int resolveAccess(MethodVisitor methodVisitor, int opcode) { + return super.resolveAccess(methodVisitor, opcode) + PrimitiveDispatcher.of(fieldDescription.getType()).box(methodVisitor); + } + + @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.ReadBoxed{" + + "fieldDescription=" + fieldDescription + + "}"; + } + } } /** @@ -2667,11 +2743,9 @@ protected ForBoxedArgument(int offset, PrimitiveDispatcher primitiveDispatcher) public int resolveAccess(MethodVisitor methodVisitor, int opcode) { switch (opcode) { case Opcodes.ALOAD: - primitiveDispatcher.loadBoxed(methodVisitor, offset); - return primitiveDispatcher.getStackSize().getSize() - 1; + return primitiveDispatcher.loadBoxed(methodVisitor, offset); case Opcodes.ASTORE: - onStore(methodVisitor); - return NO_PADDING; + return onStore(methodVisitor); default: throw new IllegalStateException("Unexpected opcode: " + opcode); } @@ -2681,8 +2755,9 @@ public int resolveAccess(MethodVisitor methodVisitor, int opcode) { * Handles writing the boxed value if applicable. * * @param methodVisitor The method visitor for which to apply the writing. + * @return The additional required stack size. */ - protected abstract void onStore(MethodVisitor methodVisitor); + protected abstract int onStore(MethodVisitor methodVisitor); @Override public int resolveIncrement(MethodVisitor methodVisitor, int increment) { @@ -2735,7 +2810,7 @@ protected static Target of(int offset, TypeDefinition type) { } @Override - protected void onStore(MethodVisitor methodVisitor) { + protected int onStore(MethodVisitor methodVisitor) { throw new IllegalStateException("Cannot write to read-only boxed parameter"); } @@ -2775,8 +2850,8 @@ protected static Target of(int offset, TypeDefinition type) { } @Override - protected void onStore(MethodVisitor methodVisitor) { - primitiveDispatcher.storeUnboxed(methodVisitor, offset); + protected int onStore(MethodVisitor methodVisitor) { + return primitiveDispatcher.storeUnboxed(methodVisitor, offset); } @Override @@ -5013,7 +5088,7 @@ public Target resolve(TypeDescription instrumentedType, MethodDescription instru 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) + } else if ((value instanceof String && target.getType().asErasure().isAssignableFrom(String.class)) || (target.getType().isPrimitive() && target.getType().asErasure().isInstanceOrWrapper(value))) { if (value instanceof Boolean) { value = (Boolean) value ? 1 : 0; @@ -5025,11 +5100,15 @@ public Target resolve(TypeDescription instrumentedType, MethodDescription instru value = (int) ((Character) value).charValue(); } return new Target.ForConstantPoolValue(value); - } else if (target.getType().asErasure().isAssignableFrom(Class.class) && value instanceof Class) { + } else if (value instanceof Class && target.getType().asErasure().isAssignableFrom(Class.class)) { return new Target.ForConstantPoolValue(Type.getType((Class) value)); - } else if (target.getType().asErasure().isAssignableFrom(Class.class) && value instanceof TypeDescription) { + } else if (value instanceof TypeDescription && target.getType().asErasure().isAssignableFrom(Class.class)) { 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)) { + } else if (value instanceof Field && target.getType().represents(Object.class)) { + return Target.ForField.ReadBoxed.of(instrumentedType, new FieldDescription.ForLoadedField((Field) value)); + } else if (value instanceof FieldDescription && target.getType().represents(Object.class)) { + return Target.ForField.ReadBoxed.of(instrumentedType, ((FieldDescription) value).asDefined()); + } else if (value instanceof Serializable && !target.getType().isPrimitive() && !target.getType().isArray() && 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()); @@ -9370,7 +9449,7 @@ protected WithCustomMapping(Map, DynamicValue> dy } /** - * Binds the supplied annotation to the supplied fixed value. + * Binds the supplied annotation to a type constant of the supplied value. * * @param type The type of the annotation being bound. * @param value The type reference to bind to this annotation. @@ -9382,6 +9461,35 @@ public WithCustomMapping bind(Class type, Ty return bind(type, new DynamicValue.ForFixedValue(value)); } + /** + * Binds the supplied annotation to the value of the supplied field. The field must be visible by the + * instrumented type and must be declared by a super type of the instrumented field. + * + * @param type The type of the annotation being bound. + * @param value The type reference to bind to this annotation. + * @param The annotation type. + * @return A new builder for an advice that considers the supplied annotation type during binding. + * @see DynamicValue.ForFixedValue + */ + public WithCustomMapping bind(Class type, Field value) { + return bind(type, new DynamicValue.ForFixedValue(value)); + } + + + /** + * Binds the supplied annotation to the value of the supplied field. The field must be visible by the + * instrumented type and must be declared by a super type of the instrumented field. + * + * @param type The type of the annotation being bound. + * @param value The type reference to bind to this annotation. + * @param The annotation type. + * @return A new builder for an advice that considers the supplied annotation type during binding. + * @see DynamicValue.ForFixedValue + */ + public WithCustomMapping bind(Class type, FieldDescription value) { + return bind(type, new DynamicValue.ForFixedValue(value)); + } + /** * Binds the supplied annotation to the supplied fixed value. * diff --git a/byte-buddy-dep/src/test/java/net/bytebuddy/asm/AdviceBoxedFieldTest.java b/byte-buddy-dep/src/test/java/net/bytebuddy/asm/AdviceBoxedFieldTest.java new file mode 100644 index 00000000000..7b7ce9c288c --- /dev/null +++ b/byte-buddy-dep/src/test/java/net/bytebuddy/asm/AdviceBoxedFieldTest.java @@ -0,0 +1,170 @@ +package net.bytebuddy.asm; + +import net.bytebuddy.ByteBuddy; +import net.bytebuddy.description.field.FieldDescription; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.dynamic.loading.ClassLoadingStrategy; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.Arrays; +import java.util.Collection; + +import static net.bytebuddy.matcher.ElementMatchers.named; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.Is.is; + +@RunWith(Parameterized.class) +public class AdviceBoxedFieldTest { + + private static final String FOO = "foo"; + + @Parameterized.Parameters + public static Collection data() { + return Arrays.asList(new Object[][]{ + {BooleanValue.class, false}, + {ByteValue.class, (byte) 0}, + {ShortValue.class, (short) 0}, + {CharacterValue.class, (char) 0}, + {IntegerValue.class, 0}, + {LongValue.class, 0L}, + {FloatValue.class, 0f}, + {DoubleValue.class, 0d}, + {ReferenceValue.class, FOO}, + }); + } + + private final Class target; + + private final Object expected; + + public AdviceBoxedFieldTest(Class target, Object expected) { + this.target = target; + this.expected = expected; + } + + @Test + public void testFieldValueAdvice() throws Exception { + Class type = new ByteBuddy() + .redefine(target) + .visit(Advice.withCustomMapping() + .bind(FieldValue.class, target.getDeclaredField(FOO)) + .to(FieldAdvice.class) + .on(named(FOO))) + .make() + .load(ClassLoadingStrategy.BOOTSTRAP_LOADER, ClassLoadingStrategy.Default.WRAPPER) + .getLoaded(); + assertThat(type.getDeclaredMethod(FOO).invoke(type.getDeclaredConstructor().newInstance()), is(expected)); + } + + @Test + public void testFieldDescriptionValueAdvice() throws Exception { + Class type = new ByteBuddy() + .redefine(target) + .visit(Advice.withCustomMapping() + .bind(FieldValue.class, new FieldDescription.ForLoadedField(target.getDeclaredField(FOO))) + .to(FieldAdvice.class) + .on(named(FOO))) + .make() + .load(ClassLoadingStrategy.BOOTSTRAP_LOADER, ClassLoadingStrategy.Default.WRAPPER) + .getLoaded(); + assertThat(type.getDeclaredMethod(FOO).invoke(type.getDeclaredConstructor().newInstance()), is(expected)); + } + + public static class FieldAdvice { + + @Advice.OnMethodExit + static void foo(@FieldValue Object value, @Advice.Return(readOnly = false) Object returned) { + returned = value; + } + } + + @Retention(RetentionPolicy.RUNTIME) + public @interface FieldValue { + /* empty */ + } + + public static class BooleanValue { + + boolean foo; + + public Object foo() { + return null; + } + } + + public static class ByteValue { + + byte foo; + + public Object foo() { + return null; + } + } + + public static class ShortValue { + + short foo; + + public Object foo() { + return null; + } + } + + public static class CharacterValue { + + char foo; + + public Object foo() { + return null; + } + } + + public static class IntegerValue { + + int foo; + + public Object foo() { + return null; + } + } + + public static class LongValue { + + long foo; + + public Object foo() { + return null; + } + } + + public static class FloatValue { + + float foo; + + public Object foo() { + return null; + } + } + + public static class DoubleValue { + + double foo; + + public Object foo() { + return null; + } + } + + public static class ReferenceValue { + + Object foo = FOO; + + public Object foo() { + return null; + } + } +} diff --git a/byte-buddy-dep/src/test/java/net/bytebuddy/asm/AdviceTest.java b/byte-buddy-dep/src/test/java/net/bytebuddy/asm/AdviceTest.java index d32526a5c20..4010abbe7a0 100644 --- a/byte-buddy-dep/src/test/java/net/bytebuddy/asm/AdviceTest.java +++ b/byte-buddy-dep/src/test/java/net/bytebuddy/asm/AdviceTest.java @@ -1205,6 +1205,22 @@ public void testPrimitiveNonAssignableCasting() throws Exception { } } + @Test(expected = IllegalStateException.class) + public void testInvisibleField() throws Exception { + new ByteBuddy() + .redefine(SampleExtension.class) + .visit(Advice.withCustomMapping().bind(Custom.class, Sample.class.getDeclaredField("object")).to(SampleExtension.class).on(named(FOO))) + .make(); + } + + @Test(expected = IllegalStateException.class) + public void testNonRelatedField() throws Exception { + new ByteBuddy() + .redefine(TracableSample.class) + .visit(Advice.withCustomMapping().bind(Custom.class, Sample.class.getDeclaredField("object")).to(SampleExtension.class).on(named(FOO))) + .make(); + } + @Test public void testUserTypeValue() throws Exception { Class type = new ByteBuddy() @@ -1937,6 +1953,14 @@ public String foobaz() { } } + public static class SampleExtension extends Sample { + + @Advice.OnMethodEnter + static void foo(@Custom Object value) { + throw new AssertionError(value); + } + } + public static class TracableSample { public static int enter, exit, inside;