26 lutego 2007

Aplikacja JPA w GlassFish v2 z Firebird v2 i Eclipse IDE 3.3

Po sukcesie z Eclipse Mylar pozostałe wtyczki, z którymi miałem problem zaczęły również w końcu działać! Okazało się, że błędna konfiguracja Eclipse IDE 3.3M5, który korzystał z biblioteki Java 1.4.2 powodowała, że wtyczki zależne od Java 5 zainstalowane nie zgłaszały błędu, ale i nie działały. Odinstalowanie Java 1.4.2 z mojego komputera było nie tylko dobrym krokiem w celu poprawy sytuacji z niedziałającymi wtyczkami, ale również zwiększenia ilości wolnego miejsca na dysku (taki maluczki niedzielny prezent).

Taki sukces wymagał odpowiedniego rozgłosu! Musiał być oznajmiony w odpowiedni sposób - przepisem na przykładową aplikację JPA z Eclipse IDE, GlassFish i Firebird.

Zainstalowawszy WTP 2.0M5 (z narzędziami do JPA i JSF) oraz GlassFish Eclipse Plugin postanowiłem popróbować się z utworzeniem prostej aplikacji JPA, która korzystałaby ze źródła danych w GlassFish v2 do Firebird v2 (temat pojawił się na grupie pl.comp.lang.java - EJB3 SunAppServer i Firebird - i akurat zbiegł się z moim powrotem do lektury specyfikacji JPA). Nigdy nie pracowałem z Firebird, ale poza poznaniem podstawowych komend do uruchomienia bazy danych i jej zatrzymania nie zamierzałem poznawać produktu więcej. To jest właśnie zaleta warstwowych aplikacji, gdzie część odpowiedzialności za tworzenie aplikacji można scedować na inne osoby w projekcie (no cóż, nie pierwszy raz występuję w kilku rolach: administratora bazy danych, serwera aplikacyjnego i programisty). W zasadzie nie potrzebowałem serwera aplikacyjnego do uruchomienia aplikacji z JPA, ale zadane pytanie na p.c.l.java dotyczyło takiego środowiska, a co więcej zarządzanie połączeniami do bazy danych zlecimy serwerowi aplikacyjnemu GlassFish (poznanie kilku komend administracyjnych nie powinno przysłonić właściwej treści wpisu).

Zanim przedstawię przepis na aplikację JPA i otaczający ją świat, kilka słów o wersjach oprogramowania w środowisku wykonawczym (uruchomieniowym):
Poniższy obrazek mówi sam za siebie - Eclipse IDE 3.3 z wtyczkami, które obiecują usprawnić pracę z technologiami Java EE 5 i przybliżyć nas do (bądź wręcz przewyższyć możliwości) NetBeans IDE 6.0 (jeszcze nie tym razem ;-))


Krok 1. Definicja GlassFish v2 w Eclipse IDE 3.3

Rozpoczynam od definicji GlassFish w Eclipse IDE.


Jak widać powyżej lista dostępnych serwerów aplikacyjnych jest imponująca, ale tym razem padło na GlassFish (reszta musi poczekać).


Po chwili Eclipse IDE już wie jak pracować z GlassFish v2. Na razie idzie gładko. GlassFish jest zdefiniowany i uruchomiony - wszystko z poziomu Eclipse IDE.

Krok 2. Uruchomienie Firebird Superserver

Uruchamiam Firebird Superserver (zalecana konfiguracja na MS Windows, na którym pracuję).
    $ cd $FIREBIRD_HOME
$ ./bin/install_super.bat
, co tworzy dwa serwisy Firebird Guardian oraz Firebird Server - oba powinny już być uruchomione.


Po tym można już założyć nową bazę danych dedykowaną dla mojej przykładowej aplikacji.

$ cd $FIREBIRD_HOME

$ ./bin/isql.exe
Use CONNECT or CREATE DATABASE to specify a database
SQL> create database 'c:\temp\glassfish-jpa.fdb' page_size 8192
CON> user 'SYSDBA' password 'masterkey';
SQL> exit;

I sprawdzam, że baza danych faktycznie jest stworzona i dostępna z zewnątrz.

$ ./bin/isql.exe
Use CONNECT or CREATE DATABASE to specify a database
SQL> connect 'localhost:c:\temp\glassfish-jpa.fdb' user 'sysdba' password 'masterkey';
Database: 'localhost:c:\temp\glassfish-jpa.fdb', User: sysdba
SQL> exit;

Dowiaduję się o możliwości utworzenia synonimów (ang. alias) dla nowoutworzonej bazy danych. Korzystanie z synonimu jest prostsze i dodatkowo skrypt tworzący pulę połączeń w GF nie akceptuje dwukropka w ścieżce bazy danych (':'), który jest separatorem w opcji --property.

