Статьи

День после JSON: Spearal и мобильные приложения

Цель проекта Spearal — предоставить библиотеку сериализации с открытым исходным кодом, которая просто работает, независимо от сложности ваших структур данных, и в большинстве случаев может использоваться в качестве простой замены JSON. Мы собираемся показать пример с сервером на базе Spring и приложением для Android.

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

Почти все приложения в настоящее время используют JSON в качестве формата сериализации «по умолчанию» для передачи данных между мобильными / веб-клиентами и серверами. Франк демонстрирует в своей статье некоторые общие проблемы с JSON и их решение в Spearal:

  • Поддержка объектных ссылок и круговых графиков
  • Использование фильтров свойств для получения разных уровней детализации
  • Встроенная поддержка JPA ленивых ассоциаций
  • Поддержка дифференциальных обновлений с частичными объектами

Решение этих проблем с помощью JSON возможно за счет большого количества написанного вручную стандартного кода, такого как классы DTO, копирование свойств между объектами (часто с помощью таких инструментов, как Dozer), дополнительных методов или параметров ресурсов сервера …

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

В этой статье я покажу пример Spearal в мобильной среде с REST-сервером Spring MVC, приложением AngularJS и REST-приложением Spring Android. Это Spring-порт примера JAX-RS из статьи Франка, который сам был разветвлен из хорошего примера Java EE 7 / AngularJS Роберто Кортеса.

Клонируй, собери и запусти демо

Выполните следующие команды (вам понадобятся Git и Maven):

git clone https://github.com/spearal-examples/spring-angular.git
cd spring-angular
mvn clean install tomcat7:run

Когда Tomcat запущен, укажите в браузере http: // localhost: 8080 / spring-angular / . Приложение AngularJS довольно просто и обеспечивает базовые операции CRUD с использованием ресурса AngularJS REST / JSON. Если вы уже попробовали javaee7-angular, это точно такое же приложение AngularJS.

Вы можете легко переключиться на сериализацию Spearal, просмотрев альтернативный URL http: // localhost: 8080 / spring-angular / index-spearal.html .

Я не буду вдаваться в подробности о том, как настроить приложение AngularJS для использования Spearal, потому что это уже было описано в статье Франка, но для подведения итогов требуется 3 следующих шага:

  1. Импортируйте среду выполнения traceur и скомпилированную библиотеку JS6 Spearal
  2. Настройте действия ресурса AngularJS с двоичной полезной нагрузкой и кодером / декодером Spearal
  3. Настройте действия ресурса AngularJS с помощью заголовков Spearal Accept / Content-Type

В идеальном мире был бы модуль Spearal / AngularJS, который еще больше упростил бы эту настройку, а пока это немного ручная работа.

Хорошо, но что еще Spearal может сделать для меня?

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

С JSON вы могли бы сделать это, написав разные DTO, например, у нас могли бы быть Person и PersonSmall, где PersonSmall будет иметь только 3 свойства id, name и description. Для адаптивного пользовательского интерфейса, в котором таблица адаптируется к доступному размеру экрана, нам потребуется еще больше версий объекта, таких как PersonSmall, PersonMedium и т. Д. Вам также придется написать разные методы, которые копируют результаты запроса в эти различные объекты или использовать инструменты, такие как Dozer. Это не только утомительно, но и довольно негибко, потому что мы должны заранее продумывать различные варианты использования и соответственно создавать код сервера.

Поддержание этих DTO — это боль, и одна из наших главных целей для Spearal — уничтожить DTO раз и навсегда. Почему бы просто не позволить сериализатору извлечь только подмножество свойств объекта, во многом как «SELECT id, name, description FROM person» в запросе SQL (да, SQL старомоден, но этот пример будет гораздо менее понятным при использовании Функция Map / Reduce). Именно для этого созданы фильтры свойств Spearal, поэтому давайте включим фильтр для таблицы в примере index-spearal.html:

