运行时实现抽象方法?

7

假设我有一个抽象类:

abstract class Foo extends Bar {

    public abstract int foo();

}

我希望你能在运行时扩展以创建一个类对象,使我能够拥有一个动态生成的类:
class FooImpl extends Foo {

    @Override
    public int foo() {
        return 5;
    }

}

我需要一个类对象来表示它,然后我可以使用反射创建新的实例。关键是我想在运行时决定方法foo()的返回值。我的想法是使用ASM创建类的字节码,然后在ClassLoader对象上使用反射定义类。
使用ASM和ClassLoader#defineClass方法生成字节码是否是以非硬编码值在运行时实现抽象方法的最佳方式?
如果是,我该如何做?我的直觉是利用ASMifierClassVisitor,但我不确定具体的方法。我知道如果其他方法都失败了,我可以手动浏览定义特定类所需的JVM指令,但我感觉必须有更简单的方法。
如果不是,什么是最好的方法,我该如何使用最佳方法?
编辑:我查看了所有答案,发现没有一个完全符合我的要求。最终,我用ASM创建了一个小型的实现。我想在这里发布它:
import org.objectweb.asm.*;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintWriter;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.HashMap;

/**
 * Created by IntelliJ IDEA.
 * User: Matt
 * Date: 9/17/11
 * Time: 12:42 PM
 */
public class OverrideClassAdapter extends ClassAdapter {

    private final HashMap<String, Object> code;
    private final String className;

    private final ClassWriter writer;

    private String superName;

    public OverrideClassAdapter(ClassWriter writer, String className, Queue<int[]> constructorCode, HashMap<String, Object> code) {
        super(writer);
        this.writer = writer;
        this.className = className;
        this.code = code;
    }

    @Override
    public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
        this.superName = name;
        if((access & Opcodes.ACC_ABSTRACT) != 0)
            access &= ~Opcodes.ACC_ABSTRACT;
        if((access & Opcodes.ACC_INTERFACE) != 0)
            access &= ~Opcodes.ACC_INTERFACE;
        cv.visit(version, access, className, signature, name, null);
    }

    @Override
    public void visitSource(String source, String debug) {
    }

    @Override
    public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
        boolean isAbstract = (access & Opcodes.ACC_ABSTRACT) != 0;
        if(isAbstract)
            access &= ~Opcodes.ACC_ABSTRACT;
        MethodWriter mw = (MethodWriter) cv.visitMethod(access, name, desc, signature, exceptions);
        Object value = code.get(name);
        if(isAbstract || value != null) {
            if(value instanceof BytecodeValue) {
                BytecodeValue returnableValue = (BytecodeValue) value;
                int[] byteCode = new int[returnableValue.getValueCode().length + 1];
                System.arraycopy(returnableValue.getValueCode(), 0, byteCode, 0, returnableValue.getValueCode().length);
                if(returnableValue.getValueCode().length > 1 && returnableValue.getValueCode()[1] == 0) {
                    byteCode[1] = writer.newConst(returnableValue.getValue());
                }
                byteCode[byteCode.length - 1] = returnableValue.getReturnCode();
                value = byteCode;
            }
            return new OverrideMethodAdapter(mw, (int[]) value);
        }
        return mw;
    }

    private class OverrideMethodAdapter extends MethodAdapter {

        private final int[] code;

        private final MethodWriter writer;

        public OverrideMethodAdapter(MethodWriter writer, int[] code) {
            super(writer);
            this.writer = writer;
            this.code = code;
        }

        @Override
        public void visitEnd() {
            try {
                Field code = MethodWriter.class.getDeclaredField("code");
                code.setAccessible(true);
                ByteVector bytes = new ByteVector();
                for(int b : this.code)
                    bytes.putByte(b);
                code.set(writer, bytes);
            } catch (Exception e) {
              e.printStackTrace();
            }
        }
    }

    public static byte[] extendClassBytes(Class clazz, String className, HashMap<String, Object> methodImpls) throws IOException {
        ClassReader cr = new ClassReader(clazz.getName());
        ClassWriter cw = new ClassWriter(0);
        cr.accept(new OverrideClassAdapter(cw, className, methodImpls), ClassReader.SKIP_DEBUG);
        cr = new ClassReader(cw.toByteArray());
        cw = new ClassWriter(ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES);
        cr.accept(cw, ClassReader.SKIP_DEBUG);
        //CheckClassAdapter.verify(new org.objectweb.asm.ClassReader(cw.toByteArray()), true, new PrintWriter(System.out));
        /*File file = new File(className + ".class");
        new FileOutputStream(file).write(cw.toByteArray());*/
        return cw.toByteArray();
    }


    public static Class extendClass(Class clazz, String className, HashMap<String, Object> methodImpls) throws IOException {
        return defineClass(extendClassBytes(clazz, className, methodImpls), className);
    }

    public static Class defineClass(byte[] code, String name) {
        try {
            Method method = ClassLoader.class.getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class);
            method.setAccessible(true);
            return (Class) method.invoke(Thread.currentThread().getContextClassLoader(), name, code, 0, code.length);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }
}

