29
August
2009

Reflection в Java (java.lang.reflect API)

Posted in: Java technologies, J2SE |

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 wrappers = new HashMap();
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 очень упростил нам способ инстанциирования объектов.

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

17 Comments »

RSS feed for comments on this post.



the.malkolm
August 29, 2009 #

Если вы “маппите” объекты, но наверно и процесс называется “маппинг”, а не “меппинг”. Ну да это так, придирашки.

August 30, 2009 #

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

August 30, 2009 #

2 the.malkolm: придирашку поправил ).

2 GrAndSE: да, нужно добавить вызов метода. А то инстанциирование есть, простановка филда есть, а вызова нет.

September 2, 2009 #

Хорошая статья, спасибо, но хотелось бы обратить ваше внимание на параметризованные классы. о них норошо написано на хабре

habrahabr.ru/blogs/java/66593/

Ant
September 5, 2009 #

Статья понравилась. Java только начал постигать, поэтому особенно благодарен.

potato
September 6, 2009 #

Спасибо, интересная статья. Только там в первом примере вместо “t”, нужно “\t”.
А также в фрагменте:

if (i > 0) p += “, “;
p += getType(params[i]) + ” param” + i;

нужно заменить “, на “

potato
September 6, 2009 #

В смысле косые кавычки на прямые

September 6, 2009 #

Исправил. Это гадкий wordpress такие ужасы творит. Вот уж не думал что нужно slash и quote тоже эскейпить.

Mika
October 16, 2009 #

Автор статьи молодец, тема интересная! Жду новых материалов!

gorgeorg
January 10, 2010 #

Доброе время суток.
Помогите разобраться в чем мои грабли. Нужно из суперкласса А скопировать в новый экземпляр В все значения полей. Пример:
class A {
private float f1;
public getf1() {return f1;}
public setf1(float f1) {this.f1=f1;}
}
class B extends A {
float fun() {return super.f1;}
public B(A a) {
this.setf1(a.getf1()); // так не желательно (очень много полей - будет некрасиво)
Class cl = A.class;
Class tcl = this.getClass();
Field[] fieldsFrom = cl.getDeclaredFields();
Field[] fieldsTo = cl.getDeclaredFields();
for (int i=0; i

Anton
January 19, 2010 #

Для того что бы можна было читать анотации в райнтайме нуже перед определением оной прописать
@Retention(RetentionPolicy.RUNTIME)
пример
http://www.java2s.com/Tutorial/Java/0020__Language/ObtainingAnnotationsatRunTimebyUseofReflection.htm

VL
February 16, 2010 #

Статья хорошая.
Пожалуйста исправьте в предложении “Путь некоторая описанная нами аннотация @Transient говорит о том, что конкретное поле не нужно среиализовать”: “Путь” на “Пусть” и “среиализовать” на “сериализовать”.

February 17, 2010 #

Здраствуйте, извените что задам вопрос не по теме просто очень надо узнать как создавать класс с параметрами.
Короче у меня есть класс User к нему нужны параметры Name и Surname

вот как я написал:
public class User(String[] Name;Surname)

February 28, 2010 #

Интересная тема!

March 3, 2010 #

как раз недавно начал изучать Java по учебнику, здесь более доступно написано… еще и на примерах… спасибо

vrungel
March 7, 2010 #

обратите внимание на метод
java.lang.reflect.Modifier.toString(int m);

March 9, 2010 #

создание сайта бесплатно чтобы заработать от 1305$ создание сайтов web дизайн разработка сайтов веб дизайн создание сайта http://web-miheeff.ru +7 (495) 782-72-56 создание сайта бесплатно чтобы заработать

Leave a comment

XHTML: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <code> <em> <i> <strike> <strong> <pre>