среда, 1 сентября 2010 г.

Взаимодействие c OSGi - Проблемы загрузки классов


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

Описание проблемы


Зачастую в фреймворках требуется инстанцировать класс по его имени, например в случае разработки ORM-системы (примерами таких систем могут служить Hibernate, EclipseLink, iBatis и т.д.) необходимо обеспечить соответствие между классом и названием таблицы, на которую он мэпится. Одним из способов указания такого соответствия является использование XML-разметки подобной следующей:

<or-mapping>

   <class name="org.example.domain.Event" table="EVENTS">

      <id name="id" column="EVENT_ID"/>

   </class>

</or-mapping>


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

Использование Class.forName() (первый неправильный способ)

Первым решением, которое приходит на ум, является использование конструкции Class.forName. Данная конструкция загружает класс, используя класслоадер, которым загружен тот класс, из которого она вызывается. В случае использования такой конструкции в среде OSGi можно столкнуться с проблемой - класс, загружаемый по имени, определен в другом бандле и, соответственно, - находится в другом ClassPath и недоступен текущему класслоадеру (а зачастую так и бывает, например, ORM-фреймворк представляет собою один бандл, а сущности регистрируются в других бандлах). Конечно, есть способы расширения ClassPath бандла, но их не всегда можно применять, да и к тому же, корректно спроектированный фреймворк, учитывающий проблему загрузки классов, может обойтись и без фреймворко-специфичных решений.

Рассмотрим пример. Пусть у нас есть ORM-сессия - класс Session, который для простоты будет синглетным, допускающая регистрацию в себе соответствий имени таблицы и класса:

package name.samolisov.classloader.demo.session.classforname;



import java.util.HashMap;

import java.util.Map;



public class Session {

   

    private static Session _session = new Session();

   

    private Map<String, Class<?>> tableToClass = new HashMap<String, Class<?>>();

   

    private Session() {}

   

    public static Session getInstance() {

        return _session;

    }

   

    public void addMapping(String clazzName, String tableName) {

        try

        {

            Class<?> clazz = Class.forName(clazzName);         

            if (clazz != null)

            {

                tableToClass.put(tableName, clazz);

                System.out.println("class " + clazz.getName() + " mapped to table " + tableName);

            }

        }

        catch (Exception e)

        {

            e.printStackTrace();

        }

    }

}

 


Данная сессия определена в одном бандле, назовем его name.samolisov.classloader.demo.session.classforname. В другом бандле - name.samolisov.classloader.demo.domain.classforname - определим сущность MyEntity, которая будет мэпиться на таблицу ENTITY в активаторе бандла:

    /*

     * (non-Javadoc)

     * @see org.osgi.framework.BundleActivator#start(org.osgi.framework.BundleContext)

     */


    public void start(BundleContext bundleContext) throws Exception {

        Session.getInstance().addMapping("name.samolisov.classloader.demo.domain.classforname.MyEntity", "ENTITY");

    }

 


При старте бандлов name.samolisov.classloader.demo.session.classforname и name.samolisov.classloader.demo.domain.classforname будет сгенерирована исключительная ситуация Class Not Found Exception: класс Session не может загрузить класс MyEntity, используя свой ClassLoader.

Использование Thread Context Class Loader (второй неправильный способ)
Существует другой широкораспространенный способ загрузить класс по его имени - использовать класслоадер контекста потока, в котором исполняется код - Thread Context Class Loader (TCCL). Именно данный способ используется во многих библиотеках, особенно из мира JavaEE поскольку данная спецификация накладывает множество ограничений: приложения исполняются в изолированных песочницах, им непозволено напрямую создавать потоки и нельзя расшаривать поток между приложениями, что позволяет гарантировать корректность TCCL текущему коду.

Спецификация OSGi накладывает гораздо меньше ограничений, в частности в бандлах можно создавать свои потоки, открывать сокеты и вызывать API, предоставляемые другими бандлами. Таким образом, OSGi-среда не гарантирует корректность TCCL текущему коду, более того, TCCL вообще не описан в спецификации OSGi и может быть равен null.

Пример. Изменим класс Session, заменив строку

Class<?> clazz = Class.forName(clazzName);


на

ClassLoader tccl = Thread.currentThread().getContextClassLoader();

Class<?> clazz = Class.forName(clazzName, true, tccl);


В результате после запуска наших бандов все равно будет выбрашено исключение Class Not Found Exception: класс Session не может загрузить класс MyEntity, используя свой класслоадер.

Решения



Прежде чем предлагать решения, следует оговориться. Если ваш фреймворк корректно работает в среде JavaSE/JavaEE, используя Class.forName или TCCL, то нужно продолжить использовать данные подходы. Однако, для совместимости с OSGi можно добавить ряд других способов загружать классы.

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

1. Использование фабрик

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

Рассмотрим пример. Создадим бандл name.samolisov.classloader.demo.session.factory, содержащий интерфейс фабрики объектов:

package name.samolisov.classloader.demo.session.factory;



public interface IObjectFactory<T> {

   

    public T createInstance(String tableName);

}

 


и сессию, которая использует данный интерфейс для манипуляции объектами:

package name.samolisov.classloader.demo.session.factory;



import java.util.HashMap;

import java.util.Map;



public class Session {

   

    private static Session _session = new Session();

   

    private Map<String, IObjectFactory<?>> tableToFactory = new HashMap<String, IObjectFactory<?>>();

   

    private Session() {}

   

