Byte Buddy官方教程(三) — 字段和方法

此翻译已经合并到Byte Buddy官网,请去官网阅读最新版的文档。
https://bytebuddy.net/#/tutorial-cn

在之前的章节中,我们创建的大多数类型没有定义任何字段和方法。然而,通过子类化Object,创建的类会继承它的超类定义的方法。让我们验证这个细节并在动态类的实例上调用toString方法。我们可以通过反射调用创建的类的构造器来获取该类的实例。

String toString = new ByteBuddy()
  .subclass(Object.class)
  .name("example.Type")
  .make()
  .load(getClass().getClassLoader())
  .getLoaded()
  .newInstance() // Java reflection API
  .toString();

Object#toString方法的实现返回实例的全限定类名和实例的16进制表示的哈希码的拼接。实际上,调用创建的类的toString方法会返回类似于example.Type@340d1fa5的字符串。

当然,这里我们还没有讲完。创建动态类的主要目的是定义新的逻辑。让我们从简单的例子开始,以演示这是怎么实现的。我们想要覆写toString方法,并返回Hello World!,而不是返回默认值:

String toString = new ByteBuddy()
  .subclass(Object.class)
  .name("example.Type")
  .method(named("toString")).intercept(FixedValue.value("Hello World!"))
  .make()
  .load(getClass().getClassLoader())
  .getLoaded()
  .newInstance()
  .toString();

添加代码中的那行包含了Byte Buddy领域特定语言的两条指令。第一个指令是Method,它允许我们覆写任何方法。选择通过移交 ElementMatcher(元素匹配器)被应用,该匹配器决定哪一个方法被覆写。Byte Buddy自带许多预定义的方法匹配器,这些被收集在ElementMatchers类中。通常,你将静态导入这个类,这样最终的代码读起来更加自然。对于上面的示例,假设了这样的静态导入,其中我们使用了named方法匹配器,它通过确切的名称来选择方法。请注意:预定义的方法匹配器是可以组合的。这样,我们可以更详细的描述方法选择,例如:

named("toString").and(returns(String.class)).and(takesArguments(0))

后面的方法选择器通过其完整的Java签名来描述toString方法,因此只能匹配到特定方法。然而,在给定的上下文中,我们知道没有其他名称为toString且签名不同的方法,这样的话原来的方法匹配器就够用了。

在选择了toString方法后,第二条指令intercept决定方法的实现,该实现将覆写所有选择的方法。为了知道如何实现一个方法,这个指令需要一个Implementation类型的参数。在上面的示例中,我们使用Byte Buddy自带的FixedValue实现。正如这个类名所表明的,这个实现实现了一个总是返回给定值的方法。我们将在本节稍后部分更详细的了解FixedValue的实现。现在,让我们仔细地看一下方法选择。

目前为止,我们仅仅拦截了一个方法。然而,在实际的应用中,事情可能会更复杂,我们可能想用不同的规则来覆写方法。让我们看一个这种场景的示例:

class Foo {
  public String bar() { return null; }
  public String foo() { return null; }
  public String foo(Object o) { return null; }
}
 
Foo dynamicFoo = new ByteBuddy()
  .subclass(Foo.class)
  .method(isDeclaredBy(Foo.class)).intercept(FixedValue.value("One!"))
  .method(named("foo")).intercept(FixedValue.value("Two!"))
  .method(named("foo").and(takesArguments(1))).intercept(FixedValue.value("Three!"))
  .make()
  .load(getClass().getClassLoader())
  .getLoaded()
  .newInstance();

在上面的示例中,我们定义了三条不同的规则来覆写方法。当研究代码时,你会注意到:第一条规则涉及Foo类中定义的任何方法,也就是示例类中的所有(三个)方法。第二条规则匹配所有名称为foo的方法,是之前选择的方法的子集。最后一个规则匹配 foo(Object) 方法,这个比之前的选择范围进一步减少了。但是,考虑到这种选择重叠,Byte Buddy是如何决定哪个规则应用于哪个方法的?

Byte Buddy以栈的形式组织覆写方法的规则。这意味着每当你注册一条用于覆写方法的新规则时,它将被压入栈顶,并且始终首先应用,直到添加一条新规则,该规则将有更高的优先级。对于上面的示例,这意味着:

  • 方法bar()第一次被named("foo").and(takesArguments(1))匹配,之后与named("foo") 匹配,这两个都没有匹配上。最后,isDeclaredBy(Foo.class)匹配器匹配成功覆写bar()方法,返回 One!
  • 类似地,foo()第一次匹配被named("foo").and(takesArguments(1)) 匹配,该匹配缺少参数导致不能成功。之后named("foo")匹配成功并覆写返回Two!
    -foo(Object)立即匹配到named("foo").and(takesArguments(1)),方法覆写后返回Three!

因为这样的组织,你应该始终最后注册更具体的方法匹配器。否则,之后注册的任何不太具体的方法匹配器可能会阻止你之前定义的规则被应用。注意,ByteByddy设置允许定义一个ignoreMethod 属性,与该方法匹配器成功匹配的方法永远不会被覆写。默认,Byte Buddy不会覆写任何synthetic方法。

