Продлить последний класс или заменить его - PullRequest
2 голосов
/ 30 сентября 2019

"Нет, это не может быть сделано"

Это обсуждалось много раз, и единственный ответ - "Нет, это не может быть сделано". Но, ребята, мы делаем код. Все теоретически возможно с течением времени.

Проблема

В основном я пытаюсь расширить последний класс, чтобы изменить одно поведение. Это невозможно сделать традиционным способом - во время компиляции с ключевым словом extend. Я искал несколько других способов:

  • создать прокси - который работает только для интерфейсов;
  • отредактировать Modifiers класса, удалить модификатор final и расширитькласс во время выполнения - это можно сделать для полей, но не для классов;
  • использовать пользовательский ClassLoader для загрузки новой версии класса - он не работает, потому что класс, который я хотел быРасширение является частью пакета java. К счастью, ClassLoader не может загрузить класс этого пакета из-за SecurityException. В противном случае это испортит много ценных бумаг.
static class TestClassLoader extends ClassLoader {
    @Override
    public Class<?> loadClass(String name) throws ClassNotFoundException {
        if (name.equals("java.io.StringReader")) {
            try {
                InputStream is = Test.class.getClassLoader().getResourceAsStream("java/io/StringReader.class");
                byte[] buf = new byte[10000];
                int len = is.read(buf);
                return defineClass(name, buf, 0, len);  // This line throws the SecurityException :(
            } catch (IOException e) {
                throw new ClassNotFoundException("", e);
            }
        }
        return getParent().loadClass(name);
    }
}

Итак, мы здесь. Есть ли какой-нибудь метод, который я мог бы использовать, чтобы заменить или расширить мой последний класс?

PS: я использую Java 8, поскольку SecurityManager более увлечен уродливыми вещами, чем в более новых версиях Java:)


Контекст

У меня есть задание - эээ, это никогда не принималось на SO: мне нужно написать модульные тесты для класса, который я не могу изменить. Эта часть действительно проста, за исключением того, что к блоку catch нельзя получить доступ:

Properties props = new Properties();
try
{
    props.load(new StringReader(rules));
} catch (IOException e)
{
    e.printStackTrace();
    throw e;
}

Отчет о покрытии кода, который прислал нам наш учитель, показывает, что он не тестировал этот блок, поэтому мы не имеемк. Но я не доверяю Java и был бы уверен, что этот блок работает должным образом. Тогда я должен проверить это!

[Примечание: поскольку это не требуется, это всего лишь средство для обнаружения новых взломов Java, и это, вероятно, даже не будет учитываться в итоговой оценке.]

Единственная переменная, к которой я могу получить доступ, это String rules. Затем я должен поработать над этим, чтобы load вызвать вызов IOException. Единственная причина для вызова load для выброса IOException - закрытие потока StringReader. Это проверяется методом ensureOpen:

/** Check to make sure that the stream has not been closed */
private void ensureOpen() throws IOException {
    if (str == null)
        throw new IOException("Stream closed");
}

Следовательно, только нуль String может заставить этот метод выбросить IOException. Тогда было бы легко ввести ноль String в rules. Плохая новость - конструктор StringReader:

public StringReader(String s) {
    this.str = s;
    this.length = s.length();
}

Он бросает NPE при прохождении null String. Erk.

Последней идеей, с которой я столкнулся, было изменение значения String, чтобы сделать его внутреннее поле value null - простым при работе с Modifiers этого поля. Это не сработало, потому что ссылка, а не само значение должно быть null;это вызовет NPE только из-за вызова метода read для StringReader:

public int read() throws IOException {
    synchronized (lock) {
        ensureOpen();
        if (next >= length)
            return -1;
        return str.charAt(next++);
    }
}

У вас есть идеи? Основа, которая сделает это для меня? Какой-нибудь путь, который я мог бы найти, чтобы решить мою бесполезную проблему?

Этот вопрос не о тестировании кода . Речь идет о о том, как взломать Java и его последние классы.

Ответы [ 3 ]

