Русский
Русский
English
Статистика
Реклама

Serialization

Оптимизация трафика при синхронизация состояний через Jsonpatch

11.12.2020 00:18:41 | Автор: admin

Задача синхронизации состояния между клиентом и сервером может быть решена разными способами, я хотел бы расказать про вариант с использованием спецификации JSON Patch, а также про спецификацию MessagePack и способ оптимизации размера пакета через библиотеку PatchPack.


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



Jsonpatch


Например, следующие документы JSON представляют состояние, документ исправления JSON для ресурса и результат применения операций исправления.


Пример состояния


{  "users": {    "1": { "name": "Foo" },    "2": { "name": "Baz", "info": "FooBaz" },  },  "objects": [    { "id": 1, "name": "Foo" },    { "id": 2, "name": "Foo", "foo:" "Baz" },  ],  "foo": { "baz": false }}

Пример JSON Patch


[  {    "op": "add",    "path": "/users/3",    "value": {       "name": "FooBaz",       "info": "test"     }  }]

В приведенном выше документе JSON:


  • свойство op указывает тип операции;
  • свойство path указывает обновляемый элемент;
  • свойство value предоставляет новое значение.

Состояние после обновления


Ниже показан ресурс в том состоянии, которое он принимает после применения приведенного выше документа JSON Patch.


{  "users": {    "1": { "name": "Foo" },    "2": { "name": "Baz", "info": "FooBaz" },    "3": { "name": "FooBaz", "info": "test" },  },  "objects": [    { "id": 1, "name": "Foo" },    { "id": 2, "name": "Foo", "foo:" "Baz" },  ],  "foo": { "baz": false }}

Операции


В следующей таблице перечислены поддерживаемые операции, которые определены в спецификации JSON Patch.


Операция Примечания
add Добавляет свойство или элемент массива. Для существующего свойства устанавливает значение.
remove Удаляет свойство или элемент массива.
replace Действует так же, как remove с последующим add в том же расположении.
move Действует так же, как remove из источника с последующим add, в котором указаны место назначения и значение из источника.
copy Действует так же, как add, в котором указаны место назначения и значение из источника.
test Возвращает успешный код состояния, если значение path совпадает с предоставленным value.

Json patch может быть реализован через метод HTTP-запрос PATCH или через сообщения websocket. Гибкость и понятность содержания каждого изменения сильно увеличивают размер этого изменения.


MessagePack


Есть разные способы для оптимизации Json состояния и изменения (JsonPatch) в частности. Один из таких способов использование спецификации сериализации MessagePack.


Про MessagePack уже были статьи на хабре, поэтому я попробую рассказать преимущества и недостатки этой спецификации для синхронизации состояния.


MessagePack более компактен (чем JSON), существует реализация на многих языках программирования и не используют какие-либо внешние зависимости.


Однако, когда состояние имеет структуру, то MessagePack недостаточно компактный, т.к. нет никакой оптимизации для повторяющихся свойств в состоянии состояния.


Patchpack


Библиотеку PatchPack разработана на основе спецификации MessaagePack, но она сериализует состояние и изменения на много эффективнее. Это достигается путем формировании схемы состояния при сериализации и использования этой схемы для десериализации. Этот подход накладывает дополнительную сложность связанную с синхронизацией схемы.


В библиотеке PatchPack схема/изменение передаются вместе с сериализованным состоянием/изменением:



Вот небольшой пример использования библиотеки:


import { PatchPack } from "patchpack"// первоначальное состояниеconst state = {  users: {    "1": { name: "Foo" },    "2": { name: "Baz", info: "FooBaz" },  },  objects: [    { id: 1, name: "Foo" },    { id: 2, name: "Foo", foo: "Baz" },  ],  foo: { baz: false }}// Создаем объект PatchPack и указываем типы, используемые в состоянииconst ppServer = new PatchPack({  State: ["users", "objects", "foo"],  User: ["name", "info"],  Object: ["id", "name", "foo"],  Foo: ["baz"]})// сериализуем состояниеconst encodedState = ppServer.encodeState(state, false)

Сериализованное состояние отправляем на клиент, создаем объект PatchPack с аналогичными типами и выполняем метода decodeState.


Для сериализации изменений (JsonPatch) используется отдельный метод:


// добавляем пользователя в коллекциюconst patch1 = { op: "add", path: "/users/3", value: { name: "FooBaz", info: "test" } }const encodedPatch1 = ppServer.encodePatch(patch1)// изменяем свойствоconst patch2 = { op: "replace", path: "/foo/baz", value: true }const encodedPatch2 = ppServer.encodePatch(patch2)

Сериализованные изменения отправляем на клиент и при помощи метода decodePatch преобразуются в формат JsonPatch.


Сравнение размера после сериализации через PatchPach, MessagePack и JSON.stringify (в байтах):


patchpack messagePack JSON.stringify
state 60 107 (+78%) 165 (+175%)
patch1 22 53 (+140%) 72 (+227%)
patch2 5 33 (+560%) 47 (+840%)

В настоящий момент поддерживается не вся спецификация JsonPatch, а только базовые операции: add, replace, remove (copy, move, test не поддерживаются).


В отличии от MessagePack библиотека PatchPack пока реализована только на JavaScript/Typescript, но в ближайшее время добавится реализация на dart и C#.


Спасибо, что прочитали, любая критика и предложения приветствуются!

Подробнее..

Protobuf vs Avro. Как сделать выбор?

29.11.2020 16:21:40 | Автор: admin

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

Размер и скорость

В сети можно найти сравнительные тесты форматов сериализации. Не стоит придавать значение конкретным числам, так как скорость сериализации/десериализации, как и размер получающихся двоичных данных, зависит от конкретной схемы данных и от реализации сериализатора. Отметим лишь, что авро и протобаф занимают лидирующие позиции в подобных тестах.

Преимущество aвро в том, что поля записи сохраняются одно-за-другим, без разделителей. Но, имея дело с авро, вам нужно где-то сохранять схему записанных данных. Она может быть прикреплена к сериализированным данным или же храниться отдельно (тогда к данным добавляется идентификатор схемы во внешнем хранилище).