在一些场景中,你或许想定义一个新方法,这个方法不会覆写超类或接口的方法。这个可以使用Byte Buddy实现。为此,你可以调用defineMethod 来定义一个方法签名。在定义方法后,你需要提供一个实现,如同方法匹配器标识的方法。注意,在方法定义后注册的方法匹配器可能会通过我们之前讨论过的堆积(stacking)原则取代该实现。

Byte Buddy允许用defineField 为给定的类定义字段。在Java中,字段不会被覆写,只能被隐藏。因此,没有字段匹配或者类似功能。

带着这些关于如何选择方法的知识,我们已经准备好学习如何实现这些方法。为此,我们现在看一下Byte Buddy自带的预定义Implementation 实现。定义自定义实现在它自己的章节中讨论,这里仅适用于需要完全自定义方法实现的用户。

深究fixed values(固定值)

我们在实战中已经看到了FixedValue实现。顾名思义,通过FixedValue 实现的方法会简单地返回一个给定的对象。一个类能以两种不同的方式记住这个对象:

  • The fixed value(固定值)被写入到类的常量池。 常量池是Java类文件格式中的一部分,它包含大量的无状态的值,用来描述任何类的属性。常量池主要用来记住类的属性,比如类名称或者方法名称。除了这些反映的属性外,常量池还有空间用来保存在类中的方法或者字段中用到的任何字符串或基本类型的值。除了字符串和基本类型的值以外,常量池还保存了其他类型的引用。
  • 保存在静态字段中的值。但是,为了实现这点,一旦类加载到JVM,这个字段必须赋予给定的值。为此,每一个动态创建的类都附带一个TypeInitializer,它可以配置执行这样的显式初始化。当 DynamicType.Unloaded被加载时,Byte Buddy会自动触发其类型初始化器,这样类就可以准备被使用了。因此,你通常不需要担心类型的初始化器。然而,如果你想在Byte Buddy之外加载动态类,在这些类加载之后手动运行它们的类初始化器是非常重要的。否则,FixedValue实现将返null 而不是需要的值,因为这个静态字段从来就没有被赋此值。然而,多数动态类型或许都不需要显式地初始化。因此,通过调用它的isAlive方法,类的初始化器会被查询它的存活状态。如果你需要手动触发一个 TypeInitializer(类型初始化器),你会发现它被DynamicType接口所暴露。

当你通过FixedValue#value(Object)实现一个方法时,Byte Buddy会分析参数的类型,并在可能的情况下将这个参数的值定义到动态类的常量池中,否则将值存储到静态字段中。然而,需要注意,如果该值存储在常量池中,选中的方法返回的实例可能具有不同的对象身份。因此,你可以指示Byte Buddy总是通过用FixedValue#reference(Object) 将对象保存在静态字段中。后一种方法被重载了reference(Object fixedValue, String fieldName),这样你就可以提供字段的名称作为第二个参数。否则,字段名称会自动从对象的哈希码中提取。这种行为的一个例外是null值。null 值从不会保存在字段中,而是简单地由其字面上的表达式表示。

你或许想知道在这个上下文中的类型安全。显然,你可以定义一个返回无效值的方法:

new ByteBuddy()
  .subclass(Foo.class)
  .method(isDeclaredBy(Foo.class)).intercept(FixedValue.value(0))
  .make();

在Java的类型系统中,编译器很难去阻止这种无效的实现。相反,当类型被创建并且非法的赋值(将一个整形赋值给一个返回String类型的方法)生效时,Byte Buddy将抛出一个IllegalArgumentException 异常。Byte Buddy会尽最大努力去保证它创建的所有类型都是合法的Java类型,并且通过在创建非法的类型时抛出异常而快速失败。

Byte Buddy的赋值行为是可以自定义的。同样,Byte Buddy只提供一个健全的默认值,它模仿了Java编译器的赋值行为。因此,Byte Buddy允许将一个类型赋值给它的任何超类,并且还考虑到将基本类型的值装箱或拆箱。然而,需要注意,Byte Buddy目前没有完全支持泛型类型,并且只考虑类型的擦除。因此,Byte Buddy可能会造成堆污染(heap pollution)。你可以实现自己的 Assigner(赋值器),而不是使用预定义的,该赋值器能够进行Java编程语言中非隐式的类型转换。我们将在本教程的最后一节中研究这种自定义实现。现在,我们只需要提及你可以在任何FixedValue 上通过调用withAssigner来定义这样的自定义赋值器。

委托方法调用

在大多数情况下,方法返回一个固定值当然是不够的。为了更好的灵活性,Byte Buddy提供了 MethodDelegation(方法委托) 实现,它在对方法调用做出反应时提供最大程度的自由。一个方法委托定义了动态创建的类方法,到另外一个可能存在于动态类型之外的方法的任何调用。这样,动态类的逻辑可以用简单的Java表示,仅通过代码生成就可以与另外的方法绑定。在讨论细节之前,我们来看一下使用MethodDelegation的示例:

class Source {
  public String hello(String name) { return null; }
}
 
class Target {
  public static String hello(String name) {
    return "Hello " + name + "!";
  }
}
 
