有 Java 编程相关的问题?

你可以在下面搜索框中键入要查询的问题!

字节码在Java中是否可以在运行时实现接口?

我正在做一个项目,其中有很多对象是由库创建的,我无法访问这些对象的创建过程

下面的片段是一个很好的例子来说明我的问题

代码:

public class Clazz {
    //The contents of Clazz are irrelevant
    //Clazz is NOT my class. I have no access to its internal structure.
    //However, I do have access to Clazz objects that were created elsewhere.
}

ExampleInterface是Clazz在编译时可能实现也可能不实现的接口

代码:

public interface ExampleInterface {
    public void run();
}

下面的代码就是我遇到的问题。注意以下几点:

  1. ^只有当c是ExampleInterface的实例时,{}才被调用
  2. getRunConditions(Clazz c)executeClazz(Clazz c)都是我无权访问的类中的私有方法
  3. 在编译时,Clazz包含名为run()的方法
  4. 例如执行者不是我的班级。我在任何地方都找不到它 方式(我甚至无法获得该类的实例)

代码:

public class ExampleExecutor {
    public void executeClazz(Clazz c) {
        if ((c instanceof ExampleInterface) && getRunConditions(c)) {
            ExampleInterface ex = (ExampleInterface) c;
            ex.run();
        }
    }
}

显然,下面的方法在语法上是不可能的,但这正是我想要实现的。基本上,如果c尚未实现ExampleInterface,请将c设置为实现ExampleInterface,然后提供必须重写的方法

注意以下几点:

  1. extendInterface({}{}是由语法组成的 为了说明我的目标
  2. run()必须在这里定义(在运行时)
  3. 我不能使用包装器或代理类作为解决方案。也就是说,Clazz对象必须最终实现ExampleInterface,我不能使用变通方法。(如果您想知道原因,请参阅this link

代码:

public void implementInterface(Clazz c) {
    if (!(c instanceof ExampleInterface)) {
        c.extendInterface(ExampleInterface {
            @Override
            public void run() {
                //code
            }
        });
    }
}

为了澄清,我遇到的问题是,我需要始终知道run()Clazz中何时被调用。如果Clazz没有实现ExampleInterface,我不知道什么时候应该调用run()

同时,我还想偶尔添加对run()的支持,因为默认情况下它不受支持。因为我无法创建Clazz对象,所以我无法通过自己实现接口来实现这一点

问题:简单地说,是否可以在运行时实现接口(并提供所需的方法)

注意:虽然唯一的解决方案可能需要反射(如果需要,请在下面发布),但我使用的库有一个安全管理器,可以阻止所有反射的使用。也就是说,一个反思性的解决方案在未来可能对其他人有用,但对我没有用处

而且,我的意思不是在我自己的程序中使用一个库。一个已经在运行的主机应用程序(我正在使用的库就是为了这个应用程序而设计的)遵循并运行我为它编写的代码。如果该应用程序不喜欢我提供的任何代码(即,与其安全管理器冲突),则该代码甚至不会被编译

为什么我需要这样做:

这和我正在使用的图书馆有关。因为ExampleExecutor是我无权访问的方法,并且我无法控制Clazz的创建,所以我无法确定何时执行run()

我需要知道run()何时执行的原因是,实际上,run()是一个事件处理程序,它是我正在使用的库的一部分

例如:mouseClicked(CustomMouseEvent evt)可能是接口CustomMouseListener的一部分的方法。有时,我正在使用的Clazz实例在单击鼠标时会引起注意(因此会继承CustomMouseListener),而其他时候则不会

Clazz实例不同,我总是关心鼠标是否被点击,并且总是需要触发事件

实际上ExampleInterface实际上是以下内容:

public interface CustomMouseListener {
    public void mouseClicked(CustomMouseEvent evt);
    public void mousePressed(CustomMouseEvent evt);
    public void mouseReleased(CustomMouseEvent evt);
    //etc
}

共 (4) 个答案

  1. # 1 楼答案

    您可以使用java instrumentation API(强制)使类适应接口。APM、AOP框架和分析器通常使用这种技术在运行时将日志记录和度量代码注入目标类。应用程序直接使用这种技术是非常罕见的。如果我在生产代码中看到这一点,至少会是一个巨大的危险信号

    尽管如此

    考虑到这些问题:

    package com.sabertiger.example;
    
    public class Clazz {
        public void purr(){
            System.out.println("Hello world");
        }
    
    }
    

    接口

    package com.sabertiger.example;
    
    public interface ExampleInterface {
        void run();
    }
    

    遗嘱执行人

    package com.sabertiger.example;
    
    public class ExampleExecutor {  
        public static void main(String[] args) {
            Clazz c=new Clazz();
            // Normally a ClassCastException
            ExampleInterface i=(ExampleInterface)(Object)(Clazz) c;
            i.run();
        }
    }
    

    正常运行会产生以下错误:

    Exception in thread "main" java.lang.ClassCastException:
      com.sabertiger.example.Clazz cannot be cast to 
      com.sabertiger.example.ExampleInterface
        at com.sabertiger.example.ExampleExecutor.main(ExampleExecutor.java:7)
    

    您可以通过转换类来提供缺少的接口和实现,从而使其正常工作:

    package com.sabertiger.instrumentation;
    
    import java.lang.instrument.ClassFileTransformer;
    import java.lang.instrument.IllegalClassFormatException;
    import java.lang.instrument.Instrumentation;
    import java.security.ProtectionDomain;
    
    import javassist.ClassPool;
    import javassist.CtClass;
    import javassist.CtMethod;
    import javassist.CtNewMethod;
    
    public class ExampleInterfaceAdapter implements ClassFileTransformer {
    
        public static void premain(String agentArgument, Instrumentation instrumentation) {
            // Add self to list of runtime transformations
            instrumentation.addTransformer(new ExampleInterfaceAdapter());
        }
    
        @Override
        // Modify only com.sabertiger.example.Clazz, return all other unmodified
        public byte[] transform(ClassLoader loader, String className,
                Class<?> classBeingRedefined, ProtectionDomain protectionDomain,
                byte[] classfileBuffer) throws IllegalClassFormatException {
    
            if(className.matches("com/sabertiger/example/Clazz")) {
                return addExampleInterface(className, classfileBuffer );            
            } else {
                return classfileBuffer;
            }
        }
    
        // Uses javassist framework to add interface and new methods to target class
        protected byte[] addExampleInterface(String className, byte[] classBytecode) {
            CtClass clazz= null;
            try {
                ClassPool pool = ClassPool.getDefault();
                clazz = pool.makeClass(new java.io.ByteArrayInputStream(classBytecode));
    
                String src=
                  "{         "+
                  "  purr(); "+
                  "}         ";
    
                //Add interface
                CtClass anInterface = pool.getCtClass("com.sabertiger.example.ExampleInterface");
                clazz.addInterface(anInterface);
    
                //Add implementation for run method
                CtMethod implementation = CtNewMethod.make(
                        CtClass.voidType,
                        "run",
                        new CtClass[0],
                        new CtClass[0],
                        src,
                        clazz);
                clazz.addMethod(implementation);
    
                classBytecode=clazz.toBytecode();
            } catch(Throwable e) {
                throw new Error("Failed to instrument class " + className, e);
            }
            return classBytecode;
        }
    
    }
    

    以及所需的舱单。MF

    Manifest-Version: 1.0
    Premain-Class: com.sabertiger.instrumentation.ExampleInterfaceAdapter
    Boot-Class-Path: javassist.jar
    

    把所有东西都装进一个罐子里,让它工作:

    jar -tf agent.jar
    META-INF/MANIFEST.MF
    com/sabertiger/instrumentation/ExampleInterfaceAdapter.class
    

    现在我们可以将Clazz传递给ExampleExecutor

    java -javaagent:agent.jar -classpath ..\instrumentation\bin com.sabertiger.example.ExampleExecutor
    Hello world
    
  2. # 2 楼答案

    根据您的java版本,您可以使用lambda表达式(与java 8一起使用)

    代码相对简单:

    Clazz o = .... // Here you obtain your object through third party library
    ExampleInterface yourInterface = o::run;
    yourInterface.run();
    

    请注意,这只适用于具有一个方法的接口。两个签名(接口和Clazz)必须匹配

  3. # 3 楼答案

    执行建议的唯一方法是使用字节码检测。您可以添加一个代理,在加载之前更改要修改的clazz的字节码

    在加载时需要这样做的原因是,许多JVM不允许您更改字段,有些JVM不允许您在类加载后添加方法

    一个更简单的解决方案是对类进行反编译、修改并再次编译。假设该类可以反编译,这将为您节省大量时间和精力

    the library I am using has a security manager that blocks the use of all reflection

    这是一个奇怪的选择,因为在调用库之前,您可以设置自己的安全管理器,但这并不能阻止您做任何事情

  4. # 4 楼答案

    我认为你想要的是不可能的;有Dynamic Proxies,但它们使用反射,而且看起来您不太可能有权访问类加载器(您可以在其中设置自己的即时字节码操作)