Статьи

Использование org.openide.util.Lookup на Android

BlueBill Mobile для Android почти готов к прайм-тайм — кроме качественных вещей. В качестве исключения из моих общих практик я не использовал TDD, так как изучение Android и настройка моего личного стиля кодирования — это уже сложная задача (мне также еще предстоит научиться запускать специальные тесты для Android, которые, если я хорошо понимаю, могут исполняться на устройстве).





 

Сегодня я провел фундаментальный раунд рефакторингов, пытаясь найти лучшее решение для проблемы, описанной в конкретном вопросе Android-часто задаваемых вопросов: «Как передать данные между действиями / службами в одном приложении?» , Для наиболее общего случая (совместно использовать любой тип Java-объекта) в Android нет специальной инфраструктуры. Официальный ответ на часто задаваемые вопросы:

Использование статического Singleton имеет свои преимущества, например, вы можете ссылаться на них, не приводя getApplication () к классу приложения, или не пытаясь повесить интерфейс на все ваши подклассы Application, чтобы ваши различные модули могли обращаться к ним. этот интерфейс вместо.

Предопределенный класс Application, который можно разделить на подклассы и предоставить собственный контекст для моего приложения, привел бы к зависимости Android практически от каждого класса моего проекта — не очень хорошо. С другой стороны, у синглтона, предназначенного в строгом смысле, есть много известных проблем, как введение ненужных связей и затруднение написания и поддержки тестов. «Хороший» объект контекста — это объект, который можно смоделировать и предоставлять при необходимости смоделированные услуги. Это не сложно сделать, но … подожди минутку. Разве org.openide.util.Lookup не совсем такой материал?

Это конечно! И так как это один из моих общих инструментов, он прекрасно подошел бы к остальной части моего кода, который уже использует его. Кроме того, у меня есть крошечная, но функциональная библиотека внедрения зависимостей, основанная на аннотациях JSR-330, которую, возможно, можно было бы использовать и на Android (чтобы сравнить с предупреждениями о производительности, которые мне давали при использовании отражения).

В конце концов, есть множество причин для использования Lookup в blueBill Mobile для Android, и на самом деле я могу с гордостью сказать, что использую его уже несколько часов.

К сожалению, все было непросто, так как Lookup не работает «из коробки»; также при использовании его с минимальными возможностями (в приложении Android у вас обычно нет модулей, которые приходят и уходят, как в обычном настольном приложении, поэтому мне в принципе не нужны — по крайней мере на данный момент — его динамические возможности).

Проблемы с загрузчиками классов Android и байт-кодом .dex. Я предположил, что понял все последствия, но мне нужно вернуться к чертежной доске, так как все мои попытки заканчивались в ClassNotFoundExceptions или ClassCastExceptions. Более того, звучит так, что невозможно загрузить ресурсы, встроенные в приложение, с помощью ClassLoader.getResources () — средства, на которое полагается Lookup для получения информации, хранящейся в META-INF / services. И последнее, но не менее важное: я хотел бы также иметь некоторые стандартные ресурсы Android, такие как Context, AssetManager и SharedPreferences, которые будут доступны в поиске по умолчанию. Эти классы не могут быть найдены с помощью обычного поиска, но должны быть каким-то образом принудительно введены в него.

Итак, мое решение на сегодня является первым для подкласса Lookup:

package it.tidalwave.bluebill.mobile.android;

import android.content.Context;
import javax.annotation.CheckForNull;
import javax.annotation.Nonnull;
import org.openide.util.Lookup;
import org.openide.util.lookup.Lookups;
import android.preference.PreferenceManager;
import it.tidalwave.bluebill.observation.ObservationManager;
import it.tidalwave.mobile.LazyLookup;
import it.tidalwave.mobile.FileSystem;
import it.tidalwave.mobile.LocationFinder;
import it.tidalwave.mobile.android.AndroidFileSystem;
import it.tidalwave.mobile.android.AndroidLocationFinder;
import it.tidalwave.bluebill.mobile.observation.ObservationClipboard;
import it.tidalwave.bluebill.mobile.observation.BlueBillObservationManager;
import it.tidalwave.bluebill.mobile.observation.DefaultObservationPreferences;
import it.tidalwave.bluebill.mobile.observation.ObservationPreferences;
import it.tidalwave.bluebill.mobile.taxonomy.TaxonomyPreferences;
import it.tidalwave.bluebill.mobile.android.location.AndroidLocationPreferences;
import it.tidalwave.bluebill.mobile.location.LocationPreferences;
import it.tidalwave.bluebill.mobile.android.taxonomy.AndroidTaxonomyPreferences;