if (method === "query") {
    action.headers[Spearal.PROPERTY_FILTER_HEADER] = Spearal.filterHeader(
        "org.spearal.examples.springangular.data.Person",
        "id", "name", "description"
    );
}

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

Остановите Tomcat, перестройте и перезапустите приложение с помощью этой команды:

mvn clean install tomcat7:run

Обновите страницу index-spearal.html. Надеемся, что все работает, как и раньше, но поскольку URL-адрес свойства довольно длинный, мы значительно сократили размер закодированных данных (что можно проверить с помощью инструментов разработчика Chrome).

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

Важно отметить, что не все свойства были закодированы, поэтому мы получаем только частичные объекты на клиенте. Это означает, что некоторые свойства объектов JavaScript, которые мы получаем из отфильтрованных запросов, не определены (что не является нулевым, но фактически неизвестно). Это определенно то, что мы хотим на клиенте, но что происходит, когда мы отправляем этот частичный объект обратно на сервер?

В Java нет понятия «неопределенное» свойство, поэтому частичные объекты создаются как прокси-серверы Javassist на сервере, и доступ к неопределенному свойству вызовет исключение UndefinedPropertyException. Хотя во многих случаях это не является проблемой, это может помешать нам напрямую использовать эти частичные объекты с JPA EntityManager, например, и привести нас к проблеме DTO, с которой вам постоянно приходится копировать свойства между объектами. К счастью, именно поэтому интеграция Spearal / JPA предоставляет оболочку EntityManager, которая поддерживает объединение частичных объектов. Эта интеграция очень проста в настройке, особенно при использовании среды Spring.

Некоторые пояснения по конфигурации Spring

Интеграция Spearal с Spring состоит из разных частей:

  • Конвертер сообщений, который кодирует / декодирует формат Spearal
  • Перехватчик тела, который извлекает фильтры свойств из заголовков http и применяет их к закодированному ответу
  • Конфигуратор Spearal, позволяющий использовать бины Spring для всех подключаемых элементов в Spearal
  • Полная поддержка JPA, позволяющая правильно кодировать ленивые ассоциации и объединять частичные объекты.

Конфигурация очень проста и может быть выполнена с использованием конфигурации XML или Java. Вот пример конфигурации XML:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:spearal="http://www.spearal.io/config"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
            http://www.springframework.org/schema/beans/spring-beans.xsd
            http://spearal.io/config
            http://spearal.io/public/spring/1.0/spearal-config-1.0.xsd">
	
	<bean id="spearalFactory" class="org.spearal.DefaultSpearalFactory"/>
    
    <spearal:rest/>
    
    <spearal:jpa/>
	
</beans>

И с конфигурацией Java:

@Configuration
@EnableWebMvc
@EnableSpearalRest
public class PersonWebConfig extends WebMvcConfigurerAdapter {
	
    @Bean
    public SpearalFactory spearalFactory() {
        return new DefaultSpearalFactory();
    }
	
    @Bean
    public RestResource restResource() {
        return new RestResource();
    }
}
@Configuration
@EnableTransactionManagement
@EnableSpearalJpa
public class PersonJpaConfig {
	
    @Bean
    public LocalContainerEntityManagerFactoryBean entityManagerFactory() {
        ...
    }	
    ...
}

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

Android-клиент

Теперь, когда у нас есть работающее веб-приложение, давайте создадим нативное приложение для Android. Мы уже используем Spring на сервере, так почему бы не использовать его и на клиенте. Клиент Spring Android REST использует ту же низкоуровневую архитектуру, что и Spring MVC REST, поэтому мы можем повторно использовать библиотеку Spearal / Spring для нашего приложения для Android.

Для запуска примера вам понадобится Android Studio . В меню VCS / Checkout из Version Control / Git выберите https://github.com/spearal-examples/spearal-example-android.git, чтобы клонировать пример в Android Studio.

По умолчанию приложение подключается к примеру серверного приложения, размещенного по адресу https://examples-spearal.rhcloud.com/spring-angular/,  поэтому вам не нужно иметь собственный работающий сервер.

