функция 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 |
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, полезность которой, вообще говоря, весьма сомнительна.