public class BlueBillLookup extends Lookup
{
private final LazyLookup lookup = new LazyLookup();

@CheckForNull
private Lookup extraLookup = Lookup.EMPTY;

public BlueBillLookup()
{
lookup.register(FileSystem.class,
AndroidFileSystem.class);
lookup.register(LocationFinder.class,
AndroidLocationFinder.class);
lookup.register(LocationPreferences.class,
AndroidLocationPreferences.class);
lookup.register(TaxonomyPreferences.class,
AndroidTaxonomyPreferences.class);
lookup.register(ObservationClipboard.class,
ObservationClipboard.class);
lookup.register(ObservationManager.class,
BlueBillObservationManager.class);
lookup.register(ObservationPreferences.class,
DefaultObservationPreferences.class);
}

@CheckForNull
public <T> T lookup (final @Nonnull Class<T> clazz)
{
final T r = extraLookup.lookup(clazz);
return (r != null) ? r : lookup.lookup(clazz);
}

@Override @Nonnull
public <T> Result<T> lookup (final @Nonnull Template<T> template)
{
final Result<T> r = extraLookup.lookup(template);
return (r != null) ? r : lookup.lookup(template);
}

public void setContext (final @Nonnull Context context)
{
extraLookup = Lookups.fixed(context,
context.getAssets(),
PreferenceManager.getDefaultSharedPreferences(context));
}
}

Как видите, реализация — это прокси, который ищет объекты в двух делегатах. Первый инициализируется в конструкторе и явно заполняется всеми моими классами обслуживания; последний создается во второй раз, после вызова setContext (). Этот подход необходим, поскольку очевидно, что нет статического метода фабрики для получения контекста, поэтому он должен быть предоставлен после того, как BlueBillLookup был создан. LazyLookup — это простая реализация, которая регистрирует класс и откладывает фактическое создание экземпляра требуемой службы как можно позже — это согласуется со стандартным поведением реализаций Lookup.

Стандартный класс Lookup имеет ряд способов для программиста предоставить свою собственную реализацию за кулисами Lookup.getDefault (): имя класса в системном свойстве или класс Lookup или Lookup.Provider, зарегистрированный в META-INF / Сервисы. К сожалению, как я уже говорил, я не смог заставить их работать из-за проблем с загрузчиком классов. Я думаю, что эти проблемы могут быть решены (в конце концов, OSGi может работать на Android, и он в значительной степени основан на загрузчике классов), но нужно учиться больше вещей. Итак, я прибег к старой уловке разработчиков NetBeans, которая опирается на рефлексию:

package it.tidalwave.bluebill.mobile.android;

import java.lang.reflect.Field;
import org.openide.util.Lookup;
import android.app.Application;

public class BlueBillApplication extends Application
{
@Override
public void onCreate()
{
super.onCreate();

try
{
final Field defaultLookup = Lookup.class.getDeclaredField("defaultLookup");
defaultLookup.setAccessible(true);
final BlueBillLookup blueBillLookup = new BlueBillLookup();
blueBillLookup.setContext(this);
defaultLookup.set(null, blueBillLookup);
}
catch (Exception e)
{
throw new RuntimeException(e);
}
}
}

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

Вывод

Что впереди? Мне нужно лучше понять часть о том, как Android управляет (не) ресурсами, встроенными в файлы JAR. Если это неосуществимое ограничение, одной из альтернатив может быть использование плагина Maven Shade, способного преобразовывать и перемещать файлы в банке; он может скопировать содержимое каталога META-INF / services / * в соответствующий ресурс в стиле Android в разделе «res» или «assets». Таким же образом я мог бы заменить код внутри org-openide-util.jar, который сканирует META-INF / services, на правильную альтернативную реализацию.

Кроме того, я также столкнулся с некоторыми несовместимостями между Lookup и средой выполнения Android. Например, внутренний класс реализации должен был быть исправлен, поскольку он вызвал исключение, которое не происходит с Sun JDK; и существует внутренняя зависимость от простого класса Swing, связанного с событиями, который недоступен в Android. Плагин Maven Shade, способный заменить отдельные части содержимого JAR и даже переименовать все ссылки на данный класс, оказался вполне подходящим для решения подобных проблем, поскольку он позволил мне предоставить исправления для сломанного класса. и отсутствующий класс без необходимости раскошелиться на оригинальные источники. Я не буду вдаваться в подробности об этом материале, так как в конце эти исправления не нужны в моем текущем транке — возможно, я вернусь к этой теме в новом посте.