    public static Session getInstance() {

        return _session;

    }

   

    public <T> void addMapping(IObjectFactory<T> factory, String tableName) {

        try

        {          

            tableToFactory.put(tableName, factory);

            System.out.println("factory " + factory.getClass().getName() + " mapped to table " + tableName);           

        }

        catch (Exception e)

        {

            e.printStackTrace();

        }

    }

   

    @SuppressWarnings("unchecked")

    public <T> T getObject(String tableName) {

        if (!tableToFactory.containsKey(tableName))

            throw new RuntimeException("table " + tableName + " not mapped");

       

        return (T) tableToFactory.get(tableName).createInstance(tableName);

    }

}

 


Используется данная сессия в два этапа: на первом регистрируется соответствие "таблица - фабрика", а затем при необходимости с помощью фабрики создается объект нужного класса:

/*

     * (non-Javadoc)

     * @see org.osgi.framework.BundleActivator#start(org.osgi.framework.BundleContext)

     */


    public void start(BundleContext bundleContext) throws Exception {

        Session.getInstance().addMapping(new MyEntityFactory(), "ENTITY");

        System.out.println(Session.getInstance().getObject("ENTITY"));

    }


При запуске бандлов будет выведено на консоль:


osgi> factory name.samolisov.classloader.demo.domain.factory.MyEntityFactory mapped to table ENTITY
name.samolisov.classloader.demo.domain.factory.MyEntity@be76c7


2. Регистрация непосредственно классов

Суть решения предельно проста - класс загружается (с использованием Class.forName() или TCCL) в том бандле, в котором и определен, а затем передается в сессию. Данное решение кажется тривиальным, однако оно требует получения на стороне бандла, регистрирующего сущности, информации о том, какой именно класс нужно загрузить. Т.е., если мы, например, реализуем ORM-фреймворк, то нам нужно будет сделать так, чтобы бандл, регистрирующий мэпинги, мог извлекать информацию из мэпинга, будь то XML-файлы или аннотации, что не всегда уместно.

Пример. Метод Session#addMapping будет выглядеть следующим образом:

    public <T> void addMapping(Class<?> clazz, String tableName) {

        try

        {          

            tableToClass.put(tableName, clazz);

            System.out.println("class " + clazz.getName() + " mapped to table " + tableName);          

        }

        catch (Exception e)

        {

            e.printStackTrace();

        }

    }


Регистрация мэпинга "таблица - класс" осуществляется так:

    /*

     * (non-Javadoc)

     * @see org.osgi.framework.BundleActivator#start(org.osgi.framework.BundleContext)

     */


    public void start(BundleContext bundleContext) throws Exception {

        Session.getInstance().addMapping(MyEntity.class, "ENTITY");

    }


3. Передача класслоадера в место загрузки

Третий способ обладает наибольшей гибкостью. Логика фреймворка не меняется, а лишь расширяется - к существующим методам, в которых классы загружаются с помощью Class.forName() или TCCL следует добавить еще один, принимающий внешний класслоадер.

Пример. Метод Session#addMapping будет выглядеть следующим образом:

    public <T> void addMapping(String className, String tableName, ClassLoader classLoader) {

        try

        {          

            Class<?> clazz = Class.forName(className, true, classLoader);

            if (clazz != null)

            {

                tableToClass.put(tableName, clazz);

                System.out.println("class " + clazz.getName() + " mapped to table " + tableName);

            }

        }

        catch (Exception e)

        {

            e.printStackTrace();

        }

    }


Регистрация класса осуществляется в данном случае так:

/*

     * (non-Javadoc)

     * @see org.osgi.framework.BundleActivator#start(org.osgi.framework.BundleContext)

     */


    public void start(BundleContext bundleContext) throws Exception {

        Session.getInstance().addMapping("name.samolisov.classloader.demo.domain.customclassloader.MyEntity",

                "ENTITY", MyEntity.class.getClassLoader());

    }


Выводы


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

Статья написана по мотивам OSGi Readiness — Loading Classes.

Скачать примеры к статье (исходники и конфигурации запуска. Rar, 64 Кб).

Понравилось сообщение - подпишитесь на блог или читайте меня в twitter

4 комментария:

i_home комментирует...

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

Pavel Kastornii комментирует...

Добрый день. Натолкнулся на вашу статью когда пытался найти решение своей проблемы. Понимаю, что не совсем корректно обращаться к вам за помощью, но обстоятельства заставляют, так как уже неделю не могу решить ее даже с привлечением российского и иностранного сообщества. Обращаюсь к вам как к эксперту по данной проблеметике. Если у вас есть время и возможность не могли бы вы взглянуть на эту задачу. Ее можно посмотреть тут http://stackoverflow.com/questions/23174582/rmi-classcastexception-in-osgi-client-accessing-ejb-from-javaee-server или на русском языке тут http://javatalks.ru/topics/41767 . Буду очень вам обязан.

Pavel Samolisov комментирует...

Павел, не знаю, актуальная ли еще просьба. Почитал обсуждение на javatalks, но как понял проблема еще не решена. Если не трудно, пришлите исходники вашего проекта на samolisov собака гмыло com.

Pavel Kastornii комментирует...

Павел, спасибо за участие. Проблема была решена. На javatalks я ответ еще не разместил, а на stackoverflow указал.

Отправить комментарий

Любой Ваш комментарий важен для меня, однако, помните, что действует предмодерация. Давайте уважать друг друга!