суббота, 30 января 2010 г.

Используем Hibernate в OSGi-среде


В предыдущем посте суровый челябинский программист обещал рассказать про особенности использования Hibernate в OSGi-среде. Точнее в среде Equinox. Чтож, приступим.

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

1. Нам нужен отдельный бандл, который инкапсулирует в себе hibernate, необходимые для его работы библиотеки и какие-то средства инициализации.

2. Все дополнительные средства (c3p0, jdbc-драйвера, oscache, hibernate.cfg.xml) выносим в отдельные фрагментные бандлы. Это позволит заменять используемые средства при необходимости (например, вместо oscache использовать ehcache).

3. Хибернейтовская сессия должна быть доступна любому бандлу, импортирующему бандл с hibernate, поэтому мы ее выносим в сервис.

4. Каждый бандл, использующий хибернейт, должен иметь возможность зарегистрировать мэпинги для своих сущностей. Для этого бандлы будут выставлять свои собственные реализации соответствующего сервиса.

5. Должна быть реализована опциональная поддержка мэпинга на основе hibernate-annotations, т.е. должен быть реализован отдельный бандл, который инкапсулирует hibernate-annotations и ejb3-persistence, а также позволяет регистрировать аннотированные классы.

6. Все используемые сервисы определить как декларативные, что позволит прозрачно разрешить проблему запуска бандлов в нужном порядке.

С требованиями разобрались, приступим к реализации. Создать бандл, инкапсулирующий хибернейт довольно просто - достаточно создать в бандле каталог lib и поместить туда следующие библиотеки:

- cglib-nodep.jar
- dom4j.jar
- hibernate3.jar
- jta.jar

Внимание: я описываю ситуацию для Hibernate 3.2.0 GA. Насколько я знаю, в новых версиях Hibernate есть изменения, в частности, касающиеся логера. Впрочем, эти изменения непринципиальны


Для того, чтобы бандл мог использовать классы, определенные в этих библиотеках их необходимо добавить в параметр Bundle-ClassPath манифеста:

  1. Bundle-ClassPath: .,

  2.  lib/cglib-nodep-2.1_3.jar,

  3.  lib/dom4j-1.6.1.jar,

  4.  lib/hibernate3.jar,

  5.  lib/jta-1.1.jar

  6.  



Помимо данных библиотек Hibernate использует логер org.apache.commons.logging и библиотеку org.apache.commons.collections, которые входят в проект Orbit, поэтому мы не будем добавлять их в lib, а будем использовать соответствующие бандлы:

  1. Import-Package: org.apache.commons.collections;version="3.2.0",

  2.  org.apache.commons.logging,

  3.  org.apache.commons.logging.impl,

  4.  org.osgi.framework;version="1.3.0"



Чтобы хибернейту были доступны классы сущностей, необходимо определить политику "дружественности":

  1. Eclipse-BuddyPolicy: registered



Теперь в манифестах всех бандлов, которые хотят использовать свои сущности достаточно написать

  1. Eclipse-RegisterBuddy: name.samolisov.hibernate



Фрагментные бандлы я подробно описывать не буду, скажу только, что в их каталоги lib следует поместить необходимые библиотеки, которые не забыть перечислить в параметре Bundle-ClassPath, а так же в каталоги src поместить необходимые файлы конфигурации, например hibernate.cfg.xml, c3p0.properties и т.д.

Займемся более интересным делом - созданием сервиса, предоставляющего фабрику сессий: IHibernateSessionFactoryService:

  1. package name.samolisov.hibernate.services;

  2.  

  3. import org.hibernate.SessionFactory;

  4. import org.hibernate.cfg.Configuration;

  5.  

  6. import name.samolisov.hibernate.exceptions.HibernateBundleException;

  7.  

  8. public interface IHibernateSessionFactoryService {

  9.  

  10.     public void startSessionFactory() throws HibernateBundleException;

  11.  

  12.     public String getSessionFactoryPropertyByName(String propertyName);

  13.  

  14.     public String getSessionFactoryUser();

  15.  

  16.     public String getSessionFactoryPassword();

  17.  

  18.     public String getSessionFactoryUrl();

  19.  

  20.     public SessionFactory getSessionFactory();

  21.  

  22.     public Configuration getConfiguration();

  23. }

  24.  



Для создания фабрики нам необходимы следующие зависимости:

1. Конфигуратор, т.е. класс Configuration или его наследник AnnotationConfiguration (мы ведь помним, что нам нужно будет обеспечить и построение фабрики по аннотациям). Конфигуратор будем получать с помощью сервиса IHibernateFactoryConfigurationService, код интерфейса которого следующий:

  1. package name.samolisov.hibernate.services;

  2.  

  3. import org.hibernate.cfg.Configuration;

  4.  

  5. public interface IHibernateFactoryConfigurationService {

  6.  

  7.     public Configuration getConfiguration();

  8.  

  9.     public int getPriority();

  10. }

  11.  



С помощью метода getPriority мы задаем приоритет конфигуратора. Суть в следующем: когда мы будем загружать конфигурацию в бандле, реализующем hibernate-annotations нам нужно будет подменить Configuration на AnnotationConfiguration. Для этого и используется механизм приоритетов: сервис, предоставляющий Configuration имеет меньший приоритет, чем сервис, предоставляющий AnnotationConfiguration, соответственно, Configuration будет использоваться только если AnnotationConfiguration не зарегистрирована.

Реализация данного сервиса весьма тривиальна. В случае Configuration она такая:

  1. package name.samolisov.hibernate.internal;

  2.  

  3. import org.hibernate.cfg.Configuration;

  4.  

  5. import name.samolisov.hibernate.services.IHibernateFactoryConfigurationService;

  6.  

  7. public class HibernateFactoryConfigurationService implements IHibernateFactoryConfigurationService {

  8.  

  9.     @Override

  10.     public Configuration getConfiguration() {

  11.         return new Configuration();

  12.     }

  13.  

  14.     @Override

  15.     public int getPriority() {

  16.         return Integer.MAX_VALUE - 1;

  17.     }

  18. }

  19.  



Инжекция конфигуратора в сервис HibernateSessionFactoryService осуществляется в методе addConfiguration, код которого следующий:

  1.     public synchronized void addConfiguration(IHibernateFactoryConfigurationService service)

  2.             throws HibernateBundleException {

  3.         if (service == null)

  4.             throw new HibernateBundleException("FactoryConfigurationService could not be null");

  5.  

  6.         if (service.getPriority() < _currentPriority) {

  7.             _sessionFactoryConfiguration = service.getConfiguration();

  8.             _currentPriority = service.getPriority();

  9.         }

  10.     }

  11.  



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

2. В фабрику необходимо передать информацию о мэпингах. Причем, если в случае использования xml-файлов данная информация представима в виде пути к ресурсу (т.е. к этому xml-файлу), то в случае использования мэпинга, основанного на аннотациях - в виде самого класса-сущности. Т.е. информация разнородна. Поэтому было принято решение завести сервис IHibernateResourcesService, который бы осуществлял добавление мэпинга в конфигурацию фабрики. Интерфейс сервиса выглядит следующим образом:

  1. package name.samolisov.hibernate.services;

  2.  

  3. import name.samolisov.hibernate.exceptions.HibernateBundleException;

  4.  

  5. import org.hibernate.cfg.Configuration;

  6.  

  7. public interface IHibernateResourcesService {

  8.  

  9.     public void addResourcesToConfiguration(Configuration configuration)

  10.             throws HibernateBundleException;

  11. }



В случае использование мэпинга, основанного на xml-файлах, метод addResourcesToConfiguration будет следующим:

  1. public void addResourcesToConfiguration(Configuration configuration) throws HibernateBundleException {

  2.         Collections.sort(_resources, HibernateMappingResource.COMPARATOR);

  3.         for (HibernateMappingResource resource : _resources)

  4.             configuration.addResource(resource.getName());

  5. }



Класс HibernateMappingResource представляет собой обыкновенный бин, инкапсулирующий путь к xml-ресурсу и порядок. Компаратор HibernateMappingResource.COMPARATOR осуществляет сортировку ресурсов по-порядку, т.е. при регистрации ресурсов в своих бандлах можно указывать приоритет их загрузки.

Привожу декларативное описание сервиса HibernateSessionFactory:

  1. <?xml version="1.0" encoding="UTF-8"?>

  2. <scr:component xmlns:scr="http://www.osgi.org/xmlns/scr/v1.1.0" activate="activate" name="name.samolisov.hibernate.factory">

  3.    <implementation class="name.samolisov.hibernate.internal.HibernateSessionFactoryService"/>

  4.    <service>

  5.        <provide interface="name.samolisov.hibernate.services.IHibernateSessionFactoryService"/>

  6.    </service>

  7.    <reference name="RESOURCE"

  8.       interface="name.samolisov.hibernate.services.IHibernateResourcesService"

  9.       bind="addResourceService"

  10.       unbind="removeResourceService"

  11.       cardinality="0..n"

  12.       policy="dynamic"/>

  13.    <reference name="CONFIGURATION"

  14.       interface="name.samolisov.hibernate.services.IHibernateFactoryConfigurationService"

  15.       bind="addConfiguration"

  16.       unbind="removeConfiguration"

  17.       cardinality="1..n"

  18.       policy="dynamic"/>

  19. </scr:component>

  20.  



Подробнее про формат описания декларативного сервиса можно прочитать в моей заметке Введение в OSGi. Декларативные сервисы - первое знакомство. Здесь стоит обратить внимание на параметр cardinality="1..n" у зависимости CONFIGURATION. Значение 1..n обозначает, что если в реестре сервисов не будет зарегистрирован ни один экземпляр сервиса IHibernateFactoryConfigurationService, то будет сгенерирована ошибка. И это правильно, т.к. без конфигуратора невозможно создать фабрику сессий.

Теперь рассмотрим сервис IHibernateSessionService, предоставляющий доступ непосредственно к сессиям, генерируемым с помощью фабрики. Интерфейс данного сервиса следующий:

  1. package name.samolisov.hibernate.services;

  2.  

  3. import name.samolisov.hibernate.exceptions.HibernateBundleException;

  4.  

  5. import org.hibernate.Session;

  6.  

  7. public interface IHibernateSessionService {

  8.  

  9.     public Session currentSession() throws HibernateBundleException;

  10.  

  11.     public void closeCurrentSession();

  12.  

  13.     public Session openNewSession() throws HibernateBundleException;

  14. }



Назначение методов, я думаю, понятно из их названия. Интересно рассмотреть некоторые аспекты реализации. Прежде всего стоит отметить, что у сервиса IHibernateSessionService только одна зависимость - собственно фабрика сессий. Инъектируется данная зависимость с помощью метода injectFactory. Помимо управления зависимостями определим метод, который будет вызываться непосредственно после создания сервиса. Данный метод указывается в атрибуте activate тега component декларативного описания сервиса. В нашем случае, в данном методе будем открывать одну сессию, чтобы она сразу была доступна для использования клиентам сервиса:

  1.     public synchronized void activate() throws HibernateBundleException {

  2.         openNewSession();

  3.     }

  4.  

  5.     public synchronized void deactivate() {

  6.         closeCurrentSession();

  7.     }

  8.  

  9.     public synchronized void injectFactory(IHibernateSessionFactoryService factory) {

  10.         _factory = factory;

  11.     }

  12.  

  13.     public synchronized void removeFactory(IHibernateSessionFactoryService factory) {

  14.         _factory = null;

  15.     }



Собственно это - все, что можно сказать о реализации бандла, предоставляющего Hibernate. Понятно, что совершенства не бывает и данный код можно улучшать и улучшать. В частности, можно реализовать стек сессий, чтобы при закрытии текущей была доступна предыдущая сессия. Это позволит наиболее рационально использовать несколько подключений к СУБД. В код фабрики же можно добавить запуск миграций или, например, создание таблиц, в случае необходимости.

Теперь поговорим об использовании Hibernate. Бандл, который выставляет свои мэпинги, должен зарегистрировать свой экземпляр сервиса IHibernateMappingResourcesConnector, например, такой:

  1. package name.samolisov.hibernate.demo;

  2.  

  3. import java.util.Arrays;

  4. import java.util.List;

  5.  

  6. import name.samolisov.hibernate.services.HibernateMappingResource;

  7. import name.samolisov.hibernate.services.IHibernateMappingResourcesConnector;

  8.  

  9. public class DemoHibernateMappingResourcesConnector implements IHibernateMappingResourcesConnector {

  10.  

  11.     private static final List<HibernateMappingResource> resources = Arrays.asList(

  12.             new HibernateMappingResource("name/samolisov/hibernate/demo/entity/MyEntity.hbm.xml"));

  13.  

  14.     @Override

  15.     public List<HibernateMappingResource> listResources() {

  16.         return resources;

  17.     }

  18. }

  19.  



Код регистрации сервиса тривиален и его я приводить не буду.

Использование Hibernate-сессии осуществляется в два этапа:

1. Получаение экземпляра сервиса IHibernateSessionService декларативно или с помощью ServiceTracker.

2. Использование хибернейтовской сессии как обычно. Саму сессию можно получить с помощью метода IHibernateSessionService#currentSession();

Пример такого использования:

  1.         if (_hibernateServiceTracker == null) {

  2.             _hibernateServiceTracker = new ServiceTracker(context,

  3.                     IHibernateSessionService.class.getName(), null);

  4.             _hibernateServiceTracker.open();

  5.         }

  6.  

  7.         IHibernateSessionService service = (IHibernateSessionService) _hibernateServiceTracker.getService();

  8.         Session session = service.currentSession();

  9.  

  10.         Transaction tx = session.beginTransaction();

  11.         try

  12.         {

  13.             String id = (String) session.save(new MyEntity("Name", 1));

  14.             System.out.println("entity id:" + id);

  15.             tx.commit();

  16.         }

  17.         catch (Exception e) {

  18.             e.printStackTrace();

  19.             tx.rollback();

  20.         }



Теперь поговорим об использовании hibernate-annotations. Для hibernate-annotations создадим новый бандл, в каталог lib которого поместим библиотеки:
- ejb3-persistence.jar
- hibernate-annotations.jar

Соответственно, параметр Bundle-Classpath будет иметь вид:

  1. Bundle-ClassPath: lib/ejb3-persistence.jar,

  2.  lib/hibernate-annotations.jar,

  3.  .



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

  1. Export-Package: javax.persistence,

  2.  javax.persistence.spi,

  3.  org.hibernate,

  4.  org.hibernate.annotationfactory,

  5.  org.hibernate.annotations,

  6.  ...



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

  1. Eclipse-RegisterBuddy: name.samolisov.hibernate

  2. Eclipse-BuddyPolicy: registered

  3. Require-Bundle: name.samolisov.hibernate;bundle-version="1.0.0"



Теперь вернемся к описанию сервиса IHibernateFactoryService. Данный сервис имеет две зависимости. Для удовлетворения зависимости в конфигураторе (сервис IHibernateFactoryConfigurationService) создадим свою реализацию данного сервиса, которая будет возвращать AnnotationConfiguration:

  1. package name.samolisov.hibernate.annotations.internal;

  2.  

  3. import org.hibernate.cfg.AnnotationConfiguration;

  4. import org.hibernate.cfg.Configuration;

  5.  

  6. import name.samolisov.hibernate.services.IHibernateFactoryConfigurationService;

  7.  

  8. public class HibernateFactoryAnnotationsConfigurationService implements IHibernateFactoryConfigurationService {

  9.  

  10.     @Override

  11.     public Configuration getConfiguration() {

  12.         return new AnnotationConfiguration();

  13.     }

  14.  

  15.     @Override

  16.     public int getPriority() {

  17.         return 1;

  18.     }

  19. }

  20.  



Помимо конфигуратора мы должны передавать в фабрику сессий аннотированные классы. Для этого создадим сервис IHibernateClassResourcesConnector. Бандлы, желающие выставить свои собственные аннотированные классы, должны будут реализовывать этот сервис, регистрируя в нем экземпляры класса HibernateAnnotatedClassResource, который представляет собой обычный бин, инкапсулирующий аннотированный класс и его порядок следования. Так же наш бандл должен реализовать сервис IHibernateResourcesService, для того, чтобы передавать зарегистрированные аннотированные классы в фабрику. Код реализации следующий:

  1. package name.samolisov.hibernate.annotations.internal;

  2.  

  3. import java.util.Collections;

  4.  

  5. import name.samolisov.hibernate.annotations.services.HibernateAnnotatedClassResource;

  6. import name.samolisov.hibernate.annotations.services.IHibernateClassResourcesConnector;

  7. import name.samolisov.hibernate.exceptions.HibernateBundleException;

  8. import name.samolisov.hibernate.services.HibernateAbstractResourcesService;

  9.  

  10. import org.hibernate.cfg.AnnotationConfiguration;

  11. import org.hibernate.cfg.Configuration;

  12.  

  13. public class HibernateClassResourcesService

  14.         extends HibernateAbstractResourcesService<HibernateAnnotatedClassResource, IHibernateClassResourcesConnector> {

  15.  

  16.     @Override

  17.     public void addResourcesToConfiguration(Configuration configuration) throws HibernateBundleException {

  18.         Collections.sort(_resources, HibernateAnnotatedClassResource.COMPARATOR);

  19.         AnnotationConfiguration annotatedConfiguration = (AnnotationConfiguration) configuration;

  20.         for (HibernateAnnotatedClassResource resource : _resources)

  21.             annotatedConfiguration.addAnnotatedClass(resource.getAnnotatedClass());

  22.     }

  23. }

  24.  



