Статьи

Помимо JSON: протокол Spearal Serialization для iOS

Spearal — это постоянная попытка предоставить протокол сериализации, который работает — в отличие от JSON — с произвольными сложными моделями данных, предлагая расширенные функции, такие как частичная сериализация объектов, встроенная поддержка неинициализированных ассоциаций JPA, согласование расходящихся моделей или фильтрация свойств объекта.

Благодаря Spearal для iOS, который представлен в этой статье через образец приложения для iPhone / Spring / JPA, проект с открытым исходным кодом Spearal теперь достиг своей первой важной вехи: все клиенты JavaScript, Android и iOS поддерживаются вместе с популярными бэкэндами Java (JAX-RS и Spring MVC).

Эта статья посвящена интеграции Spearal / iOS, и ее цель не состоит в том, чтобы дать общий обзор проекта Spearal или того, как вы можете использовать его с приложением JavaScript или Android. Чтобы узнать больше об этих предметах, пожалуйста, обратитесь к двум следующим статьям DZone:

Серверное приложение, используемое в этой демонстрации, размещено на RedHat OpenShift . Это точно такой же, который был изначально создан для демо Android. Чтобы узнать больше о конфигурации Spring MVC и, в более общем плане, о приложении на стороне сервера, прочитайте раздел «Некоторые пояснения по настройке Spring» в статье Day Day JSON: Spearal и Mobile Apps .

Авторы: приложение, использованное в этой статье, представляет собой слегка измененную версию превосходной демонстрационной версии AngularJS / Java EE, написанной Роберто Кортезом.

Клонирование, сборка и запуск демоверсии iOS

Поскольку наше приложение для iPhone будет подключаться к существующему удаленному серверу, вам понадобится только Apple Mac, работающее подключение к Интернету и XCode 6.1 : Spearal iOS написана с использованием нового быстро развивающегося языка Swift и других версий. XCode может не работать с этой демонстрацией.

Если вы выполняете эти требования, вы можете просто ввести следующую командную строку в окне терминала:

$ git clone --recursive  https://github.com/spearal-examples/spearal-example-ios.git

Затем запустите XCode, выберите «Файл» / «Открыть …» в главном меню и откройте файл spearal-example-ios / spearal-example-ios.xcworkspace. Проект должен быть собран без ошибок, и вы должны иметь возможность запустить демонстрацию с помощью обычной команды запуска (черная стрелка в верхнем левом углу окна рабочей области с выбранным «SpearalIOSExample» в качестве цели).

Приложение основано на шаблоне «Master-Detail Application», который вы можете выбрать при создании нового проекта iOS. Вот два вида демонстрации:

