Den här websidan ser mycket bättre ut i en bläddrare som följer rådande webstandard, men du kan titta på sidan med andra bläddrare också.

Bluefish :: Akvariet

Bluefish logo
Logo

Java Server Faces

Java Server Faces och enhetstester

Java Server Faces och Hibernate

Hibernate och JBoss

Lunar Linux and Zepto 4200

Att delta i projekt med öppen källkod

Java-projekten i Apache

Hibernate - en introduktion

HemsideByggaren under huven

Öppen källkod på Bluefish

JavaOne 2003

Lime

OOPSLA 2002

AOP

fixafest.nu under huven

Linux From Scratch

Bluefish skänker

XSL

JDepper

OOPSLA 1999

Java Server Faces och enhetstester

Publicerad 2004-10-12

Bakgrund

När min artikel om Java Server Faces (JSF) var klar så kände jag ett behov av att ta den kod som som skrivits under arbetet med artikeln och låta den ligga till grund ytterligare artiklar. Dessa artiklar skulle beskriva glädjen (och vedermödorna) med att vidareutveckla designen i den tämligen enkla testapplikationen till något som på ett bättre sätt underlättade:

  • enhetstestning av koden
  • byte av perssistensmekanism. Den persistensmekanism som jag primärt var intresserad av var, förutom den som byggde på xml-filer i filsystemet som jag ärvde från strutsapplikationen, Hibernate. Ambitionen var att använda Hibernate dels direkt ifrån webappen, dels göra en distribuerad lösning där persistensen sköttes av Hibernate i en J2EE server (JBoss).

Under vägen dit får jag dessutom möjlighet att beskriva:

  • hur "mock-objekt" och "stub-objekt" kan användas för att testa kod utanför dess egentliga runtime-miljö
  • hur man på ett enkel sätt kan få fram hur stor del av koden som genomlöps av testfallen, dvs "code coverage"
  • loggning mha commons-logging och Log4j i Tomcat/JBoss

Syftet med artikelserien är inte att i detalj gå igenom hur man använder de olika tekniker som presenteras utan mera att beskriva problem och tankar som dök upp under arbetets gång. I den löpande texten finns det länkar till intressanta sajter och artiklar med mera utförligt material.

I den första delen beskriver jag den grundläggande designen samt de grepp jag fick ta till för att kunna enhetstesta min applikation.

Inledning

Angreppssättet för min artikel om JSF var att ta en enkel strutsapplikation och "ersätta" Struts med JSF men utöver detta göra så få förändringar som möjligt. Strutsapplikation som jag valde var MailReader, en exempelapp som följer med Strutsdistributionen. MailReader är en enkel webbapp för registrering och administration av e-postkonton.

Diverse omknådande av koden slutade i följande enkla objektmodell:

Uploaded Image:jsf-example-classes-small.png
  • webblagret utgörs, förutom av ett antal jsp-sidor, av två JSF "backing-beans", UserBean och SubscriptionBean.
  • domänmodellen består av två interface, User och Subscription
  • persistenslagret utgörs av ett interface - UserDatabase - som definierar de metoder som används för att läsa och skriva domänmodellen samt implementationer av detta interface och persistensspecifika domänobjekt som implementerar User respektive Subscription.

Redan här finns en skiktning som bådar gott - webblagret delegerar all persistenshantering till persistenslagret och all kommunikation sker via interface.

Min första åtgärd var av rent semantisk karaktär, jag bytte namn på UserDatabase till MailReaderDao där Dao står för DataAccessObject.

"Data access object" eller DAO är ett designmönster vars uppgift är att abstrahera och kapsla in all åtkomst till datalagringen. Tanken är att ett DAO skall hindra att den metod du har valt för datalagringen lyser igenom i din klientkod. Exempelvis så skall ett DAO kasta undantag som inte är datalagringsspecifika utan av mera generell karaktär, dvs inga SQLExceptions eller HibernateExceptions skall behöva fångas upp av klientkoden.

Jag insåg också att MemoryUser och MemorySubscription kunde göras till helt generalla domänklasser och döpte om dem till UserImpl respektive SubscriptionImpl.

