在Java中的动态Mixin
在Java中,动态地进行Mixin,而无需进行黑魔法(即修改字节码)。
背景
在实施中间件模式等时,当添加某个中间件时,经常希望在请求对象中增加方法。
如果试图在不支持多重继承的Java中实现这个,最终将需要实现所有必要方法的类(或其父子关系)。
只需要必要的方法,需要的时候再添加就好了。Mixin! Mixin!
接口的默认实现。
对于那些希望在Java中实现Mixin的人们来说,Java8为接口添加了默认实现的功能是一个应该受到欢迎的事情。
-
- Java8のインタフェース実装から多重継承とMixinを考える
- Java8でmixinをがんばってみる – yojikのlog
如果保留这样的默认实现,这样就可以。
public interface Traceable {
Logger LOG = LoggerFactory.getLogger(Traceable.class);
default void writeLog() {
LOG.info(toString());
}
}
只需将其添加到实现中,日志输出方法就可以使用了。
class RequestImpl implements Request, Traceable {
@Override
public String toString() {
return getParameters().toString();
}
}
interface Request {
Map<String, String> getParameters();
}
Request req = new RequestImpl();
((Traceable) req).writeLog();
然而,不能保持状态。
interface SessionAvailable {
private Session session;
default void setSession(Session session) {
this.session = session;
}
}
这样的事情是不可能的。
如果这个对象是不需要考虑线程安全的对象,那么可以创建一个状态容器的功能如下,这样可以通过默认实现对状态进行操作。
public interface Extendable {
Object getExtension(String name);
void setExtension(String name, Object extension);
}
public interface SessionAvailable extends Extendable {
default void setSession(Session session) {
setExtensions("session", session);
}
}
动态混合
由于默认实现的存在,只需在implements关键字后列出接口,就可以使用该实现。然而,对于使用采用中间件模式实现的框架的一方来说,“在使用此中间件时,必须实现此接口”的限制可能是一个困难的限制。
因此,我们考虑在中间件中自动向请求对象添加必要的实现,并且在需要时能够处理,这样就可以考虑实现动态Mixin的功能。
以下是一个中间件实现的例子,但是如果使用方通过Mixin接口,并使用其方法,就不需要提前实现Request具有SessionAvailable的需求。
@Override
public RES handle(REQ req, MiddlewareChain next) {
final REQ request = MixinUtils.mixin(req, SessionAvailable.class);
((SessionAvailable) request).getSession();
// .....
}
如何在Java中动态地添加接口呢?近乎黑魔法的白魔法代理,可以实现这一目标。
Proxy.newProxyInstance(
classloader,
new Class[]{ Request.class, SessionAvailable.class },
new MixinProxyHandler());
如果创建代理,那么通过在第二个参数中添加的接口的方法,将由第三个参数传递的InvocationHandler进行处理。
class MixinProxyHandler {
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (method.getDeclaringClass().isAssignableFrom(original.getClass())) {
// 元の
return method.invoke(original, args);
} else {
// インタフェース側のメソッドにディスパッチ
return getMethodHandle(method)
.bindTo(proxy)
.invokeWithArguments(args);
}
}
}
方法句柄
现在问题在于,当我们尝试使用反射来执行接口的默认实现时,原始对象并没有实现添加的接口,因此无法进行调用。
使用MethodHandle而不是传统的反射,即使没有实现,也可以调用接口的默认实现。
MethodHandles.lookup()
.in(declaringClass)
.unreflectSpecial(method, declaringClass)
.bindTo(proxy)
.invokeWithArguments(args);
默认实现可以使用invokeSpecial来调用。要实际使用,需要更改Accessibility,否则可能会导致查找失败,因此会变得稍微复杂一些。请参考以下完整代码。
如果我们创建Mixin的实用程序,就可以动态地添加接口并调用其实现方法。这真是太方便了!
public class MixinUtilsTest {
@Test
public void mixin() {
Money m1 = new MoneyImpl(5);
m1 = MixinUtils.mixin(m1, ComparableMoney.class);
assertTrue(ComparableMoney.class.cast(m1).isBigger(new MoneyImpl(3)));
}
public static class MoneyImpl implements Money {
private int amount;
public MoneyImpl(int amount) {
this.amount = amount;
}
@Override
public int getAmount() {
return amount;
}
@Override
public String toString() {
return Integer.toString(amount);
}
}
public interface Money {
int getAmount();
}
public interface ComparableMoney extends Money {
default boolean isBigger(Money other) {
return getAmount() > other.getAmount();
}
}
}
请注意
目前的Java8 JVM实现中,通过MethodHandle调用invokeSpecial非常缓慢。根据实测,它的执行时间通常是普通反射的10倍到100倍。因此,在实际运营中,需要采取对静态Mixin进行优化等技巧。