Само приложение довольно простое и было создано с использованием шаблона мастер / подробностей, предложенного Android Studio при создании нового проекта.

Давайте просто посмотрим, что мы изменили, чтобы использовать ресурсы Spearal REST вместо фиктивных локальных данных.

Первым шагом является определение модели данных клиента, которая состоит из двух классов Person и PaginatedListWrapper, соответствующих тому, что предоставляет сервер. Вот, например, класс Person:

public class Person implements Serializable {

    private static final long serialVersionUID = 1L;
	
    private Long id;
    private String name;
    private String description;
    private String imageUrl;

    public Long getId() {
        return id;
    }
    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }

    public String getDescription() {
        return description;
    }
    public void setDescription(String description) {
        this.description = description;
    }

    public String getImageUrl() {
        return imageUrl;
    }
    public void setImageUrl(String link) {
        this.imageUrl = link;
    }
}

Это тот же компонент, что и на сервере, только удаленный из аннотаций JPA.

Теперь, когда у нас есть модель, мы должны иметь возможность вызывать службу REST. Чтобы реализовать это, мы собираемся вызвать методы Spring RestTemplate из Android AsyncTask, чтобы избежать блокировки пользовательского интерфейса во время удаленного вызова. Мы создаем простой базовый класс задач, который инкапсулирует общую конфигурацию и использование RestTemplate:

public abstract class AbstractRestAsyncTask<Params, Progress, Result> extends AsyncTask {

    private final RestTemplate restTemplate;
    protected boolean success = false;

    private static String url(String path) {
        return PersonListActivity.baseUrl + path;
    }

    protected AbstractRestAsyncTask() {
        restTemplate = new RestTemplate();
        restTemplate.getMessageConverters().add(new SpearalMessageConverter(SpearalFactoryHolder.getInstance()));
    }

    @Override
    protected Result doInBackground(Params... params) {
        try {
            Result result = doRestCall(params);
            success = true;
            return result;
        }
        catch (Exception e) {
            Log.e(getClass().getSimpleName(), e.getMessage(), e);
        }
        return null;
    }

    @Override
    protected void onPostExecute(final Result result) {
        if (success)
            onRestSuccess(result);
    }

    protected final <R> R getForObject(String path, Class<R> resultClass, Object... params) {
        return restTemplate.getForObject(url(path), resultClass, params);
    }

    protected final <R> R getFiltered(String path, Class<R> resultClass, SpearalPropertyFilterBuilder filter, Object... params) {
        SpearalEntity<Params> filterEntity = new SpearalEntity<Params>(SpearalFactoryHolder.getInstance(), null, null, filter);
        ResponseEntity<R> responseEntity = restTemplate.exchange(url(path), HttpMethod.GET, filterEntity, resultClass, params);
        return responseEntity.getBody();
    }

    protected final Result postForObject(String path, Class<Result> resultClass, Params object) {
        return restTemplate.postForObject(url(path), object, resultClass);
    }

    protected final void delete(String path, Object... params) {
        restTemplate.delete(url(path), params);
    }

    protected abstract Result doRestCall(Params... params);

    protected abstract void onRestSuccess(Result result);
}

Задача сначала инициализирует RestTemplate и настраивает SpearalMessageConverter. Этот конвертер сможет кодировать и декодировать формат Spearal и устанавливать правильные заголовки http Accept / Content-Type для полезной нагрузки сообщения.

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

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

public class SpearalFactoryHolder {
	
    private static SpearalFactory spearalFactory = null;
	
    public static SpearalFactory getInstance() {
        if (spearalFactory != null)
            return spearalFactory;
		
        spearalFactory = new DefaultSpearalFactory();
        spearalFactory.getContext().configure(new PackageTranslatorAliasStrategy(
            "org.spearal.examples.android", "org.spearal.samples.springangular"));
        return spearalFactory;
    }
    ...
}

