Вы, вероятно, не удивитесь, что ответ - от случая к случаю.

В этой статье я собираюсь представить анализ первопричин ошибки, с которой мы столкнулись в одном из наших проектов - в итоге было несколько экземпляров Scala object, который должен был быть синглтоном.

Сопоставление с образцом

История началась с сопоставления с образцом в ADT, например:

Это работало как шарм, пока после внесения явно не связанного изменения мы не начали получать такие ошибки, как:

scala.MatchError: Some(Slack) (of class scala.Some)

Ошибка казалась немного странной, поскольку это было действительно простое сопоставление с образцом, которое должно было сработать. Кроме того, соответствие на Email все еще работало нормально. А поскольку объект Slack должен был быть синглтоном, мы не имели ни малейшего представления о том, как он может не совпадать.

Но как-то так получилось - так что давайте откроем еще несколько фактов.

Картограф

Единственная идея, которую мы придумали, заключалась в том, что должно быть более одного экземпляра Slack - что мы смогли подтвердить, просмотрев хэш-коды в отладчике.

Изначально мы подозревали некоторую проблему с загрузкой классов, но поскольку приложение было автономным и не запускалось внутри какого-либо контейнера, это было вряд ли возможно. Проверка загрузчиков классов с помощью отладчика подтвердила, что это отвлекающий маневр.

Теперь наступает ключевой факт: значения, полученные Notifier.sendNotification, не были напрямую предоставлены нашим кодом приложения, а вместо этого считывались из некоторой конфигурации, хранящейся в MongoDB.

Кроме того, мы использовали Morphia в качестве преобразователя документа в объект, который отвечал за предоставление фактических экземпляров реализаций Notification.

Чтобы еще больше усложнить ситуацию, в нашем случае Notification был свойством двух разных объектов, скажем, A и B, поэтому его экземпляры могли быть результатом сопоставления документов из двух разных коллекций. Самое смешное, что A.notification было сопоставлено правильно, а B.notification - нет (или наоборот, но все равно только один из них работал нормально, а другой - нет).

Следующим очевидным шагом было копаться в исходном коде Morphia и проверять, как он сопоставляет документы с объектами Scala.

Отражение

Оказалось, что Morphia использует отражение Java для создания экземпляров объектов, полученных из MongoDB:

Поскольку Morphia - это библиотека Java, у нее нет возможности узнать о чем-то вроде Scala object - она ​​просто создает новый экземпляр того класса, с которым сталкивается.

Кроме того, с точки зрения JVM, object - это просто класс, который на самом деле имеет частный конструктор, поэтому ничто не может помешать вам (или Morphia) вручную создать новый экземпляр с помощью отражения.

Давайте теперь немного отвлечемся от основной истории и посмотрим, как на самом деле выглядит скомпилированный object.

Анатомия объекта

Если вы скомпилируете Notification ADT, вы получите пару результирующих классов:

$ scalac Notifications.scala
$ ls -1 *.class
Email$.class
Email.class
Notification.class
Slack$.class
Slack.class

Если вы никогда раньше не видели скомпилированный object, вы можете быть удивлены тем фактом, что компилятор генерировал не только Slack.class (чего вы, вероятно, ожидали), но также Slack$.class.

Давайте проверим эти два класса с помощью javap - инструмента командной строки в JDK, который позволяет дизассемблировать файлы классов:

$ javap -p Slack.class
Compiled from "Notifications.scala"
public final class Slack {
  public static java.lang.String toString();
  public static int hashCode();
  public static boolean canEqual(java.lang.Object);
  public static scala.collection.Iterator<java.lang.Object> productIterator();
  public static java.lang.Object productElement(int);
  public static int productArity();
  public static java.lang.String productPrefix();
}
$ javap -p Slack\$.class
Compiled from "Notifications.scala"
public final class Slack$ 
  implements  Notification,scala.Product,scala.Serializable {
  public static final Slack$ MODULE$;
  public static {};
  public java.lang.String productPrefix();
  public int productArity();
  public java.lang.Object productElement(int);
  public scala.collection.Iterator<java.lang.Object> productIterator();
  public boolean canEqual(java.lang.Object);
  public int hashCode();
  public java.lang.String toString();
  private java.lang.Object readResolve();
  private Slack$();
}

Если вы глубже погрузитесь в Slack.class с javap -c (который показывает фактический байт-код), вы заметите, что этот класс является только помощником, статические методы которого делегируют соответствующие методы в Slack$.

Теперь в классе Slack$, в самом конце, вы можете заметить реальный частный конструктор, который нельзя использовать с new (поскольку он частный), но все же можно вызвать с помощью отражения.

Другой важной частью класса Slack$ является статическое поле MODULE$, которое оказывается экземпляром singleton, созданным внутри путем вызова частного конструктора.

Пока вы обращаетесь к объекту в своем коде Scala с помощью Slack или в коде Java с помощью Slack$.MODULE$ (что выглядит некрасиво, но вполне приемлемо), он остается одноэлементным. Однако, как вы уже знаете, ничто не может помешать вам создать другие экземпляры с помощью отражения.

Кроме того, если вы посмотрите глубже в Slack$ с помощью javap -c, вы заметите, что статическое поле MODULE$ инициализируется каждый раз, когда вызывается частный конструктор:

private Slack$();
    Code:
       0: aload_0
       1: invokespecial #64                 // Method java/lang/Object."<init>":()V
       4: aload_0
       5: putstatic     #63                 // Field MODULE$:LSlack$;
       8: aload_0
       9: invokestatic  #68                 // InterfaceMethod scala/Product.$init$:(Lscala/Product;)V
      12: return

Это причина, по которой только некоторые экземпляры (точнее, только последний созданный) совпадают, а другие нет. Престижность Камилу Рафалко за указание на это.

Решение

Возвращаясь к нашему случаю с чтением Notifications из MongoDB, теперь должно быть ясно, почему сопоставление с образцом не работает: новый экземпляр Slack создавался каждый раз, когда значение десериализовалось.

Чтобы исправить это, мы решили написать собственный TypeConverter, который Morphia будет использовать при десериализации Slack объектов, который всегда будет предоставлять экземпляр синглтона вместо того, чтобы каждый раз создавать новый:

Идея настраиваемого преобразователя заключается в обработке особого случая десериализации значения Slack и возврата к поведению по умолчанию в любом другом случае.

Резюме

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

Более того, когда имеется несколько экземпляров object, их поведение может стать довольно странным - проверьте примеры ниже и убедитесь сами.

Загадка

Можете ли вы сказать, каков был бы результат выполнения приведенного ниже кода? Попробуйте это в Scala REPL или в Ammonite (который является более мощным REPL с подсветкой синтаксиса). Результат такой, как вы ожидали? Это детерминировано?

Ищете экспертов по Scala и Java?

Свяжитесь с нами!

Мы заставим технологии работать на ваш бизнес. Посмотреть проекты, которые мы успешно реализовали.