Javenue logo

Javenue

Программирование на Java

Информационные технологии

Reflection в Java - java.lang.reflect API туториал

Reflection API в Java используется для просмотра информации о классах, интерфейсах, методах, полях, конструкторах, аннотациях во время выполнения java программ. При этом знать названия исследуемых элементов заранее не обязательно.

Все классы для работы с reflection расположены в пакете java.lang.reflect. Это метод (Method), конструктор (Constructor), массив (Array), поле (Field) и многие другие.

Вместо изучения каждого из этих элементов в отдельности, для знакомства с reflection я предлагаю вам небольшой пример. Мы попытаемся в runtime вывести всю возможную информацию о следующем java-классе:

package some;

public abstract class Test implements Serializable, Cloneable {
    private int field;

    public Test(Object field) { }

    @Deprecated
    protected static void method(String[] params) { }
}

Итак, приступим.

Способы получения необходимой нам информации в некоторых местах я упростил, чтобы пример не становился слишком большим. Чуть ниже я прокомментирую некоторые фрагменты кода, на которые хотел бы обратить ваше внимание.

package some;

import java.lang.reflect.*;
import java.lang.annotation.Annotation;
import java.io.Serializable;

public abstract class Test implements Serializable, Cloneable {
    private int field;

    public Test(Object field) { }

    @Deprecated
    protected static void method(String[] params) { }
}

class Reflect {
    public static void main(String[] args) {
        Class clazz = Test.class;

        // выводим название пакета
        Package p = clazz.getPackage();
        System.out.println("package " + p.getName() + ";");

        // начинаем декларацию класса с модификаторов
        int modifiers = clazz.getModifiers();
        System.out.print(getModifiers(modifiers));
        // выводим название класса
        System.out.print("class " + clazz.getSimpleName() + " ");

        // выводим название родительского класса
        System.out.print("extends " + clazz.getSuperclass().getSimpleName() + " ");

        // выводим интерфейсы, которые реализует класс
        Class[] interfaces = clazz.getInterfaces();
        for (int i = 0, size = interfaces.length; i < size; i++) {
            System.out.print(i == 0 ? "implements " : ", ");
            System.out.print(interfaces[i].getSimpleName());
        }
        System.out.println(" {");

        // выводим поля класса
        Field[] fields = clazz.getDeclaredFields();
        for (Field field : fields) {
            System.out.println("\t" + getModifiers(field.getModifiers())
                    + getType(field.getType()) + " " + field.getName() + ";");
        }

        // выводим констукторы класса
        Constructor[] constructors = clazz.getDeclaredConstructors();
        for (Constructor c : constructors) {
            System.out.print("\t" + getModifiers(c.getModifiers()) +
                    clazz.getSimpleName() + "(");
            System.out.print(getParameters(c.getParameterTypes()));
            System.out.println(") { }");
        }

        // выводим методы класса
        Method[] methods = clazz.getDeclaredMethods();
        for (Method m : methods) {
            // получаем аннотации
            Annotation[] annotations = m.getAnnotations();
            System.out.print("\t");
            for (Annotation a : annotations)
                System.out.print("@" + a.annotationType().getSimpleName() + " ");
            System.out.println();

            System.out.print("\t" + getModifiers(m.getModifiers()) +
                    getType(m.getReturnType()) + " " + m.getName() + "(");
            System.out.print(getParameters(m.getParameterTypes()));
            System.out.println(") { }");
        }

        System.out.println("}");
    }

    static String getModifiers(int m) {
        String modifiers = "";
        if (Modifier.isPublic(m)) modifiers += "public ";
        if (Modifier.isProtected(m)) modifiers += "protected ";
        if (Modifier.isPrivate(m)) modifiers += "private ";
        if (Modifier.isStatic(m)) modifiers += "static ";
        if (Modifier.isAbstract(m)) modifiers += "abstract ";
        return modifiers;
    }

    static String getType(Class clazz) {
        String type = clazz.isArray()
                ? clazz.getComponentType().getSimpleName()
                : clazz.getSimpleName();
        if (clazz.isArray()) type += "[]";
        return type;
    }

    static String getParameters(Class[] params) {
        String p = "";
        for (int i = 0, size = params.length; i < size; i++) {
            if (i > 0) p += ", ";
            p += getType(params[i]) + " param" + i;
        }
        return p;
    }
}