Фишка протобaфа в том, что, при сериализации целых чисел, по-умолчанию, используется формат переменной длины (varint), который занимает меньше места для небольших положительных чисел. Протобаф добавляет в бинарный поток номер поля и его тип, что увеличивает итоговый размер. Также, если в сообщение входят поля типа запись (nested message в терминологии протобафа), предварительно нужно вычислить итоговый размер записи, что усложняет алгоритм сериализации и занимает дополнительное время.

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

Типы данных

Примитивные типы, представленные в обоих форматах: bool, string, int32(int), int64(long), float, double, byte[]. Протобаф также поддерживает uint32, uint64.

В протобафе, по-умолчанию, целые числа кодируются в формате varint, эффективном для небольших положительных чисел. Вы можете изменить это, указав : sint32, sint64, fixed32, fixed64, sfixed32, sixed64.

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

Оба формата поддерживают перечисления (enumerations).

Сложные типы конструируются с помощью алгебраического умножения (records в авро, message в протобафе) и сложения (union в авро, oneof в протобафе).

Для того, чтобы описать опциональное поле, в авро, необходимо использовать union с двумя вариантами, один из которых null, а в протобафе - oneof из одного варианта.

Оба формата поддерживают механизмы расширения системы типов (logical types в авро и well known types в протобафе). Таким образом обе схемы дополнительно поддерживают сериализацию даты и времени (timestamp) и продолжительности времени (duration).

В отличии от авро, протобаф не поддерживает decimal и UUID. Также авро поддерживает тип fixed - массив байт определенной длины.

Итого, хотя отсутствие поддержки decimal в протобафе - досадное упущение, система типов, тоже не будет определяющей при выборе.

Эволюция данных

Обе схемы поддерживают механизмы обратной совместимости (backward compatibility) за счет заполнения новых полей значениями по-умолчанию. В авро можно указать любое, допустимое значение, в протобафе это значение задано жестко, в зависимости от типа (0, пустая строка, false). В авро также поддерживаются альтернативные имена (aliases) для полей и именованных типов (record, enum, fixed). В протобафе имя поля не используется в двоичной сериализации, но номер поля не может быть изменен.

Для числовых типов, в авро допускаются только преобразования без потери (например int в long, float в double, но не наоборот). Протобаф более толерантен к изменению числовых типов и применяет правила преобразования, идентичные C++. Также протобаф допускает преобразования из bool в число и обратно, из целого числа в enum и обратно.

В случае, когда потребитель данных использует старую схему, а производитель более новую, важно, чтобы новый формат данных не сломал работу потребителя. Такое свойство схемы данных называется упреждающей совместимостью (forward compatibility).

Неизвестное поле в записи игнорируется обоими форматами.

В случае неизвестного значения enum, авро подставляет значение по-умолчанию, если оно задано, протобаф - нулевое значение.

Неизвестный вариант (case) в объединении (union) протобаф помечает признаком unknown. Авро же, в этом случае, выдает исключение и прерывает десериализацию.

Это серьезное ограничение авро. Если вы используете алгебраические типы данных (ADT), то авро, скорее всего, вам не подходит, так как не поддерживает упреждающую совместимость при добавлении нового варианта в объединение.

Представление в Json

Как авро, так и протобаф, поддерживают сериализацию в Json. Это может быть полезно, как для отладочных целей, так и для долгосрочного хранения данных (например, в MongoDB).

В случае протобафа, нарушится обратная совместимость, если вы поменяете название поля (на самом деле есть трюк, и вы можете переименовать поле один раз, использовав атрибут json_name для сохранения информации о прежнем имени поля). Авро же позволяет давать несколько альтернативных названий (aliases) полям.

Неприятным свойством авро является то, что массив байт (типы bytes, fixed) сохраняется в виде UTF16 строки. Это не только порождает визуальный мусор (псевдографика, переводы строк и т.п), но и может сделать Json нечитаемым, так как не все библиотеки корректно транслируют UTF16. Протобаф же сохраняет массив байт в base64.

Представление в формате Json не должно повлиять на ваш выбор, не забывайте только, что в случае протобафа, вы можете переименовать поле только один раз, а в случае авро, нужно проверить ваш стек на корректность обработки UTF16.

Влияние на архитектуру

Выбрав авро, вы должны будете решить, где хранить схему. В том случае, если вы работаете с большим количеством сообщений малого размера (например, отправляете сообщения через кафку), вам нужно хранить схемы данных в отдельном хранилище (например, использовать Schema Registry). Вам потребуется реализовать кеширование схем, что привнесет в вашу систему состояние (statefullness), будет забирать время на прогрев при запуске.

Если ваш язык программирования обладает динамической системой типов (например, python), то протобаф, с его требованием заранее задать типы всех полей, может стать для вас слишком обременительным. Авро же, наоборот, может стать хорошим решением, так как позволяет десериализировать данные на лету, зная лишь схему по которой эти данные были сохранены. Для случаев динамической типизации, в протобафе появился тип Any, но, согласно официального сайта, его поддержка в процессе разработки.

RPC

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

Протокол авро поддерживает синхронный вызов процедуры и вызов без ожидания ответа (one-way). Протокол включает в себя этап рукопожатия (handshake), в время которого стороны обмениваются схемами.

Сервис протобафа допускает как синхронный вызов, так и отправку потока данных (streaming) в обоих направлениях.

Флагманская реализация RPC для протобафа - gRPC. Несмотря на то, что gRPC, по-умолчанию, использует протобаф, теоретически он позволяет использовать и другие механизмы сериализации, включая авро. На практике же, все зависит от конкретной реализации, так, например, в дотнете, Майкрософт прилагает много усилий для того, чтобы выжать максимальную производительность из реализации gRPC в связке с протобафом, но вы вступаете на скользкую дорожку, пытаясь подменить сериалайзер, например, на авро.

Прежде чем сделать выбор, убедитесь, что в вашем стеке существует реализация RPC, удовлетворяющая ваши потребности.

Kafka

Изначально схема данных в кафке описывалась с помощью авро. В настоящее время добавлена поддержка протобафа.

Hadoop

Ситуация зеркальна gRPC. Несмотря на то, что Hadoop - это вотчина авро, с помощью библиотеки elephant-bird вы можете использовать протобаф в связке с хадупом.

Комьюнити