String helloWorld = new ByteBuddy()
  .subclass(Source.class)
  .method(named("hello")).intercept(MethodDelegation.to(Target.class))
  .make()
  .load(getClass().getClassLoader())
  .getLoaded()
  .newInstance()
  .hello("World");

在示例中,我们将Source#hello(String)方法的调用委托给Target类型,这样方法就会返回Hello World!,而不是null。为此,MethodDelegation实现识别Target 类的任何方法调用并且在这些方法中找出一个最匹配的。在上面的实例中,这是微不足道的,因为 Targe 类型只定义了一个静态方法,其中方法的参数、返回类型和名称与方法Source#name(String) 的相同,很容易识别。

实际上,委托方法的决定很可能会更复杂。那么,Byte Buddy在这些方法之间是如果选择的呢?为此,我们假定Target类被如下定义:

class Target {
  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(); }
}

你或许已经注意到上面的方法现在都被称为intercept。Byte Buddy不需要目标方法被命名为与原方法名称一样。我们很快就会仔细研究这个问题。更重要的是,如果你使用定义更改后的Target去运行之前的实例,你将观察到name(String)方法被绑定到了 intercept(String)。但是,为什么会这样?
显然,intercept(int)方法不会接收源方法的String 参数,因此不会被视为可能的匹配。但是对于 intercept(Object)方法,该方法从参数形式上看是可以绑定的,但是最终没有绑定。为了解决这种歧义,Byte Buddy再次模拟Java编译器,选择绑定参数类型最明确的的方法。因为,StringObject 更明确,所以在这三个选项中,intercept(String) 方法最终被选择。

根据目前的信息,你可能会认为方法绑定算法具有相当严格的性质。然而,我们还没有讲完。目前我们只观察了两一个约定由于配置原则的示例。如果默认的配置不满足实际需求,这个原则对于改变是开放的。实际上,MethodDelegation实现与注解一起工作,其中参数的注解决定应该为其分配什么值。但是,如果没有发现注解,Byte Buddy会将参数看作带了@Argument注解。后面的注解会导致Byte Buddy将源方法的第 n个参数分配给注解的目标方法。当注解没有显式添加时,注解n 的值会被设置为参数的索引顺序。通过这个规则,Byte Buddy将下面的代码

void foo(Object o1, Object o2)

看作如同下面带注解的方法:

void foo(@Argument(0) Object o1, @Argument(1) Object o2)

结果,被检测方法的第一个和第二个参数被分配给拦截器。如果被拦截的方法声明的参数少于两个,或者带注释的参数类型不能从检测方法的参数类型中分配,则相关拦截器方法将被丢弃。

除了@Argument注解,还有几个其他预定义的注解可以用于MethodDelegation:

  • 带有@AllArgument注解的参数必须是数组类型,并分配一个包含所有源方法参数的数组。为此,所有源方法的参数对数组的组成类型必须是可分配的。如果不是这种情况,则当前的目标方法不会被认为是可以绑定到源方法的候选方法。
  • @This注解会导致动态类型的实例分配,在这个实例上当前的拦截方法会被调用。如果被注解的参数不可分配给一个动态类型的实例,则当前方法不会被认为是可以绑定到源方法的候选方法。注意,在这个实例上的任何方法调用将会导致可能调用检测方法实现。对于调用覆写实现,你需要使用@Super注解,这个将在下面讨论。使用@This注解的一个典型原因是获取一个实例字段的访问权限。
  • 带有@Orign注解的参数必须在任何类型上是有用的,MethodConstructorExecutableClassMethodHandleMethodTypeString或者int。根据参数的类型,它被分配一个方法构造器对现在检测的原始方法和构造器的引用,或者动态创建的Class(类)的引用。当使用Java8时,还可以通过在拦截器中使用Executable类型来接收方法和构造器的引用。如果被注解的参数是一个String,则参数将分配的值为Mehtod(方法)toString方法返回的值。通常,我们建议尽可能将这些String值作为方法的标识符,而且不鼓励使用Method对象,因为它们的查找会引入显著的运行时开销。为了避免这个开销,@Origin注解还提供了一个用于缓存这些实例以供复用的属性。注意,MethodHandleMethodType存储在类的常量池中,使用这些常量的类必须至少是Java7版本。我们还建议使用稍后会讨论到的@Pipe注解,而不是使用反射来反射性地调用另一个对象上的被拦截的方法。当在类型为int的参数上使用@Origin注解时,会被分配检测方法的修饰符。

除了使用预定义的注解,Byte Buddy还允许通过注册一个或几个ParameterBinder来定义自己的注解。我们将在本教程的最后一节研究这种自定义。

除了我们目前讨论过的四个注解之外,还存在两个其它的预定义注解,它们可以授予对动态类型方法的超类实现的访问权限。这样,动态类型可以给一个类添加切面,例如方法调用的日志。用@SuperCall注解,一个方法的超类方法调用甚至可以从动态类的外部被执行。如下示例所示:

class MemoryDatabase {
  public List<String> load(String info) {
    return Arrays.asList(info + ": foo", info + ": bar");
  }
}
 
