Jak vytvořit java web aplikaci s podporou Spring a JPA

Posted by Martin on September 08, 2010
hibernate, j2ee, java, maven, spring

Nedávno se kolega ptal na to jak vytvořit a nakonfigurovat jednoduchou aplikaci využívající Spring IoC a JPA. Aplikace má sloužit jako platforma pro jednoduché testování a jedinou úlohou bylo ukládání a čtení několika málo entit. Potřeboval přidávat nové třídy do business logiky, psát testy a výstup na web.

Namísto obvyklého RTFM jsem se rozhodl sepsat tutorial popisující jak vytvořit jednotlivé vrstvy. Existuje určitě nepřeberné množství cest jak se dostat k požadovanému výsledku a tady je jedna z nich. Zdrojový kód aplikace je k dipozici ke stažení. V textu článku nejsou z důvodu udržení rozumné délky všechny zdrojové kódy, ale jen ty, které považuji za zajímavé zmínit.

Struktura aplikace a Maven

Maven je výborný nástroj pro kompilaci aplikací vytvořených v javě. Pokud odpovídá adresářová struktura definovanému standardu pak je maven schopen provést kompilace, generovat WAR nebo EJB archivy, generovt javadoc a mnohé další v podstatě bez další konfigurace. I přes určité nedostatky je jednoduchá i správa závislostí na knihovnách třetích stran. Například přidání závislosti na junit vypadá v konfiguraci například takto


	junit
	junit
	4.8.1
	test

A to je vše, pokud by měla junit závislosti na dalších knihovnách, tzv.tranzitivní závislosti, pak Maven tyto automaticky detekuje a stáhne z některé veřejné repository do lokální maven repository, kde můžou být dále referencovány. Podrobný popis všech možností Mavenu je nad rámec článku, pokud by byl zájem bylo by to na samostatné téma v blogu.

Maven archetype

Pro vytvoření kostry základní HelloWorld jsp aplikace použijeme maven-archetype-webapp arechytepe. Pokud nemáte nainstalován Maven tak jej nainstalujte a spusťte následující příkaz:

mvn archetype:create -DgroupId=net.bytesource.prototype -DartifactId=spring-helloworld -DarchetypeArtifactId=maven-archetype-webapp

Maven vytvořil nový adresář spring-helloworld obsahující základní adresářovou strukturu naší aplikace. Ke kompilaci aplikace jděte do adresáře spring-helloworld a spusťte:

mvn package

Příkaz vytvoří podadresář target ve kterém bude výsledný balík, soubor spring-helloworld.war. Maven nám také umožní jednoduše provést deploy aplikace do web kontaineru tomcat. Přidáme tedy konfiguraci maven-cargo pluginu do pom.xml v elementu


    org.codehaus.cargo
    cargo-maven2-plugin
    1.0.3
    
        true
        
            tomcat6x
            ${project.build.directory}/${download.archive-dir.tomcat6x}/${download.archive-dir.tomcat6x}
            
                ${download.url.tomcat6x}
                ${project.build.directory}
            
        
    

a definici dvou proměnných


    http://ftp.sh.cvut.cz/MIRRORS/apache//tomcat/tomcat-6/v6.0.29/bin/apache-tomcat-6.0.29.zip
    apache-tomcat-6.0.29

Pak stačí následující příkaz, který stáhne Tomcat a spustí jej s naší aplikací:

mvn cargo:start

výsledek můžete ověřit na http://localhost:8080/spring-helloworld

Poznámka tomcat je nainstalován do pracovního adresáře target. Nic se tedy neinstaluje do některého systémového adresáře a není potřeba administrátorských práv.

Import do Eclipse

Já používám vývojové prostředí Eclipse takže použiji maven-eclipse-plugin pro vytvoření projektu. Odpovídající plugin pokud vím existuje i pro IntelliJIDEA. NetBeans umí pracovat s maven projekty přímo. Takže pro Eclipse

mvn eclipse:eclipse

a následně projekt importovat.

Entita Employee a management beany

