在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进行优化等技巧。

广告
将在 10 秒后关闭
bannerAds