class LoggerInterceptor {
  public static List<String> log(@SuperCall Callable<List<String>> zuper)
      throws Exception {
    System.out.println("Calling database");
    try {
      return zuper.call();
    } finally {
      System.out.println("Returned from database");
    }
  }
}
 
MemoryDatabase loggingDatabase = new ByteBuddy()
  .subclass(MemoryDatabase.class)
  .method(named("load")).intercept(MethodDelegation.to(LoggerInterceptor.class))
  .make()
  .load(getClass().getClassLoader())
  .getLoaded()
  .newInstance();

从上面的例子中,很明显通过注入某个Callable类型的实例到LoggerInterceptor来调用超类方法,这个Callable实例从它的call方法调用原始的没有被覆写的MemoryDatabase#load(String)实现。这个辅助类在Byte Buddy的术语中被称为AuxiliaryType(辅助类型)。这个辅助类型由Byte Buddy根据需要创建,并且可以在类创建后直接从DynamicType(动态类型)接口直接访问。由于这个辅助类型,手动创建一个动态类可能会导致一些附加的类被创建,这有助于源类的实现。最后,注意@SuperCall注解也能用于Runnable类型,其中源方法的返回值会被丢弃

你可能仍然好奇这个辅助类为何能调用另一个类的超类方法,这在Java中通常是禁止的。然而,仔细观察,这种行为相当普遍,类似于以下的Java代码片段被编译时生成的编译代码。

class LoggingMemoryDatabase extends MemoryDatabase {
 
  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);
    }
  }
 
  @Override
  public List<String> load(String info) {
    return LoggerInterceptor.log(new LoadMethodSuperCall(info));
  }
}

但是,有时你可能想调用超类方法,它(超类方法)带有与在源方法的调用中分配的参数不同的参数。在Byte Buddy中通过使用@Super注解也能实现。这个注解会触发另一个辅助类型的创建,它会继承超类或动态类型的接口。与之前类似,这个辅助类会覆写所有方法以在动态类型上调用超类方法的实现。这样,上面的日志拦截器示例可以被实现来更改实际调用。

class ChangingLoggerInterceptor {
  public static List<String> log(String info, @Super MemoryDatabase zuper) {
    System.out.println("Calling database");
    try {
      return zuper.load(info + " (logged access)");
    } finally {
      System.out.println("Returned from database");
    }
  }
}

注意,分配给带有@Super注解的参数的实例和真正动态类型的实例具有不同的身份!因此,没有通过参数访问的实例字段能反应出实际的实例字段。此外,辅助实例的不可覆写方法不会委托它们的调用,而是保留原来的实现,这会在它们被调用时产生荒谬的行为。最后,假使一个带有@Super注解的参数不能代表相关动态类的超类,则该方法不会被认为是其任何方法的绑定目标。

因为@Super注解允许任何类型使用,所以我们可能需要提供这个类型如何被构造的信息。默认情况下,Byte Buddy尝试用类的默认构造器。这总是适用于隐式继承Object类型的接口。然而,当继承一个动态类型的超类时,这个类或许不能提供一个默认的构造器。如果是这种情况,或者如果一个特定的构造器应该被用来创建这种辅助类,则@Super注解允许通过设置其参数类型作为注解的constructorParameters(构造器参数)属性来识别不同构造器。这个构造器将会通过给每个参数分配相应的默认值被调用。或者,也可以使用Super.Instantiation.UNSAFE策略来创建类,这将使用Java内置的类来创建辅助类型而不是调用任何构造器。然而,注意,这种策略不一定能移植到非Oracle的JVM上,并且可能会在未来发行的JVM里不可用。至今,通过这种不安全的实例化策略被使用的内部类在几乎所有的JVM实现中都可以找到。

此外,你可能已经注意到上面的LogInterceptor声明了一个检查异常。另一方面,调用此方法的被检测的源方法没有声明任何检查异常。通常,Java编译器会拒绝编译这样的调用。然而,和编译器相比,Java运行时不会区别对待检查异常和未检查异常,且允许这样的调用。鉴于这个原因,我们决定忽略检查异常并在它们的使用中授予完全的灵活性。然而,当从动态创建的方法中抛出未检查异常时要小心,因为遇到此类异常可能会使应用的用户困惑。

方法委托模型中的另一个警告可能会引起你的注意。虽然静态类型对于实现方法是非常好的,但是严格的类型会限制代码的复用。要理解为什么,考虑一下下面的示例:

class Loop {
  public String loop(String value) { return value; }
  public int loop(int value) { return value; }
}

由于上述类的方法描述了两个类似的带有不兼容类型的签名,你将不能通过一个拦截器方法去检测这两个方法。相反,你必须提供两个具有不同签名的不同的目标方法,来满足静态类型检查。为了克服这个限制,Byte Buddy允许给方法和方法参数添加@RuntimeType注解,它指示Byte Buddy终止严格类型检查以支持运行时类型转换:

class Interceptor {
  @RuntimeType
  public static Object intercept(@RuntimeType Object value) {
    System.out.println("Invoked method with: " + value);
    return value;
  }
}