我认为你可能需要看一下合成方法。 - Shahzeb
在运行时决定 foo 的返回值?如果它返回任意类型,一个类会如何调用它? - Dave Newton
在运行时,指的是当类生成时确定foo的值,并且之后不需要更改。 - mburke13
3个回答

6
您可能想考虑使用CGLib。它可以像Java的动态代理一样处理抽象类和接口,而且它还有一个类似于java.lang.reflect.Proxy的API来实现这个功能。CGLib在后台使用ASM,但通过使用CGLib,您不必直接编写字节码。
以下是使用CGLib的示例:
package cglibtest;

import java.lang.reflect.Method;

import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

public class CGLibTest
{
    public static void main(String... args)
    {
        MyAbstract instance = (MyAbstract)Enhancer.create(MyAbstract.class, new MyInterceptor(42));
        System.out.println("Value from instance: " + instance.valueMethod());
    }

    public static class MyInterceptor implements MethodInterceptor
    {
        private final Object constantValue;

        public MyInterceptor(Object constantValue)
        {
            this.constantValue = constantValue;
        }

        @Override
        public Object intercept(Object obj, Method method, Object[] args,
                MethodProxy proxy) throws Throwable
        {
            if ("valueMethod".equals(method.getName()))
                return(constantValue);
            else
                return(null);
        }
    }

    public static abstract class MyAbstract
    {
        public abstract int valueMethod();
    }
}

我查看了cglib,但是我不太清楚如何在生成的类中实现抽象方法。你有任何文档或阅读材料可以让我了解应该做什么吗? - mburke13
@Matt 添加了一个示例,更多信息请查看网站(例如示例javadocs)。 - prunge
谢谢你的例子和链接。我已经选择了你的答案作为我的问题的最佳答案。出于好奇,使用方法拦截器是实现抽象方法的最佳/唯一方法吗?虽然它能完成工作,但我想知道当具有4或5个抽象方法的类中需要检查每个方法的名称以执行方法代码时会发生什么。它似乎有点笨重(例如,如果(abstractMethod1.equals(method.getName())doThis();else if(abstractMethod2.equals(method.getName())doThis1();等) - mburke13
你会如何称呼抽象类中的非抽象方法? - Njax3SmmM2x2a0Zf7Hpd

1

阻止您从属性中读取值5并返回它的是什么?这太简单了,我猜您一定有比返回int更复杂的目标。我同意上面的帖子,动态生成类将非常昂贵。如果您预先知道业务逻辑,则可以应用工厂模式在运行时加载定义接口的所需实现。这就是JDBC库的工作方式。

如果您事先不知道业务逻辑,并且拥有很多逻辑,则使用现成的规则引擎处理逻辑并将结果返回到Java程序中可能会更加有效。如果逻辑经常改变,则在规则引擎中维护此逻辑要容易得多。


0

是的,那个方法应该可以工作。但如果您生成了大量类,它将会很昂贵。(我们可能在谈论数十万条指令来生成字节码文件,然后加载它。然后还有内存需求,在加载类时表示类。)

另一种方法(同样昂贵)是在运行时生成源代码并编译和加载它。

最后,您应该考虑采用对象表驱动的逻辑或使用某种解释器来实现它。如果您确实需要不同的类,则可以使用Java的动态代理类机制来封装它;例如,请参见java.lang.reflect.Proxy


我正在研究代理选项,但受到抽象方法必须在抽象类而不是接口中声明的限制(我需要抽象类来扩展一个类)。类的生成将在启动时完成,数量不会超过10-100个。 - mburke13
@Matt - 你可以重构你的抽象类,使其成为一个接口和实现它的抽象类,然后在代理中包装抽象类的实例。对于那么多的类,代码生成方法不应该太昂贵,但这是一种复杂且(可能)脆弱的解决问题的方式...如果生成的方法需要很复杂的话。(如果不需要,表驱动方法会更简单。) - Stephen C

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接