Для успешной работы нашей конфигурации JPA необходимы две вещи:
- база данных для хранения данных,
- JNDI для доступа к базе данных.
Этот пост состоит из двух частей. Первая часть показывает, как использовать автономный JNDI и встроенную базу данных в памяти в тесте. Остальные главы объясняют, как работает решение.
Весь используемый код доступен на Github . Если вы заинтересованы в решении, но не хотите читать объяснения, загрузите проект с Github и прочитайте только первую главу.
JPA Test
В этой главе показано, как использовать наш код для включения в тесты автономной JNDI и встроенной базы данных в памяти. Как и почему работает решение, объясняется в оставшейся части этого поста.
Решение имеет три класса API:
-  JNDIUtil— инициализация JNDI, очистка и некоторые удобные методы,
-   InMemoryDBUtil— создание / удаление базы данных и источника данных,
-   AbstractTestCase— очистка базы данных перед первым тестом и JNDI перед каждым тестом.
  Мы используем Liquibase для поддержки структуры базы данных.  Если вы не хотите использовать Liquibase, вам придется настроить класс InMemoryDBUtil .  createDatabaseStructure метод createDatabaseStructure чтобы сделать то, что вам нужно. 
Liquibase хранит список всех необходимых изменений базы данных в файле с именем changelog. Если не указано иное, каждое изменение выполняется только один раз. Даже если файл журнала изменений применяется несколько раз к одной и той же базе данных.
использование
  Любой тестовый пример, расширенный из AbstractTestCase будет: 
- сбросить базу данных перед первым тестом,
- установить автономный JNDI или удалить все данные, хранящиеся в нем, перед каждым тестом,
- запускать Liquibase changelog для базы данных перед каждым тестом.
  Тестовый пример JPA должен расширять AbstractTestCase и переопределять метод getInitialChangeLog .  Метод должен возвращать местоположение файла изменений. 
| 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | publicclassDemoJPATest extendsAbstractTestCase {  privatestaticfinalString CHANGELOG_LOCATION = "src/test/java/org/meri/jpa/simplest/db.changelog.xml";  privatestaticEntityManagerFactory factory;  publicDemoJPATest() {  }  @Override  protectedString getInitialChangeLog() {    returnCHANGELOG_LOCATION;  }  @Test  @SuppressWarnings("unchecked")  publicvoidtestJPA() {    EntityManager em = factory.createEntityManager();    Query query = em.createQuery("SELECT x FROM Person x");    List<Person> allUsers = query.getResultList();    em.close();    assertFalse(allUsers.isEmpty());  }  @BeforeClass  publicstaticvoidcreateFactory() {    factory = Persistence.createEntityManagerFactory("Simplest");  }  @AfterClass  publicstaticvoidcloseFactory() {    factory.close();  }} | 
Примечание: было бы чище сбросить базу данных перед каждым тестом. Однако удаление и воссоздание структуры БД является дорогостоящей операцией. Это слишком сильно замедлило бы тест. Делать это только перед классом кажется разумным компромиссом.
  Хотя база данных удаляется только один раз, журнал изменений запускается перед каждым тестом.  Это может показаться пустой тратой, но у этого решения есть некоторые преимущества.  Во-первых, метод getInitialChangeLog не обязательно должен быть статическим и может быть переопределен в каждом тесте.  Во-вторых, изменения, сконфигурированные для ‘runAlways’, будут выполняться перед каждым тестом и, таким образом, могут содержать некоторые дешевые очистки или другие инициализации. 
JNDI
В этой главе объясняется, что такое JNDI, как он используется и как его настроить. Если вы не заинтересованы в теории, перейдите к следующей главе. Там создается автономный JNDI.
Основное использование
  JNDI позволяет клиентам хранить и искать данные и объекты по имени.  Доступ к хранилищу данных осуществляется через реализацию интерфейса Context . 
Следующий код показывает, как хранить данные в JNDI:
| 1 2 3 | Context ctx = newInitialContext();ctx.bind("jndiName", "value");ctx.close(); | 
Второй фрагмент кода показывает, как искать вещи в JNDI:
| 1 2 3 | Context ctx = newInitialContext();Object result = ctx.lookup("jndiName");ctx.close(); | 
Попробуйте запустить вышеупомянутое без контейнера J2EE, и вы получите ошибку:
| 1 2 3 4 5 6 7 | javax.naming.NoInitialContextException: Need to specify classname in environment or system property, or as an applet parameter, or in an application resource file:  java.naming.factory.initial at javax.naming.spi.NamingManager.getInitialContext(Unknown Source) at javax.naming.InitialContext.getDefaultInitCtx(Unknown Source) at javax.naming.InitialContext.getURLOrDefaultInitCtx(Unknown Source) at javax.naming.InitialContext.bind(Unknown Source) at org.meri.jpa.JNDITestCase.test(JNDITestCase.java:16) at ... | 
  Код не работает, потому InitialContext класс InitialContext не является реальным хранилищем данных.  Класс InitialContext может только найти другой экземпляр интерфейса Context и делегировать всю работу ему.  Он не может ни хранить данные, ни находить их. 
Фабрики контекста
  Реальный контекст, который выполняет всю работу и может хранить / находить данные, должен быть создан фабрикой контекста.  В этом разделе показано, как создать фабрику контекста и как настроить InitialContext для ее использования. 
  Каждая фабрика контекста должна реализовывать интерфейс InitialContextFactory и иметь конструктор без аргументов: 
| 01 02 03 04 05 06 07 08 09 10 | packageorg.meri.jpa.jndi;publicclassMyContextFactory implementsInitialContextFactory { @Override publicContext getInitialContext(Hashtable environment) throwsNamingException {  returnnewMyContext(); } } | 
  Наша фабрика возвращает простой контекст MyContext .  Его метод lookup всегда возвращает строку «сохраненное значение»: 
| 01 02 03 04 05 06 07 08 09 10 11 12 13 14 | classMyContext implementsContext { @Override publicObject lookup(Name name) throwsNamingException {  return"stored value"; } @Override publicObject lookup(String name) throwsNamingException {  return"stored value"; } .. the rest ...} | 
  Конфигурация JNDI передается между классами в хеш-таблице.  Ключ всегда содержит имя свойства, а значение содержит значение свойства.  Поскольку начальный контекстный конструктор InitialContext() не имеет параметров, предполагается пустая хеш-таблица.  Класс также имеет альтернативный конструктор, который принимает в качестве параметра хеш-таблицу свойств конфигурации. 
  Используйте свойство "java.naming.factory.initial" чтобы указать имя класса фабрики контекста.  Свойство определено в константе Context.INITIAL_CONTEXT_FACTORY . 
| 1 2 3 4 | Hashtable env = newHashtable();env.put(Context.INITIAL_CONTEXT_FACTORY, "className");Context ctx = newInitialContext(environnement); | 
  Следующий тест настраивает MyContextFactory и проверяет, возвращает ли созданный начальный контекст «сохраненное значение» независимо от того, что: 
| 01 02 03 04 05 06 07 08 09 10 11 12 | @Test@SuppressWarnings({ "unchecked", "rawtypes"})publicvoidtestDummyContext() throwsNamingException { Hashtable environnement = newHashtable(); environnement.put(Context.INITIAL_CONTEXT_FACTORY, "org.meri.jpa.jndi.MyContextFactory"); Context ctx = newInitialContext(environnement); Object value = ctx.lookup("jndiName"); ctx.close(); assertEquals("stored value", value);} | 
Конечно, это работает, только если вы можете предоставить хэш-таблицу с пользовательскими свойствами для исходного конструктора контекста. Это часто невозможно. Большинство библиотек используют конструктор без аргументов, показанный в начале. Они предполагают, что у исходного класса контекста есть фабрика контекста по умолчанию, и что конструктор без аргументов будет использовать его.
Менеджер по именам
  Начальный контекст использует NamingManager для создания реального контекста.  Менеджер именования имеет статический метод getInitialContext(Hashtable env) который возвращает экземпляр контекста.  Параметр env содержит свойства конфигурации, используемые для построения контекста. 
  По умолчанию менеджер именования считывает Context.INITIAL_CONTEXT_FACTORY из хеш-таблицы env и создает экземпляр указанной исходной фабрики контекста.  Затем фабричный метод создает новый экземпляр контекста.  Если это свойство не задано, диспетчер имен создает исключение. 
  Можно настроить поведение менеджеров имен.  Класс NamingManager имеет метод setInitialContextFactoryBuilder .  Если установлен начальный конструктор фабрики контекста, менеджер имен будет использовать его для создания фабрики контекста. 
Вы можете использовать этот метод только один раз. Установленный контекст фабричного компоновщика изменить нельзя.
| 1 2 3 4 5 6 | try{  MyContextFactoryBuilder builder = newMyContextFactoryBuilder();  NamingManager.setInitialContextFactoryBuilder(builder);} catch(NamingException e) {  // handle exception} | 
  Начальный фабричный контекстный конструктор должен реализовывать интерфейс InitialContextFactoryBuilder .  Интерфейс прост.  У него есть только один метод InitialContextFactory createInitialContextFactory(Hashtable env) . 
Резюме
Короче говоря, начальный контекст делегирует реальную инициализацию контекста менеджеру имен, который делегирует его фабрике контекста. Фабрика контекста создается экземпляром начального компоновщика фабрики контекста.
  Мы создадим и установим отдельную реализацию JNDI.  Точкой входа в нашу автономную реализацию JNDI является класс JNDIUtil . 
Для включения JNDI без сервера приложений необходимы три вещи:
-   реализация интерфейсов ContextиInitialContextFactory,
-   реализация интерфейса InitialContextFactoryBuilder,
- первоначальная установка фабрики контекста и возможность очистки всех хранимых данных.
Контекст и Фабрика
Мы взяли реализацию SimpleJNDI из проекта osjava и изменили ее, чтобы она лучше соответствовала нашим потребностям. В проекте используется новая лицензия BSD .
Добавьте SimpleJNDI maven-зависимость в pom.xml:
| 01 02 03 04 05 06 07 08 09 10 11 12 13 14 | simple-jndisimple-jndi0.11.4.1 | 
  SimpleJNDI поставляется с контекстом MemoryContext который живет исключительно в памяти.  Он почти не требует настройки и его состояние никогда не сохраняется в загруженном состоянии.  Это делает почти то, что нам нужно, кроме двух вещей: 
-   метод close()удаляет все сохраненные данные,
- каждый экземпляр использует свое собственное хранилище по умолчанию.
  Большинство библиотек предполагают, что метод close оптимизирует ресурсы.  Они обычно называют это каждый раз, когда загружают или хранят данные.  Если метод close удаляет все данные сразу после их сохранения, контекст становится бесполезным.  Мы должны расширить класс MemoryContext и переопределить метод close : 
| 01 02 03 04 05 06 07 08 09 10 11 12 13 14 | @SuppressWarnings({"rawtypes"}) publicclassCloseSafeMemoryContext extendsMemoryContext { publicCloseSafeMemoryContext(Hashtable env) {  super(env); } @Override publicvoidclose() throwsNamingException {  // Original context lost all data on close();  // That made it unusable for my tests.  }} | 
По соглашению система сборки / фабрики создает новый экземпляр контекста для каждого использования. Если они не обмениваются данными, JNDI нельзя использовать для передачи данных между разными библиотеками.
  К счастью, эта проблема также имеет простое решение.  Если хеш-таблица среды содержит свойство "org.osjava.sj.jndi.shared" со значением "true" , созданный контекст памяти будет использовать общее статическое хранилище.  Поэтому наша первоначальная фабрика контекста создает экземпляры CloseSafeMemoryContext и настраивает их для использования общего хранилища: 
| 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 | publicclassCloseSafeMemoryContextFactory implementsInitialContextFactory { privatestaticfinalString SHARE_DATA_PROPERTY = "org.osjava.sj.jndi.shared"; publicContext getInitialContext(Hashtable environment) throwsNamingException {  // clone the environnement  Hashtable sharingEnv = (Hashtable) environment.clone();  // all instances will share stored data  if(!sharingEnv.containsKey(SHARE_DATA_PROPERTY)) {   sharingEnv.put(SHARE_DATA_PROPERTY, "true");  }    returnnewCloseSafeMemoryContext(sharingEnv);; }} | 
Начальный контекст Factory Builder
  Наш конструктор действует почти так же, как и оригинальная реализация менеджера имен.  Если во входящей среде присутствует свойство Context.INITIAL_CONTEXT_FACTORY указанная фабрика. 
  Однако если это свойство отсутствует, построитель создает экземпляр CloseSafeMemoryContextFactory .  Оригинальный менеджер именования выдает исключение. 
  Наша реализация интерфейса InitialContextFactoryBuilder : 
| 01 02 03 04 05 06 07 08 09 10 11 12 | publicInitialContextFactory createInitialContextFactory(Hashtable env) throwsNamingException { String requestedFactory = null; if(env!=null) {  requestedFactory = (String) env.get(Context.INITIAL_CONTEXT_FACTORY); } if(requestedFactory != null) {  returnsimulateBuilderlessNamingManager(requestedFactory); }  returnnewCloseSafeMemoryContextFactory();} | 
  Метод simulateBuilderlessNamingManager использует загрузчик классов для загрузки запрошенной фабрики контекста: 
| 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 | privateInitialContextFactory simulateBuilderlessNamingManager(String requestedFactory) throwsNoInitialContextException {  try{    ClassLoader cl = getContextClassLoader();    Class requestedClass = Class.forName(className, true, cl);    return(InitialContextFactory) requestedClass.newInstance();  } catch(Exception e) {    NoInitialContextException ne = newNoInitialContextException(...);    ne.setRootCause(e);    throwne;  }}privateClassLoader getContextClassLoader() { return(ClassLoader) AccessController.doPrivileged(newPrivilegedAction() {  publicObject run() {   returnThread.currentThread().getContextClassLoader();  } });} | 
Установка Builder и очистка контекста
  Наконец, мы должны установить конструктор контекста.  Поскольку мы хотели использовать автономный JNDI в тестах, нам также требовался метод очистки всех хранимых данных между тестами.  И то, и другое делается внутри метода initializeJNDI который будет запускаться перед каждым тестом: 
| 01 02 03 04 05 06 07 08 09 10 11 | publicclassJNDIUtil { publicvoidinitializeJNDI() {  if(jndiInitialized()) {   cleanAllInMemoryData();  } else{   installDefaultContextFactoryBuilder();  } }} | 
JNDI инициализируется, если уже был установлен конструктор фабрики контекста по умолчанию:
| 1 2 3 | privatebooleanjndiInitialized() {  returnNamingManager.hasInitialContextFactoryBuilder(); } | 
Установка стандартного контекстного фабричного компоновщика:
| 1 2 3 4 5 6 7 8 9 | privatevoidinstallDefaultContextFactoryBuilder() {  try{    NamingManager.setInitialContextFactoryBuilder(newImMemoryDefaultContextFactoryBuilder());  } catch(NamingException e) {    //We can not solve the problem. We will let it go up without    //having to declare the exception every time.    thrownewConfigurationException(e);  }} | 
  Используйте исходную реализацию метода close в классе MemoryContext для очистки хранимых данных: 
| 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 | privatevoidcleanAllInMemoryData() {  CleanerContext cleaner = newCleanerContext();  try{      cleaner.close();  } catch(NamingException e) {    thrownewRuntimeException("Memory context cleaning failed:", e);  }}classCleanerContext extendsMemoryContext {   privatestaticHashtable environnement = newHashtable();  static{    environnement.put("org.osjava.sj.jndi.shared", "true");  }  publicCleanerContext() {    super(environnement);  }} | 
Apache Derby — это реляционная база данных с открытым исходным кодом, реализованная на Java. Он доступен под лицензией Apache, версия 2.0. Дерби может работать во встроенном режиме. Данные встроенной базы данных хранятся либо в файловой системе, либо в памяти.
Maven зависимость для дерби:
| 01 02 03 04 05 06 07 08 09 10 11 12 13 14 | org.apache.derbyderby10.8.2.2 | 
Создать источник данных
  Используйте экземпляр класса EmbeddedDatasource для подключения к базе данных.  Источник данных будет использовать экземпляр в памяти всякий раз, когда имя базы данных начинается с «memory:». 
Следующий код создает источник данных, указывающий на экземпляр базы данных в памяти. Если база данных еще не существует, она будет создана:
| 1 2 3 4 5 6 7 8 | privateEmbeddedDataSource createDataSource() { EmbeddedDataSource dataSource = newEmbeddedDataSource(); dataSource.setDataSourceName(dataSourceJndiName); dataSource.setDatabaseName("memory:"+ databaseName); dataSource.setCreateDatabase("create"); returndataSource;} | 
Удалить базу данных
  Самый простой способ очистить базу данных — удалить и воссоздать ее.  Создайте экземпляр встроенного источника данных, установите для атрибута соединения «drop» значение «true» и вызовите его метод getConnection .  Это отбросит базу данных и выдаст исключение. 
| 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 | privatestaticfinalString DATABASE_NOT_FOUND = "XJ004"; privatevoiddropDatabase() {  EmbeddedDataSource dataSource = createDataSource();  dataSource.setCreateDatabase(null);  dataSource.setConnectionAttributes("drop=true");  try{   //drop the database; not the nicest solution, but works   dataSource.getConnection();  } catch(SQLNonTransientConnectionException e) {   //this is OK, database was dropped  } catch(SQLException e) {   if(DATABASE_NOT_FOUND.equals(e.getSQLState())) {    //attempt to drop non-existend database    //we will ignore this error    return;    }   thrownewConfigurationException("Could not drop database.", e);  } } | 
Мы использовали Liquibase для создания структуры базы данных и тестирования данных. Структура базы данных хранится в так называемом файле журнала изменений. Это XML-файл, но вы можете включить DDL или SQL-код, если вам не хочется изучать еще один XML-язык.
Liquibase и его преимущества выходят за рамки этой статьи. Наиболее значимым преимуществом этой демонстрации является возможность многократного запуска одного и того же журнала изменений для одной и той же базы данных. Каждый прогон применяет только новые изменения в базе данных. Если файл не изменился, ничего не происходит.
Вы можете добавить журнал изменений в jar или war и запускать его при каждом запуске приложения. Это обеспечит постоянное обновление базы данных до последней версии. Нет необходимости в настройке или установке скриптов.
Добавьте зависимость Liquibase в pom.xml:
| 01 02 03 04 05 06 07 08 09 10 11 12 13 14 | org.liquibaseliquibase-core2.0.3 | 
Следующий журнал изменений создает одну таблицу с именем Person и помещает в нее одну запись ‘slash — Simon Worth’:
| 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |  xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog/1.9 <changeSetid="1"author="meri">  <comment>Create table structure for users and shared items.</comment>  <createTabletableName="person">   <columnname="user_id"type="integer">    <constraintsprimaryKey="true"nullable="false"/>   </column>   <columnname="username"type="varchar(1500)">    <constraintsunique="true"nullable="false"/>   </column>   <columnname="firstname"type="varchar(1500)"/>   <columnname="lastname"type="varchar(1500)"/>   <columnname="homepage"type="varchar(1500)"/>   <columnname="about"type="varchar(1500)"/>  </createTable> </changeSet> <changeSetid="2"author="meri"context="test">  <comment>Add some test data.</comment>  <inserttableName="person">   <columnname="user_id"valueNumeric="1"/>   <columnname="userName"value="slash"/>   <columnname="firstName"value="Simon"/>   <columnname="lastName"value="Worth"/>   <columnname="about"value="I like nature and writing my blog. The blog contains my opinions about everything."/>  </insert> </changeSet></databaseChangeLog> | 
  Использование Liquibase довольно просто.  Используйте источник данных для создания нового экземпляра Liquibase , запустите его метод update и обработайте все объявленные исключения: 
| 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 | privatevoidinitializeDatabase(String changelogPath, DataSource dataSource) {  try{   //create new liquibase instance   Connection sqlConnection = dataSource.getConnection();   DatabaseConnection db = newDerbyConnection(sqlConnection);   Liquibase liquibase = newLiquibase(changelogPath, newFileSystemResourceAccessor(), db);   //update the database   liquibase.update("test");  } catch(SQLException e) {   // We can not solve the problem. We will let it go up without   // having to declare the exception every time.   thrownewConfigurationException(DB_INITIALIZATION_ERROR, e);  } catch(LiquibaseException e) {   // We can not solve the problem. We will let it go up without   // having to declare the exception every time.   thrownewConfigurationException(DB_INITIALIZATION_ERROR, e);  } } | 
Как автономный JNDI, так и встроенная база данных в памяти работают и работают каждый раз, когда мы запускаем наши тесты. Хотя настройка JNDI, вероятно, универсальна, для построения базы данных, вероятно, потребуются конкретные изменения проекта.
Не стесняйтесь загружать пример проекта с Github и использовать / изменять то, что вы считаете полезным.
Ссылка: Запуск JNDI и JPA без контейнера J2EE от нашего партнера по JCG Марии Юрковичовой в блоге This is Stuff .