Nyní vytvoříme entitu představující zaměstnance (Employee), entity management třídu zodpovědnou za persistenci a jednoduchou třídu obsahující metody vracející seznam zaměstnanců jako řetězec, která bude volána přímo z UI vrstvy.

Entita Employee obsahuje všechny atributy zaměstnance a primární klíč ID. Jako ID je ideální použít UUID, avšak problémem je, že asi jedniný snado nakonfigurovatelný je @GenericGenerator z Hibernate anotací. Nejedná o úplně čisté řešení, protože bychom měli používat pouze funkce JPA a ne funkce poskytované konkrétní implementací. Pro tento příklad uděláme výjimku a definujeme entitu:

@Entity
@Table(name = "EMPLOYEE")
public class Employee implements Serializable {
    @Id
    @Column(name = "EMPLOYEE_ID", nullable = false, length = 32)
    @GeneratedValue(generator="system-uuid")
    @GenericGenerator(name="system-uuid", strategy="uuid")
    private String id;

    @Column(name = "FIRST_NAME", length = 30)
    private String firstName;

    @Column(name = "LAST_NAME", length = 30)
    private String lastName;

    @Version
    @Column(name = "VERSION")
    private Long version;

Pro třídu je použita anotace @Entity označující, že se jedná o JPA entitu. Anotace @Table definuje databázovou tabulku určenou k perzistenci entity.

V tomto příkladu je pro zjednodušení vynechána DAO vrstva, která obvykle slouží jako prostředník mezi business logikou a databázovou vrstvou. Při použití JPA už není až tak nezbytné DAO definovat i u reálných produkčních aplikací. Pro databázové operace nad entitou bude tedy sloužit třída EmployeeManagementServiceJpaImpl na kterou se doporučuji podívat. Vysvětlím pouze použité anotace:

  • @Repository touto anotací se obvykle označují třídy pracující s entity managerem a Spring předpokládá překlad výjimek typu DataAccessException. Dále registruje třídu k autodetekci takže bude použita pro generovaný seznam spring beanů.
  • @PersistenceContext proměnná bude obsahovat instanci EntityManager
  • @Transactional deklaruje že pro danou metodu bude použita transakce. Bez dalších parametrů se připojí k již existující transakci a v případě, že žádná není vytvořená pak vytvoří novou.

Další třídou je EmployeeUIServiceImpl.

@Service
public class EmployeeUIServiceImpl implements EmployeeUIService {