5 голосов
/ 30 сентября 2019

Существует Instrumentation API , который позволяет программному обеспечению Java, так называемым агентам Java, изменять классы во время выполнения. Это один из способов, как фреймворки Mocking выполняют свою работу. Нередко использовать его для модульного тестирования.

Но учтите, что конкретная цель внедрения кода ошибочна. В коде

Properties props = new Properties();
try
{
    props.load(new StringReader(rules));
} catch (IOException e)
{
    e.printStackTrace();
    throw e;
}

значение IOException равно никогда . Как вы сами отметили, конструктор StringReader не выбрасывает его, и то же самое относится к его read методам. load метод Properties объявляет его, потому что он имеет дело с произвольными Reader реализациями, но сам по себе не создает таких исключений.

Так что в этом сценарии, где вы читаете иза String с недавно построенной StringReader, она никогда не будет выброшено, и это спорный вопрос, чтобы придать поведение, которое на самом деле не существует, чтобы проверить сценарий, который никогда не произойдет.

1021 * Как Вы такжеВы отметили, что единственная точка, в которой он может выбросить IOException, - это метод ensureOpen(), который, как следует из названия, проверяет, открыт ли читатель. Таким образом, можно вызвать принудительное поведение, просто вызвав close() для считывателя, однако, как уже было сказано, это не соответствует фактическому поведению тестируемого кода и нет смысла вызывать тестируемое исключение, которое фактическиникогда не происходит.
3 голосов
/ 30 сентября 2019

Одним из возможных решений является использование Aspect.

Вы можете создать аспект и pointcut при выполнении окончательных методов класса. И вы можете реализовать свое поведение внутри аспекта.

1 голос
/ 01 октября 2019

Вы говорите, что изменение или замена JRE не вариант для вас. Тогда единственное решение - изменить байтовый код . Есть много способов сделать это, некоторые упоминаются также в других ответах. Я бы предложил несколько, от простых до более сложных (на мой взгляд).

1) Используйте PowerMock. Одной из важных его частей является загрузчик классов, который модифицирует байт-код MockClassLoader . Преимущество PowerMock состоит в том, что вы получаете все сигнатуры методов из исходного класса, и поэтому вы можете использовать автозаполнение в вашей IDE (Eclipse, IDEA) и, таким образом, вы можете переопределить свой метод по сравнению с AspectJ, Spring AOPили ASM, и делать меньше ошибок в именах пакетов, именах классов и методов, которые являются строками в других инструментах и, следовательно, более подвержены ошибкам.

2) Измените байт-код перед загрузкой it,Например, извлеките нужный класс из целевой библиотеки, измените его, как вам нужно, например, сделайте класс не окончательным, перепакуйте библиотеку, только затем используйте ее в своем приложении. Для этого есть разные инструменты: AspectJ (когда у вас есть исходный код и скомпилируйте его), ASM (вы также можете изменить существующий байт-код). Одно существенное отличие от других методов заключается в том, что когда вы используете такой измененный класс в своем приложении во время компиляции, ваш компилятор не знает об исходном байт-коде измененного класса и, таким образом, вы можете использовать обычное наследование класс, который был последним ранее.

3) Измените байт-код во время загрузки it. Вы можете использовать AsppectJ, Spring AOP, ASM. Самым мощным является ASM, но он также может потребовать гораздо больше усилий. AspectJ является мощным и имеет API высокого уровня по сравнению с ASM. Spring AOP довольно ограничен по сравнению с AspectJ, но его использование в некоторых случаях может быть намного проще.

4) Вы можете комбинировать различные методы. Например, вы можете использовать свой собственный загрузчик классов и сделать класс не финальным (установите один бит в 0). Это довольно небольшое изменение, вы можете сделать это даже без какой-либо библиотеки. Затем вы можете использовать CGLIB для изменения поведения класса, как вам нужно. Что отличается? Если вы используете аспекты, вы можете изменить поведение только существующих методов. Но с помощью CGLIB вы можете добавить новых методов в ваш класс.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...