Comments 13
Я не хочу убивать всё удовольствие от дотошно проделанной работы, и допускаю, что эта статья написана в больше образовательных, чем практичных целях, но не могу не задать вопрос:
А почему нельзя было сделать новый проект с единственной зависимостью — ошибочным джарником — и единственным классом — изменённой версией файла?
Или у класса было слишком много зависимостей? Но на это не похоже. Или это нельзя было юридически? Но код же в любом случае был декомпилен и модифицирован.
Юридически код в общем-то под LGPL с открытыми исходниками. И всё бы понятно, академический интерес и всё такое, но настораживает отсутствие в списке вариантов действий — просто скачать исходники и подправить нужный файлик...
Это было бы не так интересно лично для меня. :)
Если уж совсем придираться, то список вариантов действий далеко не исчерпывающий. Можете считать, что вариант "0", который про pull-request в репозиторий, является подмножеством "скачать исходники и подправить нужный файлик", но с дополнительным шагом "не только подправить, но ещё и поделиться".
Да и статьи бы в таком случае не было бы.
Да, я думал об этом, что можно просто целиком и полностью перекомпилировать ошибочный класс, но это было бы не столь интересно. И мне хотелось чуть поглубже нырнуть в JVM в хаотичном поиске знаний, как первопричина.
Так что да, это больше образовательная статья в духе буханки и троллейбуса. Плюс самая малость пользы.
public class BadClass {
public String badCheck(String value) {
System.out.println("123");
return value;
}
}
public class GoodClass {
public static String goodCheck(String value) {
System.out.println("321 " + value);
return Objects.requireNonNullElse(value, "hello!");
}
}
public class Application {
public static void main(String[] args) throws Exception {
ClassReader reader = new ClassReader(Application.class.getResourceAsStream("BadClass.class"));
ClassWriter writer = new ClassWriter(reader, ClassWriter.COMPUTE_MAXS);
reader.accept(new YourClassVisitor(writer), ClassReader.EXPAND_FRAMES);
FileOutputStream out = new FileOutputStream(Application.class.getResource("BadClass.class").getFile());
out.write(writer.toByteArray());
out.close();
new BadClass().badCheck(" test ");
}
public static class BadClassVisitor extends ClassVisitor {
public YourClassVisitor(ClassVisitor cv) {
super(Opcodes.ASM9, cv);
}
@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
if (name.equals("badCheck")) {
return new BadMethodVisitor(super.visitMethod(access, name, desc, signature, exceptions));
}
return super.visitMethod(access, name, desc, signature, exceptions);
}
private static class BadMethodVisitor extends MethodVisitor {
public YourMethodVisitor(MethodVisitor mv) {
super(Opcodes.ASM9, mv);
}
@Override
public void visitCode() {
mv.visitVarInsn(Opcodes.ALOAD, 1);
mv.visitMethodInsn(Opcodes.INVOKESTATIC,"com/lampa/liqui/GoodClass","goodCheck","(Ljava/lang/String;)Ljava/lang/String;",false);
mv.visitInsn(Opcodes.ARETURN);
}
}
}
}
Мне, если честно, было боязно смотреть, через что можно сделать это в рантайме. Например, непонятно, через какой класслоадер этот код был загружен, и мог ли бы он быть загружен через ещё какой-нибудь другой класслоадер тестовым фреймворком.
Я не очень хорошо представляю, какие побочные эффекты рантаймовое изменение может иметь, вся тема instrumentation
для меня пока сокрыта. :)
Скачать сорцы (git clone git://git.code.sf.net/p/dbunit/code.git dbunit-code.git
), git checkout dbunit-2.7.0
, отредактировать файл, mvn -DskipTests install
— не вариант? Не дольше 5 минут.
Вариант, прекрасный вариант, если баг необходимо пофиксить как можно быстрее. У меня на это ковыряние ушло часа три с перерывом, и оно мне было интересно. :)
А на написание и вычитку статьи ушло полтора дня. Попытка получения виртуальных очков в интернете была более сомнительным поступком, чем самообразование. :/
Как способ быстро вникнуть в основы строения class-файла с нуля такой подход неплох.
С практической же точки зрения можно было:
Понизить версию class-файла до 49.0 (Java 1.5). В этом случае верификация будет проводиться старым алгоритмом, без использования
StackMapTable
. Естественно, это подходит только для быстрой проверки гипотезы и только чуть лучше флага-noverify
. Не сработает, если в class-файле будет что-то, чего не было в Java 1.5. Те же лямбды, например.
Воспользоваться для редактирования class-файла нормальным инструментом, а именно — asmtools. Он поддерживается в актуальном состоянии и, к примеру, метод
modified()
выглядит в нём так:
public Method modified:"(Ljava/lang/Object;)Ljava/lang/Object;"
throws java/lang/RuntimeException
stack 1 locals 2
{
aload_1;
ifnonnull L8;
aconst_null;
goto L12;
L8: stack_frame_type same;
aload_1;
invokevirtual Method java/lang/Object.toString:"()Ljava/lang/String;";
L12:
stack_frame_type stack1;
stack_map class java/lang/Object;
areturn;
}
Дизассемблировать, пропатчить нужный метод и ассемблировать обратно — дело пяти минут.
Более скучные варианты с перекомпиляцией из исходников рассматривать не будем.
NullPointerException в чужой библиотеке, или некоторые манипуляции с байткодом