用上面的目标方法,我们现在可以为两个源方法提供单个拦截方法。注意,Byte Buddy可以对基本类型装箱和开箱。然而,要意识到使用@RuntimeType注解是以放弃类型安全为代价的,如果你将不兼容的类型混淆,可能会以ClassCastException(类转换异常)结束。

@SuperCall相同,Byte Buddy有一个@DefaultCall注解,它允许调用默认方法而不是调用方法的超类方法。如果拦截的方法实际上由被检测类型直接实现的接口声明为默认方法时,才考虑使用此参数注解的方法进行绑定。类似地,如果检测方法没有定义一个非抽象的超类方法,@SuperCall注解会阻止方法的绑定。但是,如果你想在特定的类型上调用一个默认的方法,你可以指定@DefaultCalltargetType(目标类型)参数为特定接口。用这个指定,Byte Buddy会注入一个代理实例,它可以调用给定接口的默认方法,如果这个方法存在的话。否则,带有参数注解的目标方法不会被认为是一个委托目标。显然,默认方法调用只在Java8或以上版本定义的类适用。类似地,除了@Super注解,还有一个@Default注解,它会注入一个代理以显式地调用特定的默认方法。

我们已经提到过你可以定义并注册自定义的注解与任何MethodDelegation一起使用。Byte Buddy自带一个可以使用但需要显示地安装和注册的注解。通过使用@Pipe注解,你可以将一个拦截的方法调用转发到另一个对象。@Pipe注解没有预先注册,因为Java类库在Java8之前没有一个合适的接口类型,Java8定义了Function(函数)类型。所以,你需要显式地提供一个只有一个非静态方法的类型,这个方法以Object为参数,返回另一个Object为结果。注意你仍然可以使用泛型类型,只要方法的类型通过Object类型被绑定。当然,如果你正在使用Java8,Function类型是一个更可行的选择。当在一个参数(parameter)的参数(argument)上调用方法时,Byte Buddy会将这个参数(parameter)转换为方法的声明类型,并且用相同的参数调用拦截的方法,就像调用源方法一样。在我们看一个示例之前,让我们定义一个自定义类型,你可以在Java5及以上版本中使用它:

interface Forwarder<T, S> {
  T to(S target);
}

用这个类,我们现在可以通过将方法调用转发到现有实例来实现记录访问上面的MemoryDatabase的新方案:

class ForwardingLoggerInterceptor {
 
  private final MemoryDatabase memoryDatabase; // constructor omitted
 
  public List<String> log(@Pipe Forwarder<List<String>, MemoryDatabase> pipe) {
    System.out.println("Calling database");
    try {
      return pipe.to(memoryDatabase);
    } finally {
      System.out.println("Returned from database");
    }
  }
}
 
MemoryDatabase loggingDatabase = new ByteBuddy()
  .subclass(MemoryDatabase.class)
  .method(named("load")).intercept(MethodDelegation.withDefaultConfiguration()
    .withBinders(Pipe.Binder.install(Forwarder.class)))
    .to(new ForwardingLoggerInterceptor(new MemoryDatabase()))
  .make()
  .load(getClass().getClassLoader())
  .getLoaded()
  .newInstance();

在上面的示例中,我们只转发了我们本地创建的实例的调用。然而,通过子类化一个类型来拦截一个方法的优势在于,这种方法允许增强一个存在的实例。此外,你通常会在实例级别注册拦截器,而不是在类级别注册一个静态拦截器。

目前为止,我们已经看到了大量的MethodDelegation(方法委托)实现。但是,在我们继续之前,我们想更详细了解Byte Buddy是怎样选择一个目标方法的。我们已经描述了Byte Buddy如何通过比较参数类型来解析更明确地方法,但还有更多内容。在Byte Buddy确定有资格绑定到给定源方法的候选方法后,它将解析委托给AmbiguityResolver(歧义解析器)s链。同样,你可以自由实现自己的歧义解析器,它可以补充甚至替代Byte Buddy的默认解析器。如果没有此类更改,歧义解析器链会尝试通过应用下面具有相同顺序的规则来识别一个唯一的目标方法:

  • 可以通过添加@BindingPriority注解给方法分配明确地优先级。如果一个方法优先级高于另一个方法,高优先级的方法总是优先于低优先级的方法。另外,带有@IgnoreForBinding注解的方法永远不会被视为目标方法。
  • 如果源方法和目标方法有一个相同的名称,则该目标方法优先于其它与源方法不同名的方法。
  • 如果两个方法通过使用@Argument注解绑定源方法的相同参数,则有更确切的参数类型的方法将会被考虑。在这种情况下,显式地或不注解参数隐式地提供一个注解并不重要。解析的算法类似于Java编译器解析重载方法的调用。如果两种类型都是明确的,则绑定更多参数的方法被视为目标方法。如果在解析阶段参数(parameter)应该被分配参数(argument)而不考虑参数(parameter)的类型,则可以通过设置注解的bindingMechanic属性为BindingMechanic.ANONYMOUS。此外,注意,非匿名参数需要在每个目标方法上的每个索引值都是唯一的,才能使解析算法起作用。
  • 如果一个目标方法比另一个目标方法参数多,则前一种方法优于后一种方法。