Оба продукта доступны в открытом коде.

https://github.com/apache/avro (1.7K звезд, 1.1К форков)

https://github.com/protocolbuffers/protobuf (45K звезд, 12.1К форков)

Подробнее..

Перевод Использование Google Protocol Buffers (protobuf) в Java

25.02.2021 20:10:38 | Автор: admin

Привет, хабровчане. В рамках курса "Java Developer. Professional" подготовили для вас перевод полезного материала.

Также приглашаем посетить открытый вебинар на тему gRPC для микросервисов или не REST-ом единым.


Недавно вышло третье издание книги "Effective Java" (Java: эффективное программирование), и мне было интересно, что появилось нового в этой классической книге по Java, так как предыдущее издание охватывало только Java 6. Очевидно, что появились совершенно новые разделы, связанные с Java 7, Java 8 и Java 9, такие как глава 7 "Lambdas and Streams" (Лямбда-выражения и потоки), раздел 9 "Prefer try-with-resources to try-finally" (в русском издании 2.9. Предпочитайте try-с-ресурсами использованию try-finally) и раздел 55 "Return optionals judiciously" (в русском издании 8.7. Возвращайте Optional с осторожностью). Но я был слегка удивлен, когда обнаружил новый раздел, не связанный с нововведениями в Java, а обусловленный изменениями в мире разработки программного обеспечения. Именно этот раздел 85 "Prefer alternatives to Java Serialization" (в русском издании 12.1 Предпочитайте альтернативы сериализации Java) и побудил меня написать данную статью об использовании Google Protocol Buffers в Java.

В разделе 85 "Prefer alternatives to Java Serialization" (12.1 Предпочитайте альтернативы сериализации Java) Джошуа Блох (Josh Bloch) выделяет жирным шрифтом следующие два утверждения, связанные с сериализацией в Java:

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

Нет никаких оснований для использования сериализации Java в любой новой системе, которую вы пишете.

После описания в общих чертах проблем с десериализацией в Java и, сделав эти смелые заявления, Блох рекомендует использовать то, что он называет кроссплатформенным представлением структурированных данных (чтобы избежать путаницы, связанной с термином сериализация при обсуждении Java). Блох говорит, что основными решениями здесь являются JSON (JavaScript Object Notation) и Protocol Buffers (protobuf). Мне показалось интересным упоминание о Protocol Buffers, так как в последнее время я немного читал о них и игрался с ними. В интернете есть довольно много материалов по использованию JSON (даже в Java), в то время как осведомленность о Protocol Buffers среди java-разработчиков гораздо меньше. Поэтому я думаю, что статья об использовании Protocol Buffers в Java будет полезной.

На странице проекта Google Protocol Buffers описывается как не зависящий от языка и платформы расширяемый механизм для сериализации структурированных данных. Также там есть пояснение: Как XML, но меньше, быстрее и проще. И хотя одним из преимуществ Protocol Buffers является поддержка различных языков программирования, в этой статье речь пойдет исключительно про использование Protocol Buffers в Java.

Есть несколько полезных онлайн-ресурсов, связанных с Protocol Buffers, включая главную страницу проекта, страницу проекта protobuf на GitHub, proto3 Language Guide (также доступен proto2 Language Guide), туториал Protocol Buffer Basics: Java, руководство Java Generated Code Guide, API-документация Java API (Javadoc) Documentation, страница релизов Protocol Buffers и страница Maven-репозитория. Примеры в этой статье основаны на Protocol Buffers 3.5.1.

Использование Protocol Buffers в Java описано в туториале "Protocol Buffer Basics: Java". В нем рассматривается гораздо больше возможностей и вещей, которые необходимо учитывать в Java, по сравнению с тем, что я расскажу здесь. Первым шагом является определение формата Protocol Buffers, не зависящего от языка программирования. Он описывается в текстовом файле с расширением .proto. Для примера опишем формат протокола в файле album.proto, который показан в следующем листинге кода.

album.proto

syntax = "proto3";option java_outer_classname = "AlbumProtos";option java_package = "dustin.examples.protobuf";message Album {    string title = 1;    repeated string artist = 2;    int32 release_year = 3;    repeated string song_title = 4;}

Несмотря на простоту приведенного выше определения формата протокола, в нем присутствует довольно много информации. В первой строке явно указано, что используется proto3 вместо proto2, используемого по умолчанию, если явно ничего не указано. Две строки, начинающиеся с option, указывают параметры генерации Java-кода (имя генерируемого класса и пакет этого класса) и они нужны только при использовании Java.

Ключевое слово "message" определяет структуру "Album", которую мы хотим представить. В ней есть четыре поля, три из которых строки (string), а одно целое число (int32). Два из них могут присутствовать в сообщении более одного раза, так как для них указано зарезервированное слово repeated. Обратите внимание, что формат сообщения определяется независимо от Java за исключением двух option, которые определяют детали генерации Java-классов по данной спецификации.

Файл album.proto, приведенный выше, теперь необходимо скомпилировать в файл исходного класса Java (AlbumProtos.java в пакете dustin.examples.protobuf), который можно использовать для записи и чтения бинарного формата Protocol Buffers. Генерация файла исходного кода Java выполняется с помощью компилятора protoc, соответствующего вашей операционной системе. Я запускаю этот пример в Windows 10, поэтому я скачал и распаковал файл protoc-3.5.1-win32.zip. На изображении ниже показан мой запущенный protoc для album.proto с помощью команды protoc --proto_path=src --java_out=dist\generated album.proto

Перед запуском вышеуказанной команды я поместил файл album.proto в каталог src, на который указывает --proto_path, и создал пустой каталог build\generated для размещения сгенерированного исходного кода Java, что указано в параметре --java_out.

Сгенерированный Java-класс AlbumProtos.java содержит более 1000 строк, и я не буду приводить его здесь, он доступен на GitHub. Среди нескольких интересных моментов относительно сгенерированного кода я хотел бы отметить отсутствие выражений import (вместо них используются полные имена классов с пакетами). Более подробная информация об исходном коде Java, сгенерированном protoc, доступна в руководстве Java Generated Code. Важно отметить, что данный сгенерированный класс AlbumProtos пока никак не связан с моим Java-приложением, и сгенерирован исключительно из текстового файла album.proto, приведенного ранее.

