Недавно я столкнулся со следующей ошибкой при сериализации лямбды с помощью Kryo:
com.esotericsoftware.kryo.KryoException:
java.lang.IllegalArgumentException:
Unable to serialize Java Lambda expression, unless explicitly declared e.g.,
Runnable r = (Runnable & Serializable) () -> System.out.println("Hello world!");
Если вы не распознаете синтаксис (Запускаемый и сериализуемый) , не волнуйтесь, он просто указывает, что лямбда-код должен реализовывать два типа. Это называется Пересечение типов . Лично мне самому никогда не приходилось этим пользоваться, поэтому я никогда по-настоящему об этом не задумывался. Сериализуемый – это немного уникальный интерфейс в этом отношении, так как на самом деле вам ничего не нужно реализовывать.
Без этого приведения лямбда будет считаться несериализуемой, что не делает Крио счастливым.
Как человек, который не очень часто смотрит на байт-код, я нахожу удивительным, насколько велика разница при добавлении и дополнительном приведении & Сериализуемого . Приведенные ниже примеры демонстрируют это. Для ясности я использовал следующую команду для генерации байт-кода из фрагментов кода:
javap -c -p target.classes.dev.lankydan.IntersectionCasting
Прежде чем делать какой-либо кастинг:
public class IntersectionCasting {
public static void main(String[] args) {
Function function = (message) -> "Kryo please serialize this message '" + message + "'";
}
}
Сгенерированный байт-код является:
public class dev.lankydan.IntersectionCasting {
public dev.lankydan.IntersectionCasting();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."":()V
4: return
public static void main(java.lang.String[]);
Code:
0: invokedynamic #2, 0 // InvokeDynamic #0:apply:()Ljava/util/function/Function;
5: astore_1
6: return
private static java.lang.String lambda$main$0(java.lang.String);
Code:
0: new #3 // class java/lang/StringBuilder
3: dup
4: invokespecial #4 // Method java/lang/StringBuilder."":()V
7: ldc #5 // String Kryo please serialize this message '
9: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
12: aload_0
13: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
16: ldc #7 // String '
18: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
21: invokevirtual #8 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
24: areturn
}
После отливки:
public class IntersectionCasting {
public static void main(String[] args) {
Function function =
(Function & Serializable) (message) -> "Kryo please serialize this message '" + message + "'";
}
}
Байт-код становится:
public class dev.lankydan.IntersectionCasting {
public dev.lankydan.IntersectionCasting();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."":()V
4: return
public static void main(java.lang.String[]);
Code:
0: invokedynamic #2, 0 // InvokeDynamic #0:apply:()Ljava/util/function/Function;
5: checkcast #3 // class java/io/Serializable
8: checkcast #4 // class java/util/function/Function
11: astore_1
12: return
private static java.lang.Object $deserializeLambda$(java.lang.invoke.SerializedLambda);
Code:
0: aload_0
1: invokevirtual #5 // Method java/lang/invoke/SerializedLambda.getImplMethodName:()Ljava/lang/String;
4: astore_1
5: iconst_m1
6: istore_2
7: aload_1
8: invokevirtual #6 // Method java/lang/String.hashCode:()I
11: lookupswitch { // 1
-1657128837: 28
default: 39
}
28: aload_1
29: ldc #7 // String lambda$main$2cf54983$1
31: invokevirtual #8 // Method java/lang/String.equals:(Ljava/lang/Object;)Z
34: ifeq 39
37: iconst_0
38: istore_2
39: iload_2
40: lookupswitch { // 1
0: 60
default: 135
}
60: aload_0
61: invokevirtual #9 // Method java/lang/invoke/SerializedLambda.getImplMethodKind:()I
64: bipush 6
66: if_icmpne 135
69: aload_0
70: invokevirtual #10 // Method java/lang/invoke/SerializedLambda.getFunctionalInterfaceClass:()Ljava/lang/String;
73: ldc #11 // String java/util/function/Function
75: invokevirtual #12 // Method java/lang/Object.equals:(Ljava/lang/Object;)Z
78: ifeq 135
81: aload_0
82: invokevirtual #13 // Method java/lang/invoke/SerializedLambda.getFunctionalInterfaceMethodName:()Ljava/lang/String;
85: ldc #14 // String apply
87: invokevirtual #12 // Method java/lang/Object.equals:(Ljava/lang/Object;)Z
90: ifeq 135
93: aload_0
94: invokevirtual #15 // Method java/lang/invoke/SerializedLambda.getFunctionalInterfaceMethodSignature:()Ljava/lang/String;
97: ldc #16 // String (Ljava/lang/Object;)Ljava/lang/Object;
99: invokevirtual #12 // Method java/lang/Object.equals:(Ljava/lang/Object;)Z
102: ifeq 135
105: aload_0
106: invokevirtual #17 // Method java/lang/invoke/SerializedLambda.getImplClass:()Ljava/lang/String;
109: ldc #18 // String dev/lankydan/IntersectionCasting
111: invokevirtual #12 // Method java/lang/Object.equals:(Ljava/lang/Object;)Z
114: ifeq 135
117: aload_0
118: invokevirtual #19 // Method java/lang/invoke/SerializedLambda.getImplMethodSignature:()Ljava/lang/String;
121: ldc #20 // String (Ljava/lang/String;)Ljava/lang/String;
123: invokevirtual #12 // Method java/lang/Object.equals:(Ljava/lang/Object;)Z
126: ifeq 135
129: invokedynamic #2, 0 // InvokeDynamic #0:apply:()Ljava/util/function/Function;
134: areturn
135: new #21 // class java/lang/IllegalArgumentException
138: dup
139: ldc #22 // String Invalid lambda deserialization
141: invokespecial #23 // Method java/lang/IllegalArgumentException."":(Ljava/lang/String;)V
144: athrow
private static java.lang.String lambda$main$2cf54983$1(java.lang.String);
Code:
0: new #24 // class java/lang/StringBuilder
3: dup
4: invokespecial #25 // Method java/lang/StringBuilder."":()V
7: ldc #26 // String Kryo please serialize this message '
9: invokevirtual #27 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
12: aload_0
13: invokevirtual #27 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
16: ldc #28 // String '
18: invokevirtual #27 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
21: invokevirtual #29 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
24: areturn
}
Теперь я действительно не знаю, как читать байт-код, но даже я вижу, что в версии с приведением & сериализуемым происходит гораздо больше.
Если вы можете объяснить, что там происходит, тогда будьте моим гостем и дайте мне знать.
Одна вещь, которую я могу прочитать из байт-кода выше, – это ссылки на Сериализованную Лямбду , которых раньше не было. Этот класс используется компиляторами и библиотеками для обеспечения правильной десериализации лямбд. Выполнение приведения к пересечению функции<Строка, строка> & Сериализуемой изменяет базовый тип лямбды, позволяя библиотеке, такой как Kryo, правильно понимать, как десериализовать предоставленные ей лямбды.
Добавление этого дополнительного приведения & Сериализуемого является одним из возможных решений, позволяющих Kryo десериализовать лямбды. Альтернативный маршрут предполагает создание нового интерфейса, который расширяет как базовый Функция тип, который вам нужен, так и Сериализуемый . Это полезно при создании библиотеки или API для использования другими пользователями. Позволяя им сосредоточиться исключительно на реализации своего кода, а не беспокоиться о том, чтобы обеспечить правильное приведение для удовлетворения сериализации их лямбд.
Вы могли бы использовать интерфейс, подобный приведенному ниже:
interface SerializableLambda extends Function, Serializable {}
Затем это можно использовать для замены приведения в предыдущем примере:
public class IntersectionCasting {
public static void main(String[] args) {
SerializableLambda function = (message) -> "Kryo please serialize this message '" + message + "'";
}
interface SerializableLambda extends Function, Serializable {}
}
Я добавил байт-код, сгенерированный этим изменением ниже:
public class dev.lankydan.IntersectionCasting {
public dev.lankydan.IntersectionCasting();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."":()V
4: return
public static void main(java.lang.String[]);
Code:
// NO CASTING // Mention of casting is removed and reference to new interface is added
0: invokedynamic #2, 0 // InvokeDynamic #0:apply:()Ldev/lankydan/IntersectionCasting$SerializableLambda;
5: astore_1
6: return
private static java.lang.Object $deserializeLambda$(java.lang.invoke.SerializedLambda);
Code:
0: aload_0
1: invokevirtual #3 // Method java/lang/invoke/SerializedLambda.getImplMethodName:()Ljava/lang/String;
4: astore_1
5: iconst_m1
6: istore_2
7: aload_1
8: invokevirtual #4 // Method java/lang/String.hashCode:()I
11: lookupswitch { // 1
-1657128837: 28
default: 39
}
28: aload_1
29: ldc #5 // String lambda$main$2cf54983$1
31: invokevirtual #6 // Method java/lang/String.equals:(Ljava/lang/Object;)Z
34: ifeq 39
37: iconst_0
38: istore_2
39: iload_2
40: lookupswitch { // 1
0: 60
default: 135
}
60: aload_0
61: invokevirtual #7 // Method java/lang/invoke/SerializedLambda.getImplMethodKind:()I
64: bipush 6
66: if_icmpne 135
69: aload_0
70: invokevirtual #8 // Method java/lang/invoke/SerializedLambda.getFunctionalInterfaceClass:()Ljava/lang/String;
73: ldc #9 // String dev/lankydan/IntersectionCasting$SerializableLambda
75: invokevirtual #10 // Method java/lang/Object.equals:(Ljava/lang/Object;)Z
78: ifeq 135
81: aload_0
82: invokevirtual #11 // Method java/lang/invoke/SerializedLambda.getFunctionalInterfaceMethodName:()Ljava/lang/String;
85: ldc #12 // String apply
87: invokevirtual #10 // Method java/lang/Object.equals:(Ljava/lang/Object;)Z
90: ifeq 135
93: aload_0
94: invokevirtual #13 // Method java/lang/invoke/SerializedLambda.getFunctionalInterfaceMethodSignature:()Ljava/lang/String;
97: ldc #14 // String (Ljava/lang/Object;)Ljava/lang/Object;
99: invokevirtual #10 // Method java/lang/Object.equals:(Ljava/lang/Object;)Z
102: ifeq 135
105: aload_0
106: invokevirtual #15 // Method java/lang/invoke/SerializedLambda.getImplClass:()Ljava/lang/String;
109: ldc #16 // String dev/lankydan/IntersectionCasting
111: invokevirtual #10 // Method java/lang/Object.equals:(Ljava/lang/Object;)Z
114: ifeq 135
117: aload_0
118: invokevirtual #17 // Method java/lang/invoke/SerializedLambda.getImplMethodSignature:()Ljava/lang/String;
121: ldc #18 // String (Ljava/lang/String;)Ljava/lang/String;
123: invokevirtual #10 // Method java/lang/Object.equals:(Ljava/lang/Object;)Z
126: ifeq 135
129: invokedynamic #2, 0 // InvokeDynamic #0:apply:()Ldev/lankydan/IntersectionCasting$SerializableLambda;
134: areturn
135: new #19 // class java/lang/IllegalArgumentException
138: dup
139: ldc #20 // String Invalid lambda deserialization
141: invokespecial #21 // Method java/lang/IllegalArgumentException."":(Ljava/lang/String;)V
144: athrow
private static java.lang.String lambda$main$2cf54983$1(java.lang.String);
Code:
0: new #22 // class java/lang/StringBuilder
3: dup
4: invokespecial #23 // Method java/lang/StringBuilder."":()V
7: ldc #24 // String Kryo please serialize this message '
9: invokevirtual #25 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
12: aload_0
13: invokevirtual #25 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
16: ldc #26 // String '
18: invokevirtual #25 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
21: invokevirtual #27 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
24: areturn
}
Большая часть из них та же самая, с некоторыми новыми ссылками на Сериализуемый интерфейс Lambda и удаление исходного приведения пересечения.
Как упоминалось ранее, это решение идеально подходит для авторов библиотек и API, поскольку оно позволяет разработчикам писать код в обычном режиме, не беспокоясь о приведении (например, если библиотека использует Kryo под капотом). Кроме того, поскольку интерфейс расширяет Функцию , которая является @FunctionalInterface , разработчики могут красиво писать лямбды/функции и даже не должны упоминать интерфейс, передавая его непосредственно в другую функцию или конструктор. Я лично пошел по этому пути при разработке нового API для Corda . Я хотел предоставить наиболее доступный API для разработчиков, в то же время предоставляя работающий API (я не могу позволить Kryo взорваться …).
В заключение, в этом посте, в котором не хватает большого количества информации и который усеян расширенными фрагментами байт-кода, вам нужно убрать две вещи. Вы можете сделать Java-лямбда/функцию сериализуемой с помощью пересечения типов, и вы можете убедиться, что ваши собственные API являются чистыми, создав новый интерфейс, который расширяет как желаемый тип функции, так и Сериализуемый . Это оба маршрута, которые следует учитывать при использовании библиотеки сериализации, такой как Kryo.
Если вам понравился этот пост или вы сочли его полезным (или и то, и другое), пожалуйста, не стесняйтесь подписываться на меня в Твиттере по адресу @LankyDanDev и не забудьте поделиться с кем-либо еще, кто может счесть это полезным!
Оригинал: “https://dev.to/lankydandev/serializable-java-lambdas-3aog”