    @Autowired
    private EmployeeManagementService employeeManagementService;

Jedná se o třídu z UI vrstvy, respektive teoreticky by takto mohla být architektura navržena. Použil jsem ji aby i tato jednoduchá aplikace byla rozdělěna na několik specifických vrstev a aby bylo možné pro názornost použít dependency injection. V tomto případě je takto Springem definována proměnná employeeManagementService za pomocí anotace @Autowired. Obecně není použití autowired strategie to nejlepší řešení, ale pro náš případ a pochopení jak Spring funguje bude stačit.

Spring a jeho konfigurace

Java kód naší aplikace máme hotový takže můžeme přejít ke konfiguraci. Nejdříve potřebujeme do projektu přidat další závislosti na jednotlivých spring komponentách, databázi HyperSQL, Hibernate a několika dalších knihovnen, které neše aplikace využívá. Pro kompletní seznam se podívejte do souboru pom.xml na obsah tagu <dependencies>.

Spring konfiguraci uložíme do souboru pojmenovaného src/main/resource/spring/helloworldContext.xml. Maven jej tak při vytváření aplikačního balíku umístí na správné místo. Projdeme důležité části souboru:

    

Tento element definuje java package ve kterém bude pro všechny beany označené anotacemi @Service nebo @Repository vytvořen seznam. Z tohoto seznamu může být každá java bean injektována použitím atributu s anotací @Autowired nebo @Resource. Pro příklad se podívejte do třídy EmployeeUIServiceImpl. Takto je definován její atribut employeeManagementService.



  



Konfigurace EntityManagerFactory. Parametr jpaVendorAdapter je jediné místo v aplikaci pro konfiguraci JPA implementace. Takže pokud bychom chtěli z nějakého důvodu jako implementaci persistence použít například Oracle TopLink pak stačí změnit konfiguraci tohoto atributu.







Definice datasource. Jako databáze je použita HyperSQL. Jedná se o databázový engine napsaný čistě v javě. V naši konfiguraci použijeme databázi uloženou v paměti, která nepotřebuje žádnou další konfiguraci. Engine vytvoří databázi automaticky při prvním přístupu a po ukončení bude obsah zahozen. V případě potřeby je možné HyperSQL nakonfigurovat tak aby data ukládal do souboru.




Spring transaction manager. Tento se obvykle nevolá přímo z aplikačního kódu. My uděláme výjimku v junit testech zmíněných dále.


Každá metoda nebo třída označena anotací @Transactional bude spuštěna v transakci.


Manuální definice beanu. Spring vytvoří instanci třídy EmployeeUIServiceImpl a přiřadí jí ID. Tato beana je použita v implementaci servletu ControllerServlet. Vzhledem k použití autowire strategie není potřeba tímto způsobem deklarovat všechny beany v kontextu. Spring to udělá za nás pokud jsou označeny anotacemi @Service, @Repository atd.

JUnit testy

Hlavní část aplikace je hotova takže je čas ověřit její funkčnost pomocí unit testů. Zde nám opět pomůže Maven. Pokud jsou testy v adresáři src/test/java pak jsou automaticky přeloženy a spuštěny (pokud to není explicitně zakázáno) před každým buildem. Máme tak zajištěno, že build proběhne úspěšně jen pokud prošly všechny testy.

Naše testy budou zapisovat do databáze a následně z ní číst. Zapsaná data by však mohla ovlivnit výsledky jiných testů, případně v reálné aplikaci jiné komponenty přistupující k databázi. Jedním z možných řešení je na začátku testu otevřít transakci a na konci provést rollback. Vytvoříme tedy abstraktní třídu BaseSpringTestCase, která bude sloužit jako předek všech unit testů a označíme jí anotací @ContextConfiguration obsahující cestu ke konfiguraci Spring kontextu. Třída bude obsahovat metody pro zahájení a ukončení transakcí.

Samotná testovací třída je potomek BaseSpringTestCase. Jednotlivé testy jsou public metody označené anotací @Test. Význam testu je snad ze zdrojového kódu pochopitelný. Následuje jeho zkrácená verze:

@Test
public void persistAndFindTest(){
    beginRollbackOnlyTransaction();
    try {
        // test method .....
    }
    finally {
        commitTransaction();
    }
}

Jak vidíte na začátku testovací metody je vytvořena transakce a zároveň je označena atributem rollback-only. To znamená že při úspěšném ukončení transakce (commit) i při neočekávaném ukončení dojde k rollback operaci a žádná data po skončení testu nezůstanou v databázi.

Testy je možné spouštět samostatně pomocí mavenu voláním

mvn test

Logy průběhu testů včetně například SQL statementů generovanvých Hibernate jsou vypisovány přímo do konzole a výsledky testů jsou uloženy v adresáři target/surefire-reports.

Web aplikace

Dalším krokem bude vytvoření jednoduchého grafického rozhraní aplikace. Bude se jednat o servlet a jsp stránku. Stránka zobrazuje seznam všech zaměstnanců z databáze a formulář pro vložení nového záznamu.
Seznam zaměstnanců je v servletu ControllerServlet.java načten voláním employeeUIService.getAllEmployeeNames() a předán do EmployeeViewer.jsp přes atribut requestu. V jsp stránce je seznam pomocí cyklu vypsán do tabulky.

Naopak nový záznam je předán z formuláře v jsp stránce do servletu kde je zavolána metoda pro perzistenci záznamu employeeUIService.addEmployee(firstName, lastName).