Uploaded Image:mailreader-dao-small.png

Metoderna i MailReaderDao kastar en generell DataAccessException. Detta innebär att konkreta DAO-implementeringar måste se till att fånga alla persistensspecifika undantag och omvandla dessa till denna mera generella typ av undantag. En mera genomarbetad design skulle säkert ha en mera komplett undantagshierarki så att klientkod på ett korrekt sätt skulle kunna fånga upp och hantera olika typer av undantag som bottnar i persistensproblem.

För att installera mitt DAO-objekt vid uppstart använde jag en MailReaderDaoInitializer, en servlet context listener som definieras i web.xml. Lyssnaren instansieras och körs i samband med att webappen startar upp och ansvarar för att instansiera DAO-objektet och stoppa in detta som ett attribut i applikationen. Vilken DAO-klass som skall instansieras anges som en context-parameter.

Ur web.xml:

   ...
    <!-- When using xml.based persistence -->
    <context-param>
      <param-name>dao</param-name>
      <param-value>se.bluefish.jsf.example.dao.memory.MemoryMailReaderDaoImpl </param-value>
      <description>
        Read by the MailReaderDaoInitializer to create the
        right type of data access implementation.
      </description>
    </context-param>
    <context-param>
      <param-name>databaseFile</param-name>
      <param-value>/WEB-INF/database.xml</param-value>
    </context-param>
    <listener>
      <listener-class>se.bluefish.jsf.example.dao.memory.MemoryMailReaderDaoInitializer </listener-class>
    </listener>
  ...

Behovet av subklasserna MemoryMailReaderDaoInitializer och MemoryMailReaderDaoImpl är föranledda av den extra

    <context-param>
      <param-name>databaseFile</param-name>
      <param-value>/WEB-INF/database.xml</param-value>
    </context-param>

taggen som anger namnet på filen som innehåller vår xml-databas. Databasen måste läsas in av vårt DAO-objekt, ett specialfall som endast gäller när vi användere xml-baserad persistens.

Det var nu dags att skapa lite testfall.

Hur testar vi en JSF applikation?

Vi vet alla att enhetstester är något som kontinuerligt skall skrivas och köras under hela utvecklingen (kanske tom driva hela utvecklingen) och inte skall sparas tills design och implementering är ett fullbordat faktum. Just i detta fall kan jag dock skylla på att mitt primära mål var att prova JSF och att jag därför med (tämligen) gott samvete kunde skjuta testningen framför mig.

Då domänmodellen i vår applikation är väldigt enkel och saknar affärslogik bedömde jag att det som främst behövde testas var webblagret, dvs mina "backing beans", och dess kommunikation med domänmodell och persistenslager.

Testa i eller utanför Tomcat?

Ett annat beslut som måste fattas är om testning skall ske "in-container" eller inte, dvs i vårt fall om våra tester skall köras i eller utanför Tomcat. Fördelen med att köra tester inne i Tomcat är framför allt att testerna körs i en realistisk runtime-miljö, dvs testerna har tillgång till alla tjänster och ingen simulering av dessa behöver göras.

Cactus är ett ambitiöst Jakarta-projekt som ger tillgång till ett ramverk för att köra enhetstester inne i exempelvis Tomcat.

En nackdel med "in-container" tester är att de (naturligtvis!) bara går att köra om servern är i gång med en korrekt installerad applikation, dvs innan en ny test kan köras måste applikationen deployas och servern startas om vilket inte direkt uppmuntrar en interaktion mellan kodning och test! Eftersom jag inte var intresserad av att testa min egen kods integrering med JSF utan ville koncentrera mig på webblagret så valde jag att använda "vanliga", rena JUnit-tester utanför servern.

Innan några tester kunde skrivas fanns några problem som måste lösas.

Webblagret är beroende av persistenslagret

Enhetstester skall vara oberoende av av varandra och inte göra permanenta förändringar som kan påverka andra tester. Mitt webblager, dvs mina "backing beans" pratar med persistenslagret så för att testa webblagret på ett korrekt sätt måste jag rigga mina testklasser så att de antingen

  • jobbar emot en persistensmekanism som endast är avsedd för test, exempelvis en testdatabas och städar upp efter sig i tearDown metoden, eller
  • jobbar emot ett persistenslager som bara simulerar datalagring, dvs ett "Stub object" eller "Mock Object".

