使用ByteBuddy来截获Java类实现

最近要对一个Hadoop FileSystem类(以及子类)实现进行扩展。Hadoop FileSystem实现上比较简单,在许多地方只是假设整个JVM只有一个UGI存在,所以我需要在上面做一些扩展来支持多UGI:在进行一些方法调用之前需要切换UGI(UserGroupInformation),这样可以来实现多账号切换。

JDK本身好像也有接口代理的方式来截获类的实现,但是有个要求就是截获的必需是接口,而FileSystem在Hadoop里面是一个抽象类,所以还没有办法使用这种接口代理的方式来实现。

问了ChatGPT,推荐的方式就是使用字节码来动态产生类。推荐的库有cglib, byte buddy和ASM. 我看ASM好像有点底层,cglib处于维护状态(主页上也推荐使用byte buddy), 所以看来还是byte buddy还行,毕竟我这个需求比较简单,可能用不上ASM这么强大的库。

ChatGPT给了几个代码示例,结合我自己这边的需求,我稍微整理了几个pattern


以我这个case为例,因为每个对象都要绑定UGI,所以我定义了 `UGIObject` . 并且对FileSystem做了封装

interface UGIObject {
    UserGroupInformation getUGI();

    Object getTarget();
}

abstract static class FSProxy extends FileSystem implements UGIObject {
    private FileSystem target;
    private UserGroupInformation ugi;

    public FSProxy(FileSystem target, UserGroupInformation ugi) {
        this.target = target;
        this.ugi = ugi;
    }

    @Override
    public UserGroupInformation getUGI() {
        return ugi;
    }

    @Override
    public FileSystem getTarget() {
        return target;
    }
}

在创建这个动态类型的时候,只截获感兴趣的实现。

public static Class buildFSProxyClass() {
    Class<FSProxy> cls = FSProxy.class;
    return new ByteBuddy()
            .subclass(cls)
            .method(ElementMatchers.not(ElementMatchers.isDeclaredBy(Object.class))
                    .and(ElementMatchers.not(ElementMatchers.namedOneOf("getTarget", "getUGI"))))
            .intercept(MethodDelegation.to(new GeneralInterceptor(cls.getSimpleName())))
            .make()
            .load(cls.getClassLoader(), ClassLoadingStrategy.Default.INJECTION)
            .getLoaded();
}

static class GeneralInterceptor {
    private String name;

    public GeneralInterceptor(String name) {
        this.name = name;
    }

    @RuntimeType
    public Object intercept(@This Object self, @AllArguments Object[] args, @Origin Method method,
                            @SuperMethod(nullIfImpossible = true) Method superMethod)
            throws Exception {
        UGIObject proxy = (UGIObject) self;
        System.out.printf("      [X] %s: %s\n", name, method.toString());
        // During initialization there is target.
        if (proxy.getTarget() == null) {
            return superMethod.invoke(proxy, args);
        }

        Object res = null;
        UserGroupInformation ugi = proxy.getUGI();
        Object target = proxy.getTarget();
        // No need to switch current user.
        if (ugi != null && !UserGroupInformation.getCurrentUser().equals(ugi)) {
            res = UGITools.doAs(ugi, () -> method.invoke(target, args));
        } else {
            res = method.invoke(target, args);
        }
        return res;
    }
}

最后选择合适的构造函数来进行创建

public static FileSystem createFSProxy(FileSystem target, UserGroupInformation ugi) {
    Object proxy = null;
    try {
        proxy = FSProxyClass.getConstructor(FileSystem.class, UserGroupInformation.class)
                .newInstance(target, ugi);
    } catch (Exception e) {
        throw new RuntimeException(e);
    }
    FileSystem fs = (FileSystem) proxy;
    return fs;
}

UPDATE@202312: 后面我发现这里面限制其实特别大,问题大致有两个:

所以感觉这种对象截获方法的实现,通常只能在第一层进行捕捉,而且需要确保还不是final方法,可以说限制比较大。