В результате выполнения кода мы получим следующее:

package some;
public abstract class Test extends Object implements Serializable, Cloneable {
	private int field;
	public Test(Object param0) { }
	@Deprecated 
	protected static void method(String[] param0) { }
}

Модификаторы в Java по спецификации представлены в виде целых чисел, например:

    public static final int PUBLIC = 0x00000001;

Для декодирования значений модификаторов используется класс Modifiers.

Заметьте, что в классе Class присутствуют пары методов, как например getFields и getDeclaredFields. Метод getFields возвращает только те поля, которые объявлены как public + public поля родительских классов, в то время как getDeclaredFields возвращает все поля текущего класса не зависимо от их видимости. Аналогично для getMethods и getConstructors.

Типы параметров и типы возвращаемых значений могут быть массивами. В этом случае для получения типа элементов, которые можно хранить в массиве, нужно использовать метод getComponentType.

Для получения аннотаций используется метод getAnnotations. Но метод вернет не сами классы аннотаций, а Proxy. Чтобы получить сам класс аннотации используйте метод annotationType().

Если у вас будут вопросы по приведенному выше фрагменту кода, задавайте их в комментариях к статье. Сейчас же я хочу перейти к некоторым практическим аспектам, которые иногда встречаются в реальной жизни.

1. С введением аннотаций в java 1.5 стало легко добавлять метаинформацию к классам, полям, методам и параметрам. По наличию аннотации (или ее отсутствию) можно во время выполнения программы принимать различного рода решения.

Допутим, мы пишем свой модуль для сериализации объектов. Пусть некоторая описанная нами аннотация @Transient говорит о том, что конкретное поле не нужно сериализовать. Нам поможет следующий фрагмент кода:

Field field = ...;
Transient transient = field.getAnnotation(Transient.class);
if (transient == null)
    ... // сериализуем поле

2. Работая с сервлетами часто нужно иметь возможность меппить параметры запроса на объекты. Вот небольшая часть функционала для такого меппинга из реального проекта, в котором я принимал участие:

static Map<Class , Object> wrappers = new HashMap<Class , Object>();
static {
    wrappers.put(Boolean.class, false);
    wrappers.put(Double.class, 0.0);
    wrappers.put(Float.class, 0.0f);
    wrappers.put(Character.class, (char) 0);
    wrappers.put(Integer.class, 0);
    wrappers.put(Long.class, 0L);
    wrappers.put(Byte.class, (byte) 0);
    wrappers.put(Short.class, (short) 0);
}

static Class wrapperFor(Class primitiveType) {
    for (Class wrapper : wrappers.keySet())
        try {
            // all wrappers have static final field TYPE
            if (wrapper.getField("TYPE").get(null).equals(primitiveType))
                return wrapper;
        } catch (IllegalAccessException impossible) { 
            throw new RuntimeException(impossible); 
        } catch (NoSuchFieldException impossible) { 
            throw new RuntimeException(impossible); 
        }
    return null;
}

В частности, метод wrapperFor используется для определения класса-обертки для примитивных типов. Это удобно для последующего вызова метода valueOf. К сожалению, класс Character не содержит такого метода и его нужно обрабатывать особенным образом.

3. Продлжая тему о сервлетах. В веб-фреймворке, который писали мои друзья, используется следующий контракт: действие представляет собой вызов некоторого метода someMethod у некоторого класса SomeClass. При этом url имеет вид, например, somedomain.com/SomeAction.someMethod.

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

class SomeAction {
    public void doSomething() {
        System.out.println("done");
    }
}

class ActionHandler {
    public static void main(String[] args) {
        // у нас есть коллекция действий (action)
        List<Object> actions = new ArrayList<Object>() {
            { add(new SomeAction()); }
        };
        // а также путь запроса (request path)
        String path = "/SomeAction.doSomething";

        // получаем имя класса и метода
        String className = path.substring(path.lastIndexOf("/") + 1,
                path.indexOf("."));
        String methodName = path.substring(path.indexOf(".") + 1);

        // находим нужный action
        Object action = null;
        for (Object a : actions) 
            if (a.getClass().getSimpleName().equals(className))
                action = a;
        if (action == null) /* это равносильно HTTP ERROR 404 */;

        // находим нужный метод
        Method method = null;
        try {
            method = action.getClass().getMethod(methodName);
        } catch (NoSuchMethodException e) { /* HTTP ERROR 404 */ }

        // вызываем метод
        try {
            method.invoke(action);
        } catch (IllegalAccessException e) {
            // не возникнет если следовать контракту публичных методов в action
            throw new RuntimeException(e); 
        } catch (InvocationTargetException e) {
            throw new RuntimeException(e.getCause());
        }
    }
}

