EE 6

Arquillian in testiranje storitev REST

Posted on

Arquillian je orodje za integracijsko testiranje komponent. V najbolj osnovni obliki je odgovor na vprašanje: kako naj stestiram nek EJB v njegovem okolju ? Članek ni namenjen osnovam Arquilliana, te lahko spoznate na:  http://jaxenter.com/arquillian-a-component-model-for-integration-testing.1-35304.html, ampak je namenjen prikazu testiranja:

storitve REST, ki je implementirana nad Stateless EJBjem, in glede na vhodni parameter poišče (preko JPA) določeno entiteto in jo vrne.

Za testiranje (za odjemalca) bomo uporabili knjižnico RESTEasy (http://www.jboss.org/resteasy/), za pretvorbo JSON v Java objekte in obratno, pa bomo uporabili knjižnico Jackson (http://jackson.codehaus.org).

Sam Arquillian pa bo konfiguriran v načinu Glassfish 3.1 Managed, več o tem na https://docs.jboss.org/author/display/ARQ/GlassFish+3.1+-+Managed.

Implementacija storitve REST

Da je REST sploh omogočen, moramo definirati t.i. Application Path:

@ApplicationPath("/resources")
public class RestApplication extends Application {}

nato lahko implementiramo storitev, sicer s pomočjo Stateless EJBja:

@Stateless
@Path("/alert")
public class TestResource {

    @Inject
    @PuAdminWH
    EntityManager entityManager;

    @GET
    @Path("/{id}")
    @Consumes("text/plain")
    @Produces("application/json")
    public AppJmsAlert getAlertById(@PathParam("id") int id) {

        //Tole lahko vrne NoResultException, glej Exception mapper        AppJmsAlert alert = ...koda za poizvedbo...

        return alert;
    }
}

Vidimo, da smo implementirali klic tipa GET, ki sprejme en parameter id tipa text in vrne JSON prezentacijo entitete JPA tipa AppJmsAlert, torej naš klic bo izgledal takole:

http://localhost:8080/.../resources/alert/id/1453

V primeru, da vhodni parameter ne vrne rezultata (ne vrne entitete JPA), se bo avtomatsko na strežniku sprožil NoResultException, odjemalcu pa v takem primeru želimo vrniti odgovor s statusom HTTP tipa BAD_REQUEST. Zato moramo implementrati t.i. Exception Mapper:

@Provider
public class NoResultExceptionMapper implements ExceptionMapper<NoResultException> {

    @Override
    public Response toResponse(NoResultException e) {

        return Response.status(Response.Status.BAD_REQUEST).entity(e.getMessage()).build();
    }
}

Konfiguracija orodja Arquillian

Na lokacijo src/test/resources ustvarimo dve datoteki:

 arquillian.launch in arquillian.xml

ki nam predstavljata konfiguracijo. V launch datoteko bomo vpisali t.i. Container Qualifier, ki določa, katera konfiguracija, določena v arquillian.xml, je aktualna ob zagonu testov. Zato bo njena vsebina naslednja:

remote-gf

Arquillian.xml pa ima naslednjo vsebino:

<?xml version="1.0" encoding="UTF-8"?>
<arquillian xmlns="http://jboss.org/schema/arquillian"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://jboss.org/schema/arquillian http://jboss.org/schema/arquillian/arquillian_1_0.xsd">

    <container qualifier="remote-gf" default="true">
       <configuration>
          <property name="glassFishHome">
               /Applications/NetBeans/glassfish-3.1.2.2
          </property>
       </configuration>
    </container>
</arquillian>

vidimo, da je potrebno definirati pot do kontejnerja EE, v tem primeru Glassfish, katerega bo Arquillian zagnal, v njega namestil testni war in zagnal teste.

To pa ni vse, sledi najtežji del, sicer pravilna nastavitev pom.xml-ja:

<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.8.1</version>
    <scope>test</scope>
</dependency>

<dependency>
    <groupId>org.jboss.arquillian.junit</groupId>
    <artifactId>arquillian-junit-container</artifactId>
    <version>1.0.3.Final</version>
    <scope>test</scope>
</dependency>    

<dependency>
    <groupId>org.jboss.resteasy</groupId>
    <artifactId>resteasy-jaxrs</artifactId>
    <version>2.3.4.Final</version>
    <scope>test</scope>
</dependency>

<dependency>
    <groupId>org.jboss.resteasy</groupId>
    <artifactId>resteasy-jackson-provider</artifactId>
    <version>2.3.4.Final</version>
    <scope>test</scope>
</dependency>

<dependency>
    <groupId>org.jboss.shrinkwrap.resolver</groupId>
    <artifactId>shrinkwrap-resolver-api-maven</artifactId>
    <version>2.0.0-alpha-1</version>
    <scope>test</scope>
    <type>jar</type>
</dependency>    

<dependency>
    <groupId>org.jboss.shrinkwrap.resolver</groupId>
    <artifactId>shrinkwrap-resolver-impl-maven</artifactId>
    <version>2.0.0-alpha-1</version>
    <scope>test</scope>
    <exclusions>
        <exclusion>
            <groupId>com.google.collections</groupId>
            <artifactId>google-collections</artifactId>
        </exclusion>
    </exclusions>
</dependency>

Vidimo, da smo na classpath dali naslednje stvari:

  • junit in arquillian container;
  • implementacijo resteasy in jackson;
  • shrinkwrap resolver, katerega si bomo pogledali kasneje.

Dodatno bomo definirali maven profile, v katerem pa imamo naslednjo definicijo:

<profiles>  
    <profile>
        <id>glassfish-managed-3.1</id>
        <dependencies>
            <dependency>
                <groupId>com.inteligoo</groupId>
                <artifactId>intelicommon</artifactId>
                <version>1.0</version>

                <!--
                  Ker spodaj include.dam
                -->
                <exclusions>
                    <exclusion>
                        <groupId>javax</groupId>
                        <artifactId>javaee-api</artifactId>
                    </exclusion>
                    <exclusion>
                        <groupId>org.slf4j</groupId>
                        <artifactId>slf4j-api</artifactId>
                    </exclusion>
                    <exclusion>
                        <groupId>org.slf4j</groupId>
                        <artifactId>slf4j-log4j12</artifactId>
                    </exclusion>
                </exclusions> 
           </dependency>    
           <dependency>
               <groupId>org.jboss.arquillian.container</groupId>
               <artifactId>arquillian-glassfish-managed-3.1</artifactId>
               <version>1.0.0.CR3</version>
               <scope>test</scope>
               <exclusions>
                   <exclusion>
                       <groupId>org.jboss.arquillian.container</groupId>
                       <artifactId>arquillian-container-spi</artifactId>
                   </exclusion>  
                   <exclusion>
                       <groupId>com.sun.jersey</groupId>
                       <artifactId>jersey-bundle</artifactId>
                   </exclusion>
                   <exclusion>
                       <groupId>com.sun.jersey.contribs</groupId>
                       <artifactId>jersey-multipart</artifactId>
                   </exclusion>
             </exclusions>
         </dependency>

         <!--
           Nadomestim z novejsimi, sicer imam error:
org.jboss.arquillian.container.spi.client.protocol.metadata.ProtocolMetaData.getContexts(Ljava/lang/Class;)Ljava/util/Collection;
         -->
         <dependency>
             <groupId>org.jboss.arquillian.container</groupId>
             <artifactId>arquillian-container-spi</artifactId>
             <version>1.0.3.Final</version>
             <scope>test</scope>
         </dependency>

         <!--
           Nadomestim z novejsimi, sicer imam error:
             com.sun.jersey.api.client.ClientHandlerException:

A message body writer for Java type, class com.sun.jersey.multipart.FormDataMultiPart, and MIME media type, multipart/form-data, wasnot found
          -->       
         <dependency>
             <groupId>com.sun.jersey</groupId>
             <artifactId>jersey-bundle</artifactId>
             <version>1.12</version>
             <scope>test</scope>
         </dependency>

         <dependency>
             <groupId>com.sun.jersey.contribs</groupId>
             <artifactId>jersey-multipart</artifactId>
             <version>1.12</version>
             <scope>test</scope>
         </dependency>      

         <!--
           Sicer mam error:
             Absent Code attribute in method...
         -->
         <dependency>
             <groupId>org.jboss.spec</groupId>
             <artifactId>jboss-javaee-6.0</artifactId>
             <version>1.0.0.Final</version>
             <type>pom</type>
             <scope>provided</scope>
         </dependency>      
     </dependencies>

     <build>
         <testResources>
             <testResource>
                 <directory>src/test/resources</directory>
             </testResource>
             <testResource>
                 <directory>src/test/resources-glassfish-managed</directory>
             </testResource>
         </testResources>
      </build>
   </profile>
</profiles>

POZOR! Obvezno je potrebno podrobno pregledati zgornji odsek kode, namreč v komentarjih so opisani vsi problemi, na katere sem naletel. Zato je potrebno uporabiti tudi nekaj exclusion-ov, da se stvar sploh hoče zagnati.

Priprava testov

Da lahko testiramo našo storitev s RESTEasy, moramo implementirati t.i. razred stub, ki bo samo ogrodje za našo storitev:

@Path("/alert")
public interface TestResourcesClientStub {

    @GET
    @Path("/{id}")
    @Consumes("text/plain")
    @Produces("application/json")
    public AppJmsAlert getAlertById(@PathParam("id") int id);  

}

Ogrodje bomo uporabili v naših testih Arquillian:

@RunWith(Arquillian.class)
@RunAsClient

public class TestResourceTest {   

    private static final String RESOURCE_PREFIX = RestApplication.class.getAnnotation(ApplicationPath.class).value().substring(1);

    /**
    * Resource, ki vrne npr.: http://localhost:8080/test/ v primeru, da deployamo test.war
    * V primeru Glassfish Embeddable, nam ta resource ne vrne prave vrednosti
    */
    @ArquillianResource
    private URL url;

    @Deployment(testable=false)
    public static WebArchive deploy(){   

        //0.) Za dodajanje poljubnih artifaktov iz POMa v obliki knjiznic
        final MavenDependencyResolver resolver = DependencyResolvers.use(MavenDependencyResolver.class);

        //1.) Izdelam arhiv
        WebArchive war = ShrinkWrap.create(WebArchive.class, "test.war");

        //2.) NASTAVITVE
        war.addClasses(SystemConfiguration.class, PuAdminWH.class, RestApplication.class)

        //ENTITETE
           .addPackage(AppJmsAlert.class.getPackage())   
           .addClasses(KpiReportHelper.class, KpiSettings.class)

        //REST CLASSI
           .addClasses(NoResultExceptionMapper.class, TestResource.class)

        //VSE POTREBNE KNJIZNICE: HIBERNATE + JACKSON (pretvorba objektov v JSON)
           .addAsLibraries(resolver.artifacts("org.hibernate:hibernate-core:4.1.0.Final", "org.hibernate:hibernate-entitymanager:4.1.0.Final", "org.jboss.resteasy:resteasy-jackson-provider:2.3.4.Final").resolveAsFiles())

       //BAZA
           .addAsResource("test-persistence.xml", "META-INF/persistence.xml")

       //CDI   
           .addAsWebInfResource(EmptyAsset.INSTANCE, ArchivePaths.create("beans.xml")); 

         return war;  
    }

    /**
    * Inicializacija REST EASY + JACKSON (pretvorba iz JSONa v objekte)
    */
    @BeforeClass
    public static void initResteasyClient() {

        ResteasyProviderFactory instance = ResteasyProviderFactory.getInstance();
        instance.registerProvider(ResteasyJacksonProvider.class);
        RegisterBuiltin.register(instance);
    }

    /**
    * Testiram v primeru, ko mi storitev vrne alert s pričakovanim IDjem
    *
    */
    @Test
    public void testIdExists() {
        int idExists = 490;
        
        //resteasy client
        TestResourcesClientStub client = ProxyFactory.create(TestResourcesClientStub.class,  url.toString() + RESOURCE_PREFIX);

        AppJmsAlert alert = client.getAlertById(idExists);
        Assert.assertEquals(idExists, alert.getIdAppJmsAlert().intValue());

    }

    /**
    * Testiram response storitve, ko alert z določenim idjem ne obstaja
 
    *
    */
    @Test
    public void testBadResponse() {

        int idDoesntExists = 41290;

        //resteasy client
        TestResourcesClientStub client = ProxyFactory.create(TestResourcesClientStub.class,  url.toString() + RESOURCE_PREFIX);

        try {
            AppJmsAlert alert = client.getAlertById(idDoesntExists);
        }
        catch(ClientResponseFailure e) {

            // to je ok
            if(e.getResponse().getResponseStatus().equals(Response.Status.BAD_REQUEST)){
                return;
       }
    }
    Assert.fail("No bad response for id=" + idDoesntExists);
  }
}

Pomebnejše stvari so naslednje:

  • Anotacija @ArquillainResource nam vrne del URLja, na katerem zaganjamo našo testno aplikacijo;
  • RESOURCE_PREFIX nam vrne osnovni del URLja za REST, v našem primeru je to niz po vrednosti “resources”;
  • test bomo izvajali kot odjemalec (@RunAsClient);
  • paket war po imenu “test.war”, v katerem bo naš resurs REST, ki ga testiramo, se izdela s pomočjo Maven Resolverja, ki nam omogoča, da dodamo v naš war vse pakete, neposredno iz maven repozitorija;
  • test-persistance.xml je popolnoma enak persistance.xml, saj namen tega testa ni priprava ustrezne testne baze, ampak bomo uporabili kar obstoječo bazo. Splošno to ne velja in je potrebno pripraviti tudi ustrezno testno bazo;
  • v test.war je potrebno dodati vse razrede, ki jih bomo uporabljali v testu;
  • @BeforeClass anotacija nam označuje metodo, v kateri definiramo odjemalca RESTEasy, s podporo za knjižnico JACKSON;
  • implementirali smo dva testa, sicer v prvem testu pričakujemo, da nam storitev vrne točno določeno entiteto s točno določenim IDjem, v drugem testu pa pričakujemo, da nam storitev vrne BAD_REQUEST, saj poizvedbo pripravimo tako, da namenoma damo napačen id kot parameter.

Pri zagonu našega testa, se tako zgradi paket test.war, se zažene Glassfish 3.1, v katerega se omenjeni paket namesti, in kot odjemalec se zaženeta oba testa. Sami testi niso nič posebnega, najtežji del je žal konfiguracija vseh potrebnih knjižnic (pom.xml), saj privzete knjižnice, kot so navedene na domači strani Arquilliana žal ne delujejo out-of-the-box.

Vidimo tudi, da smo definirali pot do resursov za naše teste (src/test/resources-glassfish-managed) v katerem imamo test-persistance.xml, za našo testno okolje.

Advertisement

Java EE in asinhrono izvajanje

Posted on Updated on

V tokratnem zapisu si bomo pogledali, kako izdelati “cron job”, ki bo izbrano množico nizov (to so enote dela) optimalno razpršil med vnaprej določeno število vzporednih in asinhronih izvajanj tj. workerjev.

Naloga workerja bo ta, da bo enostavno izpisal enoto dela oz. podani niz. Ko bodo vsi workerji zasedeni, bo job čakal na prvega prostega in ob končanju izvajanja določenega workerja, avtomatsko zagnal novega.  Razprševanje dela, pa se bo izvajalo enkrat na 15 sekund.

@Asynchronous

Kot vemo nam Java EE 6 neposredno ne dovoli uporabljati večnitnosti, saj za nitnost skrbi kontejner EE (in njegov Threading Pool). Ampak session beans (EJB) lahko implementirajo asinhrone metode. Metode postane asinhrone, če jih označimo z anotacijo @Asynchronous.

Velja naslednje, da bean, ki kliče tako metodo, dobi kontrolo nazaj takoj po klicu in predenj se metoda sploh začne izvajati. Dodatno, asinhrona metoda lahko vrne implementacijo java.util.concurrent.Future<V>, preko katere lahko klicajoč bean preverja, kaj se dogaja z asinhronim izvajanjem, npr. ali je izvajanje še aktivno itd.

Glede izvajanja pa bo kontejner EE bo poiskal novo nit v svojem bazenu niti in ji predal izvajanje. Celo več, v skladu s EJBji in propagacijo transakcij, se bo to izvajanje zgodilo znotraj iste transakcije. Če želimo kreirati novo transakcijo, lahko to naredimo s ustrezno anotacijo:

@TransactionAttribute(TransactionAttributeType.REQUIRES_NEW)

Izdelava workerja

Torej potrebujemo session bean (stateless), ki bo implementiral asinhrono metodo. Ta metoda bo vrnila implementacijo java.util.concurrent.Future<String>, zato moramo za naš session bean definirati tudi view, sicer local view. Namreč velja pravilo, da če želimo, da nam asinhrona metoda karkoli vrne, ne smemo uporabiti session bean brez view-a:

public interface AsyncWorker {
    public Future<String> work(String unit);
}
@Stateless
@Local(AsyncWorker.class)
public class DSLWorker implements AsyncWorker{

    @Asynchronous
    public Future<String> work(String unit) {
        System.out.println("…obdelujem enoto: " + unit);

        String status = "END";
        return new AsyncResult<String>(status);
    }
}

Zgoraj vidimo, da na koncu metode, vrnemo status po vrednosti “END” in to lahko po potrebi uporabimo v klicajočem beanu.

Izdelava cron joba

Prav tako potrebujemo session bean (stateless), ki pa bo zaradi potrebe po izvajanju vsake 15 sekund uporabil anotacijo @Schedule:

@Stateless
public class CronJobFacade {
     private int numOfExecutions = 3;

     @EJB 
     private AsyncWorker aworker;

     @Schedule(second="*/15", minute="*",hour="*", persistent=false)
     public void dispatch() {
         //SEZNAM ENOT DELA:
         List<String> list = new ArrayList<String>();
         list.add("A");
         list.add("B");
         list.add("C");
         list.add("D");

         //asinhrona izvajanja workerjev
         List<Future<String>> workers = new ArrayList<Future<String>>();

         for (String unitFromList : list) {
             //IMAMO PREVEČ WORKERJEV, ČAKAM DA SE VSAJ EDEN SPROSTI
             if(workers.size() > (numOfExecutions-1)) {

                 break_lvl_1:
                 while(true) {
                     for (Future<String> specificWorker : workers) {
                         if(specificWorker.isDone()) {
                             workers.remove(specificWorker);
                             break break_lvl_1;
                         }
                      } 
                 }
             }
             //DISPATCH
             Future<String> worker = aworker.work(unitFromList);
             workers.add(worker);

         }//for
     }
}

Zgoraj vidimo, da imamo seznam dela, ki ga moram asinhrono opraviti preko asinhronih klicev. Vnaprej določeno število vzporednih izvajanj (workerjev) nam onemogoča, da kreiramo toliko novih workerjev, kot je vseh enot dela. Namreč, ko dosežemo omejitev, začnemo v zanki čakati, da se določeno izvajanje konča. To naredimo s periodičnim preverjanjem oz klicem metode isDone() and implementacijo java.util.concurrent.Future<String>. Ta delo logike, bi lahko izvedli tudi kako drugače, vendar je prikazan lean način.

V resničnem življenju imamo kakšne bolj resne primere, kot je izpis nizov, npr.: worker kliče spletno storitev (JAX-WS) in njen rezultat pošlje na JMS vrsto in na tak način potem dosežemo paralelno klicanje storitev.

Izvorna koda za podan primer je dosegljiva na:

https://github.com/mpevec/blog/tree/master/ee-cron-async

Programabilni inject CDI s parametrom

Posted on Updated on

EJB 3 ni kontekstualen

Mehanizem dependecy injection (v nadaljevanju DI), ki ga je uvedla tehnologija EJB3 je preprost, vendar ni kontekstualen. Zato imamo na voljo  specifikacijo CDI (Context and Dependcy Injection), ki nam poleg t.i. typesafe DI omogoca tudi koncept konteksta.

V tem blogu ne bom pisal o osnovah CDIja, ampak o bolj naprednem konceptu, katerega sem uporabljal pri svojih projektih in za katere menim, da je premalo informacij na spletu.

Programabilni DI

Recimo, da imamo naslednji java interface:

public interface DealDefinitionChoice {
    public void setDefinitionChoice(Object definitionChoice);
}

in naslednjo implementacijo

public class DealDefinitionChoiceImplementation implements 
DealDefinitionChoice, Serializable {

    private Choice choice;

    @Override
    public void setDefinitionChoice(Object definitionChoice) {
        this.choice = (Choice)definitionChoice;
    }

    @Override
    public Choice getDefinitionChoice() {
        return choice;
    }
}

Velja, da se DI zgodi po kreiranju instance (v katerem je DI uporabljen) in predenj se pokliče metoda anotirana s @PostConstruct:

@Named
@SessionScoped
public class DealDefinitionChoiceTest implements Serializable {

    @Inject
    private DealDefinitionChoice choice;

    @PostConstruct
    public void init() {
        …
        Choice choice = choice.getDefinitionChoice();
    }
}

Pomeni, da se bo DI zgodil po klicu konstruktorja razreda DealDefinitionChoiceTest in rezultat DIja bomo lahko uporabili v metodi init().

Opomba: zgornje bo delovalo samo, če bomo imeli eno implementacijo DealDefinitionChoice. V primeru več implementacij, bi s CDI Qualifierjem določili, katero implementacijo želimo uporabiti pri DI.

Kaj pa če želimo izvesti najprej neko logiko in šele po izvedbi izvesti DI ? To lahko naredimo s programabilnim DI:

@Named
@SessionScoped
public class DealDefinitionChoiceTest implements Serializable {

    @Inject @Any
    private Instance<DealDefinitionChoice> choice;

    @PostConstruct
    public void init() {
       …
       Choice choice = choice.get().getDefinitionChoice()
    }
}

v zgornjem primeru se DI zgodi pri klicu get(), potrebno pa je bilo nastaviti placeholder v obliki variable choice. Uporabili smo tudi anotacijo @Any, ki nam omogoča, da placeholderju povemu, da lahko sprejme katerokoli implementacijo DealDefinitionChoice, s tem da bomo ob programabilnem DIju povedali točno katero. V tem primeru imamo samo eno implementacijo in zato ni problemov.

Programabilni DI s parametrom

Kaj pa če želimo izvesti DI po izvedbi neke logike, kjer rezultat izvedbe logike uporabimo pri samem DIju ? Pomeni, da je DI odvisen od nekega zunanjega parametra in ta parameter je ključen za izvedbo DIja ? Najlažje bomo to prikazali s uporabo t.i. producerja:

public class DealChoiceFactory {

    @Inject
    private DealDefinitionChoice choice;

    @Produces
    @DealDefinitionChoiceFinalQualifier
    public DealDefinitionChoice getChoice(InjectionPoint injectionPoint) {

        String purchaseType = null;

        for (Annotation annotation : injectionPoint.getQualifiers()) {
            if (annotation instanceof ParamDealDefinitionChoiceFinalQualifier) {
                purchaseType = ((ParamDealDefinitionChoiceFinalQualifier) annotation).getPurchaseType();
                break;
            }
        }

        choice.setDefinitionChoice(evaluate(purchaseType);
        return choice;
    }

    private Choice evaluate(String purchaseType) {
       …

       if(purchaseType.equals("…") return ...

    }
}

zgoraj imamo implementacijo metode producerja, kjer s pomočjo instance InjectionPoint pridemo do parametra String purchaseType, ki ga potem uporabimo pri določanju implementacije. Rezultat produciranja zapišemo v DealDefinitionChoiceImplementation in jo vrnemo. Instanca InjectionPoint nam tako omogoča, da se programabilno vrnemo na mesto, kjer se je zgodil DI in tako pridemo do parametra, ki pa je “shranjen v anotaciji”.

Potrebno je vedeti tudi to, da instanco InjectionPoint lahko uporabljamo samo v primeru t.i. psevdo-scopa. Namreč, v drugih scopih ta mehanizem ne more delovati, zaradi uporabe t.i. proxy-ev (glej http://docs.jboss.org/weld/reference/1.0.0/en-US/html/scopescontexts.html).

Sedaj se pojavi vprašanje, kako naj spravimo parameter do producerja, namreč mehanizem producerjev, nam to ne omogoča. Trik: pomagamo si lahko s “shranjevanjem v anotacijo”.

Najprej uvedemo CDI Qualifier, ki nam bo enolično označil producer:

@Qualifier
@Retention(RUNTIME)
@Target({METHOD, FIELD, PARAMETER, TYPE})
public @interface DealDefinitionChoiceFinalQualifier {}

nato pa uvedemo poseben CDI Qualifier, ki nam služi samo za “shranjevanje v anotacijo”:

public class ParamDealDefinitionChoiceFinalQualifier extends 
AnnotationLiteral<DealDefinitionChoiceFinalQualifier> implements 
DealDefinitionChoiceFinalQualifier {

    private String purchaseType;

    public String getPurchaseType() {
        return purchaseType;
    }

    public ParamDealDefinitionChoiceFinalQualifier(String purchaseType) 
        this.purchaseType = purchaseType;
    }
}

S tem ko smo rekli, da razred extenda AnnotationLiteral, smo ga označili kot CDI Qualifier.

Programabilni inject s parametrom pa sedaj izgleda takole:

@Named
@SessionScoped
public class DealDefinitionChoiceTest implements Serializable {

    @Inject @Any
    private Instance<DealDefinitionChoice> choice;

    @PostConstruct
    public void init() {
        …
        String purchaseType = ...

        DealDefinitionChoice dealChoice = choice.select(new ParamDealDefinitionChoiceFinalQualifier(purchaseType)).get();

    }
}

na tak način smo programerju v celoti skrili implementacijo kreiranja ustreznega choice.a, ki je odvisno od dinamično podanega parametra in DI se zgodi takrat, ko programer to želi.

Preprosto preskušanje entitete JPA 2 z Netbeans 7 in Maven

Posted on

Java EE 6 je stara dobro leto dni, NetBeans 7 Beta je zunaj že kar nekaj časa, čez dva meseca bo zunaj končna različica, Maven pa pridobiva na priljubljenosti in je dobro integriran v današnje IDE, tudi v Netbeansu. Zato sta tokrat naša cilja naslednja:

  • ustvariti nov projekt Maven s pomočjo Netbeansov, ki bo preskušal na novo ustvarjeno entiteto (JPA2) prek ogrodja JUnit
  • prikazati integracijo med Netbeansi in Mavenom

Uporabili bomo podatkovno bazo PostgreSQL, aplikacijskega strežnika pa za ta namen ne bomo potrebovali. Vse skupaj bomo zaganjali na Windows XP.

Mogoče se kdo sprašuje, kakšen je smisel uporabe JUnit-a nad poljubno entiteto, saj so v njej samo metode tipa setter/getter. Vendar, ker za tovrstno testiranje ne potrebujemo transakcij JTA (torej ne potrebujemo aplikacijskega strežnika) in ker uporabljamo nad podatkovno bazo strategijo “drop and create”, lahko nad obravnavano entiteto brez velikega vložka dela preskušamo vsaj poizvedbe JPQL.

1. Priprava okolja za Maven

Uporabili bomo Apache Maven 3.0.1. Maven je bundlan v Netbeanse, vendar bomo v našem primeru uporabili ločeno inštalacijo. Maven lahko snamete tule: http://maven.apache.org/download.html in ga odpakirate v poljuben direktorij, npr. f:/apache-maven-3.0.1. Pri sami inštalaciji je pomebno to, da nastavimo njegov bin v spremenljivko PATH. Tedaj lahko preverimo, ali je vse v redu:

Po definiciji je Maven rešitev za buildanje projektov (in še veliko več …). Pri svojem delu uporablja datoteko POM – to je datoteka XML, v kateri je opredeljen naš projekt. Projekt običajno potrebuje več t. i. artifaktov – zunanjih modulov, od katerih je odvisen. To so t. i. dependencies, ki so prav tako opredeljene v datoteki POM. Ker je tudi sam projekt nek artifakt, rečemo, da je Maven orodje za upravljanje artifaktov.

Artifakti se sprva nahajajo v oddaljenih repozitorijih in se med buildanjem projekta običajno  prenesejo v lokalni repozitorij. Zato Maven potrebuje tudi povezavo do spleta. Privzeta nastavitev za lokalni repozitorij je /Documents and Settings/User/.m2/repository. Ta je v tem trenutku še prazen.

Ker Mavena ne bomo uporabljali command line, moramo v Netbeansih nastaviti določene parametre. Znotraj Tools => Options => Miscellaneous, zavihek Maven, nastavimo pot do nameščenega Mavena in pot do lokalnega repozitorija:

2. Priprava podatkovne baze

Uporabili bomo PostgreSql, kjer bomo ustvarili novo podatkovno bazo po imenu MyAppTest. Test smo dali v ime zato, ker bomo ob zaganjanju aplikacije v tej bazi sproti brisali in ustvarjali tabele.

3. Ustvarjanje projekta

Gremo v Netbeanse in izberemo New Project, kjer potem izberemo Maven => Java Application.

Nato izpolnimo naslednje parametre:

Tako ustvarimo nov projekt  glede na archetype: maven-archetype-quickstart:1.1. Prav tako se v lokalni repozitorij naloži nekaj osnovnih potrebnih modulov (npr. maven plugin api). Sedaj smo dobili osnovno strukturo projekta:

Vidimo, da je projekt razdeljen na več delov: del, v katerem so izvorne datoteke, del v katerem so testi, del za (Test) Dependencies in del za datoteko POM, ki v tem trenutku vsebuje naslednje:

<groupId>blog.milanpevec</groupId>
 <artifactId>MyApp</artifactId>
 <version>1.0-SNAPSHOT</version>
 <packaging>jar</packaging>

 <name>MyApp</name>
 <url>http://maven.apache.org</url>

 <properties>
   <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
 </properties>

 <dependencies>
   <dependency>
     <groupId>junit</groupId>
     <artifactId>junit</artifactId>
     <version>4.8.2</version>
     <scope>test</scope>
   </dependency>
 </dependencies>

Vidimo, da je edini artifakt junit, namreč ta modul bomo potrebovali za preskušanje. Dodatno pri artifaktu popravimo različico na vrednost 4.8.2, kajti Netbeansi kot privzeto uporabljajo različico 3.

Zaradi uporabe JPA2 dodamo v datoteko še naslednje vrstice:

<build>
  <plugins>
    <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-compiler-plugin</artifactId>
      <version>2.3.2</version>
      <inherited>true</inherited>
      <configuration>
        <source>1.6</source>
        <target>1.6</target>
      </configuration>
    </plugin>
  </plugins>
</build>

S tem povemo, naj Maven v življenjskem ciklu prevajanja (compile) uporabi plugin za prevajanje Java SE različice 1.6. V nasprotnem bi se nam kasneje pri delu z JPA2 pojavljala opozorila.

4. Izdelava entitete JPA2

Izberemo File => New => Persistance => Entity Class. Obrazec izpolnimo z naslednjimi podatki:

Nastavili smo, da se entiteta po imenu Auser ustvari znotraj paketa blog.milanpevec.entities. Ostalo smo pustili privzeto. Pomembno je to, da spodaj obkljukamo “Create Persistance Unit”, da lahko v naslednjem koraku hkrati opredelimo še PU:

Za t. i. persistance providerja izberemo EclipseLink, izberemo novo povezavo do baze (ali jo ustvarimo, če je še nimamo) in izberemo strategijo “Drop and Create”, saj hočemo, da se ob preskušanju vedno pobriše shema in se na novo ustvarijo bazni objekti.

Zaradi integracije Mavena z Netbeansi so sedaj v datoteki POM samodejno opredeljeni novi artifakti, od katerih je odvisen naš projekt:

<dependency>
 <groupId>org.eclipse.persistence</groupId>
 <artifactId>eclipselink</artifactId>
 <version>2.2.0-M4</version>
</dependency>
<dependency>
 <groupId>org.eclipse.persistence</groupId>
 <artifactId>javax.persistence</artifactId>
 <version>2.0.0</version>
</dependency>

Gre za dva artifakta, ki sta uvrščena v isto skupino (org.eclipse.persistance). Artifakt Eclipselink je definiran zaradi PU, javax.persistance pa zaradi entitete JPA2. Ker bo PU uporabljal JDBC, moramo tudi zanj dodati artifakt:

<dependency>
 <groupId>postgresql</groupId>
 <artifactId>postgresql</artifactId>
 <version>9.0-801.jdbc4</version>
</dependency>

Ker omenjenih artifaktov še nimamo v lokalnem repozitoriju, nam Netbeansi prikažejo opozorila:

Zgornje lahko rešimo na več načinov. Lahko bi sprožili build in bi nam takrat naložilo vse potrebne artifakte. Lahko pa iz kontekstnega menija našega projekta izberemo “Show and resolve problems”, “Download Dependencies”:

Vendar je pred tem treba povedati, kje v oddaljenem repozitoriju se nahaja artifakt eclipselink. Netbeansi nam v datoteko POM privzeto nastavijo naslednje:

<repositories>
 <repository>
   <url>ftp://ftp.ing.umu.se/mirror/eclipse/rt/eclipselink/maven.repo</url>
   <id>eclipselink</id>
   <layout>default</layout>
   <name>Repository for library Library[eclipselink]</name>
 </repository>
</repositories>

Toda zaradi FTP-ja lahko pride do težave. Zato zgornjo vrstico nadomestimo z naslednjim:

<url>http://www.eclipse.org/downloads/download.php?r=1&amp;nf=1&amp;file=/rt/eclipselink/maven.repo</url&gt;

Sedaj Netbeansi sami naložijo manjkajoče artifakte v lokalni repozitorij.

Da bi bilo lažje slediti, kaj se dogaja med postopkom preskušanja, dodamo v PU novo lastnost (znotraj datoteke persistance.xml). Želimo, da se nam izpisujejo vse poizvedbe nad bazo. Zato dodamo lastnost eclipselink.logging.level z vrednostjo FINE:

<?xml version="1.0" encoding="UTF-8"?>
<persistence version="1.0" xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd">
 <persistence-unit name="blog.milanpevec_MyApp_PU" transaction-type="RESOURCE_LOCAL">
   <provider>org.eclipse.persistence.jpa.PersistenceProvider</provider>
   <class>blog.milanpevec.entities.Auser</class>
   <properties>
     <property name="javax.persistence.jdbc.url" value="jdbc:postgresql://localhost:5432/MyAppTest"/>
     <property name="javax.persistence.jdbc.password" value="postgres"/>
     <property name="javax.persistence.jdbc.driver" value="org.postgresql.Driver"/>
     <property name="javax.persistence.jdbc.user" value="postgres"/>
     <property name="eclipselink.ddl-generation" value="drop-and-create-tables"/>
     <property name="eclipselink.logging.level" value="FINE"/>
   </properties>
 </persistence-unit>
</persistence>

Iz zgornjega je razvidno, da bomo uporabili transakcije RESOURSE_LOCAL, saj bomo preskušanje zaganjali mimo aplikacijskega strežnika.

Pozor! EclipseLink potrebuje eksplicitno določena imena entitet, s katerimi upravlja, zato moramo dodati zgoraj znotraj <class> še ime entitete. V nasprotnem primeru dobimo “JPA exception: Object: … is not a known entity type.”

5. Izdelava testa

Najprej v našo entiteto dodamo dve lastnosti: firstname in lastname ter določimo, da primarni ključ tabele določa sekvenca auser_id_auser_seq. Prav tako opredelimo t. i. named query, ki poišče vse entitete Auser v podatkovni bazi. To poizvedbo bomo kasneje tudi preskusili.

@Entity
@NamedQuery(name=Auser.findAll,query="SELECT a FROM Auser a")
public class Auser implements Serializable {

 public final static String findAll = "entities.Message.findAll";    
 private static final long serialVersionUID = 1L;

 @Id
 @GeneratedValue(generator = "ContSeq")
 @SequenceGenerator(name = "ContSeq", sequenceName = "auser_id_auser_seq", allocationSize = 1)   
 private Long id;    
 private String firstname;    
 private String lastname;
 
 public Long getId() {
   return id;
 }
 public void setId(Long id) {
   this.id = id;
 }    
 public String getFirstname() {
   return firstname;
 }
 public void setFirstname(String firstname) {
   this.firstname = firstname;
 }
 public String getLastname() {
   return lastname;
 }
 public void setLastname(String lastname) {
   this.lastname = lastname;
 }
}

Sedaj pa si poglejmo, kako bomo sestavili test za našo entiteto. Napisali bomo testno metodo (anotacija @Test), ki se zažene samodejno zaradi omenjene anotacije. Znotraj nje bomo preskusili shranjevanje entitete v podatkovno bazo in edino opredeljeno poizvedbo. Uporabili bomo fixture @Before, ki označuje metodo, ki se zažene pred vsakim testom. V njej bomo implementirali pridobivanje trenutne transakcije. Dodatno bomo uporabili fixture @BeforeClass in @AfterClass, s katerim bomo ustvarili in zaprli naš EntityManager:

public class AuserTest  {
 
 private static EntityManagerFactory emf; 
 private static EntityManager em; 
 private static EntityTransaction tx; 
 
 @BeforeClass 
 public static void initEntityManager() throws Exception { 
   emf = Persistence.createEntityManagerFactory("blog.milanpevec_MyApp_PU"); 
   em = emf.createEntityManager(); 
 } 
 
 @AfterClass 
 public static void closeEntityManager() throws SQLException { 
   em.close(); 
   emf.close(); 
 } 
 
 @Before 
 public void initTransaction() { 
   tx = em.getTransaction(); 
 } 
 
 @Test 
 public void createAuser() throws Exception { 
 
   // Creates an instance of auser
   Auser auser = new Auser(); 
   auser.setFirstname("Janez");
   auser.setLastname("Novak"); 
 
   // Persists the auser to the database 
   tx.begin();  
   em.persist(auser); 
   tx.commit(); 
   assertNotNull("ID should not be null", auser.getId()); 
 
   // Retrieves all users from the database 
   List<Auser> ausers = em.createNamedQuery(Auser.findAllAusers).getResultList(); 
   assertEquals(1, ausers.size()); 
 } 
}

Uspešno izvedeno preskušanje ima naslednji izhod:

Zgoraj smo tako pokazali, da je mogoče zelo hitro in preprosto pripraviti okolje za preskušanje entitet. Če bi kdo želel celotno kodo projekta, mi lahko piše. Do naslednjič …