Примечание: если главное представление пусто и если вы получаете ошибку в консоли XCode, это, вероятно, связано с гибернацией OpenShift серверного приложения, если оно не использовалось в течение длительного времени. Перейдите по следующему URL- адресу ( https://examples-spearal.rhcloud.com/spring-angular/index-spearal.html ) и нажимайте кнопку «Обновить» до тех пор, пока демо-версия снова не заработает. Затем перезапустите приложение iOS. В любом случае, не стесняйтесь обращаться к нам через наш форум пользователей здесь .

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

Модель данных

На стороне сервера приложение использует один объект и компонент разбиения на страницы:

@Entity
public class Person implements Serializable {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;
    private String description;
    private String imageUrl;

    // skipped getters and setters...
}

public class PaginatedListWrapper<T> implements Serializable {

    private Integer currentPage;
    private Integer pageSize;
    private Integer totalResults;

    private String sortFields;
    private String sortDirections;
    private List<T> list;

    // skipped getters and setters...
}

На стороне клиента нам нужно скопировать эти структуры на языке Swift следующим образом:

@objc(Person)
public class Person: SpearalAutoPartialable {

    public dynamic var id:NSNumber?
    public dynamic var name:String?
    public dynamic var description_:String?
    public dynamic var imageUrl:String?
}

Сущность Person отражается в совместимый класс Swift KVO ( Key Value Observing ) с дополнительными свойствами. Этот класс наследуется от класса SpearalAutoPartialable, который отслеживает определенные свойства. Определенное свойство — это свойство, которое было установлено хотя бы один раз, даже равным nil: если вы создаете нового Person без установки какого-либо из его четырех свойств, все они будут неопределенными. Однако, например, следующий код определяет свойства id и name:

var person = Person()
person.id = nil
person.name = "I just have a name"

Позже мы увидим, как это различие между определенными и неопределенными свойствами можно использовать для сериализации только частичных объектов (т. Е. Только определенных свойств) для обновления. Затем идет класс PaginatedListWrapper:

@objc(PaginatedListWrapper)
public class PaginatedListWrapper: NSObject {

    public var currentPage:NSNumber?
    public var pageSize:NSNumber?
    public var totalResults:NSNumber?

    public var sortFields:String?
    public var sortDirections:String?
    public var list:[Person]?
}

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

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

Сервис Spring MVC, на стороне клиента

В дополнение к репликации модели данных в Swift, нам, очевидно, нужен код для отправки и получения данных на сервер Spring MVC и с него. Вы можете найти реализацию такого сервиса в файле SpearalIOSExample / SpearalIOSExample / PersonService.swift. Ради этой демонстрации мы просто поместили код инициализации Spearal и конфигурацию всех запросов REST в одно и то же место: реальное приложение с более сложной моделью, безусловно, разделит фабрики и запросы на отдельные классы.

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

import Foundation
import SpearalIOS

class PersonService {

    private let personUrl = "https://examples-spearal.rhcloud.com/spring-angular/resources/persons"
    private let session:NSURLSession
    private let factory:SpearalFactory

    init() {
        self.session = NSURLSession(configuration: NSURLSessionConfiguration.ephemeralSessionConfiguration())

        self.factory = DefaultSpearalFactory()
        let aliasStrategy = BasicSpearalAliasStrategy(localToRemoteClassNames: [
            "Person": "org.spearal.samples.springangular.data.Person",
            "PaginatedListWrapper": "org.spearal.samples.springangular.pagination.PaginatedListWrapper"
        ])
        aliasStrategy.setPropertiesAlias("Person", localToRemoteProperties: [
            "description_" : "description"
        ])
        factory.context.configure(aliasStrategy)
    }

    // skipped code...
}

Сначала мы создаем эфемерный сеанс NSURLSession, который мы будем использовать для каждого запроса к серверу. Затем идет инициализация фабрики Spearal. Нам нужна стратегия псевдонимов для отображения серверных Java-классов (полностью соответствующих их соответствующим пакетам) на их зеркала Swift. Например, класс «Лицо» Swift сопоставлен с классом «org.spearal.samples.springangular.data.Person» на стороне сервера. Эта конфигурация со словарем — не единственный способ настроить этот перевод: вы также можете закрыть его, чтобы выполнить этот перевод в более общем виде.

Затем у нас есть псевдоним свойства, который отображает свойство «description» на стороне сервера в свойство Persons «description_» на стороне клиента Persons: поскольку «description» — это предопределенное свойство NSObject, которое мы не можем просто переопределить, нам нужно использовать псевдоним в наша модель Swift, отсюда и свойство «description_» выше и конфигурация псевдонимов: нам не понадобится такой псевдоним, если модель сервера не использует каким-то образом зарезервированное имя свойства.

Теперь давайте кратко рассмотрим код, используемый для загрузки списка всех символов в главном представлении:

func listPersons(completionHandler: ((PaginatedListWrapper!, NSError!) -> Void)) {
    let request = createRequest("\(personUrl)?pageSize=100", method: "GET")

    executeRequest(request, completionHandler: { (list:PaginatedListWrapper?, error:NSError?) -> Void in
        completionHandler(list, error)
    })
}

    // skipped code...

func createRequest(url:String, method:String) -> NSMutableURLRequest {
    var request = NSMutableURLRequest(URL: NSURL(string: url)!)
    request.HTTPMethod = method
    request.addValue("application/spearal", forHTTPHeaderField: "Content-Type")
    request.addValue("application/spearal", forHTTPHeaderField: "Accept")
    return request
}

func executeRequest<T>(request:NSURLRequest, completionHandler:((T?, NSError?) -> Void)) {
    let task = session.dataTaskWithRequest(request, completionHandler: {(data, response, err) -> Void in
        var error:NSError? = err
        var value:T? = nil

        if error == nil {
            if (response as NSHTTPURLResponse).statusCode != 200 {
                error = NSError(domain: "Spearal", code: 1, userInfo: [
                    NSLocalizedDescriptionKey: "HTTP Status Code: \((response as NSHTTPURLResponse).statusCode)"
                ])
            }
            else if data.length > 0 {
                value = self.decode(data)
            }
        }

        dispatch_async(dispatch_get_main_queue()) {
            completionHandler(value, error)
        }
    })

    task.resume()
}

func decode<T>(data:NSData) -> T? {
    let printer = SpearalDefaultPrinter(SpearalPrinterStringOutput())

    let decoder:SpearalDecoder = self.factory.newDecoder(SpearalNSDataInput(data: data), printer: printer)
    let any = decoder.readAny()

    println("RESPONSE (data length: \(data.length) bytes)")
    println((printer.output as SpearalPrinterStringOutput).value)
    println("--")

    return any as? T
}

Функция listPersons создает HTTP-запрос GET по URL-адресу «https://examples-spearal.rhcloud.com/spring-angular/resources/persons?pageSize=100». Поскольку в этой демонстрации мы не хотим разбивать страницы на страницы, мы выбрали произвольно большой размер страницы, равный 100, чтобы выбрать всех людей. Функция createRequest настраивает HTTP-заголовки «Content-Type» и «Accept» для mime-типа «application / spearal».

Затем функция executeRequest использует NSURLSession для отправки запроса и декодирования ответа, заботясь о непредвиденных ошибках. Затем он вызывает заданный завершитель завершения, который отвечает за обработку результата вызова listPersons.

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

RESPONSE (data length: 3147 bytes)
#0 org.spearal.samples.springangular.pagination.PaginatedListWrapper {
    currentPage: 1,
    list: #1 [
        #2 org.spearal.samples.springangular.data.Person {
            description: "Konoha",
            id: 1,
            imageUrl: "http://img1.wikia.nocookie.net/__cb20140523045537/naruto/images/thumb/3/36/Naruto_Uzumaki.png/300px-Naruto_Uzumaki.png",
            name: "Uzumaki Naruto"
        },
    // etc.

