class ForwardingLoggerInterceptor {
private final MemoryDatabase memoryDatabase;
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中调用超类方法的另一个根本原因是构造器的定义,构造器总是会调用另一个超类的构造器或自身类的构造器。
目前为止,我们只是假设动态类的构造器总是与其直接超类的构造器类似。例如,我们可以调用