Java Reflection: Вызов методов
Инспекция и рефлективный вызов методов Java с помощью класса Method.
Объект Method описывает один метод и — что важно — позволяет его вызвать: method.invoke(target, args...). Это рефлективное ядро тест-раннеров (поиск методов @Test и их вызов), фреймворков, диспетчеризующих обработчики по имени, и мостов для скриптинга. В этой главе рассматриваются: поиск методов, сопоставление типов параметров (с которым все спотыкаются), вызов методов экземпляра и статических методов, возвращаемые значения и оборачивание исключений.
Если вы новичок в рефлексии, начните с введения в рефлексию и главы об объекте класса, поскольку всё здесь начинается с Class<?>. Чтение и запись полей данных работает аналогично и рассмотрено в разделе рефлексия полей.
Поиск методов
Метод ищется по имени и типам параметров — именно по типам параметров Java различает перегрузки:
Class<?> c = Calc.class;
Method m1 = c.getMethod("add", int.class, int.class); // public, incl. inherited
Method m2 = c.getDeclaredMethod("secret", String.class); // any access, this class only
Method[] pub = c.getMethods(); // all public methods, incl. Object's and inherited
Method[] own = c.getDeclaredMethods(); // all access levels, declared here onlyОбъекты Class параметров должны точно совпадать с объявленными типами — никакого разрешения перегрузок и расширения нет. getMethod("add", Integer.class, Integer.class) не найдёт add(int, int); необходимо передать int.class. Неверная комбинация бросает NoSuchMethodException. Для метода без аргументов не передавайте классов: getMethod("toString").
Вызов: экземпляр, статический метод и аргументы
invoke принимает сначала целевой объект, затем аргументы в виде varargs Object[]:
Calc calc = new Calc();
Method add = Calc.class.getMethod("add", int.class, int.class);
Object result = add.invoke(calc, 2, 3); // → Integer 5 (autoboxed)
int sum = (int) result; // unbox manuallyДля статического метода целевой объект игнорируется — передайте null:
Method parse = Integer.class.getMethod("parseInt", String.class);
Object n = parse.invoke(null, "42"); // → Integer 42Примитивные аргументы автоматически упаковываются в Object[]; среда выполнения распаковывает их для соответствия примитивным параметрам. Возвращаемое значение всегда Object — примитивы возвращаются в упакованном виде, методы void возвращают null.
Как всплывают исключения: InvocationTargetException
Это самая важная ловушка. Если вызванный метод бросает исключение, invoke не распространяет его напрямую. Оно оборачивается в InvocationTargetException, а настоящее исключение извлекается через getCause():
try {
riskyMethod.invoke(target);
} catch (InvocationTargetException e) {
Throwable real = e.getCause(); // the exception the method actually threw
// handle 'real', not 'e'
}Остальные проверяемые исключения относятся к самому вызову через рефлексию, а не к телу метода:
IllegalAccessException— метод недоступен и вы не вызвалиsetAccessible(true).IllegalArgumentException— неверное количество/типы аргументов или неверный тип целевого объекта.NoSuchMethodException— бросается во время поиска, не во время вызова.
Итак: ошибки поиска и неверные аргументы бросаются «напрямую», а всё, что бросает собственный код метода, упаковывается в InvocationTargetException.
Возвращаемые типы, varargs и дженерики
- Метаданные возвращаемого типа:
m.getReturnType()(стёртыйClass) иm.getGenericReturnType()(Type, сохраняет дженерики). - Параметры:
m.getParameterTypes(),m.getParameterCount()иm.getParameters()(имена доступны при компиляции с флагом-parameters). - Varargs: параметр
String...на самом деле являетсяString[]. Ищите его черезgetMethod("f", String[].class)и вызывайте, передавая реальный массив, либо полагайтесь на то, чтоinvokeпринимает завершающий массив для слота varargs. - Bridge/synthetic-методы: дженерик-классы генерируют скрытые bridge-методы; отфильтровывайте их с помощью
m.isBridge()/m.isSynthetic()при перечислении.
Разбор примера: мини-диспетчер команд
Программа строит небольшой диспетчер, который отображает строковые команды на методы объекта-сервиса, вызывает их рефлективно с разобранными аргументами, обрабатывает метод, бросающий исключение (для демонстрации разворачивания InvocationTargetException), и вызывает статическую фабрику с целевым объектом null.
Что следует вынести из запуска:
- Статическая фабрика вызвана через
factory.invoke(null)— дляstatic-метода целевой объект не важен, поэтомуnullявляется принятым соглашением. Диспетчер затем использовал тот же механизмinvokeдля методов экземпляра, передавая реальныйcalcв качестве цели. Один API — для обоих видов методов. divide(1, 0)не бросилArithmeticExceptionизinvoke. Было брошеноInvocationTargetException, а настоящийArithmeticException: / by zeroобнаружен черезgetCause(). Каждый фреймворк, вызывающий пользовательский код рефлективно, вынужден разворачивать это исключение; забывчивость — именно поэтому иногда вместо реальной ошибки в стектрейсе можно видеть непонятныйInvocationTargetException.- Поиск
addс параметрамиInteger.classзавершился ошибкойNoSuchMethodException, хотяadd(int,int)существует. Рефлексия сопоставляет типы параметров точно, без автоупаковки и расширения —int.classиInteger.class— это разные ключи. Это самая распространённая ошибка при работе с рефлексией, и именно поэтому важны литералы.classпримитивов. - Приватный метод
secretудалось вызвать только послеgetDeclaredMethod+setAccessible(true). Как и с полями, выбор метода поиска (getDeclared…) и открытие доступа (setAccessible) — это два независимых шага; оба необходимы для доступа к приватному члену. - Возвращаемые значения поступали как
Objectи приводились в месте вызова ((Calculator) factory.invoke(...)), тогда какintизaddвернулся в упакованном виде какInteger. Рефлексия не имеет статических знаний о возвращаемых типах, поэтому приведение/распаковка — ответственность вызывающей стороны, а неверное приведение проявится какClassCastExceptionво время выполнения, а не во время компиляции.