Статьи

Как и зачем сериализовать лямбды

обзор

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

Удаленные посетители

Например, так что я хочу получить доступ к ресурсу на удаленной карте, я могу использовать get / put, но, скажем, я просто хочу вернуть поле из значения карты, я могу передать лямбду в качестве посетителя для извлечения информации Я хочу.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
MapView userMap =
     Chassis.acquireMap("users", String.class, UserInfo.class);
userMap.put("userid", new UserInfo("User's Name"));
 
// print out changes
 
userInfo.registerSubscriber(System.out::println);
 
// obtain just the fullName without downloading the whole object
String name= userMap.applyToKey("userid", u -> u.fullName);
 
// increment a counter atomically and trigger
// an updated event printed with the subscriber.
userMap.asyncUpdateKey("userid", ui -> {
     ui.usageCounter++;
     return ui;
});
 
// increment a counter and return the userid
int count = userMap.syncUpdateKey("userid",
      ui -> { ui.usageCounter++; return ui;},
      ui -> ui.usageCounter);

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

Сериализуемые лямбды

Простой способ сделать лямбда-сериализуемый — добавить преобразование & Serializable к переменной, ссылающейся на реализацию лямбда-выражения.

1
2
Function<UserInfo, String> fullNameFunc = (Function<UserInfo,String> & Serializable) ui -> ui.fullName;
String fullName = userInfo.applyToKey("userid", fullNameFunc);

Как вы можете видеть это вводит много котельной плиты. Ключевой причиной использования лямбд является отказ от кода котельной плиты, так какова альтернатива?

Делаем лямбды сериализуемыми в вашем API.

К сожалению, стандартные API не могут быть изменены или подклассы, чтобы добавить это, но если у вас есть собственный API, вы можете использовать интерфейс Serializable.

1
2
3
@FunctionalInterface
public interface SerializableFunction<I, O> extends Function<I, O>, Serializable {
}

Этот интерфейс может использоваться как тип параметра.

1
2
3
default <R> R applyToKey(K key, @NotNull SerializableFunction<E, R> function) {
    return function.apply(get(key));
}

Пользователь вашего API не должен явно говорить, что лямбда-сериализуема.

1
2
// obtain just the fullName without downloading the whole object
String name= userMap.applyToKey("userid", u -> u.fullName);

Удаленная реализация сериализует лямбду, выполняет ее на сервере и возвращает результат.

Точно так же есть способы применения лямбды к карте в целом.

Запрос и подписка

Для поддержки запросов вы не можете использовать встроенный API stream (), если хотите неявно добавить Serializable. Тем не менее, вы можете создать такой, который будет максимально похож.

1
2
3
4
Map> collect = userMap.entrySet().query()
    .filter(e -> e.getKey().matches("u*d"))
    .map(e -> e.getValue())
    .collect(Collectors.groupingBy(u -> u.usageCounter));

или как отфильтрованная подписка.

1
2
3
4
// print userid which have a usageCounter > 10 each time it is incremented.        userMap.entrySet().query()
        .filter(e -> e.getValue().usageCounter > 10)
        .map(e -> e.getKey())
        .subscribe(System.out::println);

Что отличает это от обычного потокового API, так это то, что данные могут быть распределены по многим серверам, и вы получаете обратный вызов, когда это меняется на любом сервере. Только данные, которые вас интересуют, отправляются по сети, так как фильтр и карта применяются на сервере.

Сериализация Java

Java Serialization — это хорошая обобщенная, обратно совместимая библиотека сериализации. Две наиболее распространенные проблемы, которые пытаются решить альтернативы, — это производительность и кроссплатформенная сериализация.

В приведенном выше примере fullNameFunc сериализуется до более чем 700 байтов, и есть очень ограниченные возможности для его оптимизации, чтобы уменьшить размер сообщения или объем создаваемого мусора. Для сравнения: прямая двоичная сериализация YAML использует 348 с большим количеством опций для оптимизации сериализации.

Это поднимает проблему того, как сериализовать лямбду, используя альтернативный или кроссплатформенный или более быстрый формат сериализации.

Альтернативная Сериализация

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

Тем не менее, вы можете сделать это:

1
2
3
4
Method writeReplace = lambda.getClass()
                                  .getDeclaredMethod("writeReplace");
writeReplace.setAccessible(true);
SerializedLambda sl = (SerializedLambda) writeReplace.invoke(lambda);

Это дает вам объект, который вы можете проверить, чтобы извлечь содержимое лямбды. Либо посмотреть, какой метод он вызывает, либо сериализовать его. На стороне десериализации вы можете воссоздать этот объект и можете прочитатьResolve для этого объекта.

Стандартный API

В настоящее время не существует стандартного API для самоанализа лямбды. Это сделано намеренно, чтобы в будущем реализация могла быть изменена, хотя для этого нет общедоступного JEP. Однако, подобно Unsafe, который является внутренним API, я с нетерпением жду того дня, когда мы сможем использовать стандартный API вместо того, чтобы копаться во внутренностях JVM для реализации решений.

Выводы

С некоторыми изменениями в вашем API вы можете сделать лямбды-сериалы в значительной степени прозрачными для разработчика. Это облегчает использование простых распределенных систем и дает возможность оптимизировать, как это делается.

Ссылка: Как и зачем сериализовать Lambdas от нашего партнера JCG Питера Лоури из блога Vanilla Java .