Рубрики
Без рубрики

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

Недавно я столкнулся со следующей ошибкой при сериализации лямбды с помощью… Помеченный java, kryo, лямбдами, функциями.

Недавно я столкнулся со следующей ошибкой при сериализации лямбды с помощью 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”