Då gränssnittet mot persistenslagret inte är så omfattande så valde jag det sista alternativet.

Jag gjorde en "stub" implementation av MailReaderDao - StubMailReaderDao - som håller sina "persisterade" domänobjekt i RAM och ger nya objekt sina id via en enkel räknare. Genom att installera en instans av denna klass i testklasserna blev det möjligt att testa applikationens "backing beans" som upplevde det som om de persisterade sina domänobjekt på riktigt.

Notera att denna lösning inte testar persistensmekanismen utan endast de klasser som utnyttjar densamma. Är du ute efter att testa själva persistensen så måste den andra varianten beskriven här ovanför användas.

Utdrag ur MailReaderDaoStub.

 public class MailReaderDaoStub implements MailReaderDao
 {
   private Map   users          = new HashMap();
   private Map   subscriptions  = new HashMap();
   private long  id             = 1;

   ...
   public void addUser(User user) throws DataAccessException
   {
     user.setId(new Long(id++));
     users.put(user.getName(), user);
   }

   public void updateUser(User user) throws DataAccessException {}

   ...
 }

Vad är "Stub object" och "Mock Object"?

En enhetstest skall endast testa funktionalitet hos ett specifikt objekt i vår objektmodell. Nu är det ju så att få objekt lever och verkar helt isolerade utan tvärtom oftast samverkar med andra objekt, i ett specifikt kontext. Exempelvis så samverkar våra "backing beans" med DAO-objektet och objekten i domänmodellen. Ett "Stub object" såväl som ett "Mock Object" simulerar dessa samverkande objekt och har till uppgift att ersätta dessa vid test. Angreppsättet skiljer sig dock väsentligt åt.

Ett "stub object" används oftast för ett objekt som är knepigt eller resurskrävande att instansiera, en databaskoppling är ett vanligt exempel. Endast det som behöver fungera i en testmiljö implementeras och nödvändig data hanteras på enklast möjliga sätt.

Ett "mock object" däremot jobbar med förväntade resultat, dvs du talar om för ditt "mock object" hur det förväntas samverka, i form av metodanrop, resultat och kastade undantag, med det objekt du verkligen vill testa och i slutet av testen verifierar du att dina förväntningar har uppfyllts. Obegripligt? Martin Fowler förklarar det hela bättre än jag kan göra i en artikel på sin hemsida.

Notera att användningen av "Stub objects" och Mock objects" underlättas väsentligt om endast interface används som gränssnitt mellan klasserna.

Webblagret måste testas i sitt kontext

Webblagret i en JSF-applikation exekverar allid i ett JSF-kontext, dvs klasser förväntas ha tillgång till ett FacesContext som bland annat kapslar in all requestinformation. För att testa en JSF-applikation måste på något sätt detta kontext simuleras.

Strutsapplikationener har ett snarlikt problem med en elegant lösning i StrutsTestCase, en utvidgning av JUnit som gör det möjligt att exekvera testfallen i Struts egen ActionServlet. Detta möjliggör testning inte bara av actions och forms, utan också av navigering och mappning. I brist på ett lika ambitiös projekt för test av JSF applikatoner valde jag ett mera blygsamt angreppssätt.

I en JSF-applikation får varje request sin egen kontext som alltid går att komma åt genom att anropa FacesContext.getInstance(). Denna kontext är en instans av FacesContextImpl, en konkret utvidgning av den abstrakta klassen FacesContext.