Остальная часть класса PersonService.swift так же определяет другие операции, такие как getPerson, savePerson и deletePerson. Он также определяет функцию кодирования, которая регистрирует содержимое отправленных данных (если они есть) в консоли.

Не вдаваясь в подробности о том, как работает все приложение, давайте просто скажем, что синглтон PersonService создается в классе AppDelegate.swift в начале приложения и затем используется в контроллере master и detail через AppDelegate.instance (). Доступ к personService ,

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

Фильтрация неиспользуемых свойств

Если вы снова посмотрите на консоль, вы заметите, что в результате вызова listPersons отображается список людей со всеми их свойствами. Поскольку мы отображаем только имя каждого символа в главном представлении нашего приложения для iPhone, нам не нужно извлекать поля description и imageUrl. Spearal позволяет легко фильтровать эти неиспользуемые свойства. Откройте файл SpearalIOSExample / SpearalIOSExample / PersonService.swift, найдите функцию listPersons и раскомментируйте следующую строку кода:

request.addValue("org.spearal.samples.springangular.data.Person#name", forHTTPHeaderField: "Spearal-PropertyFilter")

Остановите и перезапустите приложение. Как только он запустится, взгляните на вашу консоль:

RESPONSE (data length: 567 bytes)
#0 org.spearal.samples.springangular.pagination.PaginatedListWrapper {
    currentPage: 1,
    list: #1 [
        #2 org.spearal.samples.springangular.data.Person {
            id: 1,
            name: "Uzumaki Naruto"
        },
    // etc.

С помощью одной строки кода мы сократили размер ответа с 3147 до 567 байт (примерно в 6 раз меньше), и выигрыш, конечно, был бы гораздо важнее при использовании более сложной модели данных. Обратите внимание, что нам не нужно было явно запрашивать свойство id у Persons, а только по их именам: Spearal считает первичные ключи сущностей нефильтруемыми, а фильтр «org.spearal.samples.springangular.data.Person # name» строго эквивалентен «org.spearal.samples.springangular.data.Person # номер, имя».

If you click on a Person name, you will however get the corresponding person with all its attributes because the PersonService.getPerson function is called before displaying the detail view: the filter is per-request based and is only applied during the PersonService.listPersons call.

Note: you won’t find such filtering features with JSON popular libraries. Even if you manage to code something to get the same result, Spearal is offering a built-in and consistent way to deal with such partial objects. Person instances received after this filter configuration actually contain two undefined properties: you can check for definability with person._$isDefined(«imageUrl»), which would return false here. Moreover, if such partial entity is sent back to the server for update — after changing the name of a person for example — the description and imageUrl won’t be set to null in your database: the Spearal JPA module takes care of undefined properties, you don’t face the risk of messing up your database with partial objects.

