Статьи

Java компиляция в Java

В предыдущем посте я писал о том, как генерировать прокси во время выполнения, и мы дошли до создания исходного кода Java. Однако для использования класса его необходимо скомпилировать и сгенерированный байт-код загрузить в память. Это время «компиляции». К счастью, начиная с Java 1.6 у нас есть доступ к компилятору Java во время выполнения, и мы можем, таким образом, смешивать время компиляции во время выполнения. Хотя это может привести к множеству ужасных вещей, которые обычно приводят к неосуществимому самоизменяющемуся коду в этом особом случае, это может быть полезно: мы можем скомпилировать наш прокси, сгенерированный во время выполнения.

API компилятора Java

Компилятор Java читает исходные файлы и генерирует файлы классов. (Сборка их в JAR, WAR, EAR и другие пакеты является обязанностью другого инструмента.) Исходные файлы и файлы классов не обязательно должны быть реальными файлами операционной системы, находящимися на магнитном диске, SSD или накопителе. В конце концов, Java обычно хорош в абстракции, когда дело доходит до API времени выполнения, и сейчас это так. Эти файлы представляют собой некоторые «абстрактные» файлы, к которым вам необходимо предоставить доступ через API, которые могут быть дисковыми файлами, но в то же время они могут быть почти любым другим. Как правило, было бы напрасной тратой ресурсов сохранить исходный код на диск, чтобы позволить компилятору, работающему в том же процессе, прочитать его обратно и сделать то же самое с файлами классов, когда они будут готовы.

Компилятор Java как API, доступный во время выполнения, требует, чтобы вы предоставили некоторый простой API (или SPI, который вам нравится, как термин) для доступа к исходному коду, а также для отправки сгенерированного байтового кода. Если у нас есть код в памяти, мы можем иметь следующий код ( из этого файла ):

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public Class<?> compile(String sourceCode, String canonicalClassName)
            throws Exception {
        JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
        List<JavaSourceFromString> sources = new LinkedList<>();
        String className = calculateSimpleClassName(canonicalClassName);
        sources.add(new JavaSourceFromString(className, sourceCode));
 
        StringWriter sw = new StringWriter();
        MemoryJavaFileManager fm = new MemoryJavaFileManager(
                compiler.getStandardFileManager(null, null, null));
        JavaCompiler.CompilationTask task = compiler.getTask(sw, fm, null,
                null, null, sources);
 
        Boolean compilationWasSuccessful = task.call();
        if (compilationWasSuccessful) {
            ByteClassLoader byteClassLoader = new ByteClassLoader(new URL[0],
                    classLoader, classesByteArraysMap(fm));
 
            Class<?> klass = byteClassLoader.loadClass(canonicalClassName);
            byteClassLoader.close();
            return klass;
        } else {
            compilerErrorOutput = sw.toString();
            return null;
        }
    }

Экземпляр компилятора доступен через ToolProvider и для создания задачи компиляции мы должны вызвать getTask() . Код записывает ошибки в строку с помощью средства записи строк. Файловый менеджер ( fm ) реализован в том же пакете, и он просто хранит файлы в виде байтовых массивов на карте, где ключами являются «имена файлов». Это где загрузчик классов получит байты позже, когда класс (ы) загружены. Код не предоставляет никакого диагностического слушателя (см. Документацию java-компилятора в RT), опций компилятора или классов, обрабатываемых обработчиками аннотаций. Это все нули. Последний аргумент — это список исходных кодов для компиляции. В этом инструменте мы компилируем только один отдельный класс, но поскольку API компилятора является общим и ожидает итеративный источник, мы предоставляем список. Поскольку существует другой уровень абстракции, этот список содержит JavaSourceFromString s.

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

Использование класса очень просто, и вы можете найти образцы в модульных тестах:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
private String loadJavaSource(String name) throws IOException {
        InputStream is = this.getClass().getResourceAsStream(name);
        byte[] buf = new byte[3000];
        int len = is.read(buf);
        is.close();
        return new String(buf, 0, len, "utf-8");
    }
...
    @Test
    public void given_PerfectSourceCodeWithSubClasses_when_CallingCompiler_then_ProperClassIsReturned()
            throws Exception {
        final String source = loadJavaSource("Test3.java");
        Compiler compiler = new Compiler();
        Class<?> newClass = compiler.compile(source, "com.javax0.jscc.Test3");
        Object object = newClass.newInstance();
        Method f = newClass.getMethod("method");
        int i = (int) f.invoke(object, null);
        Assert.assertEquals(1, i);
    }

Обратите внимание, что классы, которые вы создаете таким образом, доступны вашему коду только во время выполнения. Например, вы можете создавать неизменяемые версии ваших объектов. Если вы хотите иметь классы, доступные во время компиляции, вы должны использовать процессор аннотаций, такой как scriapt .

Ссылка: Компиляция Java в Java от нашего партнера JCG Питера Верхаса из блога Java Deep .