目前为止,我们仅当在MethodDelegation.to(Target.class)中通过命名特定类来将方法调用委托给一个静态方法。然而也可以委托给实例方法和构造器:

  • 通过调用MethodDelegation.to(new Target()),可以将方法调用委托给Target类的任何实例方法。注意,这包含实例的类继承层次中任何位置定义的方法,包含Object类中定义的方法。你或许想通过在任何MethodDelegation(方法委托)上调用filter(ElementMatcher)来将过滤器应用到方法委托上,从而限制候选方法的范围。这个ElementMatcher(元素匹配器)类型与之前用于在Byte Buddy领域特定语言中选择源方法的类型相同。方法委托目标的实例存储在静态字段中。类似于固定值的定义,这需要定义TypeInitializer(类型初始化器)
    或者,你可以通过MethodDelegation.toField(String)定义任何字段的使用,其中参数指定一个字段名称,所有方法委托都会转发到这个指定的字段,而不是将委托存储在静态字段中。始终记住,在此动态实例上调用方法之前,给这个字段分配一个值。否则,方法委托会导致空指针异常
  • 方法委托可用于构造给定类型的实例。通过使用MethodDelegation.toConstructor(Class),拦截方法的任何调用将返回一个给定的目标类型的实例。

正如你刚才了解的,MethodDelegation会检查注解以调整它的绑定逻辑。这些注解对于Byte Buddy是确定的,但这并不意味着带注解的类以任何形式依赖Byte Buddy。相反,Java运行时只是忽略当加载类时在类路径找不到的注解类型。这意味着在动态类创建后不再需要Byte Buddy,同时意味着,即使Byte Buddy没有在类路径上,你也可以在另一个JVM进程中加载动态类和委托其方法调用的类。

这里有几个预定义的注解可以和我们只想简要命名的MethodDelegation一起使用。如果你想要阅读更多关于这些注解的信息,你可以在代码内的文档中找到更多的信息。这些注解是:

  • @Empty:应用此注解,Byte Buddy会注入参数(parameter)类型的默认值。对于基本类型,这相当于零值,对于引用类型,值为null。使用该注解是为了避免拦截器的参数。
  • @StubValue:使用此注解,注解的参数将注入拦截方法的存根值。对于reference-return-types(返回引用类型)和void的方法,会注入null。对于返回基本类型的方法,会注入相等的0的包装类型。当使用@RuntimType注解定义一个返回Object类型的通用拦截器时,结合使用可能会非常有用。通过返回注入的值,该方法在合适地被视为基本返回类型时充当从根。
  • @FieldValue:此注解在检测类的类层次结构中定位一个字段并且将字段值注入到注解的参数中。如果没有找到注解参数兼容的可见字段,则目标方法不会被绑定。
  • @FieldProxy:使用此注解,Byte Buddy会为给定字段注入一个accessor(访问器)。如果拦截的方法表示此类方法,被访问的字段可以通过名称显式地指定,也可以从getter或setter方法名称派生。在这个注解被使用之前,需要显式地安装和注册,类似于@Pipe注解。
  • @Morph:这个注解的工作方式与@SuperCall注解非常相似。然而,使用这个注解允许指定用于调用超类方法参数。注意,仅当你需要调用具有与原始调用不同参数的超类方法时,才应该使用此注解,因为使用@Morph注解需要对所有参数装箱和拆箱。如果过你想调用一个特定的超类方法,请考虑使用@Super注解来创建类型安全的代理。在这个注解被使用之前,需要显式地安装和注册,类似于@Pipe注解。
  • @SuperMethod:此注解只能用于可从Method分配的参数类型。分配的方法被设置为允许原始代码调用的综合的访问器方法。注意,使用此注解会导致为代理类创建一个公共访问器,该代理类允许不通过security manager(安全管理器)在外部调用超类方法。
  • @DefaultMethod:类似于@SuperMethod,但用于默认方法调用。如果默认方法调用只有一种可能性,则该默认方法在唯一类型上被调用。否则,可以将类型显式地指定为注解属性。
调用超类方法

顾名思义,SuperMethodCall(超类方法调用)可以用于调用方法的超类实现。乍一看,超级实现的唯一调用看起来不是非常有用,因为这不会改变实现,只是复制了已存在的逻辑。但是,通过覆写一个方法,你可以改变方法的注解和参数,我们将在下一节研究这些内容。在Java中调用超类方法的另一个根本原因是构造器的定义,构造器总是会调用另一个超类的构造器或自身类的构造器。

目前为止,我们只是假设动态类的构造器总是与其直接超类的构造器类似。例如,我们可以调用

new ByteBuddy()
  .subclass(Object.class)
  .make()

创建一个带有默认构造器的Object类型的子类,该构造器被定义为简单地调用它的直接父类构造器,Object类型默认的构造器。然而,这个行为通过Byte Buddy没有被保证。相反,上面的代码是下面这个调用的快捷方式

new ByteBuddy()
  .subclass(Object.class, ConstructorStrategy.Default.IMITATE_SUPER_TYPE)
  .make()

其中,ConstructorStrategy(构造器策略)负责为任何给定的类创建一组预定义的构造器。除了上面的策略(复制动态类型的直接超类的每一个可见构造器)之外,还有三个其它的预定义策略:一个不创建任何构造器;另一个创建默认的构造器,该构造器会调用直接超类的默认构造器,如果没有这样的构造器,则会抛出异常;最后一种仅模仿超类的公共构造器。

在Java类文件格式中,构造器通常与方法没什么区别,这样Byte Buddy允许将它们相同对待。但是,构造器需要包含调用另一个构造器的硬编码调用才能被Java运行时接受。由于这个原因,除了SuperMethodCall,大多数预定义实现应用于构造器时将无法创建有效的Java类。

然而,通过使用自定义实现,你可以通过实现一个自定义ConstructorStrategy或者在Byte Buddy的领域特定语言中,使用defineConstructor(定义构造器)方法定义你自己的构造器。此外,我们计划向Byte Buddy添加新功能,以便开箱即用地定义更复杂的构造器。

对于类变基和重定义,构造器当然只是简单地保留,这使得ConstructorStrategy的规范过时了。相反,对于复制这些保留的构造器(和方法)的实现,需要指定一个ClassFileLocator(类文件定位器),它允许查找包含了这些构造器定义的源类。Byte Buddy会尽最大努力识别源类文件的位置,例如,通过查询对应的ClassLoader或者通过查看应用的类路径。然而,当处理自定义的类加载器时,查看可能仍然会失败。然后,就要提供一个自定义ClassFileLocator

调用默认方法

随着版本8的发布,Java编程语言引入了接口的默认方法,在Java中,通过与调用超类方法类似的语法表示默认方法的调用。作为唯一的区别,默认方法调用命名定义该方法的接口。这很有必要,因为如果两个接口用相同的签名定义了一个方法,默认方法调用将会不明确。相应地,Byte Buddy的DefaultMethodCall实现采用了优先接口列表。当拦截一个方法时,DefaultMethodCall将在第一个提到的接口上调用默认方法。例如,假定我们要实现下面两个接口:

interface First {
  default String qux() { return "FOO"; }
}
 
interface Second {
  default String qux() { return "BAR"; }
}

如果我们现在创建一个类,它实现上面两个接口并且实现了调用默认方法的qux方法,这个调用可以表示定义在FirstSecond两个接口上的默认方法的调用。然而,通过指定DefaultMethodCall优先考虑First接口,Byte Buddy知道它应该调用后一个接口的方法而不是另外一个。

new ByteBuddy(ClassFileVersion.JAVA_V8)
  .subclass(Object.class)
  .implement(First.class)
  .implement(Second.class)
  .method(named("qux")).intercept(DefaultMethodCall.prioritize(First.class))
  .make()

注意,Java8之前定义在类文件中的任何Java类都不支持默认方法。此外,你应该意识到相比于Java编程语言,Byte Buddy强制弱化对默认方法可调用性的需求。Byte Buddy只需要有类型继承结构中最具体的类来实现默认方法的接口。除了Java编程语言,它不要求这个接口是任何超类实现的最具体的接口。最后,如果你不想期望一个不明确的默认方法定义,你可以每次都使用DefaultMethodCall.unambiguousOnly()用于接收在发现不明确的默认方法调用时抛出异常的实现。通过优先化DefaultMethodCall显示相同的行为,其中,在没有优先化的接口中调用默认方法是不明确的,并且没有找到优先化的接口来定义具有兼容的签名的方法。

调用特定方法

在一些场景中,上面的实现不能满足实现更多自定义的行为。例如,有人可能想实现一个有显式行为的自定义类。例如,我们或许想要实现下面的Java类,它有一个和超类构造器参数不同的构造器:

public class SampleClass {
  public SampleClass(int unusedValue) {
    super();
  }
}

之前的SuperMethodCall实现不能实现这个类,因为Object类不能定义一个带有int类型的构造器。相反,我们可以显式地调用Object的(super constructor)超级构造器(这里没有用超类构造器,因为Object没有超类):

new ByteBuddy()
  .subclass(Object.class, ConstructorStrategy.Default.NO_CONSTRUCTORS)
  .defineConstructor(Arrays.<Class<?>>asList(int.class), Visibility.PUBLIC)
  .intercept(MethodCall.invoke(Object.class.getDeclaredConstructor()))
  .make()

用上面的代码,我们可以创建一个简单的Object子类,它定义了单个构造器,该构造器接收一个没有使用的int参数。然后通过对Object超级构造器的显式调用来实现后一个构造器。

MethodCall实现传递参数时也可以被使用。这些参数要么作为值显式的传递,要么作为需要手动设置的实例字段的值或者作为给定的参数值。此外,这个实现允许在被检测的实例之外的其他实例上调用方法。此外,它允许新实例的创建从拦截方法返回。MethodCall的类文档提供了这些功能的详情。

访问字段

FieldAccessor(字段访问器),可以实现一个方法来读取或写入一个字段值。为了与这个实现兼容,方法必须:

  • 有一个类似于void setBar(Foo f)的签名用来定义字段的setter。作为Java bean specification(Java Bean规范)中的惯例,通常这个setter将访问名为bar的字段。在此上下文中,参数类型Foo必须是这个字段类型的子类。
  • 有一个类似于Foo getBar()的签名来定义字段的getter。作为Java Bean规范中的惯例,通常这个getter将访问名为bar的字段。为此,方法返回的类型Foo必须是字段类型的超类。

创建这样一个实现很简单:只需调用FieldAccessor.ofBeanProperty()。然而,如果你不想从方法名称获取字段名称,你仍然可以通过FieldAccessor.ofField(String)显式地指定字段名。用这个方法,唯一的参数定义应该访问的字段名。如果需要,甚至允许你在这个字段不存在的情况下定义一个新字段。当访问一个现有字段时,你可以通过调用in方法来指定定义字段的类型。在Java中,在类继承结构的多个类中定义一个字段是合法的。在这个过程中,一个类的一个字段通过它的子类中定义的字段被隐藏了。没有这样显式的字段的类的定位,Byte Buddy将通过遍历类继承结构访问它遇到的第一个字段,从最具体的类开始。

让我们看一个FieldAccessor的实例应用。对于这个实例,我们假设我们收到了一些我们想在运行时子类化的UserType(用户类型)。出于此目的,我们想为每个接口表示的实例注册一个拦截器。这样,我们就可以根据我们的实际需要来提供不同的实现。然后,后一种实现应该可以通过调用相应实例上的InterceptionAccessor接口方法来交换。为了创建这种动态类型的实例,进一步,我们不想用反射,而是调用一个InstanceCreator(对象创建器)的方法,这个对象创建器充当对象工厂。下面的类型类似于此设置:

class UserType {
  public String doSomething() { return null; }
}
 
interface Interceptor {
  String doSomethingElse();
}
 
interface InterceptionAccessor {
  Interceptor getInterceptor();
  void setInterceptor(Interceptor interceptor);
}
 
interface InstanceCreator {
  Object makeInstance();
}

我们已经学会了用MethodDelegation如何拦截一个方法。使用后一种实现,我们可以定义一个实例字段的委托并命名这个字段的Interceptor。另外我们正在实现InterceptionAccessor接口并拦截接口的所有方法以实现这个字段的访问器。通过定义一个bean属性访问器,我们会得到getInterceptor的getter方法,和setInterceptor的setter方法。

Class<? extends UserType> dynamicUserType = new ByteBuddy()
  .subclass(UserType.class)
    .method(not(isDeclaredBy(Object.class)))
    .intercept(MethodDelegation.toField("interceptor"))
  .defineField("interceptor", Interceptor.class, Visibility.PRIVATE)
  .implement(InterceptionAccessor.class).intercept(FieldAccessor.ofBeanProperty())
  .make()
  .load(getClass().getClassLoader())
  .getLoaded();

使用新的dynamicUserType,我们可以实现InstanceCreator接口成为这个动态类型的工厂。同样,我们正在用已知的MethodDelegation调用动态类型的默认构造器:

InstanceCreator factory = new ByteBuddy()
  .subclass(InstanceCreator.class)
    .method(not(isDeclaredBy(Object.class)))
    .intercept(MethodDelegation.construct(dynamicUserType))
  .make()
  .load(dynamicUserType.getClassLoader())
  .getLoaded().newInstance();

注意,我们需要使用dynamicUserType的类加载器加载这个工厂。否则,当这个类型加载后对这个工厂是不可见的。

用这两种动态类型,我们最终可以创建一个动态增强UserType类型的新实例,并为其实例自定义拦截器。让我们通过将HelloWorldInterceptor拦截器应用于新创建的实例来结束这个示例。注意,我们现在是如何在不使用反射的情况下做到这一点的,这要归功于字段访问器接口和工厂。

class HelloWorldInterceptor implements Interceptor {
  @Override
  public String doSomethingElse() {
    return "Hello World!";
  }
}
 
UserType userType = (UserType) factory.makeInstance();
((InterceptionAccessor) userType).setInterceptor(new HelloWorldInterceptor());
杂项

除了目前我们讨论过的实现外,Byte Buddy还包含其他的实现:

  • StubMethod实现了一个方法,只需返回方法返回类型的默认值,而无需任何进一步的操作。这样,一个方法的调用可以被静默地抑制。例如,这种方式可以实现模拟(mock)类型。任何基本类型的默认值分别为零或者零字符。返回引用类型的方法将返回null作为默认值。
  • ExceptionMethod能用来实现一个只抛出异常的方法。如前所述,可以从任何方法抛出已检查异常,即使这个方法没有声明这个异常。
  • Forwarding实现允许简单地将方法调用转发到另一个与拦截方法的声明类型相同类型的实例。用MethodDelegation可以达到相同的效果。然而,通过Forwarding,应用更简单的委托模型,该模型可以覆盖不需要目标方法发现的用例。
  • InvocationHandlerAdapter允许使用Java类库自带的代理类中现有的InvocationHandlers
  • InvokeDynamic实现允许用bootstrap方法运行时动态绑定一个方法,这个方法从Java7可以访问。
  • 2
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值