Hibernate och JBoss.
Publicerad 2004-10-12
Inledning
I denna avslutande del i min artikelserie beskriver jag det slutliga elddopet för mitt försök att implementera en design där persistensmekanismen kan bytas ut utan att klientkoden behöver skrivas om.
Ambitionen var att flytta persistenshantering till en J2EE-server och ge webappen tillgång till denna via en fasad i form av en sessionsböna. Servern jag valde var JBoss 4.0, en open-source server, flitigt använd av Javautvecklare och med en ökande marknadsandel.
DAO och EJB i JBoss
Tanken var dels att simulera en distribuerad applikation, där själva webbappen snurrar i en JVM medan persistenshantering i JBoss snurrar i en JVM, teoretiskt på en annan burk, dels att visa på hur en deklarativ transaktionshantering kan kombineras med Hibernate. Designen som sådan är inte svår att ifrågasätta eftersom det inte finns något i min lilla testapplikation som på något sätt motiverar en distribuerad lösning men vi sparar den diskussionen till ett annat tillfälle.
Det jag ville testa var om min minidesign, utan förändringar av klientkoden, skulle kunna hantera att persistensen flyttades även rent fysiskt enligt följande schematiska bild.

Jag behövde alltså göra följande:
- skapa en sessionsböna - MailReaderSessionBean - som pratar med en implementering av MailReaderDao. Denna server DAO är väldigt lik den som användes i den icke-distribuerade lösning.
- fixa en speciell DAO på klientsidan som letar upp sessionbönan och använder denna för att hantera persistensen.
Detta ledde fram till följande design:
MailReaderSessionBean får tillgång till sitt DAO-objekt i setSessionContext:
...
public void setSessionContext(SessionContext newContext) throws EJBException
{
try {
mailReaderDao = new HibernateEjbMailReaderDaoImpl();
} catch (Exception e) {
throw new EJBException("Internal server error: Unable to create mailreader dao",e);
}
}
...
Sessionsbönans protokoll är i stort identiskt med DAO-protokollet. Det enda som skiljer är den typ av undantag som kastas.
...
/**
* @ejb.interface-method view-type = "remote"
*
* @throws EJBException Thrown if the instance could not perform
* the function requested by the container because of an system-level error.
/
public User findUser(String username) throws EJBException
{
return mailReaderDao.findUser(username);
}
...
HibernateEjbMailReaderDaoImpl får det mesta av sin funktionalitet på köpet genom arv från AbstractHibernateMailReaderDaoImpl men några detaljer kan vara värda att nämna:
⇒ JBoss (och EJB) ger oss möjlighet till en deklarativ transaktionshantering, dvs vi kan i deployment descriptorn för vår böna ange att kontainern skall hantera transaktionerna. Detta innebär att metoderna doInTransaction och doQuery endast behöver sköta sessionshantering, dvs ingen transaktionskod.
...
protected void doInTransaction(TransactionCallback callback) throws DataAccessException
{
Session session = null;
try
{
session = openSession();
callback.execute(session);
session.flush();
}
catch (Exception e)
{
log.error("Exception in doInTransaction!",e);
throw new DataAccessException(callback.getErrorMessage(),e);
}
finally
{
closeSession(session);
}
}
...
⇒ Hibernates "lazy load" fungerar inte i en distribuerad miljö eftersom all data måste ha laddats av Hibernate innan objektet kan serialiseras över till klienten. För att undvika att undantag kastas i klienten måste vi alltså se till att kollektionen med användarens prenumerationer laddas in samtidigt med användaren. Ett enkelt sätt att fixa detta på är följande:
...
protected User findUser(Long id, Session session) throws HibernateException
{
User user = super.findUser(id, session);
if (user != null)
Hibernate.initialize(user); // Load subscriptions;
return user;
}
...
Klientsidans DAO, MailReaderEJBDaoImpl använder sessionbönan för att komma åt persistensoperationerna i applikationservern. För att detta skall vara möjligt måste den först få tag i home-bönan. Detta görs genom en vanlig namnuppslagning i konstruktorn
...
public MailReaderEJBDaoImpl()
{
try
{
Hashtable env = new Hashtable();
env.put(Context.INITIAL_CONTEXT_FACTORY,"org.jnp.interfaces.NamingContextFactory");
env.put(Context.PROVIDER_URL, "localhost:1099");
env.put("java.naming.factory.url.pkgs","org.jboss.naming");
context = new InitialContext(env);
mailReaderSessionHome = getHome();
}
catch (Exception NamingException)
{
throw new RuntimeException("Internal server error: Unable to lookup MailReaderSessionHome!");
}
}
...
private MailReaderSessionHome getHome() throws NamingException
{
Object objref = context.lookup("MailReader");
return (MailReaderSessionHome) PortableRemoteObject.narrow(objref, MailReaderSessionHome.class);
}
}
...
De hårdkodade literalerna är JBoss specifika JNDI egenskaper, jag skulle naturligtvis lika gärna ha kunnat deklarera dessa i en jndi.properties fil.
Home-bönan används sedan för att få tag i en sessionsböna.
...
private MailReaderSession getBean() throws DataAccessException
{
try
{
return mailReaderSessionHome.create();
}
catch (Exception e)
{
mailReaderSessionHome = recreateHome();
try
{
return mailReaderSessionHome.create();
}
catch (Exception e1)
{
throw new DataAccessException("Internal server error: Unable to create session bean!",e1);
}
}
}
private MailReaderSessionHome recreateHome() throws DataAccessException
{
MailReaderSessionHome home = null;
int count = 0;
while(count++ < 3)
{
try
{
home = getHome();
break;
}
catch (NamingException e)
{
if (count >= 3)
throw new DataAccessException("Internal server error:
Unable to recreate reference to home bean",e);
}
}
return home;
}
public User findUser(String username) throws DataAccessException
{
try
{
return getBean().findUser(username);
}
catch (RemoteException e)
{
throw new DataAccessException("Internal server error: Unable to access session bean!");
}
}
...
Som synes så cachas en referens till vår hemböna i en instansvariabel. I händelse av en omstart av JBoss (kanske också om EJB modulen deployas om?) så blir denna referens inte längre valid och en ny namnuppslagning måste göras, därav metoden recreateHome. (Empiriskt kom jag fram till att detta inte tar första gången (åtminstone inte i JBoss) vilket förklarar räknaren.)
Hur bygger vi nu ihop detta till en fungerande, distribuerad applikation?
Under utvecklingen vill jag kunna starta och stoppa JBoss samt deploya min böna inifrån Eclipse. Själv så använder jag MyEclipse, en kommersiell (men billig!) plugin för att underlätta utveckling av J2EE applikationer. MyEclipse ger bland annat möjlighet att konfigurera ett stort antal applikationsservrar, inte bara JBoss, och installerar menyval för att starta och stoppa servrarna.
För att underlätta administrationen under utveckling så bröt jag ut min EJB böna i ett eget projekt i Eclipse.

- Mappen
jboss innehåller de filer som antingen manuellt eller via Ant måste placeras i JBoss. Exempelvis mysql-connector-java-3.0.9-stable-bin.jar som innehåller jdbc-drivrutiner för MySQL och som måste läggas i serverns lib-bibliotek.
- Mappen
EJB-module innehåller EJB- och JBoss-specifika filer. MyEclipse ser till att allt som finns i den eller de mappar vi angett som klassfilsmappar deployas som ett "exploded archive", dvs som en uppackad EJB-jar, till den server vi har valt. Genom att ange EJB-module som en källskodsmapp säkerställer vi att även dessa filer kommer med.
- alla källkodsmappar utom
EJB-module är länkar till källkodsbibliotek i grundprojektet.
- Mappen
EJB-module-log4j innehåller ytterligare filer som måste tas med i deployen om vi vill använda Log4J för loggning och själva styra över loggningen. Mer om detta längre fram i artikeln.
J2EE gillar xml och det finns ett antal xml-filer som måste finnas med på rätt ställe:
⇒ jboss-hibernate-cfg.xml
...
<hibernate-configuration>
<session-factory>
<property name="dialect">net.sf.hibernate.dialect.MySQLDialect</property>
<property name="connection.datasource">java:/jdbc/mysql/mailreader</property>
<property name="connection.driver_class">com.mysql.jdbc.Driver</property>
<property name="connection.url">jdbc:mysql://localhost/mailreader</property>
<property name="connection.username">admin</property>
<property name="connection.password">password</property>
<property name="transaction.factory_class">
net.sf.hibernate.transaction.JDBCTransactionFactory
</property>
<property name="transaction.manager_lookup_class">
net.sf.hibernate.transaction.JBossTransactionManagerLookup
</property>
<!-- Mapping files -->
<mapping resource="se/bluefish/jsf/example/dao/hibernate/UserImpl.hbm.xml"/>
<mapping resource="se/bluefish/jsf/example/dao/hibernate/SubscriptionImpl.hbm.xml"/>
</session-factory>
</hibernate-configuration>
...
Denna konfigurationsfil är snrlik den som vi använde på klienten. Några skillnader är:
<property name="connection.datasource">java:/jdbc/mysql/mailreader</property> som anger den datakälla som Hibernate kommer att använda. Själva definitionen av datakällan görs i en annan xml-fil, mysql-ds.xml, som måste ligga i serverns deploybibliotek.
<property name="transaction.factory_class">net.sf.hibernate.transaction.JDBCTransactionFactory</property> som anger vilken typ av transaktionshantering som skall användas.
⇒ META-INF/ejb-jar.xml som är en deployment descriptor för vår sessionsböna.
<ejb-jar>
<description>
Remote EJB definition for the MailReader application
</description>
<display-name>mailreader</display-name>
<enterprise-beans>
<session>
<ejb-name>MailReader</ejb-name>
<home>se.bluefish.jsf.example.ejb.MailReaderSessionHome</home>
<remote>se.bluefish.jsf.example.ejb.MailReaderSession</remote>
<ejb-class>se.bluefish.jsf.example.ejb.MailReaderSessionBean</ejb-class>
<session-type>Stateless</session-type>
<transaction-type>Container</transaction-type>
</session>
</enterprise-beans>
<assembly-descriptor>
<container-transaction>
<method>
<ejb-name>MailReader</ejb-name>
<method-intf>Local</method-intf>
<method-name>doInTransaction</method-name>
</method>
<trans-attribute>Required</trans-attribute>
</container-transaction>
</assembly-descriptor>
</ejb-jar>
Här kan vi se att namnet på bönan som används vid namnuppslagning är MailReader, att transaktionshanteringen sköts av JBoss och att alla metoder måste ligga i en transaktion.
För att slutligen få allt att fungera tillsammans krävs också en ändring i web.xml så att vår nya klient-DAO installeras:
...
<!-- Remove when not using EJB for persistence -->
<context-param>
<param-name>dao</param-name>
<param-value>se.bluefish.jsf.example.dao.ejb.MailReaderEJBDaoImpl</param-value>
<description>
Read by the MailReaderDaoInitializer to create the
right type of data access implementation.
</description>
</context-param>
<listener>
<listener-class>se.bluefish.jsf.example.dao.MailReaderDaoInitializer</listener-class>
</listener>
...
Brasklapp!
Som jag tidigare påpekat är detta knappast ett exempel på en applikation som lämpar sig för en distribuerad arkitektur! En av de stora fördelarna med att använda en sessionsböna som fasad mot persistenshanteringen är möjligheten att använda en deklarativ transaktionshantering. För att detta skall fungera är det inget krav att använda en distribuerad lösning. Det fungerar lika bra (bättre!) även om webbapp och sessionsböna snurrar i samma JVM. Då är det dessutom möjligt att använda lokala interface i stället för remote sådana vilket ger stora prestandafördelar.
För att kunna använda deklarativ transaktionshantering är det inte ens ett krav att använda EJB och en J2EE server. Spring är ett ramverk, en s.k "light weight container", som erbjuder deklarativ transaktionshantering genom AOP. Spring ligger bra till för att bli föremål för en framtida artikel!
Loggning i JBoss
JBoss använder själv Log4J för sin loggning. Konfigurering för Log4J anges i serverns conf/log4j.xml och det absolut enklaste sättet att få ut sin egen loggninginformation är att inte göra någonting alls! Den egna modulen/applikationens loggning hamnar då i serverns loggfil. Inget större arbete behöver läggas ner men det kan vara svårt att skilja ut den egna loggningen från den myriad av information som servern som default sprutar ut sig.
För att få ut loggningen i en egen fil så måste följande läggas till i JBoss log4j.xml:
...
<appender name="MAILREADER" class="biz.minaret.log4j.DatedFileAppender">
<param name="Directory" value="${jboss.server.home.dir}/log"/>
<param name="Prefix" value="mail_reader."/>
<param name="Suffix" value=".log."/>
<layout class="org.apache.log4j.PatternLayout" >
<param name="ConversionPattern" value="%d %-5p [%t] %c - %m%n"/>
</layout>
</appender>
<category name="hibernate.persistens" additivity="false">
<priority value="DEBUG"/>
<appender-ref ref="MAILREADER"/>
</category>
...
Detta ser till att loggningen för vår EJB-modul hamnar i en egen fil i serverns loggningsbibliotek. En ny fil kommer att skapas för varje dag och namnsättas mail_reader.yyyy-MM-dd.log. Observera att eftersom en appender, DatedFileAppender, som inte följer med Log4J används måste jarfilen som innehåller denna läggas i serverns lib-bibliotek.
Denna lösning fungerar ok men har den nackdelen att inte vara självbärande, dvs för att din EJB-modul skall fungera helt och fullt räcker det inte med deployment och att vissa filer måste läggas på rätt plats i JBoss filstruktur, utan dessutom måste JBoss egen fil modifieras.
Vad måste vi då göra för att kunna definiera vår loggning i en egen log4j.xml som kan deployas på vanligt sätt?
Det är tyvärr inte så enkelt som att bara inkludera en log4j.xml i den egna modulen och tro att den skall användas. JBoss klassladdning sätter här käppar i hjulet för oss genom att Log4J först initieras av JBoss själv och därför letar efter sin konfigurationsfil i JBoss conf-bibliotek. För att få det att fungera som vi vill måste följande göras:
⇒ en egen log4j.xml måste läggas i EJB modulens rot
⇒ Log4J:s jarfil, i vårt fall log4-1.2.8.jar, måste också läggas i roten
⇒ använder vi Commons Logging så måste även commons-logging.jar läggas i roten
⇒ jboss.xml med följande innehåll måste läggas till i META-INF:
...
<loader-repository>
mailreader.test:loader=MailReader-EJB.jar
<loader-repository-config>
java2ParentDelegation=false
</loader-repository-config>
</loader-repository>
...
Tillägget i jboss.xml överrider JBoss klassladdning och ser till att vår "egen" Log4J initieras i stället för den som ligger i JBoss. Det faktum att vi överrider klassladdningen resulterar dock i icke önskvärda bieffekter som yttrar sig genom ett antal exceptions som exempelvis org.apache.commons.logging.LogConfigurationException: Class org.apache.commons.logging.impl.Log4JLogger does not implement Log.
Anledningen till dessa är att JBoss nu inte kan hitta klasser som används i vår kod som den tidigare kunde hämta från jarfilerna i det egna libbiblioteket, alternativt hittar klasser som laddats med en annan klassladdare. Framför allt är det Hibernate, som i JBoss 4.0 följer med JBoss, som spökar här. Hibernate använder själv Commons Logging och Log4J för sin interna loggning. Första gången det skall loggas i Hibernate hittar JBoss "fel" commons-loggging.jar och undantaget ovan kastas. Genom att inkludera Hibernates jarfiler i vår egen modul, dvs hibernate.jar och ehcache.jar försvinner ovanstående problem.
Min åsikt, ( under förutsättning att jag har förstått problemet korrekt!), är att det faktum att vi vill definiera vår loggning i en egen log4j.xml förorsakar mer problem än det är värt. Alltså, bit i det sura äpplet och modifiera JBoss egen log4j.xml och förenkla därigenom resten av hantering högst avsevärt.
För den som vill experimentera finns alla filer som behövs för att den egenkonfigurerade loggningen skall fungera i EJB-module-log4j. Det enda som behövs göras för att testa detta är att addera denna som källkodsmapp i ditt Eclipseprojekt och redeploya till JBoss.
Sammanfattning
Detta blev en längre artikelserie än jag hade tänkt mig från början och dess scope blev också mera omfattande än vad som var avsikten. Den beskriver som tidigare sagts inte i detalj någon av de designprinciper och open-sourceprojekt som används utan mera tankar och problem som dök upp under resans gång. Vad jag hoppas att den har visat är bland annat, med hänsyn naturligtvis till att den använda applikationen inte är av någon mera komplicerad art :-),
- det krävs inget under av design för att fixa en arkitektur som är någorlunda robust och som isolerar valet av persistensmekanism från klientkod
- några tips för att förbättra användningen av enhetstester
- ett enkelt sätt att använda en applikationsserver för att få deklarativ transaktionshantering utan att det nödvändigtvis måste påverka den klientkod som skrivs
/Lars Svadängs
|