Теперь исходный Java-код AlbumProtos надо добавить в вашем IDE в перечень исходного кода проекта. Или его можно использовать как библиотеку, скомпилировав в .class или .jar.

Прежде чем двигаться дальше, нам понадобится простой Java-класс для демонстрации Protocol Buffers. Для этого я буду использовать класс Album, который приведен ниже (код на GitHub).

Album.java

package dustin.examples.protobuf;import java.util.ArrayList;import java.util.List;/** * Music album. */public class Album {    private final String title;    private final List < String > artists;    private final int releaseYear;    private final List < String > songsTitles;    private Album(final String newTitle, final List < String > newArtists,        final int newYear, final List < String > newSongsTitles) {        title = newTitle;        artists = newArtists;        releaseYear = newYear;        songsTitles = newSongsTitles;    }    public String getTitle() {        return title;    }    public List < String > getArtists() {        return artists;    }    public int getReleaseYear() {        return releaseYear;    }    public List < String > getSongsTitles() {        return songsTitles;    }    @Override    public String toString() {        return "'" + title + "' (" + releaseYear + ") by " + artists + " features songs " + songsTitles;    }    /**     * Builder class for instantiating an instance of     * enclosing Album class.     */    public static class Builder {        private String title;        private ArrayList < String > artists = new ArrayList < > ();        private int releaseYear;        private ArrayList < String > songsTitles = new ArrayList < > ();        public Builder(final String newTitle, final int newReleaseYear) {            title = newTitle;            releaseYear = newReleaseYear;        }        public Builder songTitle(final String newSongTitle) {            songsTitles.add(newSongTitle);            return this;        }        public Builder songsTitles(final List < String > newSongsTitles) {            songsTitles.addAll(newSongsTitles);            return this;        }        public Builder artist(final String newArtist) {            artists.add(newArtist);            return this;        }        public Builder artists(final List < String > newArtists) {            artists.addAll(newArtists);            return this;        }        public Album build() {            return new Album(title, artists, releaseYear, songsTitles);        }    }}

Теперь у нас есть data-класс Album, Protocol Buffers-класс, представляющий этот Album (AlbumProtos.java) и мы готовы написать Java-приложение для "сериализации" информации об Album без использования Java-сериализации. Код приложения находится в классе AlbumDemo, полный код которого доступен на GitHub.

Создадим экземпляр Album с помощью следующего кода:

/** * Generates instance of Album to be used in demonstration. * * @return Instance of Album to be used in demonstration. */public Album generateAlbum(){   return new Album.Builder("Songs from the Big Chair", 1985)      .artist("Tears For Fears")      .songTitle("Shout")      .songTitle("The Working Hour")      .songTitle("Everybody Wants to Rule the World")      .songTitle("Mothers Talk")      .songTitle("I Believe")      .songTitle("Broken")      .songTitle("Head Over Heels")      .songTitle("Listen")      .build();}

Класс AlbumProtos, сгенерированныйProtocol Buffers, включает в себя вложенный класс AlbumProtos.Album, который используется для бинарной сериализации Album. Следующий листинг демонстрирует, как это делается.

final Album album = instance.generateAlbum();final AlbumProtos.Album albumMessage    = AlbumProtos.Album.newBuilder()        .setTitle(album.getTitle())        .addAllArtist(album.getArtists())        .setReleaseYear(album.getReleaseYear())        .addAllSongTitle(album.getSongsTitles())        .build();

Как видно из предыдущего примера, для заполнения иммутабельного экземпляра класса, сгенерированного Protocol Buffers, используется паттерн Строитель (Builder). Через ссылку экземпляр этого класса теперь можно легко преобразовать объект в бинарный вид Protocol Buffers, используя метод toByteArray(), как показано в следующем листинге:

final byte[] binaryAlbum = albumMessage.toByteArray();

Чтение массива byte[] обратно в экземпляр Album может быть выполнено следующим образом:

/** * Generates an instance of Album based on the provided * bytes array. * * @param binaryAlbum Bytes array that should represent an *    AlbumProtos.Album based on Google Protocol Buffers *    binary format. * @return Instance of Album based on the provided binary form *    of an Album; may be {@code null} if an error is encountered *    while trying to process the provided binary data. */public Album instantiateAlbumFromBinary(final byte[] binaryAlbum) {    Album album = null;    try {        final AlbumProtos.Album copiedAlbumProtos = AlbumProtos.Album.parseFrom(binaryAlbum);        final List <String> copiedArtists = copiedAlbumProtos.getArtistList();        final List <String> copiedSongsTitles = copiedAlbumProtos.getSongTitleList();        album = new Album.Builder(                copiedAlbumProtos.getTitle(), copiedAlbumProtos.getReleaseYear())            .artists(copiedArtists)            .songsTitles(copiedSongsTitles)            .build();    } catch (InvalidProtocolBufferException ipbe) {        out.println("ERROR: Unable to instantiate AlbumProtos.Album instance from provided binary data - " +            ipbe);    }    return album;}

Как вы заметили, при вызове статического метода parseFrom(byte []) может быть брошено проверяемое исключение InvalidProtocolBufferException. Для получения десериализованного экземпляра сгенерированного класса, по сути, нужна только одна строка, а остальной код это создание исходного класса Album из полученных данных.

Демонстрационный класс включает в себя две строки, которые выводят содержимое исходного экземпляра Album и экземпляра, полученного из бинарного представления. В них есть вызов метода System.identityHashCode() на обоих экземплярах, чтобы показать, что это разные объекты даже при совпадении их содержимого. Если этот код выполнить с примером Album, приведенным выше, то результат будет следующим:

BEFORE Album (1323165413): 'Songs from the Big Chair' (1985) by [Tears For Fears] features songs [Shout, The Working Hour, Everybody Wants to Rule the World, Mothers Talk, I Believe, Broken, Head Over Heels, Listen]AFTER Album (1880587981): 'Songs from the Big Chair' (1985) by [Tears For Fears] features songs [Shout, The Working Hour, Everybody Wants to Rule the World, Mothers Talk, I Believe, Broken, Head Over Heels, Listen]

