Перекомпилируйте с новым определением класса для тестирования мутаций
Я пытаюсь использовать https://github.com/OpenHFT/Java-Runtime-Compiler для улучшения моих инструментов тестирования мутаций, от интенсивного использования доступа к диску до использования только в памяти компиляции.
В тестировании на мутации было два вида классов: A. мутировавший класс, класс, в котором его определение будет постоянно изменяться / изменяться и перекомпилироваться. B. другой класс, класс, определение которого не будет изменено, т. Е. Класс тестового примера, или другой класс, который нужен мутировавшему классу.
Используя openHFT/java-runtime-compiler, это можно легко сделать с помощью приведенного ниже кода, а именно путем создания нового classLoader для каждой перекомпиляции как мутированного класса, так и другого класса.
String BSourceCode = loadFromFiles(); //class definition loaded
for( someIterationCondition ){
String ASourceCode = mutation(); //some operation of generation/manipulation/mutation of ASourceCode
ClassLoader classLoader = new ClassLoader() { };
Class AClass = CompilerUtils.CACHED_COMPILER.loadFromJava( classLoader, "A" , ASourceCode);
Class BClass = CompilerUtils.CACHED_COMPILER.loadFromJava( classLoader, "B" , BSourceCode);
}
Это хорошо работает, каждый раз, когда новое определение класса A компилируется, AClass адаптируется к новому определению.
Но это не сработало бы, если бы последовательность была обратной, как в приведенном ниже коде (сначала загружен BClass, а затем AClass), что иногда требовалось, например, когда AClass использует BClass. Перекомпиляция класса A не будет соответствовать новому определению и всегда будет использовать первое определение, которое использовалось для компиляции класса A.
String BSourceCode = loadFromFiles(); //class definition loaded
for( someIterationCondition ){
String ASourceCode = mutation(); //some operation of generation/manipulation/mutation of ASourceCode
ClassLoader classLoader = new ClassLoader() { };
Class BClass = CompilerUtils.CACHED_COMPILER.loadFromJava( classLoader, "B" , BSourceCode);
Class AClass = CompilerUtils.CACHED_COMPILER.loadFromJava( classLoader, "A" , ASourceCode);
}
Я подозреваю, что мне нужно было изменить класс loadFromJava из библиотеки openHFT/java-runtime-compiler (код ниже). Я уже пробовал, опуская строки
//if (clazz != null)
//return clazz;
что я ожидал сделать так, чтобы он всегда перекомпилировал весь исходный код (даже уже скомпилированный) при каждом вызове loadFromJava. Но это дает неправильные результаты.
Пожалуйста, помогите мне указать на изменение, необходимое для того, чтобы оно заработало.
public Class loadFromJava(@NotNull ClassLoader classLoader,
@NotNull String className,
@NotNull String javaCode,
@Nullable PrintWriter writer) throws ClassNotFoundException {
Class clazz = null;
Map<String, Class> loadedClasses;
synchronized (loadedClassesMap) {
loadedClasses = loadedClassesMap.get(classLoader);
if (loadedClasses == null){
loadedClassesMap.put(classLoader, loadedClasses = new LinkedHashMap<String, Class>());
}else{
clazz = loadedClasses.get(className);
}
}
PrintWriter printWriter = (writer == null ? DEFAULT_WRITER : writer);
if (clazz != null)
return clazz;
for (Map.Entry<String, byte[]> entry : compileFromJava(className, javaCode, printWriter).entrySet()) {
String className2 = entry.getKey();
synchronized (loadedClassesMap) {
if (loadedClasses.containsKey(className2))
continue;
}
byte[] bytes = entry.getValue();
if (classDir != null) {
String filename = className2.replaceAll("\\.", '\\' + File.separator) + ".class";
boolean changed = writeBytes(new File(classDir, filename), bytes);
if (changed) {
LOG.info("Updated {} in {}", className2, classDir);
}
}
Class clazz2 = CompilerUtils.defineClass(classLoader, className2, bytes);
synchronized (loadedClassesMap) {
loadedClasses.put(className2, clazz2);
}
}
synchronized (loadedClassesMap) {
loadedClasses.put(className, clazz = classLoader.loadClass(className));
}
return clazz;
}
Большое спасибо за вашу помощь.
отредактированный
Спасибо, Питер Лори, я попробовал ваше предложение, но оно дает тот же результат, класс A придерживается первого использованного определения (в первой итерации) и не может изменить / использовать новое определение (в следующей итерации),
Я собрал симптомы, и возможное объяснение состояло в том, что в первой итерации (первый раз класс был скомпилирован / загружен) было несколько иначе, чем в следующей итерации. Оттуда я пробую пару вещей.
1-ые симптомы
Это было, когда я поместил строку вывода (System.out.println) в loadFromJava (ниже)
Class clazz = null;
Map<String, Class> loadedClasses;
synchronized (loadedClassesMap) {
loadedClasses = loadedClassesMap.get(classLoader);
if (loadedClasses == null){
loadedClassesMap.put(classLoader, loadedClasses = new LinkedHashMap<String, Class>());
System.out.println("loadedClasses Null "+className);
}else{
clazz = loadedClasses.get(className);
if(clazz == null)
System.out.println("clazz Null "+className);
else
System.out.println("clazz not Null "+className);
}
}
вывод дает:
1st Iteration (new ClassLoader and new CachedCompiler)
(when loading B):loadedClasses Null
(when loading A):clazz Null
next Iteration (new ClassLoader and new CachedCompiler)
(when loading B):loadedClasses Null
(when loading A):clazz Not Null
в первой итерации, он выдавал правильный вывод "loadClasses Null" (при загрузке B), потому что в полном загруженном файле ClassCoM не имеется classLoader, и давал "clazz Null" (при загрузке A), потому что у загруженного класса классов есть ClassLoader, но не ' не иметь имя класса А.
Однако на следующей итерации (при загрузке A) выводится "clazz Not Null", кажется, что имя класса уже сохранено в загруженном файле ClassCap.get (classLoader), чего не должно быть. Я попытался очистить загруженные классы в конструкторе CachedCompiler.
loadedClassesMap.clear();
но это дает LinkageError: loader (экземпляр main/Utama$2): попытка дублирования определения класса.
2-й симптом
более сильные признаки дифференциации в первой итерации были при проверке буфера s_fileManager.
1st Iteration (new ClassLoader and new CachedCompiler)
(when load B):CompilerUtils.s_fileManager.getAllBuffers().size()=1
(when load A):CompilerUtils.s_fileManager.getAllBuffers().size()=2
Next Iteration (new ClassLoader and new CachedCompiler)
(when load B):CompilerUtils.s_fileManager.getAllBuffers().size()=2
(when load A):CompilerUtils.s_fileManager.getAllBuffers().size()=2
Первая итерация прошла, как и ожидалось, но в следующей итерации буфер s_fileManager, похоже, уже получил размер 2, а не сброшен в 0.
Я попытался очистить FileManager Buffer в конструкторе CachedCompiler (ниже),
CompilerUtils.s_fileManager.clearBuffers();
но это дает ExceptionInInitializerError.
1 ответ
Если вы хотите использовать новый набор классов, я предлагаю не использовать один и тот же кеш классов.
String BSourceCode = loadFromFiles(); //class definition loaded
for( someIterationCondition ){
String ASourceCode = mutation(); //some operation of generation/manipulation/mutation of ASourceCode
ClassLoader classLoader = new ClassLoader() { };
CachedCompiler compiler = new CachedCompiler(null, null)
Class AClass = compiler.loadFromJava( classLoader, "A" , ASourceCode);
Class BClass = compiler.loadFromJava( classLoader, "B" , BSourceCode);
}
Это будет использовать новый кэш каждый раз и не будет зависеть от класса, загруженного в предыдущем тесте.