使用 Byte Buddy 在 运行 时间动态生成单个函数(没有子函数),表示二叉表达式树

Dynamically generate a single function (without subfunctions), representing a binary expression tree, at run time with Byte Buddy

简介

我想比较一些在 运行 时生成代码的库。目前我接触到Javassist和Byte Buddy的表面。

作为概念证明,我正在尝试解决一个小问题,这是解决更复杂问题的起点。
基本上我有一个 binary expression tree,我想将其转换成一行代码并将其加载到我的 java 运行 时间。为简单起见,到目前为止我只添加了节点和常量作为叶子。

Javasist 参考资料

我已经在 J​​avassist 中找到了执行此操作的方法(至少适用于具有两个叶子的单个节点)。代码如下所示:

public class JavassistNodeFactory{
    public DynamicNode generateDynamicNode(INode root){
        DynamicNode dynamicNode = null;
        try {
            CtClass cc = createClass();
            interceptMethod(root, cc);
            compileClass(cc);
            dynamicNode = instantiate(cc);
        }catch (Exception e){
            System.out.println("Error compiling class with javassist: "+ e.getMessage());
            e.printStackTrace();
        }
        return dynamicNode;
    }

    private DynamicNode instantiate(CtClass cc) throws CannotCompileException, IllegalAccessException, InstantiationException {
        Class<?> clazz = cc.toClass();
        return (DynamicNode) clazz.newInstance();
    }

    private void compileClass(CtClass cc) throws NotFoundException, IOException, CannotCompileException {
        cc.writeFile();
    }

    private void interceptMethod(INode root, CtClass cc) throws NotFoundException, CannotCompileException {
        CtMethod calculateMethod = cc.getSuperclass().getDeclaredMethod("calculateValue",null);
        calculateMethod.setBody("return "+ nodeToString(root)+ ";");
    }

    private CtClass createClass() throws CannotCompileException, NotFoundException {
        ClassPool pool = ClassPool.getDefault();
        CtClass cc = pool.makeClass(
                "DN"+ UUID.randomUUID().toString().replace("-","")
        );
        cc.setSuperclass(pool.get("org.jamesii.mlrules.util.runtimeCompiling.DynamicNode"));
        return cc;
    }

    private static String nodeToString(INode node){
        if (node.getName().equals("")){
            return ((ValueNode)node).getValue().toString();
        }else{
            List<? extends INode> children = node.getChildren();
            assert(children.size()==2);
            return ("("+nodeToString(children.get(0))+node.getName()+nodeToString(children.get(1))+")");
        }
    }
}

DynamicNode class 看起来像这样:

public class DynamicNode implements INode   {
    @Override
    public <N extends INode> N calc() {
        Double value = calculateValue();
        return (N) new ValueNode<Double>(value);
    }

    @Override
    public List<? extends INode> getChildren() {
        return null;
    }

    @Override
    public String getName() {
        return null;
    }

    private Double calculateValue() {
        return null;
    }
}

重要的部分是 nodeToString() 函数,我从给定的根节点生成一个由 returned 字符串表示的算术公式。 ValueNode 是具有常量值的树叶,将 return 编辑为字符串。
其他节点(只为​​我的情况添加节点)将为每个 child 递归调用函数并打印表达式周围的括号以及打印运算符(return 由 getName() 函数编辑)在两个children中间(简称:"(leftChild+rightChild)")。
calculateValue() 函数的主体将由 Javassist 在 interceptMethod() 函数中更改为 return 生成公式的结果。

字节好友尝试

我已经使用 Byte Buddy 来实现类似的解决方案。但随着我深入研究概念和文档,我越来越觉得这不是 Byte Buddy 设计的问题。大多数示例和问题似乎都集中在对其他函数的函数委托上(实际上在编译时已经存在,并且仅在 运行 时连接)。这对我来说并不是很方便,因为我无法在编译时知道我想要转换的实际树。可能可以使用底层的 ASM 库,但我想避免自己(以及我可能的继任者)处理字节码。

我有一个(显然不起作用的)基本实现,但我卡在必须为 Byte Buddy 库的 intercept() 函数提供 Implementation 的地步。我最后的状态是这样的:

public class ByteBuddyNodeFactory{
    @Override
    public DynamicNode generateDynamicNode(INode root) {
        DynamicNode dynamicNode = null;
        try {
            Class<?> dynamicType = new ByteBuddy()
                    .subclass(DynamicNode.class)
                    .name("DN"+ UUID.randomUUID().toString().replace("-",""))
    //this is the point where I have problems
    //I don't know how to generate the Implementation for the intercept() function
    //An attempt is in the nodeToImplementation() function
                    .method(ElementMatchers.named("calculateValue")).intercept(nodeToImplementation(root))
                    .make()
                    .load(Object.class.getClassLoader())
                    .getLoaded();
            dynamicNode = (DynamicNode) dynamicType.newInstance();
        } catch (Exception e) {
            System.out.println("Error compiling testclass with bytebuddy: " + e.getMessage());
            e.printStackTrace();
        }
        return dynamicNode;
    }

    private Implementation.Composable nodeToImplementation(INode node){
        if (node.getName().equals("")){
            return (Implementation.Composable)FixedValue.value(((ValueNode)node).getValue());
        }else{
            List<? extends INode> children = node.getChildren();
            assert(children.size()==2);
            switch (node.getName()){
                case ("+"):
                    //This is the point where I am completely lost
                    //This return is just the last thing I tried and may be not even correct Java code
                    // But hopefully at least my intention gets clearer
                    return (MethodCall.invoke((Method sdjk)-> {
                        return (nodeToImplementation(children.get(0)).andThen(node.getName().andThen(nodeToImplementation(children.get(1)))));
                    }));
                
                default:
                    throw new NotImplementedException();
            }
        }
    }
}

我的想法是将子函数连接在一起,因此尝试使用 Composable Implementation。我尝试 return a MethodDelegation 但正如我所提到的,我觉得这不是正确的方法。在那之后我尝试了 MethodCall 但我很快意识到我也完全不知道如何使用这个 ^^

问题

是否可以在 Byte Buddy 中像在 Javassist 中一样动态地从树结构生成函数,而无需调用尽可能多的子函数?
如果可能,我该怎么做?
如果不可能:是否可以使用其他字节码操作库,如 cglib.

我更愿意保持高于字节码的抽象级别,因为底层概念的研究应该与我的问题无关。

使用 Byte Buddy 的高级 API 无法轻松完成您尝试做的事情。相反,如果您想使用 Byte Buddy,您应该 assemble 使用 StackManipulations 的方法。堆栈操作仍然包含 Java 字节代码,但这些位应该非常简单,因此很容易实现。

Byte Buddy 不针对这种情况的原因是,您通常可以找到比 assemble 代码片段更好的代码抽象。为什么您的节点不能实现随后从您的检测方法调用的实际实现? JIT 编译器通常会优化此代码以获得与手动内联代码相同的结果。此外,您保留了可调试性并降低了代码的复杂性。