Вы, вероятно, не удивитесь, что ответ - от случая к случаю.
В этой статье я собираюсь представить анализ первопричин ошибки, с которой мы столкнулись в одном из наших проектов - в итоге было несколько экземпляров 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
Это причина, по которой только некоторые экземпляры (точнее, только последний созданный) совпадают, а другие нет. Престижность Камилу Рафалко за указание на это.
Решение
Возвращаясь к нашему случаю с чтением Notification
s из MongoDB, теперь должно быть ясно, почему сопоставление с образцом не работает: новый экземпляр Slack
создавался каждый раз, когда значение десериализовалось.
Чтобы исправить это, мы решили написать собственный TypeConverter
, который Morphia будет использовать при десериализации Slack
объектов, который всегда будет предоставлять экземпляр синглтона вместо того, чтобы каждый раз создавать новый:
Идея настраиваемого преобразователя заключается в обработке особого случая десериализации значения Slack
и возврата к поведению по умолчанию в любом другом случае.
Резюме
Хотя при нормальных обстоятельствах object
в Scala действительно являются одиночными, этого больше не может быть, когда в игру вступает отражение. С точки зрения отражения object
- не что иное, как любой другой класс с частным конструктором.
Более того, когда имеется несколько экземпляров object
, их поведение может стать довольно странным - проверьте примеры ниже и убедитесь сами.
Загадка
Можете ли вы сказать, каков был бы результат выполнения приведенного ниже кода? Попробуйте это в Scala REPL или в Ammonite (который является более мощным REPL с подсветкой синтаксиса). Результат такой, как вы ожидали? Это детерминировано?
Ищете экспертов по Scala и Java?
Мы заставим технологии работать на ваш бизнес. Посмотреть проекты, которые мы успешно реализовали.