Статьи

Dozer POJO Mapper в ОСГИ

бульдозер

Dozer — это популярная библиотека сопоставления бобов. Эта функциональность обычно требуется при переключении между моделями домена, например, чтобы изолировать внутреннее представление модели домена от модели, опубликованной клиентам через REST API.

Dozer использует загрузчик классов для загрузки как минимум следующих ресурсов:

  • Отображение XML-файлов определения. Файл определения сопоставления содержит пользовательские правила сопоставления.
  • Классы, основанные на использовании полного имени класса Class.forName().

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

Демонстрация проблемы

В среде OSGi классы каждого пакета загружаются загрузчиком классов пакета. Набор классов, доступных для пакета, включает в себя классы, упакованные в пакет, и классы, доступные через директиву Import-Package.

Чтобы использовать Dozer в комплекте, необходимо добавить следующую директиву в

MANIFEST.MF:Import-Package: org.dozer;version="[5.5,6)"

Файл определения отображения dozer.xmlможет быть помещен в пакет, например, в каталог верхнего уровня пакета.

В пакете DozerBeanMapperнеобходимо создать экземпляр и предоставить один или несколько путей к файлам определения сопоставления:

DozerBeanMapper dozerBeanMapper = new DozerBeanMapper();
dozerBeanMapper.setMappingFiles(
new ArrayList<String>(Arrays.asList(new String[]{"dozer.xml"})));

Затем преобразователь может быть использован для отображения класса , com.domain.a.Aчтобы com.domain.b.Bсо следующим кодом:

A a = new A();
B b = dozerBeanMapper.map(a, B.class);

К сожалению, это не удастся. После вызова DozerBeanMapper.map()метод попытается инициализировать сопоставления путем загрузки файлов определений сопоставления и завершится неудачно сorg.dozer.MappingException: Unable to locate dozer mapping file [dozer.xml] in the classpath.

Установка загрузчика классов потока (TCCL) на загрузчик классов пакета и перенос вызова с помощью TCCL исправит эту конкретную проблему, создавая рецепт для непереносимой хрупкой реализации. Например, если Dozer должен был быть обернут классом фасада, тогда фасад всегда должен был бы быть инициализирован, в то время как обернут TCCL, иначе внедренный отказал DozerBeanMapperбы.

// Provided that 'this' is a bundle class, getClass().getClassLoader()
// references the bundle class loader

ClassLoader threadCl = Thread.currentThread().getContextClassLoader();
Thread.currentThread().setContextClassLoader(getClass().getClassLoader());
try {
    B b = dozerBeanMapper.map(a, B.class);
} finally {
    Thread.currentThread().setContextClassLoader(threadCl);
}

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

Даже если файл сопоставления размещен в известном месте в файловой системе, а не встроен в пакет, Dozer все равно не сможет выполнить анализ файла сопоставления, поскольку попытается разрешить указанные классы, используя свой собственный org.dozer.util.DefaultClassLoader.

Обходной путь

К счастью, Dozer позволяет заменить загрузчик классов по умолчанию на собственный:

org.dozer.util.DozerClassLoader classLoader=...;
BeanContainer.getInstance().setClassLoader(classLoader);

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

  • Связки запускаются последовательно.
  • Dozer полностью настроен на этапе запуска пакета.

Первое условие предотвратит переопределение свойства загрузчика классов BeanContainer до запуска пакета.

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

Единственными недостающими частями являются пользовательский загрузчик класса Dozer и инициализация Dozer в комплекте.

Загрузчик классов Dozer, который делегирует загрузчик классов комплекта, показан ниже:

public class OsgiDozerClassLoader implements DozerClassLoader {
    private BundleContext context;

    @Override
    public Class<?> loadClass(String className) {
        try {
            return context.getBundle().loadClass(className);
        } catch (ClassNotFoundException e) {
            return null;
        }
    }

    @Override
    public URL loadResource(String uri) {
        URL url;

        url = context.getBundle().getResource(uri);

        if (url == null) {
            url = DozerClassLoader.class.getClassLoader().getResource(uri);
        }

        return url;
    }

    public void setContext(BundleContext context) {
        this.context = context;
    }
}

Затем во время запуска пакета — например, в готовом конструкторе bean-проекта или init-method— настройте пользовательский загрузчик классов, файлы отображения и принудительный анализ файлов отображения:

public DozerBeanMapper getMapper(List<String> files) {
    BeanContainer.getInstance().setClassLoader(classLoader);

    DozerBeanMapper mapper = new DozerBeanMapper();
    mapper.setMappingFiles(files);

    // Force loading of the dozer.xml now instead of loading it
    // upon the first mapping call
    mapper.getMappingMetadata();

    return mapper;
}

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

Подтверждения

Эта техника основана на аналогичном подходе, используемом в компоненте верблюдов-бульдозеров.