Это также хорошее место для настройки SpearalFactory. Здесь мы определяем конкретную стратегию псевдонимов для преобразования имен пакетов модели сервера в имена пакетов модели клиента. Используемая здесь PackageTranslatorAliasStrategy подразумевает, что имена классов и структуры пакетов в доменной модели одинаковы для клиента и сервера, но обычно это приемлемый компромисс, и в любом случае мы можем определить любой тип стратегии наложения имен для более сложных требований.

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

Сначала фрагмент списка: мы выполняем задачу загрузки в onStart (), чтобы извлечь данные для отображения в списке:

@Override
public void onStart() {
    super.onStart();

    new LoadPersonsTask().execute();
}

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

@Override
protected List<Person> doRestCall(Void... params) {
    SpearalPropertyFilterBuilder filter = mCallbacks.isTwoPane()
       ? SpearalPropertyFilterBuilder.of(Person.class, "name", "description")
       : SpearalPropertyFilterBuilder.of(Person.class, "name");
   return getFiltered("/persons?pageSize=100", PaginatedListWrapper.class, filter).getList();
}

В обратном вызове успеха мы затем настраиваем адаптер списка для отображения элементов в таблице и отображения одного или двух столбцов:

@Override
protected void onRestSuccess(final List<Person> persons) {
    if (mCallbacks.isTwoPane()) {
        setListAdapter(new ArrayAdapter<Person>(getActivity(),
            R.layout.list_item_large, android.R.id.text1, persons) {
            @Override
            public View getView(int position, View convertView, ViewGroup parent) {
                View view = super.getView(position, convertView, parent);
               ((TextView)view.findViewById(android.R.id.text1)).setText(persons.get(position).getName());
             ((TextView)view.findViewById(android.R.id.text2)).setText(persons.get(position).getDescription());
               return view;
            }
        });

        setActivatedPosition(mActivatedPosition);
    }
    else {
        setListAdapter(new ArrayAdapter<Person>(getActivity(),
            android.R.layout.simple_list_item_activated_1, android.R.id.text1, persons) {
            @Override
             public View getView(int position, View convertView, ViewGroup parent) {
                 View view = super.getView(position, convertView, parent);
                 ((TextView)view.findViewById(android.R.id.text1)).setText(persons.get(position).getName());
                 return view;
             }
         });
    }

    getListAdapter().registerDataSetObserver(new DataSetObserver() {
        @Override
        public void onChanged() {
            new LoadPersonsTask().execute();
        }
    });
}

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

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

На следующих снимках экрана показано, как приложение отображается в режимах телефона и планшета:


В заключение, существует очень небольшая разница в коде доступа к ресурсам между приложением Android, использующим JSON, и приложением Android, использующим Spearal. Заменить JSON на Spearal в существующем или новом приложении очень просто, и вы получите массу дополнительных возможностей: ссылки на объекты, круговые диаграммы, лучшую сериализацию числовых типов, частичные объекты, поддержку JPA …

Работа с частичными объектами

Как объяснялось ранее, эффект от использования фильтров свойств на стороне сервера заключается в том, что клиент получает только частичные объекты. Мы еще не нашли разумного способа реализации прокси классов в Android (хотя DexMaker выглядит хорошим вариантом), поэтому Spearal возвращается к созданию простых объектов и просто допускает значения по умолчанию (обычно нулевые) для неопределенных свойств.

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

Это может быть, например, случай, если мы получим неопределенную ленивую ассоциацию от сущности JPA, которая будет переведена как ноль, а затем отправлена ​​обратно на сервер как ноль, так как испортит базу данных, если мы объединим ее как есть.

Чтобы избежать этой проблемы, мы будем использовать здесь опцию, предоставляемую Spearal, которая заключается в определении модели данных в качестве интерфейса и позволяет Spearal использовать динамические прокси Java для реализации реального объекта данных. Таким образом, мы преобразуем класс Person в интерфейс, содержащий только методы получения и установки:

public interface Person extends Serializable {

    public Long getId();
    public void setId(Long id);

    public String getName();
    public void setName(String name);

    public String getDescription();
    public void setDescription(String description);