Здесь мы видим, что в обоих экземплярах соответствующие поля одинаковы и эти два экземпляра действительно разные. При использовании Protocol Buffers, действительно, нужно сделать немного больше работы, чем припочти автоматическом механизме сериализации Java, когда надо просто наследоваться от интерфейса Serializable, но есть важные преимущества, которые оправдывают затраты. В третьем издании книги Effective Java (Java: эффективное программирование) Джошуа Блох обсуждает уязвимости безопасности, связанные со стандартной десериализацией в Java, и утверждает, что Нет никаких оснований для использования сериализации Java в любой новой системе, которую вы пишете.


Узнать подробнее о курсе "Java Developer. Professional".

Смотреть открытый вебинар на тему gRPC для микросервисов или не REST-ом единым.

Подробнее..

C enum lt-gt string? Легко

12.09.2020 20:07:24 | Автор: admin

Вот, скажем, один из самых популярных примеров. Можно сказать, классических. Сериализуются данные в, скажем, json. В структуре есть enum-поле, которое хочется сохранять в текстовом виде (а не числом). Всё. Стоп. Простого способа решить эту элементарную задачу на C++ не существует. (c)

... Но очень хочется.

За последний год я видел, как чуть ли не в каждом проекте разработчик предлагал своё видение этой проблемы. И везде было дублирование кода, везде какие-то костыли, и "тонкости". Да что уж там, мне самому приходится время от времени возвращаться к этой теме. Хватит. Решил раз и навсегда закрыть вопрос, по крайней мере для себя.

Код далёк от совершенства (надеюсь,анонимуспоправит), но свою задачу выполняет. Может кому ипригодится:

Реализация
// enum_string.h#pragma once#define DECLARE_ENUM(T, values...)                                    \  enum class T { values, MAX };                                       \  char enum_##T##_base[sizeof(#values)] = #values;                    \  const char* T##_tokens[static_cast<__underlying_type(T)>(T::MAX)];  \  const char* const* T##_tmp_ptr = tokenize_enum_string(              \      const_cast<char*>(enum_##T##_base), sizeof(#values), T##_tokens,\      static_cast<__underlying_type(T)>(T::MAX));#define enum_to_string(T, value) \  (T##_tokens[static_cast<__underlying_type(T)>(value)])static const char* const* tokenize_enum_string(char* base,                                               int length,                                               const char* tokens[],                                               int size) {  int count = 0;  tokens[count++] = base;  for (int i = 1; i < length; ++i) {    if (base[i] == ',') {      base[i] = '\0';      if (count == size) {        return tokens;      }      do {        if (++i == length) {          return tokens;        }      } while (' ' == base[i]);      tokens[count++] = base + i;    }  }  return tokens;}static bool string_equals(const char* a, const char* b) {  int i = 0;  for (; a[i] && b[i]; ++i) {    if (a[i] != b[i]) {      return false;    }  }  return (a[i] == b[i]);}static int string_to_enum_int(const char* const tokens[], int max,                              const char* value) {  for (int i = 0; i < max; ++i) {    if (string_equals(tokens[i], value)) {      return i;    }  }  return max;}#define string_to_enum(T, value)     \  static_cast<T>(string_to_enum_int( \      T##_tokens, static_cast<__underlying_type(T)>(T::MAX), value))

Работу со строками можете без проблем заменить на ваши любимые библиотеки, большинство кода здесь - это как раз парсинг строки (уж очень хотелось обойтись без STL).

Главная идея была в том, чтобы гарантировать биективность множества enum и его строкового эквивалента, а также сделать реализацию универсальной по количеству элементов (до свидания, вырвиглазный хардкодный макрос _NARG). Ну и, чтобы использование было максимально няшным.

пример использования
// main.cpp#include <iostream>#include "enum_string.h"DECLARE_ENUM(LogLevel,  // enum class LogLevel             Alert,     // LogLevel::Alert             Critical,  // LogLevel::Critical             Error,     // LogLevel::Error             Warning,   // LogLevel::Warning             Notice,    // LogLevel::Notice             Info,      // LogLevel::Info             Debug      // LogLevel::Debug             );int main() {  // serialize  LogLevel a = LogLevel::Critical;  std::cout << enum_to_string(LogLevel, a) << std::endl;  // deserialize  switch (string_to_enum(LogLevel, "Notice")) {    case LogLevel::Alert: {      std::cout << "ALERT" << std::endl;    } break;    case LogLevel::Critical: {      std::cout << "CRITICAL" << std::endl;    } break;    case LogLevel::Error: {      std::cout << "ERROR" << std::endl;    } break;    case LogLevel::Warning: {      std::cout << "WARN" << std::endl;    } break;    case LogLevel::Notice: {      std::cout << "NOTICE" << std::endl;    } break;    case LogLevel::Info: {      std::cout << "INFO" << std::endl;    } break;    case LogLevel::Debug: {      std::cout << "DEBUG" << std::endl;    } break;    case LogLevel::MAX: {      std::cout << "Incorrect value" << std::endl;    } break;  }  return 0;}

Как по мне, в дополнительном обьяснении не нуждается.

Также, залил на github.

Любезно приглашаю критиков на ревью.

Подробнее..
Категории: C++ , Enum , Serialization

Кастомная (де) сериализация даты и времени в Spring

24.02.2021 18:11:35 | Автор: admin

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

  1. При получении запроса привести дату к серверному времени и работать с ней, а также сохранять в базу данных в таком виде

  2. В ответ возвращать дату и время с указанием серверного часового пояса

Для решения этой задачи Spring предоставляет удобный механизм для написания кастомной сериализации и десериализации. Главным его преимуществом является возможность вынести преобразования дат (и других типов данных) в отдельный конфигурационный класс и не вызывать методы преобразования каждый раз в исходном коде.

Десериализация

Для того, чтобы Spring понимал, что именно наш класс нужно использовать для (де)сериализации, его необходимо пометить аннотацией @JsonComponent

Ну а чтобы код был максимально кратким, я буду использовать внутренний статический класс, который должен быть унаследован от JsonDeserializerи параметризован нужным нам типом данных. Поскольку JsonDeserializerэто абстрактный класс, нам необходимо переопределить его абстрактный метод deserialize()