En "stub" kontext - FacesContextStub - som utvidgar FacesContext och implementerar den mest basala funktionaliteten, tillsammans med vår tidigare "stub"-implementation av persistenslagret, gjorde det möjligt att testa applikationens actionmetoder genom att skriva testkod som följer:

 ...
  protected MockHttpServletRequest  request;
  protected MockHttpServletResponse response;
  protected FacesContext            facesContext;
  protected ResourceBundle          resourceBundle;
  protected MailReaderDao           mailReaderDao;
  protected UserBean                userBean;
 ...
  public void setUp() throws Exception
  {
    super.setUp();
    ServletContext context = new MockServletContext();
    request      = new MockHttpServletRequest(context,"","");
    response     = new MockHttpServletResponse();
    facesContext = new MockFacesContext(new ExternalContextImpl(context, request, response));
    mailReaderDao  = new MailReaderDaoStub();
    facesContext.getExternalContext().getApplicationMap().put(Constants.DAO_KEY, mailReaderDao);
    resourceBundle = ResourceBundle.getBundle("se.bluefish.jsf.example.ApplicationResources");
    User user = new UserImpl("user");
    user.setPassword("password");
    mailReaderDao.addUser(user);
    userBean = createUserBean();
    addSessionAttribute(Constants.USER_KEY, userBean);

  }

  protected String doLogin()
  {
    userBean.setName("user");
    userBean.setPassword("password");
    String forward = userBean.logon();
    return forward;
  }

  public void testSuccessfulLogin() throws Exception
  {
    String forward = doLogin();
    assertEquals(forward,"success");
    assertNotNull(userBean.getUser());
    assertEquals(userBean.getUser().getName(),"user");
  }

  ...

Hur bra är testerna?

Enligt min uppfattning ligger det främsta värdet av enhetstester i den trygghet de kan ge när koden behöver förändras. Om den kod som behöver skrivas om täcks av korrekta enhetstester och dessa tester går igenom även efter det att ändringarna har implementerats så vet du åtminstone att den grundläggande funktionaliteten i koden är intakt. Ett mätvärde på enhetstester är hur stor del av koden som genomlöps då testerna körs. En 100-procentig täckning är knappast realistisk och kanske inte ens önskvärd men enhetstester kombinerade med ett verktyg för "code coverage" kan peka ut kod som inte är ordentligt testad. Därefter kan du göra en kvalificerad bedömning om detta är någonting som behöver åtgärdas eller om det går att leva med.

Viktigt är dock att inse att en hög "täckningsgrad" inte nödvändigtvis är detsamma som hög testkvalitet. Testklasser kan ju exempelvis genomlöpa produktionskod utan att testa resultatet vilket ju innebär att täckningsgraden blir missvisande som ett kvalitetsmått. Diskussioner om kodtäckning kombinerat med enhetstester finns att läsa exempelvis på CodeCoverage.

Jag använder Eclipse som utvecklingsmiljö och ville därför helst hitta ett verktyg för "code coverage" som gick att integrera i denna. Efter att ha surfat runt på nätet tvingades jag tyvärr konstatera att det saknas open-source produkter som är integrerade med Eclipse (kommersiella produkter finns dock, exempelvis Clover).

Till slut bestämde jag mig för att använda EMMA, ett projekt på Sourceforge som saknar Eclipseintegration men har bra stöd för Ant och har ett antal olika format för presentation av resultatet. Presentationen är dessutom möjlig att länka till källkoden vilket gör det lätt att se vilken kod som inte har genomlöps. Det är dessutom lätt att ange vilken eller vilka klassfiler som skall täckas. Nackdelen är möjligen att de klassfiler som skall testas måste instrumenteras för att EMMA skall kunna göra sitt jobb. Då detta är lätt att automatisera via Ant är det dock inget större problem. Instrumenteringen görs på redan kompilerade klassfiler och innebär att bytekoden manipuleras för att lägga in instruktioner för att registrera om koden genomlöps.

För att enkelt kunna utesluta mina testklasser vid instrumenteringen så lade jag alla testklasser i en egen mapp i Eclipse. Detta är en bra princip av flera anledningar, bland annat är det på så sätt lättare att se till att de inte kommer med när systemet skall levereras för produktion.

Observera att detta inte behöver innebära att testklasserna hamnar i egna paket, tvärtom kan det finnas fördelar med att testklasser ligger i samma paket som de klasser som de skall testa. Det gör det t ex möjligt att komma åt metoder som är protected eller package-protected. Dessutom så kan det med denna princip bli lättare att hitta testklasserna för en specifik klass.

Nästa steg var att lägga till ett antal targets i projektets build.xml.

    <!-- instrumented class output directory: -->
    <property name="instrumented.dir" value="${basedir}/instrumented" />

    <!-- output directory used for EMMA coverage reports: -->
    <property name="coverage.dir" value="${basedir}/coverage" />

    <target name="init">
        <mkdir dir="${instrumented.dir}" />
        <mkdir dir="${coverage.dir}" />
        <path id="instrumented.classpath" >

          <pathelement location="${instrumented.dir}" />
        </path>
        <path id="run.classpath" >
          <pathelement location="${classes.dir}" />
          <fileset dir="${lib.dir}" includes="**/*.jar" />
          <pathelement location="${catalina.home}/common/lib/servlet-api.jar" />
        </path>
        <path id="test.classpath" >
            <path refid="run.classpath" />

            <pathelement location="${test.classes.dir}" />
            <fileset dir="${test.lib.dir}" includes="**/*.jar" />
            <pathelement location="${ant.home}/lib/ant.jar"/>
            <pathelement location="${ant.home}/lib/optional.jar"/>
        </path>
        <path id="instrumented.test.classpath" >
            <path refid="instrumented.classpath" />
            <path refid="test.classpath" />
        </path>

    </target>

        <!-- path element used by EMMA taskdef below: -->
    <path id="emma.lib" >
        <pathelement location="${test.lib.dir}/emma.jar" />
        <pathelement location="${test.lib.dir}/emma_ant.jar" />
    </path>

    <!-- this loads <emma> and <emmajava> custom tasks: -->
    <taskdef resource="emma_ant.properties" classpathref="emma.lib" />

    <!-- Remove directories used by EMMA: -->
    <target name="emma.clean">
        <delete dir="${instrumented.dir}" />
        <delete dir="${coverage.dir}" />
    </target>


    <!-- Instrument all classes in ${classes.dir}: -->
    <target name="emma.instrument" depends="emma.clean,init">
        <emma enabled="true">

          <instr instrpath="${classes.dir}"
                 destdir="${instrumented.dir}"
                 metadatafile="${coverage.dir}/metadata.emma"
                 merge="false">
          </instr>
        </emma>
    </target>

    <!-- Create a report over the coverage results: -->
    <target name="emma.report">
        <emma enabled="true">
            <report sourcepath="${base.src.dir},${presentation.src.dir}">

            <!-- collect all EMMA data dumps (metadata and runtime): -->
            <infileset dir="${coverage.dir}" includes="*.emma" />
            <html outfile="${coverage.dir}/coverage.html" />
            </report>
        </emma>
    </target>

    <!-- Run all test cases with code coverage: -->
    <target name="run.coverage.tests">

        <antcall target="emma.instrument" />
        <antcall target="-internal.run.tests" >
            <param name="class.path.ref" value="instrumented.test.classpath" />
        </antcall>
        <antcall target="emma.report" />
    </target>

    <!-- Internal target. Run from other targets, not directly:-->
    <target name="-internal.run.tests">

        <junit fork="true" haltonfailure="yes" printsummary="on" >
            <classpath >
                <path refid="${class.path.ref}" />
            </classpath>
            <formatter type="brief" usefile="false"/>
            <formatter type="xml" />
            <batchtest todir="${coverage.dir}" >
                <fileset dir="${test.classes.dir}" includes="se/bluefish/jsf/example/test/*Test*.class" />
            </batchtest>

            <jvmarg value="-Demma.coverage.out.file=${coverage.dir}/coverage.emma" />
            <jvmarg value="-Demma.coverage.out.merge=true" />
        </junit>
    </target>

Här kan man se följande;

? ett antal Ant-properties för att stödja instrumentering och "code coverage":

  •   instrumented.dir - där de instrumenterade klassfilerna skall ligga
  •   coverage.dir - för rapportfiler mm, genererade vid instrumentering och testning
  •   instrumented.test.classpath - en CLASSPATH som skall användas för exekvering av testklasserna. Observera att de instrumenterade klasserna läggs före de icke instrumenterade. Gör man tvärtom genereras ingen kodtäckningsdata!

