Тема рефлексии может показаться сложной, но общее представление о ней иметь нужно, т.к. на этом строится большая часть фреймворков (включая JUnit).
Рефлексия - API, позволяющих анализировать приложение в момент исполнения.
Рефлексия начинается с того, что у нас есть класс, который называется Class
.
Объекты этого класса описывают классы, загруженные в память JVM.
Например, у вас есть класс Product
. Когда JVM загружает этот класс из байт-кода, то она создаёт объект класса Class
, который и описывает класс Product
.
Что значит описывает? Это значит смотрит на него примерно как мы с вами:
- Название
- Набор полей
- Набор методов
- и т.д.
Каждый объект знает, к какому классу он принадлежит. Когда IDEA генерирует equals
вот эта строка и проверяет, что объект относятся к одному и тому же классу: if (o == null || getClass() != o.getClass()) return false;
.
Зачем это нужно? Рефлексия позволяет нам буквально в цикле перебирать поля и методы, выполняя необходимые действия:
- JUnit смотрит, написано ли над методом
@Test
- Mockito смотрит, написано ли над полем
@Mock
,@InjectMocks
- и т.д.
И самое главное - благодаря рефлексии JUnit, Mockito и другие инструменты могут создавать объекты прямо во время исполнения программы.
Это часть "продвинутая", ничего страшного, если вы её опустите или оставите до лучших времён. Она не является критичной и необходимой для понимания остальной части курса и написана только для того, чтобы интересующиеся могли получить базовую информацию.
Итак задача: мы хотим написать некоторую пародию на JUnit, которая:
- Берёт класс
- Анализирует все его методы
- Находит все, над которыми стоит аннотация
@Test
- Создаёт для каждого такого метода новый объект и запускает на нём этот метод (тот, над которым стояло
@Test
)
Естественно, мы для простоты изложения опустим кучу нюансов и продемонстрируем основную идею. Если вам будут интересны детали, пишите в Slack-чат вопросы, мы обязательно ответим.
Итак, поехали (наш подопытный)*:
package ru.netology;
import org.junit.jupiter.api.Test;
public class DemoTest {
@Test
public void shouldBeCalled() {
System.out.println("Test method called");
}
public void shouldNotBeCalled() {
System.out.println("Invalid method called");
}
}
Примечание*: целиком код проекта вы можете найти в репозитории с кодом к лекциям в проекте reflection-sample
.
Создаём "запускалку" наших тестов:
package ru.netology;
import org.junit.jupiter.api.Test;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class Launcher {
// что такое исключения мы разберём на следующей лекции
public static void main(String[] args) throws IllegalAccessException, InstantiationException, InvocationTargetException {
// создаём объект типа `Class` (generic'и мы пока не знаем, поэтому и так "сойдёт")
// clazz или cls - общепринятое имя, т.к. class - зарезервировано
Class clazz = DemoTest.class;
// берём все методы класса
Method[] methods = clazz.getDeclaredMethods();
// перебираем методы
for (Method method : methods) {
// смотрим, есть ли над методом аннотация @Test
if (method.isAnnotationPresent(Test.class)) {
// создаём объект класса (newInstance помечен аннотацией @Deprecated, но для простоты мы будем использовать его, в противном случае, нужно выбирать конструкторы)
Object object = clazz.newInstance();
// вызываем метод на объекте
method.invoke(object);
}
}
}
}
Как вы видите, код "не особо приятный", и, в большинстве случаев, вы такой код писать не будете, если не станете сами разрабатывать библиотеки и инструменты (т.к. инструменты тестирования уже это делают за вас).
Но свою работу он выполняет, вы можете запустить, подебажить, посмотреть, как он работает.
Ключевое: вы должны понимать, что Java предоставляет нам инструменты манипулирования кодом, которые и позволяют создавать такие мощные вещи как JUnit и Mockito*.
Примечание*: на самом деле мы немного лукавим, т.к. Mockito использует ещё и библиотеку ByteBuddy, которая генерирует байт-код на лету (но это уже совсем другая история).