Возможно ли реализовать интерфейс во время выполнения на 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();
}

Следующий код - проблема, с которой я сталкиваюсь. Обратите внимание на следующее:

  • run() вызывается только тогда, когда c является экземпляром ExampleInterface.
  • getRunConditions(Clazz c) и executeClazz(Clazz c) являются частными методами в классе, к которому у меня нет доступа.
  • Во время компиляции Clazz не будет содержать метод с именем run().
  • ExampleExecutor не мой класс. У меня нет доступа к нему ни в одном (я даже не могу получить экземпляр класса).

код:

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

Очевидно, что следующий метод не является синтаксически возможным, но это то, чего я пытаюсь достичь. В принципе, если c не реализует ExampleInterface, установите c для реализации ExampleInterface, а затем предоставите методы, которые должны быть переопределены.

Обратите внимание на следующее:

  • extendInterface( Name of Interface ) является сильным синтаксисом . I созданный в попытке проиллюстрировать мою цель.
  • run() должен быть определен здесь (во время выполнения).
  • Я не могу использовать оболочку или прокси-класс в качестве решения. IE, объект Clazz должен завершить реализацию ExampleInterface, и я не могу использовать обходной путь. (обратитесь к этой ссылке, если вы хотите узнать почему).

код:

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

Чтобы прояснить, проблема, с которой я сталкиваюсь, заключается в том, что мне всегда нужно знать, когда run() вызывается в Clazz. Если Clazz никогда не реализует ExampleInterface, я не могу знать, когда следует вызывать run().

В то же время я также хотел бы иногда добавлять поддержку run(), когда она по умолчанию не поддерживается. Поскольку у меня нет доступа к созданию объектов Clazz, я не могу этого сделать, реализуя сам интерфейс.

Вопрос: Проще говоря, возможно ли реализовать интерфейс (и предоставить необходимые методы) во время выполнения?

ПРИМЕЧАНИЕ. Хотя единственное решение может потребовать отражения (и если да, то, пожалуйста, разместите его ниже), в библиотеке, которую я использую, есть менеджер безопасности, который блокирует использование всех отражений. IE, рефлексивное решение может быть полезным для других в будущем, но для меня это бесполезно.

Кроме того, я не имею в виду использование библиотеки в моей собственной программе. Выполнено уже запущенное хост-приложение (которое я использую для библиотеки), а затем запускает код, который я пишу для него. Если этому приложению не нравится какой-либо код, который я предоставляю (IE, конфликтует с его менеджером безопасности), код никогда не компилируется.

Зачем мне это нужно:

Это связано с библиотекой, которую я использую. Поскольку 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
}

Ответы

Ответ 1

Единственный способ сделать то, что вы предлагаете, - использовать байтовый код Instrumentation. Вы можете добавить агента, который изменяет байт-код clazz, который вы хотите изменить до его загрузки.

Причина, по которой вам нужно сделать это во время загрузки, заключается в том, что многие JVM не позволят вам изменять поля, а некоторые не позволяют добавлять методы после загрузки класса.

Более простым решением является декомпиляция класса, его изменение и компиляция. Предполагая, что класс может быть декомпилирован, это сэкономит вам много времени и усилий.

библиотека, которую я использую, имеет диспетчера безопасности, который блокирует использование всего отражения

Это странный выбор, потому что вы можете создать собственный SecurityManager перед вызовом библиотеки, и это не помешает вам что-либо сделать.

Ответ 2

Вы можете использовать API-интерфейс Java-инструментария для (принудительного) адаптации класса к интерфейсу. Этот метод обычно используется структурами APM, AOP и профайлерами для ввода кода измерения каротажа и показателей в целевые классы во время выполнения. Для приложений очень необычно использовать эту технику. Это был бы большой красный флаг, если я увижу это в производственном коде.

Тем не менее,

Учитывая эти Clazz:

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;
    }

}

и требуемый файл MANIFEST.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

Ответ 3

Я не думаю, что вы хотите, есть Динамические прокси-серверы, но они используют отражение, и не похоже, что у вас есть доступ к загрузчику классов (где вы можете установить свой собственный который выполнял манипулирование байт-кодом "на лету" ).

Ответ 4

В зависимости от вашей java-версии вы можете использовать выражение лямбда (с java 8).

Код был бы относительно простым:

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

Обратите внимание, что это работает только для интерфейса с одним методом. Обе подписи (интерфейс и Clazz) должны совпадать.