? ett antal interna targets för att förbereda instrumentering, utföra själva instrumenteringen samt sammanställa rapporter efter avslutad testning. Dessa används av "run.coverage.tests", som är det Ant-target som körs.

För att köra själva testklasserna används Ant:s eget task för att köra JUnit-tester.

        <!-- Internal target. Run from other targets, not directly:-->
    <target name="-internal.run.tests">
       1.    <junit fork="true" haltonfailure="yes" printsummary="on" >
       2.       <classpath >
                <path refid="${class.path.ref}" />
            </classpath>
            <formatter type="brief" usefile="false"/>
            <formatter type="xml" />
                    <batchtest todir="${coverage.dir}" >
                <fileset dir="${test.classes.dir}" includes="se/bluefish/jsf/example/test/*Test.class" />
            </batchtest>
       3.       <jvmarg value="-Demma.coverage.out.file=${coverage.dir}/coverage.emma" />
            <jvmarg value="-Demma.coverage.out.merge=true" />
             </junit>
    </target>

För att data för kodtäckning skall genereras måste följande gälla:

  1. för att den data som samlats ihop skall dumpas efter avslutat körning måste fork="true"
  2. den CLASSPATH som används måste innehålla de instrumenterade klassfilerna, före de icke-instrumenterade
  3. Argument till VM:en som talar om var och hur data skall sparas måste skickas in.

Min konfiguration gör att resultat presenteras via ett antal html-filer med start i coverage.html. Html-filerna är länkade till varandra vilket gör det möjligt att borra sig ned och komma fram till enskilda klasser, metoder och tom kodrader!

Ett exempel:

Uploaded Image:code-coverage.png

Genom att klicka på en metod i listan överst på listan hamnar vi i källkoden och kan där se vilka rader som har, respektive inte har, genomlöpts under exekveringen av testfallen!

Sammanfattningsvis så fungerar EMMA helt tillfredsställande men några mindre nackdelar finns dock:

? bristen på integrering i Eclipse vilket innebär att:

  • all exekvering måste göras via Ant. Vilket också innebär att all konfigurering måste göras i build.xml eller i en properties-fil. Detta är lite osmidigt och innebär i praktiken att man oftast konfigurerar för att köra alla testfall varje gång (vilket inte nödvändigtvis är någon nackdel för mindre projekt). Det är ju dock inget som hindrar att ni använder JUnit i Eclipse under det löpande utvecklingsarbetet och kör tester med kodtäckning som en del av er regelbundna integrerings- och byggprocess.
  • det finns ingen koppling mellan den källkod som kan visas upp i rapporten (om rapporten är i html-format) och källkoden i Eclipse, dvs du får en bra överblick över vilka rader som gemomlöpts respektive inte genomlöpts men det finns inget smidigt sätt att direkt öppna källkodsfilen. Dessutom så måste du ha en webbrowser-plugin installerad för att inte resultatet skall öppnas upp i en browser utanför Eclipse. Detta är dock ett mindre problem.

? instrumenteringen som måste göras av de klassfiler som skall testas.

  • I min lilla testapplikationen är tiden instrumenteringen tar försumbar men i ett större projekt är det möjligt att det kan bli ett problem. EMMAs inkrementella instrumentering och möjligheten att via filter dela upp sina tester torde dock innebära att det inte blir något problem i praktiken.

När jag precis hade skrivit klart detta avsnitt hittade jag faktiskt en Eclipse-plugin som uppfyller de flesta av mina önskemål! djUnit är framtagen är av en japansk utvecklare och använder open-source versionen av JCoverage för att hantera kodtäckning.

Den används analogt med den befintliga JUnit-pluginen till Eclipse, dvs du kan markera en kodtycklig nod med källkod i ditt projekt och exekvera alla enhetstester som finns i den markerade koden. djUnit dyker upp under en egen flik i projektets egenskapsfönster där du bland annat kan ange klasser som skall undantas från kodtäckning. Resultatet presenteras på ett sätt som är snarlikt EMMAs men med något smärre överskådlighet, ett problem som troligen blir mera märkbart ju större applikationen blir. Resultatrapporten är dock väl integrerad i Eclipse så det går att öppna en testad klass direkt i en editor där rader som inte genomlöpts är markerad med en Eclipse "marker".

