/var/log/life.log
Блог программиста из солнечной Бурятии

функция eval в java своими руками

Функция eval присуща скриптовым языкам, не смотря на то, что Java не является скриптовым языком и такого метода там нет, существует возможность реализовать её самим, Java предоставляет для этого инструменты. Определимся, что для этого нужно:
1) генерация кода для компиляции
2) компилирование в байт-код
3) загрузка байт-кода и его исполнение

1. Генерация кода для компиляции

Для того чтобы выполнить код переданый в функцию eval, нужно загрузить класс и вызвать метод содержащий этот код, как следствие компилируемый класс будет выглядеть следующим образом:

public class SpecialClassToCompile {
    public void evalFunc(){
     /*
     * Вставляемый код для выполнения
     */

     }
}

2. Компиляция кода

В Java SE 6 был добавлен пакет javax.tools, предоставляющий стандартный API для компиляции исходного кода. Компилятор не входит в jre и для компиляции необходим jdk. Для вызова Java компилятора из программы необходим доступ к интерфейсу JavaCompiler. С его помощью можно указать CLASSPATH, путь до исходников и папку где будут создаваться скомпилированные классы. Класс ToolProvider включает реализацию интерфейса JavaCompiler и содержит метод getSystemJavaCompiler(), который возвращает экземпляр JavaCompiler.
Есть 2 способа компиляции
1 способ

int run(InputStream in,
    OutputStream out,
    OutputStream err,
    String... arguments)

in, out, err — входной, выходной и ошибок потоки ввода вывода соответственно, если передан в качестве параметра null, то будет использоваться System.out и т.д. arguments это перечисление через запятую аргументов, передаваемых компилятору. В случае ошибки компиляции, возвращается не нулевой результат. Нам это вариант не совсем подходит т.к. компиляция производится из файлов и опять же в файлы.
2 способ
Это метод getTask() позволяющий более гибко подойти к вопросу компиляции

JavaCompiler.CompilationTask getTask(
    Writer out,
    JavaFileManager fileManager,
    DiagnosticListener diagnosticListener,
    Iterable options,
    Iterable classes,
    Iterable compilationUnits
)

Многие из этих параметром могут быть безболезненно установлены в null, в этом случае будут использоваться значения по умолчанию
out: System.err
fileManager: стандартный файловый менеджер
diagnosticListener: стандартное поведение компилятора
options: компилятору не будет передано дополнительных опций
classes: без имён классов для обработки аннотаций
diagnosticListener — используется отловки ошибок возникших при компиляции и вывода диагностических сообщений.
Последний аргумент compilationUnits не может быть установлен в null, это как раз то, что будем компилировать.
StandardJavaFileManager содержит 2 метода, которые возвращают Iterable:

Iterable<? extends JavaFileObject> getJavaFileObjectsFromFiles(
    Iterable<? extends File> files)
Iterable<? extends JavaFileObject> getJavaFileObjectsFromStrings(
    Iterable<String> names)

в первом случае files это объекты класса File, а во втором имена файлов. Для того же чтобы получить JavaFileObject из строки создадим класс обвёртку MemorySource:

class MemorySource extends SimpleJavaFileObject {
    private String src;
    public MemorySource(String name, String src) {
        super(URI.create("string:///" + name.replace('.','/') + Kind.SOURCE.extension),Kind.SOURCE);
        this.src = src;
    }
    public CharSequence getCharContent(boolean ignoreEncodingErrors) {
        return src;
    }
}

В методе getTask устраивает всё, кроме того, что байт-код скомпилированного класса будет писаться в файл. Для того чтобы байт-код не писался в файл, создадим свой файловый менеджер:

class SpecialJavaFileManager extends ForwardingJavaFileManager<StandardJavaFileManager> {
    private SpecialClassLoader classLoader;
    public SpecialJavaFileManager(StandardJavaFileManager fileManager, SpecialClassLoader specClassLoader) {
        super(fileManager);
        classLoader = specClassLoader;
    }
    public JavaFileObject getJavaFileForOutput(Location location, String name, JavaFileObject.Kind kind, FileObject sibling) throws IOException {
        MemoryByteCode byteCode = new MemoryByteCode(name);
        classLoader.addClass(byteCode);
        return byteCode;
    }
}

SpecialClassLoader — загрузчик классов который будет рассмотрен в 3 части.
Единственный метод, который нужно будет переопределить для нашей цели это getJavaFileForOutput, который должен возвращать объект класса JavaFileObject. В этом возвращаемом объекте и кроется вся магия записи байт-кода в память, этот объект класса MemoryByteCode:

class MemoryByteCode extends SimpleJavaFileObject {
    private ByteArrayOutputStream oStream;
    public MemoryByteCode(String name) {
        super(URI.create("byte:///" + name.replace('/','.') + Kind.CLASS.extension),Kind.CLASS);
    }
    public OutputStream openOutputStream() {
        oStream = new ByteArrayOutputStream();
        return oStream;
    }
    public byte[] getBytes() {
        return oStream.toByteArray();
    }
}

openOutputStream переопределённый метод из SimpleJavaFileObject, с помощью которого и будет байт-код записываться в память, а не в файл.
getBytes нужен, чтобы потом получить этот самый байт-код.

3. Загрузка скомпилированного кода.

про загрузку java-классов хорошо написано в тут Блог сурового челябинского программиста: Пишем свой загрузчик java-классов, поэтому, сразу рассмотрю код загрузчика

class SpecialClassLoader extends ClassLoader {
    private MemoryByteCode byteCode;
    protected Class<?> findClass(String name) {
        return defineClass(name, byteCode.getBytes(), 0, byteCode.getBytes().length);
    }
    public void addClass(MemoryByteCode code) {
        byteCode = code;
    }
}

загрузчик прост, метод addClass запоминает скомпилированный байт-код.
И при необходимости создаётся экземпляр этого класса.

Результат:

Собрав всё воедино получаем класс Eval:

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintStream;
import java.net.URI;
import java.util.Arrays;

import javax.tools.Diagnostic;
import javax.tools.DiagnosticCollector;
import javax.tools.FileObject;
import javax.tools.ForwardingJavaFileManager;
import javax.tools.JavaCompiler;
import javax.tools.JavaFileObject;
import javax.tools.SimpleJavaFileObject;
import javax.tools.StandardJavaFileManager;
import javax.tools.ToolProvider;
import javax.tools.JavaCompiler.CompilationTask;


public class Eval {
    public void exec(String code) throws Exception {
        String src = "public class SpecialClassToCompile {"+
        "public void evalFunc(){"+
        code+
        "}"+
        "}";
        SpecialClassLoader classLoader = new SpecialClassLoader();
        compileMemoryMemory(src, "SpecialClassToCompile", classLoader, System.err);
        Class<?> c = Class.forName("SpecialClassToCompile", false, classLoader);
        Object o = c.newInstance();
        c.getMethod("evalFunc", new Class[] {}).invoke(o, new Object[] { });

    }
    public void compileMemoryMemory(String src, String name, SpecialClassLoader classLoader, PrintStream err) {
        JavaCompiler javac = ToolProvider.getSystemJavaCompiler();
        DiagnosticCollector<JavaFileObject> diacol = new DiagnosticCollector<JavaFileObject>();
        StandardJavaFileManager standartFileManager = javac.getStandardFileManager(diacol, null, null);
        SpecialJavaFileManager fileManager = new SpecialJavaFileManager(standartFileManager, classLoader);
        CompilationTask compile = javac.getTask(null, fileManager, diacol, null, null,
                Arrays.asList(new JavaFileObject[] { new MemorySource(name, src) }));
        boolean status = compile.call();
        if(err != null) {
            err.println("Compile status: " + status);
            for(Diagnostic<? extends JavaFileObject> dia : diacol.getDiagnostics()) {
                err.println(dia);
            }
        }
    }
}
/**
 * Класс для создания кода из строки
 * @author vampirus
 *
 */

class MemorySource extends SimpleJavaFileObject {
    private String src;
    public MemorySource(String name, String src) {
        super(URI.create("string:///" + name.replace('.','/') + Kind.SOURCE.extension),Kind.SOURCE);
        this.src = src;
    }
    public CharSequence getCharContent(boolean ignoreEncodingErrors) {
        return src;
    }
}
/**
 * Класс для записи байткода в память
 * @author vampirus
 *
 */

class MemoryByteCode extends SimpleJavaFileObject {
    private ByteArrayOutputStream oStream;
    public MemoryByteCode(String name) {
        super(URI.create("byte:///" + name.replace('/','.') + Kind.CLASS.extension),Kind.CLASS);
    }
    public OutputStream openOutputStream() {
        oStream = new ByteArrayOutputStream();
        return oStream;
    }
    public byte[] getBytes() {
        return oStream.toByteArray();
    }
}
/**
 * Файловый менеджер
 * @author vampirus
 *
 */

class SpecialJavaFileManager extends ForwardingJavaFileManager<StandardJavaFileManager> {
    private SpecialClassLoader classLoader;
    public SpecialJavaFileManager(StandardJavaFileManager fileManager, SpecialClassLoader specClassLoader) {
        super(fileManager);
        classLoader = specClassLoader;
    }
    public JavaFileObject getJavaFileForOutput(Location location, String name, JavaFileObject.Kind kind, FileObject sibling) throws IOException {
        MemoryByteCode byteCode = new MemoryByteCode(name);
        classLoader.addClass(byteCode);
        return byteCode;
    }
}
/**
 * Загрузчик
 * @author vampirus
 *
 */

class SpecialClassLoader extends ClassLoader {
    private MemoryByteCode byteCode;
    protected Class<?> findClass(String name) {
        return defineClass(name, byteCode.getBytes(), 0, byteCode.getBytes().length);
    }
    public void addClass(MemoryByteCode code) {
        byteCode = code;
    }
}

тестим:

public class Main {

    /**
     * @param args
     */

    public static void main(String[] args) {
        // TODO Auto-generated method stub
        Eval e = new Eval();
        try {
            e.exec("System.out.println("testing");");
            e.exec("System.out.println("testing2");");
        } catch (Exception e1) {
            // TODO Auto-generated catch block
            e1.printStackTrace();
        }
    }

}

Естественно, применение такого способа не ограничивается функцией eval, полезность которой, вообще говоря, весьма сомнительна.

10 комментариев на запись «функция eval в java своими руками»

  1. reson пишет:

    А чем нельзя использовать либу javaassist, которой jdk не нужен?
    Стоит еще отметить, что кодогенерацию, например, запрещено использовать по стандарту J2ee 5. Т.е.  под сервером приложений код работать не будет.

  2. reson пишет:

    А чем нельзя использовать либу javaassist, которой jdk не нужен?
    Стоит еще отметить, что кодогенерацию, например, запрещено использовать по стандарту J2ee 5. Т.е.  под сервером приложений код работать не будет.

  3. VampiRUS пишет:

    Ну да это, не для серверной платформы.

  4. VampiRUS пишет:

    Ну да это, не для серверной платформы.

  5. Vitaliy пишет:

    Неплохо. Вопрос на засыпку автору — для решения каких реальных практических задач пригодится run time компиляция?

  6. VampiRUS пишет:

    Нам она была в нужна какой-то сомнительной вещи для генерации/тестирования автоматов в рантайме в среде unimod :), но тогда этого сделать не удалось, немного спустя, когда и надобность в этом пропала решил попробовать всё-таки сделать её.

  7. Vitaliy пишет:

    Неплохо. Вопрос на засыпку автору — для решения каких реальных практических задач пригодится run time компиляция?

  8. VampiRUS пишет:

    Нам она была в нужна какой-то сомнительной вещи для генерации/тестирования автоматов в рантайме в среде unimod :), но тогда этого сделать не удалось, немного спустя, когда и надобность в этом пропала решил попробовать всё-таки сделать её.

  9. Xafizoff пишет:

    А вы не могли бы ссылку кинуть на спецификацию, где запрещается использовать javax.tool в J2EE?

  10. Вячеслав Игорович пишет:

    Просто бомба, то что надо, искал такое пол дня, конечно переделал под нужные задачи, но ОЧЕНЬ СПАСИБО, конечно было бы лучше бы без jdk, через jre, но хоть что-то))

Оставить комментарий

Вы так же можете оставить комментарий используя ваш OpenId, для этого нужно войти.