    public String getImageUrl();
    public void setImageUrl(String imageUrl);
}

Очевидно, что мы больше не можем делать new Person (), поэтому мы должны использовать небольшой вспомогательный метод в SpearalFactory:

spearalFactory.getContext().newInstance(clazz)

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

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

@ManyToOne(fetch=FetchType.LAZY)
private Person bestFriend;

public Person getBestFriend() {
   return bestFriend;
}
public void setBestFriend(Person bestFriend) {
   this.bestFriend = bestFriend;
}

@ManyToOne
private Person worstEnemy;

public Person getWorstEnemy() {
    return worstEnemy;
}
public void setWorstEnemy(Person worstEnemy) {
   this.worstEnemy = worstEnemy;
}

Эти изменения уже развернуты в расположении по адресу https://examples-spearal.rhcloud.com/spring-angular-v2/index-spearal.html, где вы должны увидеть дополнительный столбец Worst Enemy (не используйте версию JSON на странице /index.html, он не работает с ленивыми ассоциациями).

Чтобы указать приложению Android этот URL-адрес, просто используйте значок стрелки в правом верхнем углу перед значком «плюс» и выберите « Пример v2» .

Вы можете использовать одно и то же «устаревшее» приложение Android (не обновленное до новой модели), причем старая версия сервера и новая без каких-либо изменений, при этом наличие или отсутствие новых свойств в клиентской модели должным образом обрабатывается серверная Spearal десериализация и интеграция JPA.

Примечание: если вы хотите, вы можете, конечно, добавить свой собственный URL локального сервера (с вашим реальным IP-адресом, устройство не может получить доступ к localhost) в меню.

Дифференциальные обновления

Наличие этих частичных объектов на клиенте имеет еще одно полезное применение: клиент может отправлять только некоторые свойства, а не полный объект. Например, мы можем отправить только те свойства, которые изменил пользователь. Мы можем видеть, как это работает в методе applyTextValue:

applyTextValue(person, "name", getView(), R.id.form_name);
...
private static void applyTextValue(Object object, String propertyName, View rootView, int textViewId) {
    try {
        Method getter = object.getClass().getMethod("get" + propertyName.substring(0, 1).toUpperCase() + propertyName.substring(1));
        Object currentValue = getter.invoke(object);
        String value = ((TextView)rootView.findViewById(textViewId)).getText().toString();
        if (value.equals(currentValue))
            Spearal.undefine(object, propertyName);
        else {
            Method setter = object.getClass().getMethod("set" + propertyName.substring(0, 1).toUpperCase() + propertyName.substring(1), String.class);
            setter.invoke(object, value);
        }
    }
    catch (Exception e) {
        // Should probably do something
    }
}

В основном мы получаем существующее значение объекта и сравниваем его со значением TextView. Если он изменился, мы обновляем объект с помощью метода setter, а если нет, мы «отменяем определение» значения частичного объекта, чтобы он не был отправлен в сеть. В конце, то, что будет отправлено по проводам, это просто маленький объект, содержащий только измененные свойства. С другой стороны, сервер слияния сможет обработать это правильно.

Вывод

Мы видели, как Spearal может сделать наши ресурсы REST более дружественными к данным. Мы также видели, как мы можем устранить потребность в DTO во многих обычных случаях.

Spearal в основном продуман и разработан для двух вариантов использования:

  • Контролируемые среды, в которых вы создаете как сервер, так и клиенты
  •  

  • Внешние API, где вы хотите предоставить своим пользователям строго типизированную модель данных и библиотеку, а не только набор ресурсов REST / JSON

Конечно, это не означает, что вы не можете использовать его для других случаев!

Следующим важным шагом является реализация iOS, первый релиз которой должен быть готов в ближайшее время.

Это только начало проекта, поэтому обратная связь очень ценится, и, конечно, приветствуются сообщения и сообщения об ошибках.

Следите за нами в Твиттере: @Spearal и присоединяйтесь к нашему форуму пользователей.