En spontan reaktion är att i väntan på en bättre integrering av EMMA i Eclipse så kompletterar dessa två verktyg varandra. Använd djUnit under det dagliga arbetet för att köra en eller ett mindre antal enhetstester och spara EMMA till en mera komplett testkörning i samband med integrering/deployment.

Loggning

Loggning av flödet hos en applikation under körning kan vara en ovärderlig hjälp vid felsökning eller bara för att konstatera att tillståndet hos vår applikation är oförändrat gott.

För vår testapplikation valde jag att använda Commons Logging, ett open-source projekt hos Jakarta. Detta loggningsramverk fungerar som ett tunt abstraktionslager ovanpå andra loggningsramverk, så som Log4J eller JDK 1.4:s eget loggningsramverk.

Vilket loggningsramverk som används styrs av konfigurationsfiler och koden blir helt oberoende av ditt val. Det fungerar även utan explicit konfigurering, bara genom att inkludera commons-logging.jar i din applikation kommer din loggning att fungera då Commons Logging i detta fall väljer loggning enligt en förutbestämd prioritetsordning. Denna prioritetsordning väljer Log4J om den finns i din CLASSPATH.

Vill du själv styra applikationens loggning gör du detta genom att skapa filen commons-loggings.properties och spara den i roten av din CLASSPATH.

Exempel på innehållet i en commons-logging.properties:

# If this property isn´t set Log4J is used by default if in the classpath.
# Properties for SimpleLog is by default read from simplelog.properties
#org.apache.commons.logging.Log=org.apache.commons.logging.impl.SimpleLog

# Properties for Log4J is by default read from log4j.properties
org.apache.commons.logging.Log=org.apache.commons.logging.impl.Log4JLogger

Notera att om du vill ha en annan loggning än Log4J så måste den definieras i denna fil om det är så att Log4J finns i din CLASSPATH.

Loggningen av Log4J styrs av egenskaper definierade i log4j.properties som enklast läggs på samma plats som commons-loggings.properties. Vill du ha ett annat namn på denna fil måste detta anges i common-loggings.properties.

Exempel från log4j.properties;

log4j.rootCategory=ERROR,stdout, D
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout

log4j.appender.D=biz.minaret.log4j.DatedFileAppender
log4j.appender.D.layout=org.apache.log4j.PatternLayout
log4j.appender.D.layout.ConversionPattern=%d %-5p [%t] %c - %m%n
log4j.appender.D.Directory=/logs
log4j.appender.D.Prefix=mail_reader.
log4j.appender.D.Suffix=.log

log4j.logger.memory.persistens=DEBUG
log4j.logger.hibernate.persistens=DEBUG

DatedFileAppender är en appender som namnsätter loggfilerna med datum, dvs vid en konfiguration som ovan kommer filerna att namnsättas som mail_reader.2004-09-22.log. Första loggningen efter midnatt ger en ny fil, med ett nytt datum.

Det går också att använda en konfigureringsfil i xml-format. Defaultnamn på denna är då log4j.xml.

Sammanfattningsvis:

  • vill du använda Log4J se bara till att dess jar finns i din CLASSPATH.
  • vill du konfigurera dess loggning (och det vill du!) definiera den i log4j.properties eller log4j.xml eller se till att filen läggs i din CLASSPATH.

I övrigt är både Log4J och Commons Logging väl dokumenterat på deras respektive hemsidor.

Jag får tillfälle att återkomma till loggning när jag beskriver mina problem med att få det att fungera i JBoss.

I nästa del i artikelserien kommer jag att beskriva hur jag införde Hibernate och MySQL som persistensmekanism i min testapplikation.

/Lars Svadängs

Bluefish logo

Bluefish AB :: Sturegatan 34 :: 114 36 Stockholm :: tel 08 459 93 30 :: fax 08 459 93 39 :: e-post info@bluefish.se