@JsonComponentpublic class CustomDateSerializer {        public static class LocalDateTimeDeserializer extends JsonDeserializer<LocalDateTime> {            @Override        public LocalDateTime deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) {            return null;        }    }}

Из параметров метода получаем переданную клиентом строку, проверяем её на null и получаем из неё объект класса ZonedDateTime

public LocalDateTime deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) {    String date = jsonParser.getText();    if (date.isEmpty() || isNull(date) {        return null;    }    ZonedDateTime userDateTime = ZonedDateTime.parse(date);}

Для получения разницы во времени, у переменной userDateTime нужно вызвать метод withZoneSameInstant() и передать в него текущую серверную таймзону. Нам остаётся лишь преобразовать полученную дату кLocalDateTime

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

public static class LocalDateTimeDeserializer extends JsonDeserializer<LocalDateTime> {    @Override    public LocalDateTime deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException {        String date = jsonParser.getText();        if (date.isEmpty()) {            return null;        }        try {            ZonedDateTime userDateTime = ZonedDateTime.parse(date);            ZonedDateTime serverTime = userDateTime.withZoneSameInstant(ZoneId.systemDefault());            return serverTime.toLocalDateTime();        } catch (DateTimeParseException e) {            try {                return LocalDateTime.parse(date);            } catch (DateTimeParseException ex) {                throw new IllegalArgumentException("Error while parsing date", ex);            }        }    }}

Предположим, что серверное времяUTC+03. Таким образом, когда клиент передаёт дату 2021-01-21T22:00:00+07:00, в нашем контроллере мы уже можем работать с серверным временем

public class Subscription {    private LocalDateTime startDate;      // standart getters and setters}
@RestController public class TestController {    @PostMapping  public void process(@RequestBody Subscription subscription) {    // к этому моменту поле startDate объекта subscription будет равно 2021-01-21T18:00  }}

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

С сериализацией алгоритм действий похожий. Нам нужно унаследовать класс от JsonSerializer, параметризовать его и переопределить абстрактный метод serialize()

Внутри метода мы проверим нашу дату на null, добавим к ней серверную таймзону и получим из неё строку. Остаётся лишь отдать её клиенту

public static class LocalDateTimeSerializer extends JsonSerializer<LocalDateTime> {    @Override    public void serialize(LocalDateTime localDateTime, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {        if (isNull(localDateTime)) {            return;        }        OffsetDateTime timeUtc = localDateTime.atOffset(ZoneOffset.systemDefault().getRules().getOffset(LocalDateTime.now()));        jsonGenerator.writeString(timeUtc.toString());    }}

Круто? Можно в прод? Не совсем. В целом, этот код будет работать, но могут начаться проблемы, если серверная таймзона будет равна UTC+00. Дело в том, что конкретно для этого часового пояса id таймзоны отличается от стандартного формата. Посмотрим в документацию класса ZoneOffset

Таким образом, имея серверную таймзону UTC+03, на выходе мы получим строку следующего вида: 2021-02-21T18:00+03:00.Но если же оно UTC+00, то получим 2021-02-21T18:00Z

Поскольку мы работаем со строкой, нам не составит труда немного изменить код, дабы на выходе мы всегда получали дату в одном формате. Объявим две константы одна из них будет равна дефолтному id UTC+00, а вторая которую мы хотим отдавать клиенту, и добавим проверку - если серверное время находится в нулевой таймзоне, то заменим Z на +00:00. В итоге наш сериализотор будет выглядеть следующим образом

public static class LocalDateTimeSerializer extends JsonSerializer<LocalDateTime> {    private static final String UTC_0_OFFSET_ID = "Z";    private static final String UTC_0_TIMEZONE = "+00:00";    @Override    public void serialize(LocalDateTime localDateTime, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {        if (!isNull(localDateTime)) {            String date;            OffsetDateTime timeUtc = localDateTime.atOffset(ZoneOffset.systemDefault().getRules().getOffset(LocalDateTime.now()));            if (UTC_0_OFFSET_ID.equals(timeUtc.getOffset().getId())) {                date = timeUtc.toString().replace(UTC_0_OFFSET_ID, UTC_0_TIMEZONE);            } else {                date = timeUtc.toString();            }            jsonGenerator.writeString(date);        }    }}

Итого

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

Полностью исходный код можно посмотреть здесь

Подробнее..
Категории: Java , Spring , Json , Maven , Spring-boot , Serialization , Date , Deserialization

Из песочницы База данных на ScriptableObject c системой сейвазагрузки

14.09.2020 16:14:47 | Автор: admin

Введение


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


Для хранения таких данных существует много способов кто-то хранит их в таблицах, в xml или json файлах, которые редактируют собственными инструментами. Unity предоставляет свой способ Scriptable Objects (SO), которые мне нравится тем, что для их визуального представления не нужно писать свой редактор, легко делать ссылки на ассеты игры и друг на друга, а с появлением Addressables эти данные можно легко и удобно хранить вне игры и обновлять отдельно.


В этой статье я хотел бы рассказать о своей библиотеке SODatabase, с помощью которой можно удобно создавать, редактировать и использовать в игре (редактировать и сериализовать) scriptable objects.


Создание и редактирование SO


Создание и редактирование SOшек я веду в отдельном окне, которое чем-то похоже на окна проекта с инспектором слева находится дерево папок (папка, в которой находятся все SOшки группа в addressables), а справа инспектор выделенной SOшки.


Интерфейс


Для отрисовки такого WindowEditorа я использую библиотеку Odin Inspector. Кроме того, я использую сериализацию для SO из этой библиотеки она значительно расширяет стандартную юнитиевскую сериализацию, позволяя хранить полиморфные классы, глубокую вложенность, ссылки на классы.


Создание SO


Создание новых SO происходит через нажатие кнопки в этом окне там нужно выбрать тип нужной сошки, и она создаётся в папке. Для того, чтобы тип SO появился в этом окне в качестве варианта, SO должен наследоваться от DataNode, который имеет лишь одно дополнительное поле к ScriptableObject


public string FullPath { get; }

Это путь к данной SO, с помощью которого к ней можно будет обратиться в рантайме.


Доступ к SO в игре


В рантайме обычно нужно либо получить какую-то конкретную модель, например, SO со списком настроек какого-либо окна, либо набор моделей из папки например, список айтемов, где модель каждого айтема представляет собой отдельный SO.
Для этого в static классе SODatabase есть два основных метода, которые возвращают либо весь список моделей из нужной папки, либо конкретную модель из папки с определённым именем.


public static T GetModel<T>(string path) where T : DataNode   public static List<T> GetModels<T>(string path, bool includeSubFolders = false) where T : DataNode

Уточню, что один раз в начале игры перед запросом моделей SODatabase нужно проинициализировать, чтобы обновились и прогрузились данные из Addressables.


Загрузка и сохранение


Один из недостатков ScriptableObject по сравнению с хранением данных с сериализацией в собственном формате является то, что в них нельзя записывать данные из игры в рантайме. То есть по сути ScriptableObject предназначены для хранения статичных данных. Но любой игре нужна загрузка и сохранение, и я реализую это через те же самые SO из базы данных.


Возможно это не идиоматичный способ совмещать базу статичных моделей игры с загрузкой и сохранением динамических данных, но в моём опыте ещё ни разу не было случая, когда это создало бы какие-то неудобства, но при этом есть ряд ощутимых плюсов. Например, с помощью тех же инспекторов SOшек можно смотреть игровые данные в эдиторе и менять их. Можно удобно загружать сейвы игроков, смотреть их содержимое и редактировать в unity, не используя никаких внешних утилит и собственных редакторов для визуализации xml или других форматов.


Я достигаю этого, сериализуя динамические поля в ScriptableObject с помощью JSON.


Класс DataNode родительский класс всех SO, хранящихся в SODatabase, помечен как


[JsonObject(MemberSerialization.OptIn, IsReference = true)]

и все его JsonProperty сериализуются в файл save.txt при сохранении игры. Соответственно при инициализации SODatabase кроме запроса данных об изменении addressables происходит JsonConvert.PopulateObject для каждой динамической модели из SODatabase, используя данные из этого файла.


Для того, чтобы это работало гладко, я сериализую ссылки на SO (которые могут являтся динамическими полями, помеченными как JsonProperty) в строку-путь, и потом десериализую обратно в ссылки на SO при загрузке. Есть ограничение данные на игровые ассеты динамическими быть не могут. Но это не фундаментальное ограничение, просто у меня ещё не было случая, когда такие динамические данные потребовались бы, поэтому я не реализовывал специальную сериализацию для таких данных.


Примеры


В классе-стартере игры инициализация и загрузка данных


async void Awake(){    await SODatabase.InitAsync(null, null);    await SODatabase.LoadAsync();}

и сохранение стейта при выходе


private void OnApplicationPause(bool pauseStatus){    if (pauseStatus)        SODatabase.Save();}private void OnApplicationQuit(){    SODatabase.Save();}        

В рпг для хранения информации об игроке я прямо создаю PlayerSO, в котором одни только динамические поля имя, количество экспы игрока, кристаллов и так далее. Также хорошей практикой по-моему является создание статической строки с путём, по которому я сохраняю данную модель в SODatabase, чтобы потом обращаться к ней в рантайме.


public class PlayerSO : DataNode{    public static string Path => "PlayerInfo/Player";    [JsonProperty]    public string Title = string.Empty;    [JsonProperty]    public int Experience;}

Точно также для инвентаря игрока я создаю PlayerInventorySO, где храню список ссылок на айтемы игрока (каждый айтем представляет собой ссылку на статичный SO из SODatabase).


 public class PlayerInventorySO : DataNode {     public static string Path => "PlayerInfo/PlayerInventory";     [JsonProperty]     public List<ItemSO> Items = new List<ItemSO>(); }

Бывают наполовину статические, наполовину динамические данные например, квесты. Возможно, это не лучший подход, но я прямо в моделях QuestSO со статической информацией о квестах (название, описание, цели и т.д.) храню динамическую информацию по прогрессу в этом квесте. Таким образом гейм-дизайнер в одном инспекторе видит всю инфу о текущем состоянии квеста и его описание.


public class QuestNode : BaseNode{    public static string Path = "QuestNodes";    //Editor    public virtual string Title { get; } = string.Empty;    public virtual string Description { get; } = string.Empty;    public int TargetCount;    //Runtime    [JsonProperty]    private bool finished;    public bool Finished    {        get => finished;        set => finished = value;    }}

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


var playerSO = SODatabase.GetModel<PlayerSO>(PlayerSO.Path);var playerInventorySO = SODatabase.GetModel<PlayerInventorySO>(PlayerInventorySO.Path);var questNodes = SODatabase.GetModels<QuestNode>(QuestNode.Path, true);

Текущее состояние библиотеки


В продакшене несколько лет использовался прообраз этой библиотеки в ней аналогичное окно-проводник для создания/редактирования моделей, которые содержали статичные и динамические данные, но все эти модели не использовали SO, а были целиком в json. Из-за этого для каждой модели приходилось писать свой эдитор вручную, ссылки моделей друг на друга и игровые ассеты(спрайты и т.д.) делались довольно неудобными способами. Переход на SO совершён в прошлом году, и пока всего одна игра с SODatabase ушла в релиз, но в ней не использовались Addressables.


На addressables я перешёл совсем недавно для использования в текущем проекте (на разработку которой я ищу в команду второго программиста в партнёры). В данный момент идёт активное допиливание этой библиотеки под нужды этой игры.


Библиотека лежит в открытом доступе на github. Написана с использованием Nullable из c# 8, соответственно требует Unity 2020.1.4 в качестве минимальной версии.

Подробнее..

Сериализация в JSON и иммутабельный объект. О пакете built_value для Flutter

07.11.2020 14:06:38 | Автор: admin


Иногда JSON от API необходимо конвертировать в объект и желательно в иммутабельное значение. На Dart это возможно, но для этого необходимо много кодить для каждого из объектов. К счастью, существует пакет, который поможет Вам все это выполнить, и в этой статье я Вам расскажу об этом способе.

Наша цель:

1. Сериализация

final user = User.formJson({"name": "Maks"});final json = user.toJson();

2. Использование как значения

final user1 = User.formJson({"name": "Maks"});final user2 = User((b) => b..name='Maks');if (user1 == user2) print('Один и тот же пользователь');

3. Иммутабельность

user.name = 'Alex'; // Неверноfinal newUser = user.rebuild((b) => b..name='Alex'); // Верно


Устанавливаем пакеты


Открываем файл pubspec.yaml на нашем Flutter проекте и добавляем пакет built_value на dependencies:

  ...  built_value: ^7.1.0

А также добавляем пакеты built_value_generator и build_runner на dev_dependencies. Эти пакеты помогут генерировать необходимые коды.

dev_dependencies:

 ...  build_runner: ^1.10.2  built_value_generator: ^7.1.0

Сохраняем файл pubspec.yaml и запускаем flutter pub get чтобы получить все необходимые пакеты.

Создаем built_value


Давайте создадим простой класс чтобы увидеть как все работает.

Создаем новый файл user.dart:

import 'package:built_value/built_value.dart';part 'user.g.dart';abstract class User implements Built<User, UserBuilder> {  String get name;  User._();  factory User([void Function(UserBuilder) updates]) = _$User;}

Итак, мы создали простой абстрактный класс User с одним полем name, указали, что наш класс является частью user.g.dart и основная имплементация находится там, в том числе и UserBuilder. Чтобы автоматически создать этот файл, необходимо запустить это в командной строке:

flutter packages pub run build_runner watch

или

flutter packages pub run build_runner build

Запускаем с watch чтобы не перезапускать каждый раз, когда что-то меняется в классе.

После этого мы видим что появился новый файл user.g.dart. Можно посмотреть что внутри и увидеть сколько времени мы сэкономим автоматизируя этот процесс. Когда добавим еще поля и сериализацию этот файл станет еще больше.

Давайте проверим что у нас получилось:

final user = User((b) => b..name = "Max");print(user);print(user == User((b) => b..name = "Max")); // trueprint(user == User((b) => b..name = "Alex")); // false

nullable


Добавляем новое поле surname на User класс:

abstract class User implements Built<User, UserBuilder> {  String get name;  String get surname;...}

Если попробовать вот так:

final user = User((b) => b..name = 'Max');

То мы получим ошибку:

Tried to construct class "User" with null field "surname".

Чтобы surname сделать опциональным нужно использовать nullable:

@nullableString get surname;

или же нужно каждый раз давать surname:

final user = User((b) => b  ..name = 'Max'  ..surname = 'Madov');print(user);

Built Collection


Давайте используем массивы. Для этого нам поможет BuiltList:

import 'package:built_collection/built_collection.dart';...abstract class User implements Built<User, UserBuilder> {  ...  @nullable  BuiltList<String> get rights;...

final user = User((b) => b  ..name = 'Max'  ..rights.addAll(['read', 'write']));print(user);

Enum


Необходимо ограничить rights, так чтобы кроме read, write и delete не принимал другие значения. Для этого в новом файле под именем right.dart создаем новый EnumClass:

import 'package:built_collection/built_collection.dart';import 'package:built_value/built_value.dart';part 'right.g.dart';class Right extends EnumClass {  static const Right read = _$read;  static const Right write = _$write;  static const Right delete = _$delete;  const Right._(String name) : super(name);  static BuiltSet<Right> get values => _$rightValues;  static Right valueOf(String name) => _$rightValueOf(name);}

User:

@nullableBuiltList<Right> get rights;

Теперь rights принимает только тип Right:

final user = User((b) => b  ..name = 'Max'  ..rights.addAll([Right.read, Right.write]));print(user);

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


Чтобы эти объекты можно было легко конвертировать в JSON и обратно нам нужно добавить к нашим классам еще пару методов:

...import 'package:built_value/serializer.dart';import 'serializers.dart';...abstract class User implements Built<User, UserBuilder> {...  Map<String, dynamic> toJson() => serializers.serializeWith(User.serializer, this);  static User fromJson(Map<String, dynamic> json) =>serializers.deserializeWith(User.serializer, json);  static Serializer<User> get serializer => _$userSerializer;}

В принципе для сериализации хватит и этого:

static Serializer<User> get serializer => _$userSerializer;

Но для удобства добавим методы toJson и fromJson.

Также добавляем одну строку в класс Right:

import 'package:built_value/serializer.dart';,,,class Right extends EnumClass {...  static Serializer<Right> get serializer => _$rightSerializer;}

И нужно создать еще один файл с именем serializers.dart:

import 'package:built_collection/built_collection.dart';import 'package:built_value/serializer.dart';import 'package:built_value/standard_json_plugin.dart';import 'package:built_value_demo/right.dart';import 'package:built_value_demo/user.dart';part 'serializers.g.dart';@SerializersFor([Right, User])final Serializers serializers =(_$serializers.toBuilder()..addPlugin(StandardJsonPlugin())).build();

Каждый новый Built класс необходимо добавить в @SerializersFor([...]) чтобы сериализация работала как надо.

Теперь можно проверять что у нас получилось:

final user = User.fromJson({  "name": "Max",  "rights": ["read", "write"]});print(user);print(user.toJson());

final user2 = User((b) => b  ..name = 'Max'  ..rights.addAll([Right.read, Right.write]));print(user == user2); // true

Давайте поменяем значения:

final user3 = user.rebuild((b) => b  ..surname = "Madov"  ..rights.replace([Right.read]));print(user3);

Дополнительно


По итогу найдутся и те кто скажут, что все равно необходимо писать довольно много. Но если Вы пользуетесь Visual Studio Code рекомендую установить снипет под названием Built Value Snippets и тогда можно все это генерировать автоматически. Для этого поищите в Marketplace или пройдите по этой ссылке.

После установки напишите в Dart файле bv и вы сможете увидеть какие опции существуют.

Если Вы не хотите чтобы Visual Studio Code показывал сгенерированные *.g.dart файлы, нужно открыть Settings и поискать Files: Exclude, после чего нажмите на Add Pattern и добавьте **/*.g.dart.

Что дальше?


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

P.S. Буду очень рад и благодарен Вам, если поделитесь своими методами, которые посчитаете практичнее и эффективнее предложенного мной.

GitHub проект

Пакеты:
pub.dev/packages/built_value
pub.dev/packages/built_value_generator
pub.dev/packages/build_runner
Подробнее..
Категории: Dart , Flutter , Json , Serialization , Immutable

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru