Русский
Русский
English
Статистика
Реклама

Получение Method из Method Reference в Java

Я столкнулся с проблемой как получить из method reference вида


Function<String, Integer> fun = String::length;

вызываемый метод класса (или хотя бы его имя), т.е. в примере это java.lang.String.length(). Как выяснилось, не одного меня волновал этот вопрос, нашлись такие обсуждения на stackoverflow [1], [2], проекты на GitHub, которые так или иначе касаются этой проблемы [1], [2], [3], но не один из них не дает ровно то, что нужно. На Хабре ibessonov предложил свое решение, а apangin свое в комментарии. Вариант Андрея мне понравился, но вскоре выяснились некоторые связанные с ним проблемы. Во-первых, это основано на внутренних классах JDK, все это работает только в Java 8, но не в более поздних версиях. Во-вторых, этот метод имеет ряд ограничений, например для BiFunction или BiConsumer он выдает неверное значение (это как раз обсуждается в комментариях).


В общем, перебрав несколько вариантов, удалось найти тот, который не имеет этих изьянов и работает в поздних версиях JDK SerializedLambda. Забегая вперед сразу скажу, что это работает только с функциональными интерфейсами, помеченными как java.io.Serializable (т.е. с java.util.function.Function работать не будет), но в целом это не проблемное ограничение.


Зачем вообще это нужно?


Перед тем как перейти к решению, отвечу на резонный вопрос а зачем это может понадобиться?
В моем случае это используется в тестовых фреймворках, чтобы добавить диагностическую информацию о вызываемом методе, который определялся через method reference. Это позволяет сделать одновременно лаконичный и compile-безопасный код. Например, можно сделать "where" hamcrest matcher, использующий функцию-extractor для значений:


List<SamplePojo> list = Arrays.asList(        new SamplePojo()                .setName("name1"),        new SamplePojo()                .setName("name2"));// successassertThat(list, hasItem(where(SamplePojo::getName, equalTo("name1"))));// fails with diagnostics:// java.lang.AssertionError: // Expected: every item is Object that matches "name1" after call SamplePojo.getName//     but: an item was "name2"assertThat(list, everyItem(where(SamplePojo::getName, equalTo("name1"))));

Обратите внимание, что в случае ошибки тест упадет с диагностикой "after call SamplePojo.getName". Для однострочного теста это кажется избыточным, но hamcrest-выражения могут иметь многоуровневую вложенность, поэтому лишняя детализация не помешает.


SerializedLambda


Класс java.lang.invoke.SerializedLambda это сериализованное представление лямбда-выражения. Важно уточнить, что интерфейс лямбда-выражения должен быть помечен как Serializable:


@FunctionalInterfacepublic interface ThrowingFunction<T, R> extends java.io.Serializable {    R apply(T t) throws Exception;}

Как следует из javadoc класса, чтобы получить объект SerializedLambda, следует вызвать приватный writeReplace на лямбда-объекте:


