Статьи

NetBeans RCP & JPA (часть 2)

В моем предыдущем посте в DZone я говорил о том, как сообщить JPA о существовании нескольких файлов persistence.xml, возможно, по одному для каждого модуля RCP NetBeans , и объединить их вместе. Были некоторые грубые углы, и теперь я исправил один, кроме того, я также решил еще одну проблему интеграции с JPA.

 

Пожалуйста, имейте в виду, что большая часть кода, который я вам показываю, теперь сделана из blueMarine и доступна по адресу https://openbluesky.dev.java.net/svn/openbluesky/trunk/src/Libraries/JPA — использованная редакция за эту статью 225.

 

Как вы знаете, JPA — это всего лишь спецификация API, которая реализуется различными поставщиками, например, Hibernate , TopLink ( TopLink Essentials , часть Glassfish , является эталонной реализацией) и OpenJPA . Это означает, что у вас есть две группы JAR-файлов: одна, включающая только общедоступные классы API (для простоты я назову jpa.jar), а другая, включающая все компоненты реализации, плюс необходимые библиотеки (опять же, для простоты, Я назову это vendor.jar, хотя обычно там несколько файлов).

Для максимальной гибкости и мобильности вы обычно делаете следующие вещи:

  1. скомпилируйте ваше приложение против jpa.jar, игнорируя vendor.jar
  2. положить в classpath jpa.jar и vendor.jar от вашего любимого разработчика

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

Эта проблема

Когда я пытался сделать это впервые с BlueMarine , несколько месяцев назад, я не смог решить это. Основная идея, конечно же, состоит в том, чтобы создать два модуля оболочки библиотеки RCP NetBeans, первый из которых называется JPA, содержащий jpa.jar, а второй — Hibernate, а также vendor.jar; конечно, Hibernate Wrapper зависит от JPA Wrapper. Вы также можете создать модуль с именем TopLink, который снова зависит от JPA, и так далее. Ваши модули, которые используют классы в javax.persistence. *, Будут просто объявлять зависимость только от JPA, и затем вы можете добавить в свое приложение RCP либо модуль Hibernate, либо модуль TopLink по своему желанию.

Это не работает из-за механизма динамического обнаружения, реализованного в jpa.jar. По сути, класс Persistence ищет реализацию JPA, запрашивая загрузчик классов для получения всех файлов META-INF / services / javax.persistence.spi.PersistenceProvider, которые содержат имя класса реализации che. Хотя это работает в стандартной Java, в NetBeans RCP он не обнаруживает Hibernate, поскольку … JPA Wrapper не зависит от Hibernate Wrapper! Хотя вводить эту зависимость формально было бы неправильно, вы не можете принять ее как хак, иначе вы получите циклическую зависимость (помните, что Hibernate Wrapper уже зависит от JPA Wrapper), которая не разрешена платформой NetBeans RCP ,

До сих пор мне приходилось помещать jpa.jar и vendor.jar в модуль Hibernate, и весь мой код зависел от него.

Решение

Одним из способов решения этой проблемы является создание небольшого исправления для источников JPA, точнее в классе javax.persistence.Persistence, и замена механизма обнаружения на RCP NetBeans, то есть с помощью Lookup:

package javax.persistence;

import java.util.Map;
import java.util.logging.Logger;
import javax.persistence.spi.PersistenceProvider;
import org.openide.util.Lookup;
import it.tidalwave.netbeans.jpa.PersistenceProviderDecorator;

public class Persistence
{
private static final String CLASS = Persistence.class.getName();
private static final Logger logger = Logger.getLogger(CLASS);

private static PersistenceProvider persistenceProvider;

public Persistence()
{
}

public static EntityManagerFactory createEntityManagerFactory (final String persistenceUnitName)
{
return createEntityManagerFactory(persistenceUnitName, null);
}

public synchronized static EntityManagerFactory createEntityManagerFactory (final String persistenceUnitName,
final Map properties)
{
if (persistenceProvider == null)
{
final PersistenceProvider delegate = Lookup.getDefault().lookup(PersistenceProvider.class);

if (delegate == null)
{
throw new PersistenceException("No PersistenceProvider delegate found");
}

logger.info("PersistenceProvider delegate: " + delegate.getClass());
persistenceProvider = new PersistenceProviderDecorator(delegate);
}

final EntityManagerFactory emf = persistenceProvider.createEntityManagerFactory(persistenceUnitName, properties);

if (emf == null)
{
throw new PersistenceException("No Persistence provider for EntityManager named " + persistenceUnitName);
}

return emf;
}
}

Теперь класс сможет обнаруживать реализации JPA в любом модуле, потому что Lookup по умолчанию имеет глобальную видимость. Чтобы создать этот новый модуль JPA, я просто извлек источники javax.persistence. * Из дистрибутива Glassfish и исправил их — это нормально, поскольку они выпускаются через исключение GPL v2 + Classpath, так что вы только вынужден перераспределить то, чем я занимаюсь.

 