Для вызова, в метод invoke нужно передать инстанс, у которого и будет вызываться наш метод.

Обратите внимание, что InvocationTargetException оборачивает ошибку, которая может произойти в методе, вызываемом через reflection. Для получения самого исключения необходимо взять причину (cause) у инстанса InvocationTargetException.

4. Часто программистам приходится работать с чужими проприетарными библиотеками. Случается, что где-то в библиотеке значения по-умолчанию некоторых полей вас не устраивают, но изменить их через API нет возможности. Или некоторый метод закрыт модификатором доступа private.

Вместо декомпиляции-исправления-компиляции есть способ, который иногда выручает. Это установка флага acccessible. Давайте установим значение 10 для поля field класса Inaccessible:

class Inaccessible {
    private int field;
    public String toString() {
        return field + "";
    }
}

class GrantAccess {
    public static void main(String[] args) throws Exception {
        Inaccessible o = new Inaccessible();
        System.out.println(o);

        Field field = o.getClass().getDeclaredField("field");
        field.setAccessible(true);
        field.set(o, 10);

        System.out.println(o);
    }
}

Программа выведет сначала 0, а потом 10.

5. При написании своих контейнеров (на подобии IoC), вам может пригодиться способ для инстанциирования объектов с помощью default контруктров. Это можно сделать следующим образом:

Constructor constructor = ...;
Object object = constructor.newInstnace();

Кстати, мой друг не так давно показал мне интересный способ инстанциирования объектов, который он обнаружил в sun'овских интерналсах. Нам даже посчастливилось воспользоваться данным способом на практике в виде такого вот метода:

    static <T> T instance(Class<T> c) {
        try {
            ReflectionFactory reflectionFactory = ReflectionFactory.getReflectionFactory();

            @SuppressWarnings("unchecked")
            Constructor<T> constructor = reflectionFactory.newConstructorForSerialization(c, Object.class.getConstructor());

            return constructor.newInstance();
        } catch (java.lang.Exception e) {
            throw new RuntimeException(e);
        }
    }

А дело было вот в чем. Нам дали API бинарного протокола, который был написан мягко говоря плохо. Каждый transfer объект (а их около 40) содержал большое количество параметров (2-6), каждый из которых необходимо было проставлять через конструктор. Ни один из параметров на нашей стороне вообще не использовался - эти параметры заполнялись и использовались уже на стороне сервера.

Собственно метод instance очень упростил нам способ инстанциирования объектов.

Пока что все. Жду ваших комментариев.



Комментариев: 6

  Выйти

  * для публикации комментариев нужно  

GrAndSE:

Статья понравилась. Миниатюрная шпаргалка-cookbook по Reflection API. Не хватает только примера вызова метода.
И ещё большое спасибо за хитрый метод инстанцирования. Впервые встречаю. Понравилось

jmadhead:

Интересная статья, спасибо. У меня к вам вопрос. Можно ли с помощью Reflection API перечислить все классы пакета, либо все субклассы класса? Или может есть какой-то другой способ?

c0nst:

В reflection API нет такой функциональности. Можно попробовать просто по classpath лазить.

С одного из форумов:

“Retrieving a list of classes in a package is not possible by design - different classes from the same package might exist in different JARs or be loaded by different types of classloaders. A classloader could potentially even dynamically generate a class’s bytecode at runtime.”

DigitIx:

скажите как через reflection получить информацию об исключениях бросаемых классом?

Denis W:

Подскажите , как узнать значение поля "по умолчанию" К примеру Class{ Int i = 10; List<Long> l = new ArrayList<>(); Int n; Интересно знать в runtime i, l, n поменялись?

German Bakarev:

getModifiers(int m) зачем этот метод? велосипед Modifier.toString(field.getModifiers()) все модификаторы выкатит