Принципы в заметке общие для почти любого языка программирования и системы исполнения, но акцент будет на jvm. Рассмотрим два основных подхода по модификации программы:
Метафора, связанная с изображением в заметке: программа — это основное здание, а результат трансформации программы — вспомогательная конструкция.
Начнем с вопроса зачем программе модифицировать другую программу. Метапрограммирование помогает уменьшить объем boilerplate кода в проекте и концентрироваться на главном, улучшить читаемость кода и решить задачи которые сложно решить другим способом.
Простейший пример в java — JavaBeans и get/set методы для доступа к полям класса, также примером может служить создание билдеров для класса, автореализации equals/hash в IDE и т.п.
Следующий пример — это логирование, автоматическое управление транзакциями. Все к чему привыкли при использовании Spring Framework и редко задумываемся как это реализовано. Но даже Spring создал сложности с конфигурацией и инициализацией фреймворка для новичков, что послужило причиной появления «магического» Spring Boot/Spring Roo. Но это отдельная тема, мы же вернемся к теме модификации программы.
Обфускация и минимизация кода, инструментирующие профилировщики и мир DevOps третий пример для чего нужно модифицировать программу. Думаю, что пропустил другие важные примеры, вы можете дополнить в комментариях.
Итак, зачем модифицировать программу мы определились, теперь рассмотрим, как это обычно делают.
Можно модифицировать байт-код программы и это самый распространенный способ. Делать это можно как сразу после компиляции, но перед сборкой jar, так и при загрузке класса. В первом случае это будет плагин системы сборки проекта, во втором специальный загрузчик классов или java агент, либо механизм hotswap в jvm. Подход с агентами и загрузчиками классов очень похож на самомодифицирующиеся программы в машинном коде и полиморфные вирусы.
Байт-код файла, который загружает jvm имеет структуру, описанную в официальной документации о формате класса
Приложение javap позволяет просматривать байт-код скомпилированного класса
Но модифицировать байт-код вручную имеет смысл только в учебных целях или если вы ниндзя, который любит создавать и преодолевать сложности. The Java Virtual Machine Specification отвечает на большинство вопросов на этом этапе.
В промышленном программировании работу с байт-кодом упрощают библиотеки ASM, javassist, BCEL, CGLIB. После парсинга байт кода, тот же ASM позволяет программисту работать с байт кодом как через Tree API, так и в событийной модели, используя шаблон visitor. Кроме анализа, возможна и модификация, добавление новых инструкций, полей, методов и т.п. В природе существуют и другие библиотеки работы с байт кодом, но их используют реже.
Пример использования API ASM для
Аспектно-ориентированный подход можно считать высокоуровневым способом модификации программы. В реализации AspectJ на уровне агента, загрузчика классов или плагина вся «магия» АОП превращается в манипуляции с байт-кодом классов. Но как это видит программист при разработке отличается от того как модифицируется байт код «под капотом» с помощью того же ASM и BCEL. Если интересно, что фактически добавляет AspectJ в классы вашего приложения, можно включить дамп модифицированных классов и по локоть влезть в этот код, например, с помощью Java Decompiler.
В AspectJ разработчик определяет действия в виде классов, аннотируя их как аспекты и указывая в каких точках программы(Pointcut) их следует вызывать. Синтаксис определения pointcut выражений также достаточно высокоуровневый. Такой подход к модификации байт-кода более прост в использовании для программиста.
Трансформация программы за счет модификации байт-кода имеет свои сильные и слабые стороны:
Теория и практика трансформации исходного кода давно применяется в метапрограммировании, Prolog, Lisp, макросах и препроцессорах языков программирования.
При данном подходе исходный текст программы трансформируется либо дополняется другой программой перед компиляцией, а затем компилируется. Работать удобнее не с самим текстом программы, а с абстрактным синтаксическим деревом построенным из него (abstract syntax tree, AST).
Опять же, удобство метапрограммирования зависит от поддержки его в самом языке программирования. Существует шутка
Поэтому в jvm чуть сложнее, хоть механизм reflection и является частью языка и платформа может динамически загружать и исполнять байт-код. Сразу на ум приходят две технологии, которые используют кодогенерацию — JPA static metamodel generator и jaxb code generation. Другой пример — project Lombok, который позволяет автоматически реализовать, то что раньше генерировалось IDE или писалось вручную и поддерживалось разработчиками.
Реализовано в Lombok это с помощью модификации AST пользовательской программы и кодогенерации.
Схожая функциональность, со своими ограничениями, есть и в java для аннотаций с областью видимости compile time — Annotation Processing Tool.
В случае с парсингом java исходного кода, лучше чем javac и eclipse java compiller вряд ли кто справится. Есть альтернативы, такие как Spoon и JTransformer, но насколько полно они поддерживают спецификацию и сложные классы, даже нет желания проверять.
Раз уж речь идет о jvm, то трансформация исходного текста программы на Groovy является частью самого языка, подобные возможности есть и в языке Scala.
Итак, в модификации исходного кода программы есть слабые и сильные стороны:
Самая хардкорная часть этой заметки — мысли про перекомпиляцию в рантайм. Модификация и компиляция AST java кода в момент выполнения может быть нужна, если требуются костыли: проект либо полный капролит, либо делать и поддерживать десятки форков разных версий очень трудоемко, либо если его менеджмент и разработчики считают его идеальным и не позволяют никому его модификацию, но при этом исходный текст есть в enterprise maven репозитарии. И этот подход нужен только если задачу невозможно или неудобно решать двумя ранее описанными классами трансформации программы.
С компиляцией все относительно просто. JavaCompiler API позволяет скомпилировать программу из исходного кода в момент выполнения, предоставляя интерфейс независимый от реализации. При изучении манифеста и исходных текстов eclipse EJC компилятора, обнаружил что и он поддерживает JavaCompiler API.
Но при анализе текста программы все равно нет публичного и универсального API для работы с AST. Т.е. придется работать либо с com.sun.source.tree.* либо org.eclipse.jdt.core.dom.*
Задача с поиском исходного текста класса легко решается, если проект публиковался в maven репозитарий вместе с артефактом типа source и в jar с классами есть файлы pom.properties или pom.xml, либо есть некий словарь соответствия названия/хеша артефакта исходному коду соответствующего jar файла и способ получить эти исходники во время работы программы.
Плюсы — для трансформации доступно больше информации, чем есть в байт-коде, применение не требует пересборки проекта и почти так же удобно как и применение AspectJ агента, но при этом трансформацию невозможно было выполнить средствами преобразования байт-кода, либо очень трудоемко.
Минусы такие же, как и у прошлого подхода: память, время, требование к наличию исходного текста программы и способа его найти для данного класса.
Примеры вышесказанного в виде кода ejc+maven будут в ближайшие месяцы, да и задача выбрана вполне жизненная. Сталкивались ли вы с подобным? Какие задачи из вашей практики можно было бы элегантно решить только с помощью трансформации java кода и рекомпиляции во время выполнения?
Кстати, возможности компилятора TinyCC и его размер доказывают, что такой подход возможен и для C программ.
В заметке мы рассмотрели несколько подходов по модификации программы, их сильные и слабые стороны. Модификация исполняемого кода более распространена, но не все задачи можно решить без наличия исходного кода программы и его последующей трансформации.
- манипуляции с исполняемым кодом программы после компиляции или во время загрузки кода;
- изменение исходного кода перед компиляцией.
Метафора, связанная с изображением в заметке: программа — это основное здание, а результат трансформации программы — вспомогательная конструкция.
Зачем модифицировать?
Начнем с вопроса зачем программе модифицировать другую программу. Метапрограммирование помогает уменьшить объем boilerplate кода в проекте и концентрироваться на главном, улучшить читаемость кода и решить задачи которые сложно решить другим способом.
Простейший пример в java — JavaBeans и get/set методы для доступа к полям класса, также примером может служить создание билдеров для класса, автореализации equals/hash в IDE и т.п.
Следующий пример — это логирование, автоматическое управление транзакциями. Все к чему привыкли при использовании Spring Framework и редко задумываемся как это реализовано. Но даже Spring создал сложности с конфигурацией и инициализацией фреймворка для новичков, что послужило причиной появления «магического» Spring Boot/Spring Roo. Но это отдельная тема, мы же вернемся к теме модификации программы.
Обфускация и минимизация кода, инструментирующие профилировщики и мир DevOps третий пример для чего нужно модифицировать программу. Думаю, что пропустил другие важные примеры, вы можете дополнить в комментариях.
Итак, зачем модифицировать программу мы определились, теперь рассмотрим, как это обычно делают.
Модификация исполняемых инструкций программы
Можно модифицировать байт-код программы и это самый распространенный способ. Делать это можно как сразу после компиляции, но перед сборкой jar, так и при загрузке класса. В первом случае это будет плагин системы сборки проекта, во втором специальный загрузчик классов или java агент, либо механизм hotswap в jvm. Подход с агентами и загрузчиками классов очень похож на самомодифицирующиеся программы в машинном коде и полиморфные вирусы.
Байт-код файла, который загружает jvm имеет структуру, описанную в официальной документации о формате класса
Приложение javap позволяет просматривать байт-код скомпилированного класса
Пример из официальной документации
Исходный текст класса:
Вывод javap на консоль для байт-кода этого класса:
import java.awt.*;
import java.applet.*;
public class DocFooter extends Applet {
String date;
String email;
public void init() {
resize(500,100);
date = getParameter("LAST_UPDATED");
email = getParameter("EMAIL");
}
public void paint(Graphics g) {
g.drawString(date + " by ",100, 15);
g.drawString(email,290,15);
}
}
Вывод javap на консоль для байт-кода этого класса:
Compiled from "DocFooter.java"
public class DocFooter extends java.applet.Applet {
java.lang.String date;
java.lang.String email;
public DocFooter();
Code:
0: aload_0
1: invokespecial #1 // Method java/applet/Applet."<init>":()V
4: return
public void init();
Code:
0: aload_0
1: sipush 500
4: bipush 100
6: invokevirtual #2 // Method resize:(II)V
9: aload_0
10: aload_0
11: ldc #3 // String LAST_UPDATED
13: invokevirtual #4 // Method getParameter:(Ljava/lang/String;)Ljava/lang/String;
16: putfield #5 // Field date:Ljava/lang/String;
19: aload_0
20: aload_0
21: ldc #6 // String EMAIL
23: invokevirtual #4 // Method getParameter:(Ljava/lang/String;)Ljava/lang/String;
26: putfield #7 // Field email:Ljava/lang/String;
29: return
public void paint(java.awt.Graphics);
Code:
0: aload_1
1: new #8 // class java/lang/StringBuilder
4: dup
5: invokespecial #9 // Method java/lang/StringBuilder."<init>":()V
8: aload_0
9: getfield #5 // Field date:Ljava/lang/String;
12: invokevirtual #10 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
15: ldc #11 // String by
17: invokevirtual #10 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
20: invokevirtual #12 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
23: bipush 100
25: bipush 15
27: invokevirtual #13 // Method java/awt/Graphics.drawString:(Ljava/lang/String;II)V
30: aload_1
31: aload_0
32: getfield #7 // Field email:Ljava/lang/String;
35: sipush 290
38: bipush 15
40: invokevirtual #13 // Method java/awt/Graphics.drawString:(Ljava/lang/String;II)V
43: return
}
Но модифицировать байт-код вручную имеет смысл только в учебных целях или если вы ниндзя, который любит создавать и преодолевать сложности. The Java Virtual Machine Specification отвечает на большинство вопросов на этом этапе.
В промышленном программировании работу с байт-кодом упрощают библиотеки ASM, javassist, BCEL, CGLIB. После парсинга байт кода, тот же ASM позволяет программисту работать с байт кодом как через Tree API, так и в событийной модели, используя шаблон visitor. Кроме анализа, возможна и модификация, добавление новых инструкций, полей, методов и т.п. В природе существуют и другие библиотеки работы с байт кодом, но их используют реже.
Пример использования API ASM для
анализа байт-кода
/***
* ASM examples: examples showing how ASM can be used
* Copyright (c) 2000-2011 INRIA, France Telecom
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* 3. Neither the name of the copyright holders nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
* THE POSSIBILITY OF SUCH DAMAGE.
*/
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.tree.AbstractInsnNode;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.IincInsnNode;
import org.objectweb.asm.tree.MethodNode;
import org.objectweb.asm.tree.VarInsnNode;
import org.objectweb.asm.tree.analysis.Analyzer;
import org.objectweb.asm.tree.analysis.BasicValue;
import org.objectweb.asm.tree.analysis.BasicVerifier;
import org.objectweb.asm.tree.analysis.Frame;
import org.objectweb.asm.tree.analysis.SourceInterpreter;
import org.objectweb.asm.tree.analysis.SourceValue;
import org.objectweb.asm.util.TraceMethodVisitor;
import org.objectweb.asm.util.Textifier;
/**
* @author Eric Bruneton
*/
public class Analysis implements Opcodes {
public static void main(final String[] args) throws Exception {
ClassReader cr = new ClassReader("Analysis");
ClassNode cn = new ClassNode();
cr.accept(cn, ClassReader.SKIP_DEBUG);
List<MethodNode> methods = cn.methods;
for (int i = 0; i < methods.size(); ++i) {
MethodNode method = methods.get(i);
if (method.instructions.size() > 0) {
if (!analyze(cn, method)) {
Analyzer<?> a = new Analyzer<BasicValue>(
new BasicVerifier());
try {
a.analyze(cn.name, method);
} catch (Exception ignored) {
}
final Frame<?>[] frames = a.getFrames();
Textifier t = new Textifier() {
@Override
public void visitMaxs(final int maxStack,
final int maxLocals) {
for (int i = 0; i < text.size(); ++i) {
StringBuilder s = new StringBuilder(
frames[i] == null ? "null"
: frames[i].toString());
while (s.length() < Math.max(20, maxStack
+ maxLocals + 1)) {
s.append(' ');
}
System.err.print(Integer.toString(i + 1000)
.substring(1)
+ " "
+ s
+ " : "
+ text.get(i));
}
System.err.println();
}
};
MethodVisitor mv = new TraceMethodVisitor(t);
for (int j = 0; j < method.instructions.size(); ++j) {
Object insn = method.instructions.get(j);
((AbstractInsnNode) insn).accept(mv);
}
mv.visitMaxs(0, 0);
}
}
}
}
/*
* Detects unused xSTORE instructions, i.e. xSTORE instructions without at
* least one xLOAD corresponding instruction in their successor instructions
* (in the control flow graph).
*/
public static boolean analyze(final ClassNode c, final MethodNode m)
throws Exception {
Analyzer<SourceValue> a = new Analyzer<SourceValue>(
new SourceInterpreter());
Frame<SourceValue>[] frames = a.analyze(c.name, m);
// for each xLOAD instruction, we find the xSTORE instructions that can
// produce the value loaded by this instruction, and we put them in
// 'stores'
Set<AbstractInsnNode> stores = new HashSet<AbstractInsnNode>();
for (int i = 0; i < m.instructions.size(); ++i) {
AbstractInsnNode insn = m.instructions.get(i);
int opcode = insn.getOpcode();
if ((opcode >= ILOAD && opcode <= ALOAD) || opcode == IINC) {
int var = opcode == IINC ? ((IincInsnNode) insn).var
: ((VarInsnNode) insn).var;
Frame<SourceValue> f = frames[i];
if (f != null) {
Set<AbstractInsnNode> s = f.getLocal(var).insns;
Iterator<AbstractInsnNode> j = s.iterator();
while (j.hasNext()) {
insn = j.next();
if (insn instanceof VarInsnNode) {
stores.add(insn);
}
}
}
}
}
// we then find all the xSTORE instructions that are not in 'stores'
boolean ok = true;
for (int i = 0; i < m.instructions.size(); ++i) {
AbstractInsnNode insn = m.instructions.get(i);
int opcode = insn.getOpcode();
if (opcode >= ISTORE && opcode <= ASTORE) {
if (!stores.contains(insn)) {
ok = false;
System.err.println("method " + m.name + ", instruction "
+ i + ": useless store instruction");
}
}
}
return ok;
}
/*
* Test for the above method, with three useless xSTORE instructions.
*/
public int test(int i, int j) {
i = i + 1; // ok, because i can be read after this point
if (j == 0) {
j = 1; // useless
} else {
try {
j = j - 1; // ok, because j can be accessed in the catch
int k = 0;
if (i > 0) {
k = i - 1;
}
return k;
} catch (Exception e) { // useless ASTORE (e is never used)
j = j + 1; // useless
}
}
return 0;
}
}
Аспектно-ориентированный подход можно считать высокоуровневым способом модификации программы. В реализации AspectJ на уровне агента, загрузчика классов или плагина вся «магия» АОП превращается в манипуляции с байт-кодом классов. Но как это видит программист при разработке отличается от того как модифицируется байт код «под капотом» с помощью того же ASM и BCEL. Если интересно, что фактически добавляет AspectJ в классы вашего приложения, можно включить дамп модифицированных классов и по локоть влезть в этот код, например, с помощью Java Decompiler.
В AspectJ разработчик определяет действия в виде классов, аннотируя их как аспекты и указывая в каких точках программы(Pointcut) их следует вызывать. Синтаксис определения pointcut выражений также достаточно высокоуровневый. Такой подход к модификации байт-кода более прост в использовании для программиста.
Подробнее показывал и рассказывал на примерах в цикле публикаций на хабре
- Диагностируем причину, выживаем в JAR hell: не дышим серой и не варимся в котле
- Публикация логов в Elasticsearch — жизнь без регулярных выражений и без logstash
- Протоколирование JDBC запросов и их параметров в существующем приложении
- Хабр шелл: встраиваем кросплатформенный ssh server в java приложение
- Внедрение веб консолей в jvm процесс на примере SonarQube
- Доклад: «Аспектно-ориентированное программирование в распределенных системах для java разработчиков и QA»
- Напильники бывают разные или повествование про «напильник» для java программ
Трансформация программы за счет модификации байт-кода имеет свои сильные и слабые стороны:
Плюсы | Минусы |
---|---|
подход работает при отсутствии исходных текстов программы | сложность анализа и модификации, при нетривиальных трансформациях |
независимость от компилятора | отсутствие информации, доступной только в исходном коде |
Трансформация AST исходного кода, метапрограммирование
Теория и практика трансформации исходного кода давно применяется в метапрограммировании, Prolog, Lisp, макросах и препроцессорах языков программирования.
При данном подходе исходный текст программы трансформируется либо дополняется другой программой перед компиляцией, а затем компилируется. Работать удобнее не с самим текстом программы, а с абстрактным синтаксическим деревом построенным из него (abstract syntax tree, AST).
Опять же, удобство метапрограммирования зависит от поддержки его в самом языке программирования. Существует шутка
В Лиспе, если охота аспектно-ориентированного программирования, нужно лишь настругать немного макросов, и готово. В Java, нужен Грегор Кичалес, создающий новую фирму, и месяцы и годы попыток заставить всё работать.Петер Норвиг
Поэтому в jvm чуть сложнее, хоть механизм reflection и является частью языка и платформа может динамически загружать и исполнять байт-код. Сразу на ум приходят две технологии, которые используют кодогенерацию — JPA static metamodel generator и jaxb code generation. Другой пример — project Lombok, который позволяет автоматически реализовать, то что раньше генерировалось IDE или писалось вручную и поддерживалось разработчиками.
Аннотации проекта Lombok
val
Finally! Hassle-free final local variables.
@ NonNull
or: How I learned to stop worrying and love the NullPointerException.
@ Cleanup
Automatic resource management: Call your close() methods safely with no hassle.
@ Getter / @ Setter
Never write public int getFoo() {return foo;} again.
@ ToString
No need to start a debugger to see your fields: Just let lombok generate a toString for you!
@ EqualsAndHashCode
Equality made easy: Generates hashCode and equals implementations from the fields of your object.
@ NoArgsConstructor, @ RequiredArgsConstructor and @ AllArgsConstructor
Constructors made to order: Generates constructors that take no arguments, one argument per final / non-null field, or one argument for every field.
@ Data
All together now: A shortcut for @ ToString, @ EqualsAndHashCode, @ Getter on all fields, and @ Setter on all non-final fields, and @ RequiredArgsConstructor!
@ Value
Immutable classes made very easy.
@ Builder
… and Bob's your uncle: No-hassle fancy-pants APIs for object creation!
@ SneakyThrows
To boldly throw checked exceptions where no one has thrown them before!
@ Synchronized
synchronized done right: Don't expose your locks.
@ Getter(lazy=true)
Laziness is a virtue!
@ Log
Captain's Log, stardate 24435.7: «What was that line again?»
Finally! Hassle-free final local variables.
@ NonNull
or: How I learned to stop worrying and love the NullPointerException.
@ Cleanup
Automatic resource management: Call your close() methods safely with no hassle.
@ Getter / @ Setter
Never write public int getFoo() {return foo;} again.
@ ToString
No need to start a debugger to see your fields: Just let lombok generate a toString for you!
@ EqualsAndHashCode
Equality made easy: Generates hashCode and equals implementations from the fields of your object.
@ NoArgsConstructor, @ RequiredArgsConstructor and @ AllArgsConstructor
Constructors made to order: Generates constructors that take no arguments, one argument per final / non-null field, or one argument for every field.
@ Data
All together now: A shortcut for @ ToString, @ EqualsAndHashCode, @ Getter on all fields, and @ Setter on all non-final fields, and @ RequiredArgsConstructor!
@ Value
Immutable classes made very easy.
@ Builder
… and Bob's your uncle: No-hassle fancy-pants APIs for object creation!
@ SneakyThrows
To boldly throw checked exceptions where no one has thrown them before!
@ Synchronized
synchronized done right: Don't expose your locks.
@ Getter(lazy=true)
Laziness is a virtue!
@ Log
Captain's Log, stardate 24435.7: «What was that line again?»
Реализовано в Lombok это с помощью модификации AST пользовательской программы и кодогенерации.
Схожая функциональность, со своими ограничениями, есть и в java для аннотаций с областью видимости compile time — Annotation Processing Tool.
В случае с парсингом java исходного кода, лучше чем javac и eclipse java compiller вряд ли кто справится. Есть альтернативы, такие как Spoon и JTransformer, но насколько полно они поддерживают спецификацию и сложные классы, даже нет желания проверять.
Раз уж речь идет о jvm, то трансформация исходного текста программы на Groovy является частью самого языка, подобные возможности есть и в языке Scala.
Итак, в модификации исходного кода программы есть слабые и сильные стороны:
Плюсы | Минусы |
---|---|
большее количество информации, чем в байт-коде | этап компиляции или интерпретации (память, время) |
возможности, подобные рефакторингу в IDE | требование к наличию исходных текстов, способа автоматически найти их для заданного класса/jar |
Трансформация AST кода и рекомпиляция во время выполнения
Самая хардкорная часть этой заметки — мысли про перекомпиляцию в рантайм. Модификация и компиляция AST java кода в момент выполнения может быть нужна, если требуются костыли: проект либо полный капролит, либо делать и поддерживать десятки форков разных версий очень трудоемко, либо если его менеджмент и разработчики считают его идеальным и не позволяют никому его модификацию, но при этом исходный текст есть в enterprise maven репозитарии. И этот подход нужен только если задачу невозможно или неудобно решать двумя ранее описанными классами трансформации программы.
С компиляцией все относительно просто. JavaCompiler API позволяет скомпилировать программу из исходного кода в момент выполнения, предоставляя интерфейс независимый от реализации. При изучении манифеста и исходных текстов eclipse EJC компилятора, обнаружил что и он поддерживает JavaCompiler API.
Но при анализе текста программы все равно нет публичного и универсального API для работы с AST. Т.е. придется работать либо с com.sun.source.tree.* либо org.eclipse.jdt.core.dom.*
Задача с поиском исходного текста класса легко решается, если проект публиковался в maven репозитарий вместе с артефактом типа source и в jar с классами есть файлы pom.properties или pom.xml, либо есть некий словарь соответствия названия/хеша артефакта исходному коду соответствующего jar файла и способ получить эти исходники во время работы программы.
Плюсы — для трансформации доступно больше информации, чем есть в байт-коде, применение не требует пересборки проекта и почти так же удобно как и применение AspectJ агента, но при этом трансформацию невозможно было выполнить средствами преобразования байт-кода, либо очень трудоемко.
Минусы такие же, как и у прошлого подхода: память, время, требование к наличию исходного текста программы и способа его найти для данного класса.
Примеры вышесказанного в виде кода ejc+maven будут в ближайшие месяцы, да и задача выбрана вполне жизненная. Сталкивались ли вы с подобным? Какие задачи из вашей практики можно было бы элегантно решить только с помощью трансформации java кода и рекомпиляции во время выполнения?
Кстати, возможности компилятора TinyCC и его размер доказывают, что такой подход возможен и для C программ.
В заметке мы рассмотрели несколько подходов по модификации программы, их сильные и слабые стороны. Модификация исполняемого кода более распространена, но не все задачи можно решить без наличия исходного кода программы и его последующей трансформации.