Dealing with outdated data models

A common issue with mobile applications (or, more generally, with any connected application installed on a client device) is that you may have to deal with several outdated versions before everybody has upgraded. If your server data model has evolved, usually by adding new properties, outdated clients must be able to work as before and the data these clients send to the server, even if the new properties are missing, shouldn’t nullify any existing values.

With Spearal, you basically don’t need to care about such issue: an outdated entity with missing properties is detected as a partial object by the server and only defined properties are used when updating your database. Without going into details about this feature, let’s just say it is implemented in the JPA2 module of Spearal.

To give an example of such data model evolution, we have set up another OpenShift server here: https://examples-spearal.rhcloud.com/spring-angular-v2/index-spearal.html. If you browse to this url, you will notice a new column in the persons data grid: each Manga character can now have a worst enemy and this association can be circular (we are usually the worst enemy of our worst enemy). In addition to the new worst enemy association (and because it doesn’t have to be that bad) we also have a new best friend association, which is lazily fetched. Our new person entity has now the following properties:

@Entity
public class Person implements Serializable {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;
    private String description;
    private String imageUrl;

    @ManyToOne(fetch=FetchType.LAZY)
    private Person bestFriend;
 
    @ManyToOne // eagerly fetched
    private Person worstEnemy;

    // skipped getters and setters...
}

Let’s now connect our iPhone application to this upgraded server. Edit again the SpearalIOSExample/SpearalIOSExample/PersonService.swift file and do the following changes:

// private let personUrl = "https://examples-spearal.rhcloud.com/spring-angular/resources/persons"
private let personUrl = "https://examples-spearal.rhcloud.com/spring-angular-v2/resources/persons"

Restart the application. It just looks the same and the list of person’s names is displayed as usual in the master view. Because we didn’t remove the filter which asks for person’s names only, we only received, just like before, the id and name properties of each person. Click on one of the characters (eg. Hatake Kakashi). This is what you should now see in your console:

RESPONSE (data length: 473 bytes)
#0 org.spearal.samples.springangular.data.Person {
    description: "Konoha",
    id: 2,
    imageUrl: "http://img1.wikia.nocookie.net/__cb20140616090940/naruto/images/thumb/b/b3/KakashiHatake.png/300px-KakashiHatake.png",
    name: "Hatake Kakashi",
    worstEnemy: #1 org.spearal.samples.springangular.data.Person {
        bestFriend: nil,
        description: "Missing-nin",
        id: 20,
        imageUrl: "http://img3.wikia.nocookie.net/__cb20100623204832/naruto/images/thumb/e/e8/Orochimaru2.jpg/300px-Orochimaru2.jpg",
        name: "Orochimaru",
        worstEnemy: @0
    }
}

Let’s quickly explain what we are seeing. Hatake Kakashi has a worst enemy who is Orochimaru. Orochimaru, circularly, has Hatake Kakashi as its worst enemy: the «@0» notation stands for a reference to the object at «#0» (Hatake Kakashi). Then, Hatake Kakashi has a best friend in the database, which was lazily fetched and ignored by the serializer server-side. That’s why there is no «bestFriend» property for Hatake Kakashi: uninitialized association are considered as undefined and just skipped. On the other hand, Orochimaru doesn’t have a best friend, the association is set to null in the database, and its «bestFriend» property is, accordingly, present and set to nil in the serialized data.

It is worth noting that these extra properties aren’t interfering with the deserialization process client-side, even if our client model isn’t in sync with the server one: unknown properties are just skipped, they don’t prevent from deserializing correct data.

If we modify the Hatake Kakashi name and click on the «Save» button, everything works as expected. There is no changes in the console except for the modified name and we can see that the «bestFriend» property of Hatake Kakashi is still missing: even if the client application is unaware of the best friend association, the existing association wasn’t broken in the database during the update. However, if you look again at your console just before the last response, you will notice that nothing was sent for the best friend and worst enemy properties:

REQUEST (data length: 220 bytes)
#0 org.spearal.samples.springangular.data.Person {
    id: 2,
    name: "Hatake Kakashe",
    description: "Konoha",
    imageUrl: "http://img1.wikia.nocookie.net/__cb20140616090940/naruto/images/thumb/b/b3/KakashiHatake.png/300px-KakashiHatake.png"
}

