我們的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 Con??tainer運行 。
翻譯自: https://www.javacodegeeks.com/2012/04/jndi-and-jpa-without-j2ee-container.html