W pliku $FIREBIRD_HOME/aliases.conf umieściłem poniższą linię:

glassfish-jpa=c:\temp\glassfish-jpa.fdb

Krok 3. Definicja źródła danych do Firebird w GlassFish

Postępujemy zgodnie z dokumentacją GlassFish'a - Chapter 14: Using the JDBC API for Database Access.

Istnieją dwie możliwości zdefiniowania źródła danych w GlassFish:
Wybieram opcję pierwszą - polecenie asadmin create-jdbc-connection-pool.

Najpierw należy przekopiować sterownik JDBC Firebird'a - jaybird-full-2.1.1.jar - do katalogu $GLASSFISH_HOME/domains/domain1/lib/ext.

Następnie wydać polecenia z poziomu konsoli:

$ ./bin/asadmin.bat create-jdbc-connection-pool \
--datasourceclassname org.firebirdsql.jdbc.FBWrappingDataSource \
--restype javax.sql.DataSource \
--property password=masterkey:userName=sysdba:databaseName=//localhost/glassfish-jpa:pooling=true:minSize=5:maxSize=30:type=type4 \
FirebirdPool

$ ./bin/asadmin.bat ping-connection-pool FirebirdPool
Command ping-connection-pool executed successfully.

$ ./bin/asadmin.bat create-jdbc-resource --connectionpoolid FirebirdPool jdbc/Firebird

W konsoli można sprawdzić, że połączenie faktycznie zostało zdefiniowane, a jak później się okaże i dostępne dla aplikacji.

Krok 4. Utworzenie encji Customer

Mając zainstalowaną i co ważne działającą wtyczkę Eclipse Dali skrzętnie korzystam z jego pomocy przy utworzeniu encji.


, a następnie

Po chwili mam kompletną klasę encji, która jest o tyle ciekawa, że jak się później okazało wymagała adnotacji wcześniej przeze mnie nie wykorzystywanych - @NamedQuery oraz @Column z atrybutem columnDefinition. Użycie @NamedQuery było opcjonalne i służyło jedynie upraszczeniu zarządzania zapytaniem (możliwe późniejsze modyfikacje) podczas, gdy użycie z @Column(columnDefinition="NUMERIC(10)") było konieczne dla poprawnego działania TopLink z Firebird. TopLink jest domyślnym środowiskiem JPA w GlassFish i niestety nie posiada wsparcia dla Firebird, a domyślna konfiguracja TopLink wskazuje na Oracle i stąd pewne rozbieżności w tworzonych komendach SQL.

package pl.jaceklaskowski.jpa.firebird.entities;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.NamedQuery;

@Entity
@NamedQuery(name = "znajdzWszystkichKlientow", query = "SELECT c FROM Customer c")
public class Customer {

private int pesel;

private String name;

public Customer() {
}

public Customer(int pesel, String name) {
this.pesel = pesel;
this.name = name;
}

@Id
@Column(columnDefinition = "NUMERIC(10)")
public int getPesel() {
return pesel;
}

public void setPesel(int pesel) {
this.pesel = pesel;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

}

Do pełnej konfiguracji encji Customer potrzebny był jeszcze plik konfiguracyjny - persistence.xml.

<?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="firebird">
<jta-data-source>jdbc/Firebird</jta-data-source>
<class>pl.jaceklaskowski.jpa.firebird.entities.Customer</class>
<properties>
<property name="toplink.ddl-generation" value="drop-and-create-tables"/>
<property name="toplink.logging.level" value="ALL"/>
</properties>
</persistence-unit>
</persistence>


Krok 5. Utworzenie aplikacji internetowej - firebird

Stworzyłem bardzo trywialny servlet, który podlega zarządzaniu zależnościami w Java EE 5 i mogłem skorzystać z adnotacji.

package pl.jaceklaskowski.jpa.firebird.servlets;

import java.io.IOException;
import java.util.List;

import javax.annotation.Resource;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.transaction.UserTransaction;

import pl.jaceklaskowski.jpa.firebird.entities.Customer;

@SuppressWarnings("serial")
public class CreateCustomerServlet extends javax.servlet.http.HttpServlet implements javax.servlet.Servlet {

@PersistenceContext(unitName = "firebird")
private EntityManager em;

@Resource
private UserTransaction ut;

@SuppressWarnings("unchecked")
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
int pesel = Integer.parseInt(request.getParameter("pesel"));
String name = request.getParameter("name");
if (pesel > 0 && name != null) {
Customer customer = new Customer(pesel, name);
try {
ut.begin();
em.persist(customer);
ut.commit();
} catch (Exception e) {
throw new ServletException(e);
}
}
List<Customer> customers = em.createNamedQuery("znajdzWszystkichKlientow").getResultList();
request.setAttribute("customers", customers);
request.getRequestDispatcher("/customers.jsp").forward(request, response);
}
}