    public void init() throws ServletException {
        WebApplicationContext ctx = WebApplicationContextUtils.getRequiredWebApplicationContext(getServletContext());
        employeeUIService = (EmployeeUIService) ctx.getBean(EmployeeUIService.class.getName());
    }

Do servletu ControllerServlet je bean employeeUIService spravovaný springem nastaven “ručně” tak že je v metodě init(), volané při inicializaci servletu, získán aplikační kontext a z něj pomocí identifikátoru přiřazen bean. Všimněte si, že jako ID beanu je zvoleno jméno interface což je jedna z cest jak si udržet pořádek ve velkém množství beanů spravovaných springem.

Důležitým konfiguračním souborem webové aplikace je web.xml, podíváme se na jednotlivé jeho části.


contextConfigLocation
WEB-INF/classes/spring/helloworldContext.xml


org.springframework.web.context.ContextLoaderListener

parametr contextConfigLocation definuje cestu ke konfiguraci našeho Spring kontextu. Jedná se o stejný konfigurační soubor, který byl použit i pro JUnit testy. ContextLoaderListener je listener inicializující Spring a jeho WebApplicationContext. V konfiguraci následují dále definice servletů a jejich mapování na url.

Posledním souborem je persistence.xml definovaný v tomto případě skutečně minimalisticky. Definuje persistence unit do které budou automaticky zahrnuty všechny entity (@Entity) – v našem případě pouze Employee.

To jsou všechny části aplikace. Pro její překlad, spuštění testů, vytvoření WAR balíku a deploy do Tomcatu použijeme následující příkaz

mvn package cargo:start

Pokud nedošlo k žádné chybě pak najdeme aplikaci na adrese http://localhost:8080/spring-helloworld/employees

Zdrojový kód aplikace ke stažení.

Tags: , , , , ,

8 Comments to Jak vytvořit java web aplikaci s podporou Spring a JPA

Petr Prochazka
October 15, 2010

Zdravim,
dekuji za zajimavy getting started clanek.
Jenom by mne zajimalo, proc neni anotace Autowired prilis dobra volba?
Zkousel jsem najit nejaky clanek o tomto, ale marne.
Mate nejaky link, ktery toto popisuje nebo vlastni zkusenost?

Diky

Marek
October 15, 2010

Poznate Spring ROO? Na taketo start-up aplikacie je to uplne idealna vec.

To co je popisovane v clanku, spravite pomocou ROO za 2 minuty.

Naozaj odporucam. http://www.springsource.org/roo

Martin
October 15, 2010

len taka drobnost … IDEA vie (aspon vo verzii 9.X) tiez pracovat priamo s .pom :)

Jaroslav
October 15, 2010

Na ID entity je ideálne UUID ???. Prečo preboha dlhokánske a hlavne stringové idéčko? Čo vám bráni použiť celé číslo ?

Martin
October 15, 2010

@Jaroslav: jen teď mě napadá několik důvodů. Především UUID bude, narozdíl od celého čísla, pravděpodobně unikátní v rámci různých databází a tak případné kopírování záznamů, obnovy ze zálohy atd budou bez případných kolizí. Dále třeba to, že pro databázi v běžící clusteru do které se vkládají velké množství záznamů může sekvence být úzkým místem z hlediska výkonu. A nevím proč Vám vadí string? Indexovat to jde výborně a pamatovat si to nepotřebujete, pro takové účely se bude vždycky hodit nějaké druh “business id”

Martin
October 15, 2010

@Martin: ad IDEA, díky to jsem nevěděl. Zatím pořád zůstávám u Eclipse i když jsem se několikrát pokusil přejít k netbeans :-)

Martin
October 15, 2010

@Marek: ad Spring ROO, slyšel jsem o tom, ale ještě jsem to nezkoušel. Zkoušel jsem třeba AppFuse, která se taky hodí na rychlé prototypování.

Martin
October 16, 2010

@Petr Prochazka ad autowiring, tohle je asi na delší diskuzi a nevím jestli je jednoznačná odpověď. Důvod je například ten, že interface u kterého použijete @Autowired může mít více implementací. Pak nemůžete použít “byType” atd. A proti autowiring “byName” je zase možnost kolize v názvech beanů. Link mě teď žádný nenapadá, zkuste dokumentaci ke springu.