Действительно, я думаю, что мог бы быть способ реализовать эту вещь без исправления существующих файлов: вы могли бы просто встроить в JPA Wrapper прокси-провайдер, который в свою очередь вызвал бы Lookup. Я попробую это перед написанием третьей части этой серии.

 

Подключение стандартного Lookup даст нам дополнительную гибкость, о чем вы узнаете, прочитав последний абзац этого поста.

Сглаживание грубого угла

Если вы читаете мою предыдущую статью , вы вспомните, что я сделал что-то, установив определенный ClassLoaderDecorator через вызовы Persistence.createEntityManagerFactory (…), но для этого требовалось иметь единую точку входа в JPA. Теперь, после переопределения Постоянства, это уже не так. Вы, вероятно, заметили в приведенном выше листинге, что я обернул обнаруженного провайдера в PersistenceProviderDecorator:

package it.tidalwave.netbeans.jpa;

import java.util.Map;
import java.util.logging.Logger;
import javax.persistence.EntityManagerFactory;
import javax.persistence.spi.PersistenceProvider;
import javax.persistence.spi.PersistenceUnitInfo;

public class PersistenceProviderDecorator implements PersistenceProvider
{
private static final String CLASS = PersistenceProviderDecorator.class.getName();
private static final Logger logger = Logger.getLogger(CLASS);

private final PersistenceProvider delegate;

private MultiPersistenceXMLManager multiPersistenceXMLManager;

interface EntityManagerFactoryFactory
{
public EntityManagerFactory createEntityManagerFactory();
}

public PersistenceProviderDecorator (final PersistenceProvider delegate)
{
this.delegate = delegate;
}

public EntityManagerFactory createEntityManagerFactory (final String puName,
final Map properties)
{
return createEntityManagerFactory(new EntityManagerFactoryFactory()
{
public EntityManagerFactory createEntityManagerFactory()
{
return delegate.createEntityManagerFactory(puName, properties);
}
});
}

public EntityManagerFactory createContainerEntityManagerFactory (final PersistenceUnitInfo puInfo,
final Map properties)
{
return createEntityManagerFactory(new EntityManagerFactoryFactory()
{
public EntityManagerFactory createEntityManagerFactory()
{
return delegate.createContainerEntityManagerFactory(puInfo, properties);
}
});
}

private synchronized EntityManagerFactory createEntityManagerFactory (final EntityManagerFactoryFactory factory)
{
if (multiPersistenceXMLManager == null)
{
multiPersistenceXMLManager = new MultiPersistenceXMLManager();
}

final Thread currentThread = Thread.currentThread();
final ClassLoader saveClassLoader = currentThread.getContextClassLoader();
currentThread.setContextClassLoader(new ClassLoaderDecorator(saveClassLoader, multiPersistenceXMLManager));

try
{
return factory.createEntityManagerFactory();
}
finally
{
currentThread.setContextClassLoader(saveClassLoader);
}
}
}

 

Как видите, теперь дело за декоратором, чтобы установить ClassLoaderDecorator там, где это необходимо. Это означает, что теперь я могу использовать обычный код JPA и автоматически получать улучшения RCP, такие как поддержка нескольких файлов persistence.xml (и других вещей, которые я мог бы добавить в будущем).

Но жизнь не так проста

После первоначального волнения по поводу этого рефакторинга я застрял в серьезной ошибке Hibernate. Когда Hibernate загружается и пытается создать CGLIB-расширенные классы сущностей, вы получаете

java.lang.NoClassDefFoundError: org/hibernate/proxy/HibernateProxy

Это не проблема зависимостей модуля RCP: это скорее проблема Hibernate HHH-2317, поскольку — если я хорошо понимаю — он пытается найти HibernateProxy, используя загрузчик классов классов сущностей. Несмотря на то, что ему два года и некоторые люди предоставили предварительные исправления, проблема все еще остается открытой.

 

Не будьте слишком пессимистичны: возможно, вы не столкнетесь с этой проблемой. В blueMarine у ​​меня есть CGLIB в отдельной обертке библиотеки, от которой зависит Hibernate, потому что мне также нужен прямой доступ к CGLIB в некоторых частях приложения. Может быть, если вы поместите CGLIB в Hibernate Library Wrapper, эта вещь исчезнет …

 

Я ранее confrontated с этим вопросом в Bluemarine и нашел обходной путь: отключить отложенную загрузку. Я не собираюсь давать вам много подробностей, но отложенная загрузка — это особенность многих реализаций JPA при работе с отношениями: в основном, когда вы выбираете сущность, которая связана с другими, вы можете выбрать, чтобы охотно получать весь набор связанные объекты, или ленивый загрузить их. В последнем случае обычно выполняется дополнительная хитрость с CGLIB, и для этого требуется специальный усилитель от Hibernate. Если вам не нужна ленивая загрузка (в данный момент у меня нет JPA для управления отношениями), вы можете отключить эту функцию, и оказывается, что для этого будет выбран другой улучшитель кода, на который не влияет HHH-2317 ,

Чтобы отключить отложенную загрузку, вы должны поместить default-lazy = «false» в каждый файл отображения Hibernate для ваших объектов. Жаль, что если вы используете простой JPA API, у вас нет файлов отображения Hibernate, и, похоже, нет другого места, например, свойств конфигурации, для отключения этой функции (ребята, Hibernate, это действительно позор; — )

Что делать?

Что ж, если вы не можете использовать свойства конфигурации, вы всегда можете попытаться программно исправить объекты конфигурации внутри Hibernate. Глядя на источники HibernatePersistence, класса, реализующего PersistenceProvider, я обнаружил, что:

  1. Создает экземпляр Ejb3Configuration
  2. Это заполняет это должным образом (например, смотря на persistence.xml)
  3. Он использует его для создания EntityManagerFactory

К счастью, шаги 2 и 3 разделены, поэтому можно вставить код посередине, чтобы исправить конфигурацию:

    private void patch (final Ejb3Configuration config)
{
for (final Iterator i = config.getClassMappings(); i.hasNext(); )
{
final PersistentClass persistentClass = (PersistentClass)i.next();
persistentClass.setLazy(false);
logger.fine("Set lazy=false for " + persistentClass.getEntityName());
}
}

По сути, идея состоит в том, чтобы заменить HibernatePersistence следующим классом:

package it.tidalwave.bluemarine.persistence;

import java.util.Iterator;
import java.util.Map;
import java.util.Properties;
import java.util.TreeSet;
import java.util.logging.Logger;
import javax.persistence.EntityManagerFactory;
import javax.persistence.spi.PersistenceProvider;
import javax.persistence.spi.PersistenceUnitInfo;
import org.hibernate.ejb.Ejb3Configuration;
import org.hibernate.mapping.PersistentClass;

public class HibernatePersistenceDecorator implements PersistenceProvider
{
private static final String CLASS = HibernatePersistenceDecorator.class.getName();
private static final Logger logger = Logger.getLogger(CLASS);

public EntityManagerFactory createEntityManagerFactory (final String persistenceUnitName, final Map overridenProperties)
{
final Ejb3Configuration cfg = new Ejb3Configuration();
final Ejb3Configuration configured = cfg.configure(persistenceUnitName, overridenProperties);
patch(configured);
final EntityManagerFactory emf = configured != null ? configured.buildEntityManagerFactory() : null;
return emf;
}

public EntityManagerFactory createContainerEntityManagerFactory (final PersistenceUnitInfo info, final Map map)
{
final Ejb3Configuration cfg = new Ejb3Configuration();
final Ejb3Configuration configured = cfg.configure(info, map);
patch(configured);
final EntityManagerFactory emf = configured != null ? configured.buildEntityManagerFactory() : null;
return emf;
}

private void patch (final Ejb3Configuration config)
{
for (final Iterator i = config.getClassMappings(); i.hasNext(); )
{
final PersistentClass persistentClass = (PersistentClass)i.next();
persistentClass.setLazy(false);
logger.fine("Set lazy=false for " + persistentClass.getEntityName());
}
}
}

 

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

 

Теперь, как я могу сказать системе использовать мою реализацию сервиса вместо оригинальной? Дело в том, что я не хочу помещать это исправление в модуль JPA, который должен быть общим; это должно идти в код BlueMarine.

Поиск на помощь

Поиск теперь помогает, так как он позволяет расширению синтаксиса для META-INF / services заменить существующую реализацию. Поэтому мне просто нужно добавить в blueMarine файл META-INF / services / javax.persistence.spi.PersistenceProvider, который содержит:

#-org.hibernate.ejb.HibernatePersistence
it.tidalwave.bluemarine.persistence.HibernatePersistenceDecorator

и вуаля, игра окончена.

Итак, урок заключается в том, что теперь вы можете использовать JPA в приложении RCP NetBeans, как в обычном контексте Java SE / EE, и, подключив средство Lookup из NetBeans, вы сможете пользоваться некоторыми дополнительными функциями. Теперь весь blueMarine был реорганизован, и код зависит только от модуля JPA; если я хочу заменить JPA-провайдера, мне просто нужно создать новый Library Wrapper и поместить его вместо Hibernate (предположим, что я не нахожу других проблем, связанных с загрузкой классов …).

Для третьей части я попытаюсь решить еще одну проблему. На данный момент мне нужно добавить базу данных Library Wrapper (в моем случае Derby) в качестве зависимости от Hibernate, иначе он не сможет открыть соединение. Конечно, мне это не нравится, так как я предпочитаю иметь независимую реализацию JPA-провайдера, а затем подключать нужную базу данных JDBC Wrapper по своему желанию. Поскольку это проблема загрузчика классов, и у меня уже есть пользовательский загрузчик классов в моем PersistenceProviderDecorator, я думаю, что это выполнимо.

Как я сказал в начале, часть этого кода для JPA теперь подвергается рефакторингу из проекта blueMarine и находится внутри OpenBlueSky. Если есть интерес к нему, я мог бы представить его в проекте platformX, чтобы стать расширением платформы. Позвольте мне услышать ваши отзывы.