z poniższym deskryptorem instalacji aplikacji internetowej - WEB-INF/web.xml:

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee web-app_2_5.xsd" version="2.5">
<display-name>firebird-web</display-name>
<servlet>
<description></description>
<display-name>CreateCustomerServlet</display-name>
<servlet-name>CreateCustomerServlet</servlet-name>
<servlet-class>pl.jaceklaskowski.jpa.firebird.servlets.CreateCustomerServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>CreateCustomerServlet</servlet-name>
<url-pattern>/createCustomer</url-pattern>
</servlet-mapping>
<welcome-file-list>
<welcome-file>customers.jsp</welcome-file>
</welcome-file-list>
</web-app>

Pozostało stworzyć stronę prezentującą wyniki działania aplikacji - customers.jsp:

<%@ page language="java" contentType="text/html; charset=ISO-8859-1"
pageEncoding="ISO-8859-1"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Klienci</title>
</head>
<body>
<form action="createCustomer">
<table>
<tr>
<td>PESEL:</td>
<td><input name="pesel" /></td>
</tr>
<tr>
<td>Nazwa:</td>
<td><input name="name" /></td>
</tr>
<tr>
<td><input type="submit" value="Stwórz" /></td>
</tr>
</table>
</form>
<table border="0" cellpadding="1" cellspacing="1">
<tr>
<td>Lp.</td>
<td>PESEL</td>
<td>Nazwa</td>
</tr>
<c:forEach var="customer" items="${customers}" varStatus="status">
<tr>
<td>${status.count}</td>
<td>${customer.pesel}</td>
<td>${customer.name}</td>
</tr>
</c:forEach>
</table>
</body>
</html>
Krok 6. Uruchomienie aplikacji

Wybieram menu kontekstowe Run As->Run on Server:


i po chwili w zakładce konsoli pojawiają się nieciekawe, ale oczekiwane komunikaty o uruchomieniu aplikacji:

Buildfile: C:\.eclipse\jpa\.metadata\.plugins\org.eclipse.jst.server.generic.core\serverdef\sunappsrv-ant.xml
deploy.j2ee.web:
[echo] C:/.eclipse/jpa/.metadata/.plugins/org.eclipse.wst.server.core/tmp0/firebird-web
[jar] Building jar: C:\.eclipse\jpa\.metadata\.plugins\org.eclipse.jst.server.generic.core\serverdef\firebird-web.war
tools:
deploy:
[exec] Command deploy executed successfully.
deploy-url-message:
[echo] Application Deployed at: http://127.0.0.1:8080/firebird-web
BUILD SUCCESSFUL
Total time: 8 seconds


a w dzienniku zdarzeń GlassFish'a:

[#|2007-02-26T08:18:15.546+0100|INFO|sun-appserver9.1|oracle.toplink.essentials.session.file:/C:/apps/glassfish/domains/domain1/applications/j2ee-modules/firebird-web/WEB-INF/lib/f
irebird.jar-firebird|_ThreadID=14;_ThreadName=Thread-24;|Not able to detect platform for vendor name [Firebird 2.0*WI-V2.0.0.12748 Firebird 2.0/tcp (dev)/P10]. Defaulting to [oracl
e.toplink.essentials.platform.database.DatabasePlatform]. The database dialect used may not match with the database you are using. Please explicitly provide a platform using proper
ty toplink.platform.class.name.|#]

[#|2007-02-26T08:18:15.562+0100|CONFIG|sun-appserver9.1|oracle.toplink.essentials.session.file:/C:/apps/glassfish/domains/domain1/applications/j2ee-modules/firebird-web/WEB-INF/lib
/firebird.jar-firebird.connection|_ThreadID=14;_ThreadName=Thread-24;|connecting(DatabaseLogin(
platform=>DatabasePlatform
user name=> ""
connector=>JNDIConnector datasource name=>null
))|#]

[#|2007-02-26T08:18:15.562+0100|CONFIG|sun-appserver9.1|oracle.toplink.essentials.session.file:/C:/apps/glassfish/domains/domain1/applications/j2ee-modules/firebird-web/WEB-INF/lib
/firebird.jar-firebird.connection|_ThreadID=14;_ThreadName=Thread-24;|Connected: jdbc:firebirdsql:java://localhost/glassfish-jpa
User: sysdba
Database: Firebird 2.0*WI-V2.0.0.12748 Firebird 2.0/tcp (dev)/P10 Version: WI-V2.0.0.12748 Firebird 2.0*WI-V2.0.0.12748 Firebird 2.0/tcp (dev)/P10
Driver: Jaybird JCA/JDBC driver Version: 2.1|#]

[#|2007-02-26T08:18:15.562+0100|CONFIG|sun-appserver9.1|oracle.toplink.essentials.session.file:/C:/apps/glassfish/domains/domain1/applications/j2ee-modules/firebird-web/WEB-INF/lib
/firebird.jar-firebird.connection|_ThreadID=14;_ThreadName=Thread-24;|connecting(DatabaseLogin(
platform=>DatabasePlatform
user name=> ""
connector=>JNDIConnector datasource name=>null
))|#]

[#|2007-02-26T08:18:15.562+0100|CONFIG|sun-appserver9.1|oracle.toplink.essentials.session.file:/C:/apps/glassfish/domains/domain1/applications/j2ee-modules/firebird-web/WEB-INF/lib
/firebird.jar-firebird.connection|_ThreadID=14;_ThreadName=Thread-24;|Connected: jdbc:firebirdsql:java://localhost/glassfish-jpa
User: sysdba
Database: Firebird 2.0*WI-V2.0.0.12748 Firebird 2.0/tcp (dev)/P10 Version: WI-V2.0.0.12748 Firebird 2.0*WI-V2.0.0.12748 Firebird 2.0/tcp (dev)/P10
Driver: Jaybird JCA/JDBC driver Version: 2.1|#]

, czyli działa! A zatem na dzisiaj koniec.

Problemy

Najczęstszy to brak pamięci, który pojawia się pracując z plikami XML w tak "rozszerzonym" Eclipse IDE. Być może zwiększenie pamięci naprawiłoby problem, ale nie chciało mi się szukać.

I drugi, równie bolesny, to utrata stanu GlassFish przez wtyczkę GlassFish Eclipse plugin, kiedy w trakcie zatrzymywania serwera próbowałem uruchomić go ponownie. Rozwiązanie okazało się wymagające monitorowania stanu GF - podgląd dziennika zdarzeń GF ($GLASSFISH_HOME/domains/domain1/logs/server.log). Możemy uruchomić GF dopiero po pojawieniu się następujących wpisów w dzienniku zdarzeń:

[#|2007-02-26T08:19:39.796+0100|INFO|sun-appserver9.1|javax.enterprise.system.core|_ThreadID=15;_ThreadName=RMI TCP Connection(6)-127.0.0.1;|CORE5052: Application shutdown complete
.|#]

[#|2007-02-26T08:19:40.109+0100|INFO|sun-appserver9.1|javax.enterprise.system.tools.deployment|_ThreadID=15;_ThreadName=RMI TCP Connection(6)-127.0.0.1;|[AutoDeploy] Disabling Auto
Deployment service.|#]

Wcześniejsza próba uruchomienia będzie objawiała się tym, że Eclipse będzie raportował o próbie uruchomienia GF (Status=Starting...)


mimo, że tak na prawdę nie kontroluje GF i komunikat będzie trwał i trwał. Jedynym wyjściem wydaje się być zatrzymanie serwera z poziomu Eclipse IDE (tj. wciśnięcie ikony w postaci czerwonego kwadratu) i ponowne jego uruchomienie.

Czasami podczas uaktualniania aplikacji (ponownej jej instalacji na serwerze) pojawiał się błąd:

Buildfile: C:\.eclipse\jpa\.metadata\.plugins\org.eclipse.jst.server.generic.core\serverdef\sunappsrv-ant.xml
deploy.j2ee.web:
[echo] C:/.eclipse/jpa/.metadata/.plugins/org.eclipse.wst.server.core/tmp0/firebird-web
[jar] Building jar: C:\.eclipse\jpa\.metadata\.plugins\org.eclipse.jst.server.generic.core\serverdef\firebird-web.war
tools:
deploy:
[exec] Command deploy executed successfully with following warning messages: WARNING:
[exec] The following file(s) were left(locked) from the previous deployment; they were not updated during this deployment and could interfere with the application:
[exec] C:\apps\glassfish\domains\domain1\applications\j2ee-modules\firebird-web\WEB-INF\lib\firebird.jar
deploy-url-message:
[echo] Application Deployed at: http://127.0.0.1:8080/firebird-web
BUILD SUCCESSFUL
Total time: 4 seconds

co obchodzimy przez zatrzymanie GF i ponowne uruchomienie.

Skłaniam się ku stwierdzeniu, że najmniej uciążliwym sposobem uaktualnienia aplikacji na serwerze GlassFish z użyciem wtyczki GlassFish Eclipse plugin jest po prostu zatrzymywanie serwera i jej instalacja za pomocą menu kontekstowego Publish (co automatycznie uruchomi również serwer, jeśli zatrzymany).