@Nullableprivate static SerializedLambda getSerializedLambda(Serializable lambda) {    for (Class<?> cl = lambda.getClass(); cl != null; cl = cl.getSuperclass()) {        try {            Method m = cl.getDeclaredMethod("writeReplace");            m.setAccessible(true);            Object replacement = m.invoke(lambda);            if (!(replacement instanceof SerializedLambda)) {                break;            }            return (SerializedLambda) replacement;        } catch (NoSuchMethodException e) {            // skip, continue        } catch (IllegalAccessException | InvocationTargetException | SecurityException e) {            throw new IllegalStateException("Failed to call writeReplace", e);        }    }    return null;}

Дальше все нужные детали достаем из SerializedLambda, нужно только несложное преобразование. Например, имена классов записаны через "/"(слеш) вместо точек, а примитивные типы сделаны сокращениями. Например getImplMethodSignature() возвращает строку "(Z)V", это означает один аргумент (внутри скобок) типа boolean ("Z"), тип возвращаемого значения void ("V"):


static Class<?>[] parseArgumentClasses(String implMethodSignature) {    int parenthesesPos = implMethodSignature.indexOf(')');    if (!implMethodSignature.startsWith("(") || parenthesesPos <= 0) {        throw new IllegalStateException("Wrong format of implMethodSignature " + implMethodSignature);    }    String argGroup = implMethodSignature.substring(1, parenthesesPos);    List<Class<?>> classes = new ArrayList<>();    for (String token : argGroup.split(";")) {        if (token.isEmpty()) {            continue;        }        classes.add(parseType(token, false));    }    return classes.toArray(new Class[0]);}private static Class<?> parseType(String typeName, boolean allowVoid) {    if ("Z".equals(typeName)) {        return boolean.class;    } else if ("B".equals(typeName)) {        return byte.class;    } else if ("C".equals(typeName)) {        return char.class;    } else if ("S".equals(typeName)) {        return short.class;    } else if ("I".equals(typeName)) {        return int.class;    } else if ("J".equals(typeName)) {        return long.class;    } else if ("F".equals(typeName)) {        return float.class;    } else if ("D".equals(typeName)) {        return double.class;    } else if ("V".equals(typeName)) {        if (allowVoid) {            return void.class;        } else {            throw new IllegalStateException("void (V) type is not allowed");        }    } else {        if (!typeName.startsWith("L")) {            throw new IllegalStateException("Wrong format of argument type "                    + "(should start with 'L'): " + typeName);        }        String implClassName = typeName.substring(1);        return implClassForName(implClassName);    }}private static Class<?> implClassForName(String implClassName) {    String className = implClassName.replace('/', '.');    try {        return Class.forName(className);    } catch (ClassNotFoundException e) {        throw new IllegalStateException("Failed to load class " + implClassName, e);    }}

Остается только найти подходящий метод в интерфейсе, чтобы вернуть правильный результат:


@Nullablepublic static Method unreferenceLambdaMethod(Serializable lambda) {    SerializedLambda serializedLambda = getSerializedLambda(lambda);    if (serializedLambda != null            && (serializedLambda.getImplMethodKind() == MethodHandleInfo.REF_invokeVirtual            || serializedLambda.getImplMethodKind() == MethodHandleInfo.REF_invokeStatic)) {        Class<?> cls = implClassForName(serializedLambda.getImplClass());        Class<?>[] argumentClasses = parseArgumentClasses(serializedLambda.getImplMethodSignature());        return Stream.of(cls.getDeclaredMethods())                .filter(method -> method.getName().equals(serializedLambda.getImplMethodName())                        && Arrays.equals(method.getParameterTypes(), argumentClasses))                .findFirst().orElse(null);    }    return null;}

А с конструкторами работает?


Работает. Тут будет другой тип serializedLambda.getImplMethodKind().


@Nullablepublic static Constructor<?> unreferenceLambdaConstructor(Serializable lambda) {    SerializedLambda serializedLambda = getSerializedLambda(lambda);    if (serializedLambda != null            && (serializedLambda.getImplMethodKind() == MethodHandleInfo.REF_newInvokeSpecial)) {        Class<?> cls = implClassForName(serializedLambda.getImplClass());        Class<?>[] argumentClasses = parseArgumentClasses(serializedLambda.getImplMethodSignature());        return Stream.of(cls.getDeclaredConstructors())                .filter(constructor -> Arrays.equals(constructor.getParameterTypes(), argumentClasses))                .findFirst().orElse(null);    }    return null;}

Пример:


// new Integer(String)ThrowingFunction<String, Integer> fun = Integer::new;Constructor<?> constructor = unreferenceLambdaConstructor(fun);

Совместимость


Это работает в Java 8, Java 11, Java 14, не требует внешних библиотек или доступа к приватным api JDK, не требует дополнительных параметров запуска JVM. Кроме того, применимо и к статическим методам и к методам с разным количеством аргументов (т.е. не только Function-подобные).
Единственное неудобство для каждого вида функций придется создать сериализуемое представление, например:


@FunctionalInterfacepublic interface SerializableBiFunction<T, U, R> extends Serializable {    R apply(T arg1, U arg2);}// Integer.parseInt(String, int)SerializableBiFunction<String, Integer, Integer> fun = Integer::parseInt;Method method = unreferenceLambdaMethod(fun);

Полную реализацию можно найти тут.


Готовый утилитарный метод


Вы можете скопировать класс в свой проект, но я бы не рекомендовал использовать это вне scope test. Кроме того, можно добавить зависимость:


<dependency>    <groupId>com.github.seregamorph</groupId>    <artifactId>hamcrest-more-matchers</artifactId>    <version>0.1</version>    <scope>test</scope></dependency>

и вызвать


import static com.github.seregamorph.hamcrest.TestLambdaUtils.unreferenceLambdaMethod;...ThrowingFunction<String, String> fun = String::toLowerCase;Method method = unreferenceLambdaMethod(fun);assertEquals("toLowerCase", method.getName());
Источник: habr.com
К списку статей
Опубликовано: 12.10.2020 12:10:07
0

Сейчас читают

Комментариев (0)
Имя
Электронная почта

Java

Hamcrest

Lambda functions

Ненормальное программирование

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru