Pull to refresh

Comments 16

Я сталкивался с подобными трудностями при интеграции JPA + JTA в OSGI окружение. Решение вышло немного другим.
  • DataSource -> DataSourceFactory (org.osgi.service.jdbc.DataSourceFactory)
  • Внутри DataSourceFactory проверяется есть ли доступный TransactionManager (мы используем geromnio tm) и возвращается декорированный DS.
  • Далее вешается листенер на старт бандлов (org.osgi.framework.BundleListener) и проверяется наличиее мета тега Meta-Persistence
  • При нахождении этого тега берется persistence.xml и начинается инициализации EMF
    1. Формируется смешанный класслоадер для конкретной реализации JPA (eclipselink или hibernate) и бандла с ресурсом persistence.xml
    2. Формируется DataSource из настроек в persistence.xml и переопределений в системных настройках.
    3. Выбирается стратегия использования транзакций
    4. Вместо конкретной инициализации EMF подсовываем прокси EMF который в свою очередь имплементирует org.osgi.framework.ServiceFactory
       4.1 Далее на первый вызов getService(Bundle bundle, ServiceRegistration registration) инстациируется сам EMF и увеличивается счетчик ссылок на сервис
       4.2 На вызов метода ungetService(Bundle bundle, ServiceRegistration registration, Object service) происходит провека количества ссылок на сервис и если оно равно нулю то EMF уничтожается.


Внутри спрятано еще достаточно много деталей (как отрабатывает рестарт бандла с провайдером, с ресурсом), но концепция в двух словах должна быть понятна.
В OSGi, для JTA сейчас используется описанный вами подход.
В файле манифеста указывается параметр «Meta-Persistence». Всю работу взваливает на себя система хуков (Bundle Hook Service
Specification).

JTA не всегда подходит под специфику задач. Скажем, есть у нас несколько версий документов. Для понимания, представим документ в виде XML структуры (xsd описание). По мере развития системы структуры документов изменялись. В системе есть WEB сервис для импорта документов. Для примера, скажем, документов из системы 1С.
Напрашивается решение из нескольких OSGi компонентов, каждый для своей версии структуры документов.
Каждый OSGi компонент приводит к сканированию набора классов, Entity компонентов.
Через некоторое время, старые версии документов будут встречаться всё реже и реже. Значит, наша система должна уметь каким-то образом выгружать все классы OSGi компонентов. В том числе, весь Java Reflection, задействованный в MyBatis или в JPA.
На текущий момент, единственным решением является «Declarative Services Specification». Сведения о сервисе опубликованы в реестре, а классы не подняты. При освобождении сервиса, ungetService, класс может быть выгружен OSGi Framework. Далее, GC может очистить раздел JVM permgen от занимаемых классов.

OSGi hook не дадут нам нужного эффекта — освобождения JVM permgen. Нужно полностью останавливать компонент.

JTA реализация в JPA выглядит красиво для конечного потребителя. На самом деле, это очень большой и запутанный раздел. EntityManager не может ответить, работает ли в соединение в распределенной транзакции или нет. Даже запросив TransactionManager на предмет активности транзакции, мы не знам, работает ли текущий EntityManager в рамках этой транзакции или в рамках другой транзакции.
TransactionManager tm;
Transaction t_old = tm.getTransaction();
tm.suspend();
Transaction t_new = tm.begin();

Вот, где окажется flush или persist?
Я скажу — в t_old. А ведь мы сказали, затормозить старую транзакцию и начать новую.

В будущей версии mybatis-guice этот вопрос решен. Весь код, в методе выполнится в новой транзакции.
@Inject MyMapper mapper;

@Transactional(Transactional.TxType.REQUIRES_NEW)
void doFoo() {
	mapper.doAny();
}

А в JTA, нам надо открыть новый EntityManager. Поправьте меня, если это не так.
Все почти так. EM нужно создавать в контексте открытой транзакции, в этом случае к EM она будет привязана. Даже проще говоря нужно открыть Connection и он будет к привязан к тразакции, а уже к Connection будет привязан EntityManager. (и может быть как XA так и простой)

Как в вашем примере, будет создано ровно тоже самое, будет открыто новое соединение, и оно будет привязано к транзакции.

Простейший пример:
Есть JTADataSource (например org.apache.commons.dbcp2.managed.BasicManagedDataSource)
Есть TM
TransactionManager tm;
DataSource ds;
    
Transaction t1 = tm.begin();
Connection conn = ds.openConnection();

....
conn.close();
t1.suspend();
Transaction t2 = tm.begin();
conn = ds.openConnection();

....
conn.close();
t1.commit();
t2.commit();
Другими словами. Когда создается новый объект EntityManager всё решается. Все зарегистрированные и измененные объекты, сохраняются этим менеджером.
А вот с соединениями к базе данных беда. Да мы можем открыть новое соединение в новой транзакции. Но все изменения объектов, сделанные до открытия транзакции, выполняться в новой транзакции.
Значит, нам нужно открепить объект от старого EM (detach) и прикрепить к новому EM (push). После завершения вложенной транзакции, вернуть объект в старый EM. Эта процедура для того, чтобы лишний раз не поднимать часть объектов из базы данных. Ведь в разных EM кеш объектов не пересекается.
Немного сложная технология. А если есть прикрепленные объекты (one2one, one2many, many2one), тогда, после открепления основного объекта, надо откреплять все сопроводительные объекты.
Мне это немного надоело в JDO. По этой причине перешел на MyBatis.

JPA и JDO, красивые технологии, но в них столько нюансов, что иногда хочется выключить монитор, чтобы не видеть стек невероятных ошибок.
А вот выгрузка класса произойдет только тогда когда не будет на него ссылок, а ссылки остануться как минимум в бандле после первой загрузки, потому что каждый бандл внутрни себя содержит свой класслоадер, и если посмотреть на реализацию например в felix (org.apache.felix.framework.BundleWiringImpl.BundleClassLoader) он наследуется от java.lang.ClassLoader в котором загруженные классы попадают в кеш, соотвествеено они соберуться только после того как пропадут ссылки на класслоадер, а на него пропадут ссылки после деинсталяции бандла (судя по коду).
Хорошо, каждый компонент, OSGi bundle, загружается в своем ClassLoader. И выгружается, как только пропадают связи других компонентов на текущий компонент. Это теория. А что у нас с практикой?
На практике совсем плохо. При поднятии компонента, BundleListener читает манифест и видит параметр «Meta-Persistence». Создается OSGi Service, который привязан к данному компоненту. Далее, этот сервис регистрируется в ServiceTracker. Для чего понадобился сервис? Для итого, чтобы он мог свободно получать доступ к классам Entity объектов. Сервис создает JTA фабрику.
Как вы думаете, когда освобождается данный сервис? Правильный ответ — когда OSGi Framework останавливает OSGi Bundle, в котором зарегистрирован сервис. Другими словами, по доброй воле сервис не останавливается. Получается, что память от созданных объектов освобождается, а сами классы не выгружаются, так как JTA фабрика не разрушается в работающем сервисе. Как то так.

Альтернатива этому мероприятию — Declarative Services. OSGi Framework поднимает все компоненты (OSGi bundle). Если есть активатор, он будет запущен. Есть параметр «Meta-Persistence», система создаст сервис с поднятой JPA фабрикой. Есть spring или blueprint, создаст сервисы для этих служб.
Декларативный сервис — может тихо проследовать дальше, не поднимая классов. Да, есть аналог BundleActivator, который заставит загрузить классы.
Декларативный сервис тем и полезен, что создается по требованию, когда из ServiceReferenсe требуем сам сервис. Как только сервис освобождается, с помощью ungetService, он сразу разрушается. Ни кто не держит BundleContext и классы из ClassLoader могут свободно выгружаться.
Если созданный декларативный сервис захватить с помощью ServiceTracker, мы не получим выигрыша. ServiceTracker по доброй воле не отпускает захваченные сервисы. Ему надо говорить close().

Честно, я не видел спецификации, как в JPA можно динамически загружать и выгружать сведения об Entity компонентах. В JDO спецификации такой функционал предусмотрен.
Зачем это нужно? Для примера рассмотрим WEB сервис, принимающий разные типы документов в систему. Каждый вызов метода приема нового документа ведет к обработке одного типа документа. Десятки других типов документов не нужны. Процесс поднятия JTA Factory очень дорогая операция. По этой причине, она идет в момент поднятия OSGi Bundle и регистрации сервиса.
В MyBatis можно загружать mapper интерфейсы по необходимости. Тем самым, поднятие фабрики в MyBatis становится дешевой операцией.
только создается ServiceFactory а не сам сервис, я писал выше. Как только никто не использует этот сервис, в ServiceFactory разрушается EMF. В этом то и дело что SCR (Declarative Services) как раз и работают имено через SF.

Декларативный сервис тем и полезен, что создается по требованию, когда из ServiceReferenсe требуем сам сервис. Как только сервис освобождается, с помощью ungetService, он сразу разрушается.


Это не совсем верно, точнее это верно только для DelayedComponent, и то в зависимости от настроек. Для остальных например в SCR примерно следующий код:
    public void ungetService(Bundle bundle, ServiceRegistration registration, Object service) {
        if(usesCount.get() > 0){
            long usesCountNow = usesCount.decrementAndGet();
            log.debug("== Uses count of {} service are {}", original.getClass(), usesCountNow);
        }
    }


Реализуется это все через ServiceFactory.

/**
 * Allows services to provide customized service objects in the OSGi
 * environment.
 * 
 * <p>
 * When registering a service, a {@code ServiceFactory} object can be used
 * instead of a service object, so that the bundle developer can gain control of
 * the specific service object granted to a bundle that is using the service.
 * 
 * <p>
 * When this happens, the {@code BundleContext.getService(ServiceReference)}
 * method calls the {@code ServiceFactory.getService} method to create a service
 * object specifically for the requesting bundle. The service object returned by
 * the {@code ServiceFactory} is cached by the Framework until the bundle
 * releases its use of the service.
 * 
 * <p>
 * When the bundle's use count for the service is decremented to zero (including
 * the bundle stopping or the service being unregistered), the
 * {@code ServiceFactory.ungetService} method is called.
 * 
 * <p>
 * {@code ServiceFactory} objects are only used by the Framework and are not
 * made available to other bundles in the OSGi environment. The Framework may
 * concurrently call a {@code ServiceFactory}.
 * 
 * @param <S> Type of Service
 * @see BundleContext#getService(ServiceReference)
 * @ThreadSafe
 * @version $Id: 535776e702ec5ace54f577218ff8f7920741558b $
 */

public interface ServiceFactory<S> {


На выгрузке классов никто не экономит, вот посмотрел на работаюший пару месяцев сервис там как загрузились 2,5к классов так и остались загруженными, если выгрузить бандл классы этого бандла будут выгружены, в противном случае они останутся загруженными.
Динамические сервисы, по умолчанию, являются уничтожаемыми SCR компонентами (DelayedComponent).
Если SCR компонент не реализует интерфейсы, будет создан сразу (Immediate).
Если для SCR компонента указать название фабрики, будет режим фабрики.

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

Теперь представим работу с уничтожаемым динамическим сервисом. При первом запросе сервиса, Framework поднимет реализацию сервиса. Каждый Bundle, запрашивающий данный сервис, получит один и тот же объект. При разрегистрации компонента, когда счетчик использования достигает нуля, объект сервиса будет уничтожен.

Прочувствуйте разницу на сложном примере. У нас есть JAXB объекты, генерированные из xsd схемы. Для работы нам надо поднять JaxbContext. Если количество классов в схеме около сотни, процесс будет достаточно медленным. Можно поднять JaxbContext в фабрике и передавать его в каждый создаваемый экземпляр. Когда все экземпляры сервисов будут уничтожены, фабрика может обнулить данный параметр. А можно поднять уничтожаемый динамический сервис. При активации, он поднимет JaxbContext. При деактивации, JaxbContext будет уничтожен по ходу уничтожения объекта сервиса.

Теперь подумаем, как ведет себя JPA в фабрике. В пределах одной транзакции сервис запрашивается из двух компонентов, OSGi bundle. Для каждого будет создан свой сервис со своим EntityManager. Первый bundle создал часть объектов и не сделал flush. После этого работает второй компонент. Программист думает, что часть нужной информации доступна, так как она сделана в этой транзакции. Но в базе данных объектов еще нет и сервер базы данных отдает устаревшие данные. Теперь, раздается общий commit по транзакции. Как вы думаете, какой из двух EntityManager запишет свои данные раньше? Ответ — второй. Для этого надо углубиться в диаграммы описания работы JTA транзакций.
Вы разве этого хотели достичь своей фабрикой?
Я точно, так не хочу.
Ребята, а приведите пожалуйста примеры модульных систем (где в каждом модуле свои сущности), где есть требование выгружать динамически модули/загружать новые? И откуда растут такие требования?
В качестве примера можно рассмотреть обработку данных с федерального сайта государственных закупок.
За прошлый год структура документов сменилась семь раз. В каждой версии порядка 40 типов документов. 7 * 40 = 280 типов документов. Получается, что в системе, для каждой версии свой компонент с JAXB объектами. Средний объем бинарных кодов JAXB классов от 4М до 6М. При таких объемах начинаешь воевать за крошки в JVM permgen. Далее, строим некую систему хранения данных своей организации. Появляется новая версия документов. Зачем ломать тот код, что уже работает? Делаем новую систему раскладки под новую версию документов.
Задачи использования полученных данных можно придумать самому. И это будет уже другая история.

Проблема не столько в Entity объектах. В MyBatis можно обойтись объектами, созданными из JAXB модели. Главная задача, это выгрузка классов. А для этого надо аккуратно убирать все классы обработки, где используются импорты JAXB классов. Кто у нас главный поставщик этих классов обработки? Правильно, сервис со своей JPA фабрикой или MyBatis фабрикой.
А зачем решать задачу выгрузки классов в Вашем примере? Чтобы 7 раз в год обновить версии документов?
Документы этого сайта были предложены только в качестве примера. Количество проходящих документов за день довольно большое. Документы не будут меняться от версии к версии хотя бы потому, что их очень много.

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

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

osgi по «историческим» или иным каким то объективным причинам?
Как бы сказать получше.
1) У меня проект с использует Apache Camel. В OSGi все Camel процессы стартуют автоматически.
2) В Apache Karaf есть хорошая WebConsole для управления параметрами сервисов. Изменил, нажал сохранить, система работает с этими параметрами. Опять же, изобретать и отлаживать ничего не надо.
3) Большое количество версий документов. На уровне OSGi сервисов я указываю параметр версии обрабатываемых документов. Указываю в фильтре версию обрабатываемого документа. Система возвращает нужный сервис. В EJB я такого функционала не обнаружил.
4) В сочетании Apache Camel и OSGi сервисов, я использую динамическую регистрацию процессов (recepientList). Тем самым, получаю одну систему приема документов и много систем обработки.
5) Можно поставить hawt.io для визуального отслеживания производительности системы.

Да бы не писать разные наборы Entity компонентов под разные системы обработки, использую трансформацию XML => JAXB => MyBatis. Да, некоторые XML радуют размерами в десятки мегабайт.
Использую отдельный OSGi компонент (bundle + service) под обработку документов одной версии.
Система поднимает OSGi сервис для обработки документа определенной версии. После обработки, сервис становится не востребованным и Framework может освободить память.

Делаем упор на то, что версий документов может быть много. Поднимать Entity, сразу для всех типов документов одной версии, дело не благодарное. В MyBatis используется динамическое поднятие интерфейсов обращения к базе данных. Это значительно ускоряет процесс. В рамках JPA надо дробить систему еще сильнее — один OSGi bundle для каждого документа одной версии.

Везде я указывал, что можно использовать WEB сервис для получения входного документа. На самом деле, в проекте документы появляются по расписанию и большими пачками. До тысячи документов в пачке. Apache Camel процессы позволяют вести обработку в несколько потоков. Логично предположить, что маленькие документы обрабатываются быстрее, а большие медленнее.
караф какой версии?
был опыт карафа 2 — так там даже перегрузить бандл было нельзя — либо ничего не менялось либо глючило либо висло, тоже самое при попытке запустить один и тот же бандл с разными версиями, т.е. даже основная фишка osgi «hot redeploy» не работала и приходилось рестартовать караф всегда. Ну и основное неудобство — использование только osgi версий библиотек, которых либо может не быть вообще и тогда пляска с бубном и упаковка вручную либо есть но почти всегда отстают от обычной, классический пример — guava: osgi версия древняя.
Проект Apache Karaf развивается не так быстро. Примерно один фикс релиз в пол года. Задержки вызваны медленным развитием дополнительных компонентов. Для примера, 4 версия всё еще на стадии тестирования, хотя первые альфы были года 2 назад.
Так что, все работают на версиях 2 или 3. Должен отметить, что есть очень интересная версия — 2.4.x. По сути, это стабильный функционал от 4 версии. Если сравнивать версии 2.4.x и 3.0.x, то младшая версия выглядит гораздо привлекательнее.
Правда, в выборе версии Apache Karaf не последнюю роль играет используемая версия Apache Camel.
Apache Camel развивается гораздо быстрее. Жизненный цикл одного фикс релиза составляет 3 месяца. На сайте проекта есть таблица соответствия версий проектов Apache Camel и Apache Karaf. В выборе версий желательно придерживаться данных этой таблицы. В последнее время, весной, проект Apache Camel начал хандрить. Люди постарались выпустить версию 2.15.0. В этой версии очень много интересных решений и многие ожидали. В частности, этот релиз переходил на Apache Karaf 2.4.x. Как обычно, люди старались и некоторые ошибки просто упустили.
Так к слову, Apache Camel потерял функционал управления из командной консоли Apache Karaf. Я отметил данный нюанс на форуме проекта. Разработчики постарались в сжатые сроки, через 20 дней, выдать исправленную версию. Как говориться, спешка нужна при ловле блох. Пришлось еще через месяц выпускать новую версию фикс релиза.

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

Горячая замена. Да, есть проблема в OSGi Blueprint. Не хочет отпускать захваченный сервис. По этой причине надо останавливать компонент (bundle) сервиса и запускать снова.

По поводу использования не OSGi библиотек. По началу, я тоже ломал голову, что надо делать OSGi wrapper. Решение пришло с другой стороны. В Apache Karaf есть файл описания плана развертки особенностей (features). Пример команды установки
features:instal war

Пример установки Google Guava, библиотеки, которая давно обзавелась OSGi манифестом:

  <bundle>mvn:com.google.guava/guava/15.0</bundle>


Пример установки JDBC драйвера, не обладающего OSGi манифестом:

  <bundle>wrap:mvn:com.oracle/ojdbc14/10.2.0.5</bundle>

Sign up to leave a comment.

Articles

Change theme settings