Пару слов о проблемах и методах их решения. Вообще Hibernate и HibernateAnnotations могут совместно работать, только если находятся в одном ClassPath. В частности, класс ExtendedMappings из HibernateAnnotations наследуется от класса Mappings из Hibernate и их конструкторы определены с модификатором доступа по-умолчанию. Т.е. конструктору наследника конструктор предка будет доступен только если оба класса находятся в одном пакете. Однако у нас hibernate3.jar и hibernate-annotations.jar находятся в разных бандлах, а значит классы их них загружаются разными загрузчиками, а значит - с точки зрения ява-машины находятся в разных пакетах. В итоге при попытке создать фабрику сессий мы получим замечательный IllegalAccessError. Чтобы избежать такого поведения, необходимо в бандле, который предоставляет хибернейт, создать пакет org.hibernate.cfg, скопировать туда исходник класса Mappings и изменить в этом классе участок

  1. Mappings(

  2.             final Map classes,

  3.             final Map collections,

  4.             ...



на

  1. public Mappings(

  2.             final Map classes,

  3.             final Map collections,

  4.             ...



К сожалению, более элегантного решения я не знаю, да и сомневаюсь, что оно возможно.

Теперь об использовании HibernateAnnotations. В принципе, особых отличий от использования Hibernate нет. Бандл, желающий зарегистрировать свои аннотированные классы должен реализовать сервис IHibernateClassResourcesConnector, например так:

  1. package name.samolisov.hibernate.annotations.demo;

  2.  

  3. import java.util.Arrays;

  4. import java.util.List;

  5.  

  6. import name.samolisov.hibernate.annotations.services.HibernateAnnotatedClassResource;

  7. import name.samolisov.hibernate.annotations.services.IHibernateClassResourcesConnector;

  8. import name.samolisov.hibernate.annotations.demo.entity.AnnEntity;

  9.  

  10. public class DemoHibernateClassResourcesConnector implements IHibernateClassResourcesConnector {

  11.  

  12.     private static final List<HibernateAnnotatedClassResource> resources = Arrays.asList(

  13.             new HibernateAnnotatedClassResource(AnnEntity.class));

  14.  

  15.     @Override

  16.     public List<HibernateAnnotatedClassResource> listResources() {

  17.         return resources;

  18.     }

  19. }

  20.  



В манифесте такого бандла необходимо прописать зависимость и от бандла, предоставляющего хибернейт, и от бандла, предоставляющего аннотации:

  1. Require-Bundle: name.samolisov.hibernate.annotations;bundle-version="1.0.0",

  2.  name.samolisov.hibernate;bundle-version="1.0.0"



"Дружить" такой бандл должен с бандлом, предоставляющим аннотации:

  1. Eclipse-RegisterBuddy: name.samolisov.hibernate.annotations



В заключении, хочется отметить, что при активном использовании созданных бандлов, возможно, придется добавить зависимости. Например, добавить antlr, который используется при оптимизации рефлексии. Желательно придерживаться правила: если существует бандл, предоставляющий добавляемую зависимость (в проекте Orbit, SpringSource-репозитории и т.д.), то лучше добавить этот бандл, нежели добавлять непосредственно библиотеку в каталог lib.

Суровый готов ответить на ваши вопросы и замечания.

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

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

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

Здравствуйте. В приложенном архиве во всех проектах есть файл build.properties. Всегда пользовался maven, ant. В Eclipse в меню Project пункт Build не активен. Чем собрать проект с build.properties понять не могу.
Из манифеста я смог получить build.xml для ANT, но сборка не приносит плодов (хотя BUILD SUCCESSFULL). В общем, вопрос: с помощью чего можно собрать проекты и получить бандлы?

Заранее благодарен. И вообще благодарен за столько полезную статью.

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

Здравствуйте, Илья. В Eclipse PDE есть т.н. механизм экспорта бандлов в jar-архивы. Описание данного механизма можно прочитать здесь: http://samolisov.blogspot.com/2010/05/eclipse-rcp-rcp.html

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

А где можно посмотреть исходный код к этой статье?

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

К сожалению исходный код не сохранился.

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

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