package net.bytebuddy; import net.bytebuddy.agent.ByteBuddyAgent; import net.bytebuddy.agent.builder.AgentBuilder; import net.bytebuddy.description.annotation.AnnotationDescription; import net.bytebuddy.description.method.MethodDescription; import net.bytebuddy.description.method.ParameterDescription; import net.bytebuddy.description.modifier.Visibility; import net.bytebuddy.description.type.TypeDescription; import net.bytebuddy.dynamic.ClassFileLocator; import net.bytebuddy.dynamic.DynamicType; import net.bytebuddy.dynamic.loading.ClassLoadingStrategy; import net.bytebuddy.dynamic.loading.ClassReloadingStrategy; import net.bytebuddy.dynamic.scaffold.InstrumentedType; import net.bytebuddy.dynamic.scaffold.subclass.ConstructorStrategy; import net.bytebuddy.implementation.*; import net.bytebuddy.implementation.bind.MethodDelegationBinder; import net.bytebuddy.implementation.bind.annotation.*; import net.bytebuddy.implementation.bytecode.ByteCodeAppender; import net.bytebuddy.implementation.bytecode.StackManipulation; import net.bytebuddy.implementation.bytecode.assign.Assigner; import net.bytebuddy.implementation.bytecode.assign.primitive.PrimitiveTypeAwareAssigner; import net.bytebuddy.implementation.bytecode.constant.IntegerConstant; import net.bytebuddy.implementation.bytecode.constant.TextConstant; import net.bytebuddy.implementation.bytecode.member.MethodInvocation; import net.bytebuddy.implementation.bytecode.member.MethodReturn; import net.bytebuddy.pool.TypePool; import net.bytebuddy.test.utility.AgentAttachmentRule; import net.bytebuddy.test.utility.JavaVersionRule; import net.bytebuddy.utility.JavaModule; import org.hamcrest.CoreMatchers; import org.junit.Rule; import org.junit.Test; import org.junit.rules.MethodRule; import org.objectweb.asm.MethodVisitor; import org.objectweb.asm.Opcodes; import java.lang.annotation.Annotation; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.instrument.Instrumentation; import java.lang.reflect.Method; import java.net.URL; import java.net.URLClassLoader; import java.util.Arrays; import java.util.List; import java.util.concurrent.Callable; import static net.bytebuddy.matcher.ElementMatchers.*; import static net.bytebuddy.matcher.ElementMatchers.not; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.CoreMatchers.*; import static org.hamcrest.MatcherAssert.assertThat; import static org.mockito.Mockito.mock; public class ByteBuddyTutorialExamplesTest { private static final String DEFAULT_METHOD_INTERFACE = "net.bytebuddy.test.precompiled.SingleDefaultMethodInterface"; private static final String CONFLICTING_DEFAULT_METHOD_INTERFACE = "net.bytebuddy.test.precompiled.SingleDefaultMethodConflictingInterface"; @Rule public MethodRule javaVersionRule = new JavaVersionRule(); @Rule public MethodRule agentAttachmentRule = new AgentAttachmentRule(); // Other than in the tutorial, the tests use a wrapper strategy for class loading to retain the test's repeatability. @SuppressWarnings("unused") private static void println(String s) { /* do nothing */ } @Test public void testHelloWorld() throws Exception { Class<?> dynamicType = new ByteBuddy() .subclass(Object.class) .method(named("toString")).intercept(FixedValue.value("Hello World!")) .make() .load(getClass().getClassLoader(), ClassLoadingStrategy.Default.WRAPPER) .getLoaded(); assertThat(dynamicType.getDeclaredConstructor().newInstance().toString(), is("Hello World!")); } @Test @SuppressWarnings("unchecked") public void testExtensiveExample() throws Exception { Class<? extends Function> dynamicType = new ByteBuddy() .subclass(Function.class) .method(named("apply")) .intercept(MethodDelegation.to(new GreetingInterceptor())) .make() .load(getClass().getClassLoader(), ClassLoadingStrategy.Default.WRAPPER) .getLoaded(); assertThat(dynamicType.getDeclaredConstructor().newInstance().apply("Byte Buddy"), is((Object) "Hello from Byte Buddy")); } @Test public void testTutorialGettingStartedUnnamed() throws Exception { DynamicType.Unloaded<?> dynamicType = new ByteBuddy() .subclass(Object.class) .make(); assertThat(dynamicType, notNullValue()); } @Test public void testTutorialGettingStartedNamed() throws Exception { DynamicType.Unloaded<?> dynamicType = new ByteBuddy() .subclass(Object.class) .name("example.Type") .make(); assertThat(dynamicType, notNullValue()); } @Test public void testTutorialGettingStartedNamingStrategy() throws Exception { DynamicType.Unloaded<?> dynamicType = new ByteBuddy() .with(new GettingStartedNamingStrategy()) .subclass(Object.class) .make(); assertThat(dynamicType, notNullValue()); Class<?> type = dynamicType.load(getClass().getClassLoader(), ClassLoadingStrategy.Default.WRAPPER).getLoaded(); assertThat(type.getName(), is("i.love.ByteBuddy.Object")); } @Test public void testTutorialGettingStartedClassLoading() throws Exception { Class<?> dynamicType = new ByteBuddy() .subclass(Object.class) .make() .load(getClass().getClassLoader(), ClassLoadingStrategy.Default.WRAPPER) .getLoaded(); assertThat(dynamicType, notNullValue()); } @Test @AgentAttachmentRule.Enforce(redefinesClasses = true) public void testTutorialGettingStartedClassReloading() throws Exception { ByteBuddyAgent.install(); FooReloading foo = new FooReloading(); try { new ByteBuddy() .redefine(BarReloading.class) .name(FooReloading.class.getName()) .make() .load(FooReloading.class.getClassLoader(), ClassReloadingStrategy.fromInstalledAgent()); assertThat(foo.m(), is("bar")); } finally { ClassReloadingStrategy.fromInstalledAgent().reset(FooReloading.class); // Assure repeatability. } assertThat(foo.m(), is("foo")); } @Test public void testTutorialGettingStartedTypePool() throws Exception { TypePool typePool = TypePool.Default.ofClassPath(); ClassLoader classLoader = new URLClassLoader(new URL[0], null); // Assure repeatability. new ByteBuddy().redefine(typePool.describe(UnloadedBar.class.getName()).resolve(), ClassFileLocator.ForClassLoader.ofClassPath()) .defineField("qux", String.class) .make() .load(classLoader, ClassLoadingStrategy.Default.INJECTION); assertThat(classLoader.loadClass(UnloadedBar.class.getName()).getDeclaredField("qux"), notNullValue(java.lang.reflect.Field.class)); } @Test public void testTutorialGettingStartedJavaAgent() throws Exception { new AgentBuilder.Default().type(isAnnotatedWith(Rebase.class)).transform(new AgentBuilder.Transformer() { @Override public DynamicType.Builder<?> transform(DynamicType.Builder<?> builder, TypeDescription typeDescription, ClassLoader classLoader, JavaModule module) { return builder.method(named("toString")).intercept(FixedValue.value("transformed")); } }).installOn(mock(Instrumentation.class)); } @Test public void testFieldsAndMethodsToString() throws Exception { String toString = new ByteBuddy() .subclass(Object.class) .name("example.Type") .make() .load(getClass().getClassLoader(), ClassLoadingStrategy.Default.WRAPPER) .getLoaded() .getDeclaredConstructor() .newInstance() .toString(); assertThat(toString, startsWith("example.Type")); } @Test public void testFieldsAndMethodsDetailedMatcher() throws Exception { assertThat(TypeDescription.OBJECT .getDeclaredMethods() .filter(named("toString").and(returns(String.class)).and(takesArguments(0))).size(), is(1)); } @Test public void testFieldsAndMethodsMatcherStack() throws Exception { Foo foo = new ByteBuddy() .subclass(Foo.class) .method(isDeclaredBy(Foo.class)).intercept(FixedValue.value("Hello World!")) .method(named("foo")).intercept(FixedValue.value("Hello Foo!")) .method(named("foo").and(takesArguments(1))).intercept(FixedValue.value("...")) .make() .load(getClass().getClassLoader(), ClassLoadingStrategy.Default.WRAPPER) .getLoaded() .getDeclaredConstructor() .newInstance(); assertThat(foo.bar(), is("Hello World!")); assertThat(foo.foo(), is("Hello Foo!")); assertThat(foo.foo(null), is("...")); } @Test(expected = IllegalArgumentException.class) public void testFieldsAndMethodsIllegalAssignment() throws Exception { new ByteBuddy() .subclass(Foo.class) .method(isDeclaredBy(Foo.class)).intercept(FixedValue.value(0)) .make(); } @Test public void testFieldsAndMethodsMethodDelegation() throws Exception { String helloWorld = new ByteBuddy() .subclass(Source.class) .method(named("hello")).intercept(MethodDelegation.to(Target.class)) .make() .load(getClass().getClassLoader(), ClassLoadingStrategy.Default.WRAPPER) .getLoaded() .getDeclaredConstructor() .newInstance() .hello("World"); assertThat(helloWorld, is("Hello World!")); } @Test public void testFieldsAndMethodsMethodDelegationAlternatives() throws Exception { String helloWorld = new ByteBuddy() .subclass(Source.class) .method(named("hello")).intercept(MethodDelegation.to(Target2.class)) .make() .load(getClass().getClassLoader(), ClassLoadingStrategy.Default.WRAPPER) .getLoaded() .getDeclaredConstructor() .newInstance() .hello("World"); assertThat(helloWorld, is("Hello World!")); } @Test public void testFieldsAndMethodsMethodSuperCall() throws Exception { MemoryDatabase loggingDatabase = new ByteBuddy() .subclass(MemoryDatabase.class) .method(named("load")).intercept(MethodDelegation.to(LoggerInterceptor.class)) .make() .load(getClass().getClassLoader(), ClassLoadingStrategy.Default.WRAPPER) .getLoaded() .getDeclaredConstructor() .newInstance(); assertThat(loggingDatabase.load("qux"), is(Arrays.asList("qux: foo", "qux: bar"))); } @Test public void testFieldsAndMethodsMethodSuperCallExplicit() throws Exception { assertThat(new LoggingMemoryDatabase().load("qux"), is(Arrays.asList("qux: foo", "qux: bar"))); } @Test @JavaVersionRule.Enforce(8) public void testFieldsAndMethodsMethodDefaultCall() throws Exception { // This test differs from the tutorial by only conditionally expressing the Java 8 types. Object instance = new ByteBuddy(ClassFileVersion.JAVA_V8) .subclass(Object.class) .implement(Class.forName(DEFAULT_METHOD_INTERFACE)) .implement(Class.forName(CONFLICTING_DEFAULT_METHOD_INTERFACE)) .method(named("foo")).intercept(DefaultMethodCall.prioritize(Class.forName(DEFAULT_METHOD_INTERFACE))) .make() .load(getClass().getClassLoader(), ClassLoadingStrategy.Default.WRAPPER) .getLoaded() .getDeclaredConstructor() .newInstance(); Method method = instance.getClass().getMethod("foo"); assertThat(method.invoke(instance), is((Object) "foo")); } @Test public void testFieldsAndMethodsExplicitMethodCall() throws Exception { Object object = new ByteBuddy() .subclass(Object.class, ConstructorStrategy.Default.NO_CONSTRUCTORS) .defineConstructor(Visibility.PUBLIC).withParameters(int.class) .intercept(MethodCall.invoke(Object.class.getDeclaredConstructor())) .make() .load(getClass().getClassLoader(), ClassLoadingStrategy.Default.WRAPPER) .getLoaded() .getDeclaredConstructor(int.class) .newInstance(42); assertThat(object.getClass(), CoreMatchers.not(CoreMatchers.<Class<?>>is(Object.class))); } @Test public void testFieldsAndMethodsSuper() throws Exception { MemoryDatabase loggingDatabase = new ByteBuddy() .subclass(MemoryDatabase.class) .method(named("load")).intercept(MethodDelegation.to(ChangingLoggerInterceptor.class)) .make() .load(getClass().getClassLoader(), ClassLoadingStrategy.Default.WRAPPER) .getLoaded() .getDeclaredConstructor() .newInstance(); assertThat(loggingDatabase.load("qux"), is(Arrays.asList("qux (logged access): foo", "qux (logged access): bar"))); } @Test public void testFieldsAndMethodsRuntimeType() throws Exception { Loop trivialGetterBean = new ByteBuddy() .subclass(Loop.class) .method(isDeclaredBy(Loop.class)).intercept(MethodDelegation.to(Interceptor.class)) .make() .load(getClass().getClassLoader(), ClassLoadingStrategy.Default.WRAPPER) .getLoaded() .getDeclaredConstructor() .newInstance(); assertThat(trivialGetterBean.loop(42), is(42)); assertThat(trivialGetterBean.loop("foo"), is("foo")); } @Test public void testFieldsAndMethodsForwarding() throws Exception { MemoryDatabase memoryDatabase = new MemoryDatabase(); MemoryDatabase loggingDatabase = new ByteBuddy() .subclass(MemoryDatabase.class) .method(named("load")).intercept(MethodDelegation .withDefaultConfiguration() .withBinders(Pipe.Binder.install(Forwarder.class)) .to(new ForwardingLoggerInterceptor(memoryDatabase))) .make() .load(getClass().getClassLoader(), ClassLoadingStrategy.Default.WRAPPER) .getLoaded() .getDeclaredConstructor() .newInstance(); assertThat(loggingDatabase.load("qux"), is(Arrays.asList("qux: foo", "qux: bar"))); } @Test public void testFieldsAndMethodsFieldAccess() throws Exception { ByteBuddy byteBuddy = new ByteBuddy(); Class<? extends UserType> dynamicUserType = byteBuddy .subclass(UserType.class) .defineField("interceptor", Interceptor2.class, Visibility.PRIVATE) .method(not(isDeclaredBy(Object.class))).intercept(MethodDelegation.toField("interceptor")) .implement(InterceptionAccessor.class).intercept(FieldAccessor.ofBeanProperty()) .make() .load(getClass().getClassLoader(), ClassLoadingStrategy.Default.WRAPPER) .getLoaded(); InstanceCreator factory = byteBuddy .subclass(InstanceCreator.class) .method(not(isDeclaredBy(Object.class))).intercept(MethodDelegation.toConstructor(dynamicUserType)) .make() .load(dynamicUserType.getClassLoader(), ClassLoadingStrategy.Default.WRAPPER) .getLoaded() .getDeclaredConstructor() .newInstance(); UserType userType = (UserType) factory.makeInstance(); ((InterceptionAccessor) userType).setInterceptor(new HelloWorldInterceptor()); assertThat(userType.doSomething(), is("Hello World!")); } @Test public void testAttributesAndAnnotationForClass() throws Exception { assertThat(new ByteBuddy() .subclass(Object.class) .annotateType(new RuntimeDefinitionImpl()) .make() .load(getClass().getClassLoader(), ClassLoadingStrategy.Default.WRAPPER) .getLoaded() .isAnnotationPresent(RuntimeDefinition.class), is(true)); } @Test public void testAttributesAndAnnotationForMethodAndField() throws Exception { Class<?> dynamicType = new ByteBuddy() .subclass(Object.class) .method(named("toString")) .intercept(SuperMethodCall.INSTANCE) .annotateMethod(new RuntimeDefinitionImpl()) .defineField("foo", Object.class) .annotateField(new RuntimeDefinitionImpl()) .make() .load(getClass().getClassLoader(), ClassLoadingStrategy.Default.WRAPPER) .getLoaded(); assertThat(dynamicType.getDeclaredMethod("toString").isAnnotationPresent(RuntimeDefinition.class), is(true)); assertThat(dynamicType.getDeclaredField("foo").isAnnotationPresent(RuntimeDefinition.class), is(true)); } @Test public void testCustomImplementationMethodImplementation() throws Exception { assertThat(new ByteBuddy() .subclass(SumExample.class) .method(named("calculate")).intercept(SumImplementation.INSTANCE) .make() .load(getClass().getClassLoader(), ClassLoadingStrategy.Default.WRAPPER) .getLoaded() .getDeclaredConstructor() .newInstance() .calculate(), is(60)); } @Test public void testCustomImplementationAssigner() throws Exception { assertThat(new ByteBuddy() .subclass(Object.class) .method(named("toString")) .intercept(FixedValue.value(42).withAssigner(new PrimitiveTypeAwareAssigner(ToStringAssigner.INSTANCE), Assigner.Typing.STATIC)) .make() .load(getClass().getClassLoader(), ClassLoadingStrategy.Default.WRAPPER) .getLoaded() .getDeclaredConstructor() .newInstance() .toString(), is("42")); } @Test public void testCustomImplementationDelegationAnnotation() throws Exception { assertThat(new ByteBuddy() .subclass(Object.class) .method(named("toString")) .intercept(MethodDelegation.withDefaultConfiguration() .withBinders(StringValueBinder.INSTANCE) .to(ToStringInterceptor.class)) .make() .load(getClass().getClassLoader(), ClassLoadingStrategy.Default.WRAPPER) .getLoaded() .getDeclaredConstructor() .newInstance() .toString(), is("Hello!")); } public enum IntegerSum implements StackManipulation { INSTANCE; @Override public boolean isValid() { return true; } @Override public Size apply(MethodVisitor methodVisitor, Implementation.Context implementationContext) { methodVisitor.visitInsn(Opcodes.IADD); return new Size(-1, 0); } } public enum SumMethod implements ByteCodeAppender { INSTANCE; @Override public Size apply(MethodVisitor methodVisitor, Implementation.Context implementationContext, MethodDescription instrumentedMethod) { if (!instrumentedMethod.getReturnType().asErasure().represents(int.class)) { throw new IllegalArgumentException(instrumentedMethod + " must return int"); } StackManipulation.Size operandStackSize = new StackManipulation.Compound( IntegerConstant.forValue(10), IntegerConstant.forValue(50), IntegerSum.INSTANCE, MethodReturn.INTEGER ).apply(methodVisitor, implementationContext); return new Size(operandStackSize.getMaximalSize(), instrumentedMethod.getStackSize()); } } public enum SumImplementation implements Implementation { INSTANCE; @Override public InstrumentedType prepare(InstrumentedType instrumentedType) { return instrumentedType; } @Override public ByteCodeAppender appender(Target implementationTarget) { return SumMethod.INSTANCE; } } public enum ToStringAssigner implements Assigner { INSTANCE; @Override public StackManipulation assign(TypeDescription.Generic source, TypeDescription.Generic target, Typing typing) { if (!source.isPrimitive() && target.represents(String.class)) { MethodDescription toStringMethod = TypeDescription.OBJECT.getDeclaredMethods() .filter(named("toString")) .getOnly(); return MethodInvocation.invoke(toStringMethod).virtual(source.asErasure()); } else { return StackManipulation.Illegal.INSTANCE; } } } public enum StringValueBinder implements TargetMethodAnnotationDrivenBinder.ParameterBinder<StringValue> { INSTANCE; @Override public Class<StringValue> getHandledType() { return StringValue.class; } @Override public MethodDelegationBinder.ParameterBinding<?> bind(AnnotationDescription.Loadable<StringValue> annotation, MethodDescription source, ParameterDescription target, Implementation.Target implementationTarget, Assigner assigner, Assigner.Typing typing) { if (!target.getType().asErasure().represents(String.class)) { throw new IllegalStateException(target + " makes wrong use of StringValue"); } StackManipulation constant = new TextConstant(annotation.loadSilent().value()); return new MethodDelegationBinder.ParameterBinding.Anonymous(constant); } } public interface Forwarder<T, S> { T to(S target); } @Retention(RetentionPolicy.RUNTIME) private @interface Unsafe { /* empty */ } @Retention(RetentionPolicy.RUNTIME) private @interface Secured { /* empty */ } @SuppressWarnings("unused") public interface Interceptor2 { String doSomethingElse(); } @SuppressWarnings("unused") public interface InterceptionAccessor { Interceptor2 getInterceptor(); void setInterceptor(Interceptor2 interceptor); } @SuppressWarnings("unused") public interface InstanceCreator { Object makeInstance(); } @Retention(RetentionPolicy.RUNTIME) public @interface RuntimeDefinition { } @Retention(RetentionPolicy.RUNTIME) public @interface StringValue { String value(); } private @interface Rebase { } public interface Function { Object apply(Object arg); } public static class GreetingInterceptor { public Object greet(Object argument) { return "Hello from " + argument; } } @SuppressWarnings("unchecked") public static class FooReloading { public String m() { return "foo"; } } @SuppressWarnings("unchecked") public static class BarReloading { public String m() { return "bar"; } } @SuppressWarnings("unused") public static class Account { private int amount = 100; @Unsafe public String transfer(int amount, String recipient) { this.amount -= amount; return "transferred $" + amount + " to " + recipient; } } @SuppressWarnings("unused") public static class Bank { public static String obfuscate(@Argument(1) String recipient, @Argument(0) Integer amount, @Super Account zuper) { //System.out.println("Transfer " + amount + " to " + recipient); return zuper.transfer(amount, recipient.substring(0, 3) + "XXX") + " (obfuscated)"; } } private static class GettingStartedNamingStrategy extends NamingStrategy.AbstractBase { @Override protected String name(TypeDescription superClass) { return "i.love.ByteBuddy." + superClass.getSimpleName(); } } @SuppressWarnings("unused") public static class Foo { public String foo() { return null; } public String foo(Object o) { return null; } public String bar() { return null; } } @SuppressWarnings("unused") public static class Source { public String hello(String name) { return null; } } @SuppressWarnings("unused") public static class Target { public static String hello(String name) { return "Hello " + name + "!"; } } @SuppressWarnings("unused") public static class Target2 { public static String intercept(String name) { return "Hello " + name + "!"; } public static String intercept(int i) { return Integer.toString(i); } public static String intercept(Object o) { return o.toString(); } } public static class MemoryDatabase { public List<String> load(String info) { return Arrays.asList(info + ": foo", info + ": bar"); } } public static class LoggerInterceptor { public static List<String> log(@SuperCall Callable<List<String>> zuper) throws Exception { println("Calling database"); try { return zuper.call(); } finally { println("Returned from database"); } } } @SuppressWarnings("unused") public static class ChangingLoggerInterceptor { public static List<String> log(String info, @Super MemoryDatabase zuper) { println("Calling database"); try { return zuper.load(info + " (logged access)"); } finally { println("Returned from database"); } } } @SuppressWarnings("unused") public static class Loop { public String loop(String value) { return value; } public int loop(int value) { return value; } } @SuppressWarnings("unused") public static class Interceptor { @RuntimeType public static Object intercept(@RuntimeType Object value) { println("Invoked method with: " + value); return value; } } @SuppressWarnings("unused") public static class UserType { public String doSomething() { return null; } } @SuppressWarnings("unused") public static class HelloWorldInterceptor implements Interceptor2 { @Override public String doSomethingElse() { return "Hello World!"; } } private static class RuntimeDefinitionImpl implements RuntimeDefinition { @Override public Class<? extends Annotation> annotationType() { return RuntimeDefinition.class; } } public abstract static class SumExample { public abstract int calculate(); } public static class ToStringInterceptor { public static String makeString(@StringValue("Hello!") String value) { return value; } } private static class UnloadedBar { } public class ForwardingLoggerInterceptor { private final MemoryDatabase memoryDatabase; public ForwardingLoggerInterceptor(MemoryDatabase memoryDatabase) { this.memoryDatabase = memoryDatabase; } public List<String> log(@Pipe Forwarder<List<String>, MemoryDatabase> pipe) { println("Calling database"); try { return pipe.to(memoryDatabase); } finally { println("Returned from database"); } } } @SuppressWarnings("unchecked") class LoggingMemoryDatabase extends MemoryDatabase { @Override public List<String> load(String info) { try { return LoggerInterceptor.log(new LoadMethodSuperCall(info)); } catch (Exception exception) { throw new RuntimeException(exception); } } private class LoadMethodSuperCall implements Callable { private final String info; private LoadMethodSuperCall(String info) { this.info = info; } @Override public Object call() throws Exception { return LoggingMemoryDatabase.super.load(info); } } } }