我们的JPA配置需要两件事才能成功运行:
- 数据库来存储数据,
- JNDI访问数据库。
这篇文章分为两个部分。 第一部分显示了如何在测试中使用独立的JNDI和嵌入式内存数据库。 其余各章说明了该解决方案的工作方式。
所有使用的代码都可以在Github上找到 。 如果您对解决方案感兴趣,但不想阅读说明,请从Github下载项目,并仅阅读第一章。
JPA测试
本章说明如何在测试中使用我们的代码来启用独立的JNDI和嵌入式内存数据库。 本文的其余部分将说明该解决方案的工作方式和原因。
该解决方案具有三个“ API”类:
-
JNDIUtil
– JNDI初始化,清理和一些便捷方法, -
InMemoryDBUtil
–数据库和数据源的创建/删除, -
AbstractTestCase
–在第一次测试之前清理数据库,在每次测试之前清理JNDI。
我们使用Liquibase维护数据库结构。 如果您不想使用Liquibase,则必须自定义InMemoryDBUtil
类。 调整方法createDatabaseStructure
以执行所需的操作。
Liquibase将所有需要的数据库更改的列表保存在名为changelog的文件中。 除非另行配置,否则每个更改仅运行一次。 即使将更改日志文件多次应用于同一数据库。
用法
从AbstractTestCase
扩展的任何测试用例都将:
- 在第一次测试之前删除数据库,
- 在每次测试之前安装独立的JNDI或删除其中存储的所有数据,
- 每次测试之前,请对数据库运行Liquibase changelog。
JPA测试用例必须扩展AbstractTestCase
并重写getInitialChangeLog
方法。 该方法应返回changelog文件位置。
public class DemoJPATest extends AbstractTestCase {private static final String CHANGELOG_LOCATION = "src/test/java/org/meri/jpa/simplest/db.changelog.xml";private static EntityManagerFactory factory;public DemoJPATest() {}@Overrideprotected String getInitialChangeLog() {return CHANGELOG_LOCATION;}@Test@SuppressWarnings("unchecked")public void testJPA() {EntityManager em = factory.createEntityManager();Query query = em.createQuery("SELECT x FROM Person x");List<Person> allUsers = query.getResultList();em.close();assertFalse(allUsers.isEmpty());}@BeforeClasspublic static void createFactory() {factory = Persistence.createEntityManagerFactory("Simplest");}@AfterClasspublic static void closeFactory() {factory.close();}}
注意:在每次测试之前删除数据库会更清洁。 但是,删除和重新创建数据库结构是昂贵的操作。 这会大大降低测试用例的速度。 仅在上课之前这样做似乎是一种合理的妥协。
虽然数据库仅删除一次,但更改日志在每次测试之前运行。 可能看起来很浪费,但是此解决方案具有一些优势。 首先, getInitialChangeLog
方法不必是静态的,并且可以在每个测试中覆盖。 其次,配置为“ runAlways”的更改将在每次测试之前运行,因此可能包含一些廉价的清理或其他初始化操作。
日本国家发展研究院
本章说明什么是JNDI,如何使用它以及如何配置它。 如果您对理论不感兴趣,请跳至下一章。 在此创建独立的JNDI。
基本用法
JNDI允许客户端通过名称存储和查找数据和对象。 通过接口Context
的实现访问数据存储。
以下代码显示了如何在JNDI中存储数据:
Context ctx = new InitialContext();
ctx.bind("jndiName", "value");
ctx.close();
第二段代码显示了如何在JNDI中查找内容:
Context ctx = new InitialContext();
Object result = ctx.lookup("jndiName");
ctx.close();
尝试在没有J2EE容器的情况下运行以上代码,您将得到一个错误:
javax.naming.NoInitialContextException: Need to specify class name in environment or system property, or as an applet parameter, or in an application resource file: java.naming.factory.initialat 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
类只能找到Context
接口的另一个实例,并将所有工作委托给它。 它既无法存储数据也无法找到它们。
上下文工厂
真正的上下文,即完成所有工作并能够存储/查找数据的上下文,必须由上下文工厂创建。 本节说明如何创建上下文工厂以及如何配置InitialContext
以使用它。
每个上下文工厂必须实现InitialContextFactory
接口,并且必须具有无参数构造函数:
package org.meri.jpa.jndi;public class MyContextFactory implements InitialContextFactory {@Overridepublic Context getInitialContext(Hashtable environment) throws NamingException {return new MyContext();}}
我们的工厂返回一个简单的上下文,称为MyContext
。 其lookup
方法始终返回字符串“存储值”:
class MyContext implements Context {@Overridepublic Object lookup(Name name) throws NamingException {return "stored value";}@Overridepublic Object lookup(String name) throws NamingException {return "stored value";}.. the rest ...
}
JNDI配置在哈希表中的类之间传递。 键始终包含属性名称,而值包含属性值。 由于初始上下文构造函数InitialContext()
没有参数,因此假定为空哈希表。 该类还有一个替代构造函数,该构造函数将配置属性哈希表作为参数。
使用属性"java.naming.factory.initial"
来指定上下文工厂类名称。 该属性在Context.INITIAL_CONTEXT_FACTORY
常量中定义。
Hashtable env = new Hashtable();
env.put(Context.INITIAL_CONTEXT_FACTORY, "className");Context ctx = new InitialContext(environnement);
下一步测试配置MyContextFactory
并检查创建的初始上下文是否返回“存储值”,无论如何:
@Test
@SuppressWarnings({ "unchecked", "rawtypes" })
public void testDummyContext() throws NamingException {Hashtable environnement = new Hashtable();environnement.put(Context.INITIAL_CONTEXT_FACTORY, "org.meri.jpa.jndi.MyContextFactory");Context ctx = new InitialContext(environnement);Object value = ctx.lookup("jndiName");ctx.close();assertEquals("stored value", value);
}
当然,仅当您可以将具有自定义属性的哈希表提供给初始上下文构造函数时,此方法才有效。 这通常是不可能的。 大多数库使用开头所示的无参数构造函数。 他们假定初始上下文类具有可用的默认上下文工厂,并且无参数构造函数将使用该默认工厂。
命名经理
初始上下文使用NamingManager
创建真实上下文。 命名管理器具有静态方法getInitialContext(Hashtable env)
,该方法返回上下文的实例。 参数env
包含用于构建上下文的配置属性。
默认情况下,命名管理器从env
哈希表中读取Context.INITIAL_CONTEXT_FACTORY
并创建指定的初始上下文工厂的实例。 然后,工厂方法将创建一个新的上下文实例。 如果未设置该属性,则命名管理器将引发异常。
可以自定义命名管理员的行为。 NamingManager
类具有setInitialContextFactoryBuilder
方法。 如果设置了初始上下文工厂构建器,则命名管理器将使用它来创建上下文工厂。
您只能使用此方法一次。 已安装的上下文工厂生成器无法更改。
try {MyContextFactoryBuilder builder = new MyContextFactoryBuilder();NamingManager.setInitialContextFactoryBuilder(builder);
} catch (NamingException e) {// handle exception
}
初始上下文工厂构建器必须实现InitialContextFactoryBuilder
接口。 界面很简单。 它只有一个方法InitialContextFactory createInitialContextFactory(Hashtable env)
。
摘要
简而言之,初始上下文将实际的上下文初始化委托给命名管理器,命名管理器将其委托给上下文工厂。 上下文工厂由初始上下文工厂构建器的实例创建。
我们将创建并安装独立的JNDI实现。 我们的独立JNDI实现的入口点是JNDIUtil
类。
在没有应用程序服务器的情况下启用JNDI需要三件事:
-
Context
和InitialContextFactory
接口的实现, -
InitialContextFactoryBuilder
接口的实现, - 初始上下文工厂构建器的安装以及清除所有存储数据的能力。
上下文和工厂
我们从osjava项目中获取了SimpleJNDI实现,并对其进行了修改以更好地满足我们的需求。 该项目使用了新的BSD许可证 。
将SimpleJNDI maven依赖项添加到pom.xml中:
simple-jndisimple-jndi0.11.4.1
SimpleJNDI带有一个MemoryContext
上下文,该上下文仅位于内存中。 它几乎不需要任何配置,并且其状态永远不会保存下来。 它几乎满足了我们的需求,除了两件事:
- 它的
close()
方法删除所有存储的数据, - 每个实例默认使用其自己的存储。
大多数库都假定close方法优化了资源。 他们倾向于在每次加载或存储数据时调用它。 如果close方法在存储完所有数据后立即删除它们,则上下文将无用。 我们必须扩展MemoryContext
类并重写close
方法:
@SuppressWarnings({"rawtypes"})
public class CloseSafeMemoryContext extends MemoryContext {public CloseSafeMemoryContext(Hashtable env) {super(env);}@Overridepublic void close() throws NamingException {// Original context lost all data on close();// That made it unusable for my tests. }}
按照约定,建造者/工厂系统会为每次使用创建新的上下文实例。 如果它们不共享数据,则不能使用JNDI在不同库之间传输数据。
幸运的是,这个问题也很容易解决。 如果环境哈希表包含值为"true"
属性"org.osjava.sj.jndi.shared"
"true"
,则创建的内存上下文将使用公共静态存储。 因此,我们的初始上下文工厂将创建CloseSafeMemoryContext
实例,并将其配置为使用公共存储:
public class CloseSafeMemoryContextFactory implements InitialContextFactory {private static final String SHARE_DATA_PROPERTY = "org.osjava.sj.jndi.shared";public Context getInitialContext(Hashtable environment) throws NamingException {// clone the environnementHashtable sharingEnv = (Hashtable) environment.clone();// all instances will share stored dataif (!sharingEnv.containsKey(SHARE_DATA_PROPERTY)) {sharingEnv.put(SHARE_DATA_PROPERTY, "true");}return new CloseSafeMemoryContext(sharingEnv);;}}
初始上下文工厂生成器
我们的构建器的行为几乎与原始命名管理器实现相同。 如果传入环境中存在属性Context.INITIAL_CONTEXT_FACTORY
,则将创建指定的工厂。
但是,如果缺少此属性,则构建器将创建CloseSafeMemoryContextFactory
的实例。 原始的命名管理器将引发异常。
我们对InitialContextFactoryBuilder
接口的实现:
public InitialContextFactory createInitialContextFactory(Hashtable env) throws NamingException {String requestedFactory = null;if (env!=null) {requestedFactory = (String) env.get(Context.INITIAL_CONTEXT_FACTORY);}if (requestedFactory != null) {return simulateBuilderlessNamingManager(requestedFactory);}return new CloseSafeMemoryContextFactory();
}
方法simulateBuilderlessNamingManager
使用类加载器加载请求的上下文工厂:
private InitialContextFactory simulateBuilderlessNamingManager(String requestedFactory) throws NoInitialContextException {try {ClassLoader cl = getContextClassLoader();Class requestedClass = Class.forName(className, true, cl);return (InitialContextFactory) requestedClass.newInstance();} catch (Exception e) {NoInitialContextException ne = new NoInitialContextException(...);ne.setRootCause(e);throw ne;}
}private ClassLoader getContextClassLoader() {return (ClassLoader) AccessController.doPrivileged(new PrivilegedAction() {public Object run() {return Thread.currentThread().getContextClassLoader();}});
}
构建器安装和上下文清理
最后,我们必须安装上下文工厂生成器。 当我们想在测试中使用独立的JNDI时,我们还需要一种方法来清除测试之间的所有存储数据。 两者都在initializeJNDI
方法内部完成,该方法将在每次测试之前运行:
public class JNDIUtil {public void initializeJNDI() {if (jndiInitialized()) {cleanAllInMemoryData();} else {installDefaultContextFactoryBuilder();}}}
如果已经设置了默认上下文工厂生成器,那么将初始化JNDI:
private boolean jndiInitialized() {return NamingManager.hasInitialContextFactoryBuilder();}
安装默认上下文工厂生成器:
private void installDefaultContextFactoryBuilder() {try {NamingManager.setInitialContextFactoryBuilder(new ImMemoryDefaultContextFactoryBuilder());} catch (NamingException e) {//We can not solve the problem. We will let it go up without//having to declare the exception every time.throw new ConfigurationException(e);}
}
使用原始的方法实现close
在MemoryContext
类清理存储数据:
private void cleanAllInMemoryData() {CleanerContext cleaner = new CleanerContext();try {cleaner.close();} catch (NamingException e) {throw new RuntimeException("Memory context cleaning failed:", e);}
}class CleanerContext extends MemoryContext {private static Hashtable environnement = new Hashtable();static {environnement.put("org.osjava.sj.jndi.shared", "true");}public CleanerContext() {super(environnement);}}
Apache Derby是用Java实现的开源关系数据库。 根据Apache许可证2.0版提供。 Derby能够以嵌入式模式运行。 嵌入式数据库数据存储在文件系统或内存中。
对Derby的Maven依赖关系:
org.apache.derbyderby10.8.2.2
创建数据源
使用EmbeddedDatasource
类的实例连接到数据库。 每当数据库名称以“ memory:”开头时,数据源将使用一个内存中实例。
以下代码创建指向内存数据库实例的数据源。 如果数据库尚不存在,将创建它:
private EmbeddedDataSource createDataSource() {EmbeddedDataSource dataSource = new EmbeddedDataSource();dataSource.setDataSourceName(dataSourceJndiName);dataSource.setDatabaseName("memory:" + databaseName);dataSource.setCreateDatabase("create");return dataSource;
}
删除数据库
清理数据库的最简单方法是删除并重新创建它。 创建嵌入式数据源的实例,将连接属性“ drop”设置为“ true”,并调用其getConnection
方法。 它将删除数据库并引发异常。
private static final String DATABASE_NOT_FOUND = "XJ004";private void dropDatabase() {EmbeddedDataSource dataSource = createDataSource();dataSource.setCreateDatabase(null);dataSource.setConnectionAttributes("drop=true");try {//drop the database; not the nicest solution, but worksdataSource.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 errorreturn ; }throw new ConfigurationException("Could not drop database.", e);}}
我们使用Liquibase创建数据库结构和测试数据。 数据库结构保存在所谓的变更日志文件中。 它是一个xml文件,但是如果您不想学习另一种xml语言,则可以包含DDL或SQL代码。
Liquibase及其优点不在本文讨论范围之内。 此演示最相关的优势是它能够对同一数据库多次运行同一变更日志。 每次运行仅将新更改应用于数据库。 如果文件未更改,则什么都不会发生。
您可以将更改日志添加到jar或war中,并在每次启动应用程序时运行它。 这样可以确保数据库始终更新为最新版本。 无需配置或安装脚本。
将Liquibase依赖项添加到pom.xml:
org.liquibaseliquibase-core2.0.3
在更新日志之后,将创建一个名为Person的表,并将一个条目“斜杠– Simon Worth”放入其中:
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog/1.9"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog/1.9
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-1.9.xsd"><changeSet id="1" author="meri"><comment>Create table structure for users and shared items.</comment><createTable tableName="person"><column name="user_id" type="integer"><constraints primaryKey="true" nullable="false" /></column><column name="username" type="varchar(1500)"><constraints unique="true" nullable="false" /></column><column name="firstname" type="varchar(1500)"/><column name="lastname" type="varchar(1500)"/><column name="homepage" type="varchar(1500)"/><column name="about" type="varchar(1500)"/></createTable></changeSet><changeSet id="2" author="meri" context="test"><comment>Add some test data.</comment><insert tableName="person"><column name="user_id" valueNumeric="1" /><column name="userName" value="slash" /><column name="firstName" value="Simon" /><column name="lastName" value="Worth" /><column name="homePage" value="http://www.slash.blogs.net" /><column name="about" value="I like nature and writing my blog. The blog contains my opinions about everything." /></insert></changeSet></databaseChangeLog>
Liquibase的使用非常简单。 使用数据源创建新的Liquibase
实例,运行其update
方法并处理所有声明的异常:
private void initializeDatabase(String changelogPath, DataSource dataSource) {try {//create new liquibase instanceConnection sqlConnection = dataSource.getConnection();DatabaseConnection db = new DerbyConnection(sqlConnection);Liquibase liquibase = new Liquibase(changelogPath, new FileSystemResourceAccessor(), db);//update the databaseliquibase.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.throw new ConfigurationException(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.throw new ConfigurationException(DB_INITIALIZATION_ERROR, e);}}
每次我们运行测试时,独立的JNDI数据库和嵌入式内存数据库都已启动并正在运行。 尽管JNDI设置可能是通用的,但数据库的构建可能需要对项目进行特定的修改。
可以从Github上免费下载示例项目,并使用/修改任何有用的内容。
参考: This is Stuff博客上的JCG合作伙伴 Maria Jurcovicova的JNDI和JPA Without J2EE Container运行 。
翻译自: https://www.javacodegeeks.com/2012/04/jndi-and-jpa-without-j2ee-container.html