Note: dealing correctly with outdated client models and preventing unwanted overrides in your database is a built-in feature of Spearal JPA2. While you can imagine several workarounds to achieve the same functionality with JSON, there is no ways — as far as I know — to get this result without taking great care of what you are doing client and server side (refer to this article to know more).

Moreover, JSON would simply fail with cyclic structures like the ones we are using here: while the JSON version of the application is working with the old model (see https://examples-spearal.rhcloud.com/spring-angular/index.html), it fails immediately with the new one (see https://examples-spearal.rhcloud.com/spring-angular-v2/index.html).

Sending differential updates

If you deal with entities that contain a large number of properties, it can be useful to have a simple way to send diffs instead of full objects for update. This can be achieved very easily with Spearal, thanks to its concept of partial objects.

To show a quick example of this feature, edit the SpearalIOSExample/SpearalIOSExample/DetailViewController.swift file and do the following change in the saveBtnClick function:

// let person = personFromUI()
let person = personDiffFromUI()

Rerun the application, edit one of our persons, change only its name (for example) and save it. This is what you can find in your console:

REQUEST (data length: 73 bytes)
#0 org.spearal.samples.springangular.data.Person {
    id: 2,
    name: "Hatake Kakashi"
}

As you can see, only the id and name properties were sent to server, the other fields being simply ignored. When the person is reloaded after the save operation, you will notice that other properties are still there: the differential update with just the id and the new name didn’t nullify anything. Let’s now have a look at the personFromUI and the alternate personDiffFromUI functions:

private func personFromUI() -> Person {
    let person = Person()

    person.id = self.person!.id
    person.name = self.nameText!.text
    person.description_ = self.descriptionText!.text
    person.imageUrl = self.imageUrlText!.text

    return person
}

private func personDiffFromUI() -> Person {
    let person = Person()

    person.id = self.person!.id

    if self.person!.name != self.nameText!.text {
        person.name = self.nameText!.text
    }
    if self.person!.description_ != self.descriptionText!.text {
        person.description_ = self.descriptionText!.text
    }
    if self.person!.imageUrl != self.imageUrlText!.text {
        person.imageUrl = self.imageUrlText!.text
    }

    return person
}

The personFromUI function creates a new person and populates its properties with values from the UI, regardless of whether they have been modified or not. On the contrary, however, the alternate personDiffFromUI function only sets properties that have been modified, leaving the others undefined. Spearal serialization is aware of such undefined properties and, accordingly, skips them when the entity is sent to the server for update.

Note: differential updates should be calculated by reflection or a kind of intelligent bindings instead of what we have done, property by property, in the code above. This is outside of the scope of this article. You could certainly do something similar with JSON, by copying in a dictionary only updated properties. However, it wouldn’t be that simple to deal with these partial objects server-side. While Spearal automatically distinguishes between undefined and null properties, standard JSON libraries aren’t offering the same feature: you must be very careful with properties that are null because they were missing and those that are null because they are actually set to null.

How to use Spearal iOS in your own projects?

Spearal iOS is based on the new Apple Swift language and building binary distributions of Swift frameworks isn’t recommended yet (see this Apple blog post). The recommended way is to create a new XCode workspace, add a reference to the Spearal iOS source code and create your new iOS project alongside.

To get the Spearal iOS source code, you can download a specific release from Github here or clone it with this command:

$ git clone https://github.com/spearal/SpearalIOS.git

You can then checkout the tag you want to work with (eg. git checkout 0.2.0) or stay with the HEAD revision (be careful with the HEAD branch, some commits can be unfortunate). From your new XCode workspace, creating a reference to the Spearal framework is then just a matter of adding the SpearalIOS.xcodeproj file. From your application and in every Swift file where you want to use Spearal, just add this import:

import SpearalIOS

Conclusion and future work

With Spearal iOS, as we said at the beginning of this article, Spearal has reached its first major milestone: support HTML/JavaScript and popular mobile platforms with applications connected to a Java backend. While Spearal is still in an early development stage, we believe that it can be already used in many non-critical software with all the benefits we have exemplified in this article and the previous ones (AngularJS / JAX-RS and Android / Spring MVC).

In no particular order, future work will focus on:

  • Technical documentation,
  • Formal specification of the Spearal serialization format,
  • Support for other platforms (contributions are welcomed),
  • And — of course — improvements and bug fixes.

Feedback would be very appreciated, especially if you plan using or working with us on Spearal. Don’t hesitate to comment on this article or to contact us in the Spearal forum!

Spearal website: http://spearal.io.
Follow us on Twitter: @Spearal.