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

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

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

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

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

1
2
3
4
5
6
7
public class SpecialClassToCompile {
    public void evalFunc(){
     /*
     * Вставляемый код для выполнения
     */

     }
}

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

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

1
2
3
4
int run(InputStream in,
    OutputStream out,
    OutputStream err,
    String... arguments)

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

1
2
3
4
5
6
7
8
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:

1
2
3
4
Iterable<? extends JavaFileObject> getJavaFileObjectsFromFiles(
    Iterable<? extends File> files)
Iterable<? extends JavaFileObject> getJavaFileObjectsFromStrings(
    Iterable<String> names)

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

1
2
3
4
5
6
7
8
9
10
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 устраивает всё, кроме того, что байт-код скомпилированного класса будет писаться в файл. Для того чтобы байт-код не писался в файл, создадим свой файловый менеджер:

1
2
3
4
5
6
7
8
9
10
11
12
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
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-классов, поэтому, сразу рассмотрю код загрузчика

1
2
3
4
5
6
7
8
9
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
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;
    }
}

тестим:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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, полезность которой, вообще говоря, весьма сомнительна.