niedziela, 27 grudnia 2009

dzlaczego nie lubię Mavena, cz. I

W świecie sposobów budowania aplikacji javowych królują 2 rozwiązania: Apache Ant i Maven2. Od jakiegoś czasu uczestniczę w sporym projekcie opartym o to drugie rozwiązanie. O ile Maven (1) miał opinie rozwiązania nie do końca dopracowanego, o tyle głosy na temat Mavena2 są w większości pochlebne: bo lepszy, bo nowszy, bo deklaratywny, bo sam martwi się o zależności.
Teoretycznie wszystko wygląda ładnie, a w praktyce...
... a w praktyce potrzebuję skorzystać z biblioteki XML-RPC Server. Co robię w takim przypadku?
Wchodzę na stronę http://mvnrepository.com/
i wpisuję nazwę szukanej biblioteki.
Coś znalazłem, więc dodaję zależność do pliku pom.xml

<dependencies>
<dependency>
<groupId>org.apache.xmlrpc</groupId>
<artifactId>xmlrpc-server</artifactId>
<version>3.1.2</version>
</dependency>
</dependencies>

i kompiluję projekt, a tu niespodzianka:

[INFO] ------------------------------------------------------------------------
[ERROR] BUILD FAILURE
[INFO] ------------------------------------------------------------------------
[INFO] Compilation failure
error: error reading /home/mateusz/.m2/repository/javax/servlet/servlet-api/2.3/servlet-api-2.3.jar; error in opening zip file


Hm... chwila refleksji... i nie wiadomo o co chodzi. Z pomocą przychodzi cat

cat ~/.m2/repository/javax/servlet/servlet-api/2.3/servlet-api-2.3.jar
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<html><head>
<title>301 Moved Permanently</title>
</head><body>
<h1>Moved Permanently</h1>
<p>The document has moved <a href="http://download.java.net/maven/1/javax.servlet/jars/servlet-api-2.3.jar">here</a>.</p>
<hr>
<address>Apache Server at maven-repository.dev.java.net Port 443</address>
</body></html>


No tak... jar to to nie jest.
Aby skompilować projekt, trzeba usunąć ten tekstowy "jar":

rm ~/.m2/repository/javax/servlet/servlet-api/2.3/servlet-api-2.3.jar


i dodać w pliku pom.xml repozytorium


<repositories>
<repository>
<id>java.net</id>
<name>java.net</name>
<url>http://download.java.net/maven/1</url>
<layout>legacy</layout>
</repository>
</repositories>


Cóż, nowe możliwości, nowe problemy... ale przy servlet API? A może to z moim podejściem do mavena jest coś nie tak? Przyznam się, że w głównej mierze polega ono na rozpoznaniu walką.

sobota, 31 października 2009

O testach wydajnościowych słów kilka

Testy wydajnościowe, jak to testy... robić trzeba. Jak to pisze Misko: You do test, right? Zazwyczaj aby je przeprowadzić korzystałem z Apache JMeter. Polecam ten program i może nawet kiedyś zamieszczę prosty przewodnik, jak zacząć z nim przygodę. Dzisiaj jednak skupię się na innym problemie i innym narzędziu.
Przeprowadzając testy wydajnościowe, najważniejszym w moim odczucie problemem, z jakim się spotyka osoba pisząca test jest odwzorowanie charakterystyki żądań obsługiwanych przez aplikację. Tak przy okazji, wiedzieliście, że aplikacja to "ozdobny wzór naszywany na tło z innego materiału"? A spieszyć to "uczynić kogoś pieszym" Coraz bardziej lubię słowniki.

A priori trudno określić, jak użytkownicy będą korzystać z naszego programu i w co będą klikać. Kiedy będą żądane obrazki, kiedy pliki css i różne inne. Obserwując jednak to, co się dzieje na serwerze, możemy utworzyć całkiem sprawne scenariusze.

A co się dzieje, możemy odczytać z logów. Zakładam tutaj, że ruch sieciowy obsługiwany jest przez serwer WWW Apache, za którym stoi serwer aplikacji, z naszą aplikacją internetową.
Apache trzyma informacje o tym kto i do jakich zasobów wysyła żądania w pliku access.log.
Podglądam jego zawartość poleceniem
tail -f /var/logs/apache2/access.log


83.2.107.220 - - [31/Oct/2009:23:03:08 +0100] "GET /swiat/komentarze/_resource/fs/img/ajax_loading_small.gif HTTP/1.0" 301 284 "http://beta.polityka.pl/swiat/komentarze/1500387,1,po-unijnym-szczycie.read" "Opera/9.80 (Windows NT 5.1; U; pl) Presto/2.2.15 Version/10.00" 0/338
83.2.107.220 - - [31/Oct/2009:23:03:08 +0100] "GET /swiat/komentarze/_resource/fs/res/img-blogi.png HTTP/1.0" 301 276 "http://beta.polityka.pl/swiat/komentarze/1500387,1,po-unijnym-szczycie.read" "Opera/9.80 (Windows NT 5.1; U; pl) Presto/2.2.15 Version/10.00" 0/246
83.2.107.220 - - [31/Oct/2009:23:03:08 +0100] "GET /swiat/komentarze/_resource/res/20000457/cust/f120x80 HTTP/1.0" 200 4025 "http://beta.polityka.pl/swiat/komentarze/1500387,1,po-unijnym-szczycie.read" "Opera/9.80 (Windows NT 5.1; U; pl) Presto/2.2.15 Version/10.00" 0/1362
83.2.107.220 - - [31/Oct/2009:23:03:08 +0100] "GET /swiat/komentarze/_resource/fs/img/presseurop.png HTTP/1.0" 301 275 "http://beta.polityka.pl/swiat/komentarze/1500387,1,po-unijnym-szczycie.read" "Opera/9.80 (Windows NT 5.1; U; pl) Presto/2.2.15 Version/10.00" 0/337
83.2.107.220 - - [31/Oct/2009:23:03:08 +0100] "GET /swiat/komentarze/_resource/fs/img/polityka_logo_small.png HTTP/1.0" 301 280 "http://beta.polityka.pl/swiat/komentarze/1500387,1,po-unijnym-szczycie.read" "Opera/9.80 (Windows NT 5.1; U; pl) Presto/2.2.15 Version/10.00" 0/264
83.2.107.220 - - [31/Oct/2009:23:03:08 +0100] "GET /_resource/fs/img/close.png HTTP/1.0" 304 - "http://beta.polityka.pl/swiat/komentarze/1500387,1,po-unijnym-szczycie.read" "Opera/9.80 (Windows NT 5.1; U; pl) Presto/2.2.15 Version/10.00" 0/256
83.2.107.220 - - [31/Oct/2009:23:03:08 +0100] "GET /_resource/fs/res/img-blogi.png HTTP/1.0" 304 - "http://beta.polityka.pl/swiat/komentarze/1500387,1,po-unijnym-szczycie.read" "Opera/9.80 (Windows NT 5.1; U; pl) Presto/2.2.15 Version/10.00" 0/229
83.2.107.220 - - [31/Oct/2009:23:03:08 +0100] "GET /_resource/fs/img/presseurop.png HTTP/1.0" 304 - "http://beta.polityka.pl/swiat/komentarze/1500387,1,po-unijnym-szczycie.read" "Opera/9.80 (Windows NT 5.1; U; pl) Presto/2.2.15 Version/10.00" 0/251
83.2.107.220 - - [31/Oct/2009:23:03:08 +0100] "GET /_resource/fs/img/polityka_logo_small.png HTTP/1.0" 304 - "http://beta.polityka.pl/swiat/komentarze/1500387,1,po-unijnym-szczycie.read" "Opera/9.80 (Windows NT 5.1; U; pl) Presto/2.2.15 Version/10.00" 0/222
83.2.107.220 - - [31/Oct/2009:23:03:08 +0100] "GET /_resource/fs/img/ajax_loading_small.gif HTTP/1.0" 304 - "http://beta.polityka.pl/swiat/komentarze/1500387,1,po-unijnym-szczycie.read" "Opera/9.80 (Windows NT 5.1; U; pl) Presto/2.2.15 Version/10.00" 0/220


Jest to bardzo cenne źródło żądań wysyłanych do aplikacji, z których stworzę scenariusz testowy. Wydobywam więc wszystkie żądania z godziny 9tej i zapisuję je w pliku /tmp/requests.txt


cat /var/log/apache2/access.log | grep 31/Oct/2009:09 | awk '{print $7}' > /tmp/requests.txt


I już jestem w ogródku. Czas przywitać się z gąską...
Pozostało jakoś z pliku o strukturze:

/swiat/komentarze/_resource/fs/img/ajax_loading_small.gif
/swiat/komentarze/_resource/fs/res/img-blogi.png
/swiat/komentarze/_resource/res/20000457/cust/f120x80

wykonać żądania do poszczególnych zasobów.

Tu z pomocą przychodzi program Apache Benchmark i kilka poleceń linuksowych.

Odpalam więc scenariusz poleceniem:
for i in $(cat /tmp/requests.txt);do ab -n 2 -c 2 "http://nazwahosta$i"; done

-n 2 odpali każde żądanie 2 razy
-c 2 zrobi to w 2 wątkach

Dobre efekty daje odpalenie kilku scenariuszy testowych, np. z godziny 8, 9, 10, 11, zrównoleglonych screenem, czyli
screen for i in $(cat /tmp/requests.txt);do ab -n 2 -c 2 "http://nazwahosta$i"; done

A jakie są twoje patenty, na testy wydajnościowe?

sobota, 15 sierpnia 2009

Słabe referencje... z czym to się je?

Dzisiaj święto Wojska Polskiego. Parady wojskowej nie będzie z powodu kryzysu, ale sklepy (z zupełnie zresztą innego powodu) pozamykane. A jeść trzeba...

Może skuszę się na słabą referencję... ale z czym to się je?

Referencja jak referencja, spotkał się z nią każdy:

String string = new String("123");

Słaba referencja wygląda trochę inaczej

Reference<String> refString = new WeakReference<String>(new String("123"));

a dostęp do obiektu, na który referencja wskazuje uzyskujemy za pomocą wywołania metody get()

refString,get();

Nie o wygląd jednak tutaj chodzi.

Dopóki obiekt w naszym programie wskazywany jest przez zwykłą referencję, nie zostanie on usunięty z pamięci przez jakże uwielbiany przez wszystkich Garbage Collector.

Natomiast obiekt wskazywany przez słabą referencję, może być usunięty przez GC jeżeli zacznie brakować pamięci, mimo, że referencja do niego cały czas jest obecna w programie. Metoda get() zwróci wtedy wartość null.

Nie wierzysz?

Oto krótki program obrazujący działanie słabych (i mocnych) referencji:

package pl.matt;

import java.lang.ref.Reference;
import java.lang.ref.WeakReference;
import java.util.ArrayList;


public class WeakReferenceTest {

int[] tab = new int[1024];

public static void main(String[] args) {
// zwykła referencja
String string = new String("123");

// słaba referencja
Reference<String> refString = new WeakReference<String>(new String("456"));

System.out.println(string);
System.out.println(refString.get());

java.util.List<WeakReferenceTest> list = new ArrayList<WeakReferenceTest>();

for (int i = 0; i < 1000000; i++) {
WeakReferenceTest test = new WeakReferenceTest();
list.add(test);
if (refString.get() == null) {
System.out.println(string);
System.out.println(i + " " + refString.get());
break;
}
}
}

}

Program w pętli przydziela pamięć tworząc nowe obiekty klasy WeakReferenceTest(). W pewnym momencie zaczyna się uruchamiać Garbage Collector i usuwa z pamięci obiekt który wskazywała słaba referencja.

Oprócz słabych referencji, w javie mamy jeszcze "średnio mocne" referencje SoftReference. Zasada ich działania jest podobna, z tym, że obiekty przez nie wskazywane żyją dłużej.

Obiekt wskazywany przez miękką referencję zostanie usunięty z pamięci tuż przed rzuceniem wyjątku java.lang.OutOfMemoryError.

Doskonałe narzędzia, to implementacji mechanizmów pamięci podręcznej.

Żeby dopełnić obrazu całości... są jeszcze referencje fantomowe PhantomReference. Obiekty wskazywane przez te referencje są usuwane przy każdym uruchomieniu GC, o ile nie wskazuje na nie żadna inna referencja. Można je stosować, aby mieć informację o tym, kiedy dany obiekt został usunięty z pamięci.

Aż się głodny zrobiłem, czas chyba zjeść coś bardziej konkretnego.

PS. Czego używacie do przeglądania RSSów? Ja jak dotąd Thunderbirda, ale ciągle szukam czegoś lepszego.

sobota, 1 sierpnia 2009

Seam z EhCache

W poprzednim wpisie wygenerowaliśmy sobie pierwszą aplikację Seamową przy pomocy wtyczki JBoss Tools.

Sposób bardzo fajny do szybkiej prezentacji, ale nie oszukujmy się, jest to sposób dobry dla dziewczyn. Do poważniejszych rozwiązań się nie nadaje.
Głównym minusem jest brak skryptu budującego aplikację, czy to antowego, czy to mavenowego.

Dlatego w dzisiejszym wpisie wygenerujemy sobie prostą aplikację Seamową, przy pomocy narzędzia seam-gen. Kilka dni temu światłu dziennemu ukazał się Seam 2.2.0.GA, ale ja posłużę się wersją 2.1.1.GA, głównie dlatego, że mam już ją zainstalowaną.

Wchodzę zatem do katalogu, w którym zainstalowałem Seama i konfiguruję go wykonując polecenie
./seam setup

mateusz@mateusz-laptop:/opt/jboss-seam-2.1.1.GA$ ./seam setup
SEAM_HOME: /opt/jboss-seam-2.1.1.GA
Using seam-gen sources from: /opt/jboss-seam-2.1.1.GA/seam-gen
Buildfile: /opt/jboss-seam-2.1.1.GA/seam-gen/build.xml

init:

setup:
[echo] Welcome to seam-gen :-)
[input] Enter your Java project workspace (the directory that contains your Seam projects) [/home/mateusz/priv/workspace/] [/home/mateusz/priv/workspace/]

[input] Enter your JBoss AS home directory [/opt/jboss-5.0.1.GA/] [/opt/jboss-5.0.1.GA/]

[input] Enter the project name [seam_test] [seam_test]

[echo] Accepted project name as: seam_test
[input] Do you want to use ICEfaces instead of RichFaces [n] (y, [n])

[input] skipping input as property icefaces.home.new has already been set.
[input] Select a RichFaces skin [ruby] (blueSky, classic, deepMarine, DEFAULT, emeraldTown, japanCherry, [ruby], wine)

[input] Is this project deployed as an EAR (with EJB components) or a WAR (with no EJB support) [ear] ([ear], war)

[input] Enter the Java package name for your session beans [pl.matt.session] [pl.matt.session]

[input] Enter the Java package name for your entity beans [pl.matt.model] [pl.matt.model]

[input] Enter the Java package name for your test cases [pl.matt.test] [pl.matt.test]

[input] What kind of database are you using? [mysql] (hsql, [mysql], oracle, postgres, mssql, db2, sybase, enterprisedb, h2)

[input] Enter the Hibernate dialect for your database [org.hibernate.dialect.MySQLInnoDBDialect] [org.hibernate.dialect.MySQLInnoDBDialect]

[input] Enter the filesystem path to the JDBC driver jar [/opt/jboss-seam-2.1.1.GA/lib/hsqldb.jar] [/opt/jboss-seam-2.1.1.GA/lib/hsqldb.jar]
/home/mateusz/java/lib/mysql-connector-java-5.1.7-bin.jar
[input] Enter JDBC driver class for your database [com.mysql.jdbc.Driver] [com.mysql.jdbc.Driver]

[input] Enter the JDBC URL for your database [jdbc:mysql:///test] [jdbc:mysql:///test]

[input] Enter database username [xxx] [xxx]

[input] Enter database password [xxx] [xxx]

[input] skipping input as property hibernate.default_schema.entered has already been set.
[input] Enter the database catalog name (it is OK to leave this blank) [] []

[input] Are you working with tables that already exist in the database? [n] (y, [n])

[input] Do you want to drop and recreate the database tables and data in import.sql each time you deploy? [n] (y, [n])

[delete] Deleting: /opt/jboss-seam-2.1.1.GA/seam-gen/build.properties
[propertyfile] Creating new property file: /opt/jboss-seam-2.1.1.GA/seam-gen/build.properties
[echo] Installing JDBC driver jar to JBoss AS
[echo] Type './seam create-project' to create the new project


podaję ścieżki do przestrzeni roboczej, do serwera JBoss, parametry bazy danych, nazwy pakietów i inne ustawienia aplikacji.

następnie tworzę projekt poleceniem
./seam create-project


mateusz@mateusz-laptop:/opt/jboss-seam-2.1.1.GA$ ./seam create-project
SEAM_HOME: /opt/jboss-seam-2.1.1.GA
Using seam-gen sources from: /opt/jboss-seam-2.1.1.GA/seam-gen
Buildfile: /opt/jboss-seam-2.1.1.GA/seam-gen/build.xml

init:

init-properties:
[echo] /opt/jboss-5.0.1.GA/

validate-workspace:

validate-project:

icefaces-staging-copy:

initcopy:

initpoms:
[echo] Setting up dependencies
[copy] Copying 1 file to /opt/jboss-seam-2.1.1.GA/classes/poms
[artifact:install] [INFO] Installing /opt/jboss-seam-2.1.1.GA/classes/poms/root.pom to /home/mateusz/.m2/repository/org/jboss/seam/root/2.1.1.GA/root-2.1.1.GA.pom
[copy] Copying 1 file to /opt/jboss-seam-2.1.1.GA/classes/poms
[artifact:install] [INFO] Installing /opt/jboss-seam-2.1.1.GA/classes/poms/parent.pom to /home/mateusz/.m2/repository/org/jboss/seam/parent/2.1.1.GA/parent-2.1.1.GA.pom
[copy] Copying 1 file to /opt/jboss-seam-2.1.1.GA/classes/poms
[copy] Copying 1 file to /opt/jboss-seam-2.1.1.GA/classes/poms
[copy] Copying 1 file to /opt/jboss-seam-2.1.1.GA/classes/poms
[copy] Copying 1 file to /opt/jboss-seam-2.1.1.GA/classes/poms
[copy] Copying 1 file to /opt/jboss-seam-2.1.1.GA/classes/poms
[copy] Copying 1 file to /opt/jboss-seam-2.1.1.GA/classes/poms
[copy] Copying 1 file to /opt/jboss-seam-2.1.1.GA/classes/poms
[copy] Copying 1 file to /opt/jboss-seam-2.1.1.GA/classes/poms
[copy] Copying 1 file to /opt/jboss-seam-2.1.1.GA/classes/poms
[copy] Copying 1 file to /opt/jboss-seam-2.1.1.GA/classes/poms
[copy] Copying 1 file to /opt/jboss-seam-2.1.1.GA/classes/poms
[copy] Copying 1 file to /opt/jboss-seam-2.1.1.GA/classes/poms
[copy] Copying 1 file to /opt/jboss-seam-2.1.1.GA/classes/poms
[copy] Copying 1 file to /opt/jboss-seam-2.1.1.GA/classes/poms
[copy] Copying 1 file to /opt/jboss-seam-2.1.1.GA/classes/poms

copyseam:

copyseamdependencies:

copyjbossembedded:

copy-icefaces-home:

copy-icefaces-maven:

copy-lib:
[echo] Copying Seam and dependencies to the /home/mateusz/priv/workspace//seam_test/lib directory...
[copy] Copying 121 files to /home/mateusz/priv/workspace/seam_test/lib
[copy] Copied 5 empty directories to 2 empty directories under /home/mateusz/priv/workspace/seam_test/lib
[echo] Copying JBoss Embedded configuration to the /home/mateusz/priv/workspace//seam_test/bootstrap directory...
[copy] Copying 30 files to /home/mateusz/priv/workspace/seam_test/bootstrap

file-copy-war:

file-copy-ear:
[echo] Copying resources needed for EAR deployment to the /home/mateusz/priv/workspace//seam_test/resources directory...
[copy] Copying 1 file to /home/mateusz/priv/workspace/seam_test/resources/WEB-INF
[copy] Copying 1 file to /home/mateusz/priv/workspace/seam_test
[copy] Copying 1 file to /home/mateusz/priv/workspace/seam_test
[copy] Copying 1 file to /home/mateusz/priv/workspace/seam_test
[copy] Copying 1 file to /home/mateusz/priv/workspace/seam_test
[copy] Copying 7 files to /home/mateusz/priv/workspace/seam_test/resources

setup-filters:

file-copy:
[copy] Copying 1 file to /home/mateusz/priv/workspace/seam_test
[copy] Copying 3 files to /home/mateusz/priv/workspace/seam_test/resources
[copy] Copying 11 files to /home/mateusz/priv/workspace/seam_test/resources
[copy] Copying 4 files to /home/mateusz/priv/workspace/seam_test
[copy] Copying 1 file to /home/mateusz/priv/workspace/seam_test
[copy] Copying 1 file to /home/mateusz/priv/workspace/seam_test
[copy] Copying 1 file to /home/mateusz/priv/workspace/seam_test
[copy] Copying 1 file to /home/mateusz/priv/workspace/seam_test/.settings
[copy] Copying 1 file to /home/mateusz/priv/workspace/seam_test
[copy] Copying 1 file to /home/mateusz/priv/workspace/seam_test
[mkdir] Created dir: /home/mateusz/priv/workspace/seam_test/nbproject
[copy] Copying 3 files to /home/mateusz/priv/workspace/seam_test/nbproject
[copy] Copying 1 file to /home/mateusz/priv/workspace/seam_test/resources
[copy] Copying 1 file to /home/mateusz/priv/workspace/seam_test/resources
[copy] Copying 1 file to /home/mateusz/priv/workspace/seam_test/resources
[copy] Copying 1 file to /home/mateusz/priv/workspace/seam_test/resources
[copy] Copying 1 file to /home/mateusz/priv/workspace/seam_test/resources
[copy] Copying 1 file to /home/mateusz/priv/workspace/seam_test/resources
[copy] Copying 12 files to /home/mateusz/priv/workspace/seam_test/view
[copy] Copying 9 files to /home/mateusz/priv/workspace/seam_test/view
[copy] Copying 1 file to /home/mateusz/priv/workspace/seam_test/src/hot/pl/matt/session
[copy] Copying 3 files to /home/mateusz/priv/workspace/seam_test
[mkdir] Created dir: /home/mateusz/priv/workspace/seam_test/src/main/pl/matt/model
[mkdir] Created dir: /home/mateusz/priv/workspace/seam_test/src/test/pl/matt/test
[copy] Copying 1 file to /home/mateusz/priv/workspace/seam_test/src/test

create-project:
[echo] A new Seam project named 'seam_test' was created in the /home/mateusz/priv/workspace/ directory
[echo] Type './seam explode' and go to http://localhost:8080/seam_test
[echo] Eclipse Users: Import the project using File > Import... > Existing Projects into Workspace, set the root directory to /home/mateusz/priv/workspace/, then select the project named seam_test
[echo] NetBeans Users: Open the project using File > Open project... and select the project folder /home/mateusz/priv/workspace//seam_test
[echo] IDEA Users: Open the project using File > Open project... and select the file /home/mateusz/priv/workspace//seam_test/seam_test.ipr

BUILD SUCCESSFUL


i zgodnie z instrukcją

Eclipse Users: Import the project using File > Import... > Existing Projects into Workspace, set the root directory to /home/mateusz/priv/workspace/, then select the project named seam_test

importuję projekt do Eclipsa.

W pakiecie pl.matt.model tworzę klasę Employee

package pl.matt.model;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;

import org.hibernate.validator.Email;
import org.hibernate.validator.Length;
import org.hibernate.validator.Max;
import org.hibernate.validator.Min;
import org.hibernate.validator.NotNull;

@Entity
@Table(name="employees")
public class Employee {

@Id
@GeneratedValue(strategy=GenerationType.IDENTITY)
private int id;

@Length(max=128)
@NotNull
private String name;

@Column(length=128)
@NotNull
private String surname;

@Email
@NotNull
@Column(length=128)
private String email;

@Min(0)
@Max(130)
@NotNull
private int age;

public int getId() {
return id;
}

public void setId(int id) {
this.id = id;
}

public String getName() {
return name;
}

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

public String getSurname() {
return surname;
}

public void setSurname(String sureName) {
this.surname = sureName;
}

public String getEmail() {
return email;
}

public void setEmail(String email) {
this.email = email;
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}

}


i generuję do niej prostego CRUDa poleceniem
./seam generate-ui

mateusz@mateusz-laptop:/opt/jboss-seam-2.1.1.GA$ ./seam generate-ui
SEAM_HOME: /opt/jboss-seam-2.1.1.GA
Using seam-gen sources from: /opt/jboss-seam-2.1.1.GA/seam-gen
Buildfile: /opt/jboss-seam-2.1.1.GA/seam-gen/build.xml

init:

init-properties:
[echo] /opt/jboss-5.0.1.GA/

validate-workspace:

validate-project:

init-generate:

generate-ui:
[echo] Building project 'seam_test' to generate views and controllers

init:

groovy.compile:

groovy.copy:

compile:

copyclasses:

jar:
[copy] Copying 1 file to /home/mateusz/priv/workspace/seam_test/exploded-archives/seam_test.ear/seam_test.jar/META-INF
[copy] Copying 1 file to /home/mateusz/priv/workspace/seam_test/exploded-archives/seam_test.ear/seam_test.jar
[hibernate] Executing Hibernate Tool with a JPA Configuration
[hibernate] 1. task: generic exportertemplate: view/list.xhtml.ftl
[hibernate] 2009-08-01 18:56:21 org.hibernate.cfg.annotations.Version
[hibernate] INFO: Hibernate Annotations 3.3.0.GA
[hibernate] 2009-08-01 18:56:21 org.hibernate.cfg.Environment
[hibernate] INFO: Hibernate 3.2.4.sp1
[hibernate] 2009-08-01 18:56:21 org.hibernate.cfg.Environment
[hibernate] INFO: hibernate.properties not found
[hibernate] 2009-08-01 18:56:21 org.hibernate.cfg.Environment buildBytecodeProvider
[hibernate] INFO: Bytecode provider name : cglib
[hibernate] 2009-08-01 18:56:21 org.hibernate.cfg.Environment
[hibernate] INFO: using JDK 1.4 java.sql.Timestamp handling
[hibernate] 2009-08-01 18:56:21 org.hibernate.ejb.Version
[hibernate] INFO: Hibernate EntityManager 3.3.1.GA
[hibernate] 2009-08-01 18:56:22 org.hibernate.cfg.AnnotationBinder bindClass
[hibernate] INFO: Binding entity from annotated class: pl.matt.model.Employee
[hibernate] 2009-08-01 18:56:22 org.hibernate.cfg.annotations.EntityBinder bindTable
[hibernate] INFO: Bind entity pl.matt.model.Employee on table employees
[hibernate] 2009-08-01 18:56:22 org.hibernate.validator.Version
[hibernate] INFO: Hibernate Validator 3.0.0.GA
[hibernate] 2009-08-01 18:56:22 org.hibernate.tool.Version
[hibernate] INFO: Hibernate Tools 3.2.2.GA
[hibernate] 2. task: generic exportertemplate: view/view.xhtml.ftl
[hibernate] 3. task: generic exportertemplate: view/view.page.xml.ftl
[hibernate] 4. task: generic exportertemplate: view/edit.xhtml.ftl
[hibernate] 5. task: generic exportertemplate: view/edit.page.xml.ftl
[hibernate] 6. task: generic exportertemplate: src/EntityList.java.ftl
[hibernate] 7. task: generic exportertemplate: view/list.page.xml.ftl
[hibernate] 8. task: generic exportertemplate: src/EntityHome.java.ftl
[hibernate] 9. task: generic exportertemplate: view/layout/menu.xhtml.ftl
[javaformatter] Java formatting of 3 files completed. Skipped 0 file(s).
[echo] Type './seam restart' and go to http://localhost:8080/seam_test

BUILD SUCCESSFUL
Total time: 4 seconds



Uruchamiam JBossa i wchodzę na adres:
http://localhost:8080/seam_test/
moim oczom ukazuje się znzna już aplikacja


Dodaję zatem jednego użytkownika


i otwieram w trzech zakładkach. Na konsoli JBossa obserwuję zapytania SQL jakie wykonuje aplikacja:

INFO [STDOUT] Hibernate:
select
employee0_.id as id2_0_,
employee0_.age as age2_0_,
employee0_.email as email2_0_,
employee0_.name as name2_0_,
employee0_.surname as surname2_0_
from
employees employee0_
where
employee0_.id=?
INFO [STDOUT] Hibernate:
select
employee0_.id as id2_0_,
employee0_.age as age2_0_,
employee0_.email as email2_0_,
employee0_.name as name2_0_,
employee0_.surname as surname2_0_
from
employees employee0_
where
employee0_.id=?
INFO [STDOUT] Hibernate:
select
employee0_.id as id2_0_,
employee0_.age as age2_0_,
employee0_.email as email2_0_,
employee0_.name as name2_0_,
employee0_.surname as surname2_0_
from
employees employee0_
where
employee0_.id=?


Jak widać, trzy razy tem sam SELECT.

Zakładając, że tylko nasza aplikacja kożysta z bazy danych test, możemy się pokusić, o włączenie pamięci podręcznej drugiego poziomu, która umożliwi wyrugowanie powtarzających się zapytań.

Dostawców pamięci drugiego poziomu jest kilku. Ja się zdecyduję na najbardziej chyba popularny EhCache.

Uzupełniam zatem spis plików jar, znajdujący się w pliku
deployed-jars-ear.list, które chcę aby znalazły się w aplikacji ear o wpisy:

commons-lang.jar
commons-collections.jar
commons-beanutils.jar
richfaces-ui.jar
commons-digester.jar
commons-beanutils.jar
commons-lang.jar
hibernate.jar
hibernate-all.jar
hibernate-annotations.jar
hibernate-commons-annotations.jar
hibernate-entitymanager.jar
hibernate-validator.jar
cglib.jar
asm.jar
ehcache.jar


w pliku
persistence-dev.xml
dodaję wpisy


<property name="hibernate.cache.use_second_level_cache" value="true" />
<property name="hibernate.cache.provider_class" value="org.hibernate.cache.EhCacheProvider" />


Cały plik wygląda następująco:

<?xml version="1.0" encoding="UTF-8"?>
<!-- Persistence deployment descriptor for dev profile -->
<persistence 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"
version="1.0">

<persistence-unit name="seam_test">
<provider>org.hibernate.ejb.HibernatePersistence</provider>
<jta-data-source>java:/seam_testDatasource</jta-data-source>
<properties>
<property name="hibernate.dialect" value="org.hibernate.dialect.MySQLInnoDBDialect" />
<property name="hibernate.hbm2ddl.auto" value="update" />
<property name="hibernate.show_sql" value="true" />
<property name="hibernate.format_sql" value="true" />
<property name="jboss.entity.manager.factory.jndi.name"
value="java:/seam_testEntityManagerFactory" />
<property name="hibernate.cache.use_second_level_cache"
value="true" />
<property name="hibernate.cache.provider_class" value="org.hibernate.cache.EhCacheProvider" />
</properties>
</persistence-unit>

</persistence>

natomiast klasę Employee dekoruję adnotacją

@Cache(usage=CacheConcurrencyStrategy.READ_WRITE)


i restartuję aplikację.
Start serwera kończy się komunikatem błędu:


ERROR [AbstractKernelController] Error installing to Start: name=persistence.unit:unitName=seam_test.ear/seam_test.jar#seam_test state=Create
java.lang.IllegalArgumentException: Cache name cannot contain '/' characters.
at net.sf.ehcache.Cache.setName(Cache.java:1272)
at net.sf.ehcache.CacheManager.addCache(CacheManager.java:501)
at org.hibernate.cache.EhCacheProvider.buildCache(EhCacheProvider.java:87)
at org.hibernate.cache.CacheFactory.createCache(CacheFactory.java:61)
at org.hibernate.impl.SessionFactoryImpl.(SessionFactoryImpl.java:214)
at org.hibernate.cfg.Configuration.buildSessionFactory(Configuration.java:1294)
at org.hibernate.cfg.AnnotationConfiguration.buildSessionFactory(AnnotationConfiguration.java:915)
at org.hibernate.ejb.Ejb3Configuration.buildEntityManagerFactory(Ejb3Configuration.java:730)
at org.hibernate.ejb.HibernatePersistence.createContainerEntityManagerFactory(HibernatePersistence.java:127)
at org.jboss.jpa.deployment.PersistenceUnitDeployment.start(PersistenceUnitDeployment.java:301)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
at java.lang.reflect.Method.invoke(Method.java:597)
at org.jboss.reflect.plugins.introspection.ReflectionUtils.invoke(ReflectionUtils.java:59)
at org.jboss.reflect.plugins.introspection.ReflectMethodInfoImpl.invoke(ReflectMethodInfoImpl.java:150)
at org.jboss.joinpoint.plugins.BasicMethodJoinPoint.dispatch(BasicMethodJoinPoint.java:66)
at org.jboss.kernel.plugins.dependency.KernelControllerContextAction$JoinpointDispatchWrapper.execute(KernelControllerContextAction.java:241)
at org.jboss.kernel.plugins.dependency.ExecutionWrapper.execute(ExecutionWrapper.java:47)
at org.jboss.kernel.plugins.dependency.KernelControllerContextAction.dispatchExecutionWrapper(KernelControllerContextAction.java:109)
at org.jboss.kernel.plugins.dependency.KernelControllerContextAction.dispatchJoinPoint(KernelControllerContextAction.java:70)
at org.jboss.kernel.plugins.dependency.LifecycleAction.installActionInternal(LifecycleAction.java:221)
at org.jboss.kernel.plugins.dependency.InstallsAwareAction.installAction(InstallsAwareAction.java:54)
at org.jboss.kernel.plugins.dependency.InstallsAwareAction.installAction(InstallsAwareAction.java:42)
at org.jboss.dependency.plugins.action.SimpleControllerContextAction.simpleInstallAction(SimpleControllerContextAction.java:62)
at org.jboss.dependency.plugins.action.AccessControllerContextAction.install(AccessControllerContextAction.java:71)
at org.jboss.dependency.plugins.AbstractControllerContextActions.install(AbstractControllerContextActions.java:51)
at org.jboss.dependency.plugins.AbstractControllerContext.install(AbstractControllerContext.java:348)
at org.jboss.dependency.plugins.AbstractController.install(AbstractController.java:1598)
at org.jboss.dependency.plugins.AbstractController.incrementState(AbstractController.java:934)
at org.jboss.dependency.plugins.AbstractController.resolveContexts(AbstractController.java:1062)
at org.jboss.dependency.plugins.AbstractController.resolveContexts(AbstractController.java:984)
at org.jboss.dependency.plugins.AbstractController.install(AbstractController.java:774)
at org.jboss.dependency.plugins.AbstractController.install(AbstractController.java:540)
at org.jboss.deployers.vfs.deployer.kernel.BeanMetaDataDeployer.deploy(BeanMetaDataDeployer.java:121)
at org.jboss.deployers.vfs.deployer.kernel.BeanMetaDataDeployer.deploy(BeanMetaDataDeployer.java:51)
at org.jboss.deployers.spi.deployer.helpers.AbstractSimpleRealDeployer.internalDeploy(AbstractSimpleRealDeployer.java:62)
at org.jboss.deployers.spi.deployer.helpers.AbstractRealDeployer.deploy(AbstractRealDeployer.java:50)
at org.jboss.deployers.plugins.deployers.DeployerWrapper.deploy(DeployerWrapper.java:171)
at org.jboss.deployers.plugins.deployers.DeployersImpl.doDeploy(DeployersImpl.java:1439)
at org.jboss.deployers.plugins.deployers.DeployersImpl.doInstallParentFirst(DeployersImpl.java:1157)
at org.jboss.deployers.plugins.deployers.DeployersImpl.doInstallParentFirst(DeployersImpl.java:1178)
at org.jboss.deployers.plugins.deployers.DeployersImpl.doInstallParentFirst(DeployersImpl.java:1210)
at org.jboss.deployers.plugins.deployers.DeployersImpl.install(DeployersImpl.java:1098)
at org.jboss.dependency.plugins.AbstractControllerContext.install(AbstractControllerContext.java:348)
at org.jboss.dependency.plugins.AbstractController.install(AbstractController.java:1598)
at org.jboss.dependency.plugins.AbstractController.incrementState(AbstractController.java:934)
at org.jboss.dependency.plugins.AbstractController.resolveContexts(AbstractController.java:1062)
at org.jboss.dependency.plugins.AbstractController.resolveContexts(AbstractController.java:984)
at org.jboss.dependency.plugins.AbstractController.change(AbstractController.java:822)
at org.jboss.dependency.plugins.AbstractController.change(AbstractController.java:553)
at org.jboss.deployers.plugins.deployers.DeployersImpl.process(DeployersImpl.java:781)
at org.jboss.deployers.plugins.main.MainDeployerImpl.process(MainDeployerImpl.java:698)
at org.jboss.system.server.profileservice.ProfileServiceBootstrap.loadProfile(ProfileServiceBootstrap.java:304)
at org.jboss.system.server.profileservice.ProfileServiceBootstrap.start(ProfileServiceBootstrap.java:205)
at org.jboss.bootstrap.AbstractServerImpl.start(AbstractServerImpl.java:405)
at org.jboss.Main.boot(Main.java:209)
at org.jboss.Main$1.run(Main.java:547)
at java.lang.Thread.run(Thread.java:619)


O co chodzi?

Otuż... chodz o błąd, który byłem tak uprzejmy i zgłosiłem.

JBoss wstawia slashe do nazw fragmentów pamięci podręcznej, a EhCache próbuje tworzyć pliki o nazwie takiej jak nazwa fragmentu pamięci podręcznej i ZONK. Co z tym fantem zrobić?

Napisać własny CacheProvider. Wbrew pozorom nie jest to takie trudne.
Tworzę więc klasę
JBoss5EhCacheProvider
która zamieni w nazwach pamięci podręcznej znaki '/' na '_'.


package pl.matt.cache;

import java.util.Properties;

import org.hibernate.cache.Cache;
import org.hibernate.cache.CacheException;
import org.hibernate.cache.CacheProvider;

public class JBoss5EhCacheProvider implements CacheProvider {

private CacheProvider delegate = new org.hibernate.cache.EhCacheProvider();

@Override
public Cache buildCache(String name, Properties props) throws CacheException {
name = changeName(name);
return delegate.buildCache(name, props);
}

private String changeName(String name) {
if (name != null) {
return name.replace('/', '_');
}
return name;
}

@Override
public boolean isMinimalPutsEnabledByDefault() {
return delegate.isMinimalPutsEnabledByDefault();
}

@Override
public long nextTimestamp() {
return delegate.nextTimestamp();
}

@Override
public void start(Properties arg0) throws CacheException {
delegate.start(arg0);
}

@Override
public void stop() {
delegate.stop();
}

}



i ustawiam ją w pliku persistence-dev.xml jako CacjeProvider.

<property name="hibernate.cache.provider_class" value="pl.matt.cache.JBoss5EhCacheProvider" />


Czas na kolejny restart aplikacji. Chyba się udało...

[ServerImpl] JBoss (Microcontainer) [5.0.1.GA (build: SVNTag=JBoss_5_0_1_GA date=200902232048)] Started in 1m:4s:297ms


Otwieram kilka razy pracownika w różnych zakładkach.
W konsoli widzę:

[STDOUT] Hibernate:
select
employee0_.id as id0_,
employee0_.age as age0_,
employee0_.email as email0_,
employee0_.name as name0_,
employee0_.surname as surname0_
from
employees employee0_ limit ?

tylko raz. Kilka zapytań o obiekty, a do bazy zapytanie poszło jedno.
Sukces pełną gębą.

niedziela, 28 czerwca 2009

Para buch, koła w ruch... czyli aplikacja CRUD z JBoss Seam w 5... no może 15 minut ;).

Stoi na stacji lokomotywa, ciężka ogromna, aż pot z niej spływa... że tak zacznę słowami poety.

Z końmi, grubasami i armatą, które rozmieszczone są w kolejnych wagonach ciągniętych przez pojazd się przy okazji dzisiejszego wpisu nie spotkamy, ale... z parą jak najbardziej.

Przedstawię jak najprościej wygenerować aplikację CRUD z wykorzystaniem szkieletu aplikacyjnego JBoss Seam. Posłużę się środowiskiem Eclipse 3.4.2 z zainstalowaną wtyczką JBoss Tools 3.0. Użyję też bazy MySql 5.0 oraz serwera aplikacji Java EE JBoss 5.0.1.GA. Niezbędna okaże się również instalacja Seama. Użyję wersji 2.1.1.GA.

Jeżeli potrzebujesz aplikacji webowej, aby zaliczyć projekt, świetnie trafiłeś. Wystarczy przerwa między zajęciami, ale ad rem...

Instaluję Eclipse, bazę MySQL, JBossa 5.0.1.GA oraz wtyczkę JBoss Tools oraz Pudelek Eclipse Plugin.

W Eclipsie, konfiguruje środowisko uruchomieniowe JBossa. W tym celu otwieram menu preferencji
Window -> Preferences
i znajduję tam zakładkę
Server / Runtime Environments
na której klikam przycisk
Add...

Odszukuję pozycję
JBoss 5.0 Runtime
która znajduje się w katalogu
JBoss Comunity


Klikam
Next
wskazuję katalog z zainstalowanym JBossem


i klikam
Finish

Pora stworzyć nowy projekt. Z menu
File -> New -> Project...
wybieram
Seam Web Project


Nadaję nazwę projektowi (radzę zacząć małą literą):
seamApplication

Z menu
Configuration
wybieram
Dynamic Web Project With Seam 2.1 (technology preview)


Klikam kilka razy
Next >
i zatrzymuję się na zakładce, na której muszę wskazać środowisko uruchomieniowe Seam
Seam Runtume.
W tej samej zakładce wybieram rodzaj aplikacji jako
ear.


Definiuję środowisko uruchomieniowe Seama, klikając

Add...


i wskazuję katalog, w którym zainstalowałem Seama 2.1.1.GA.

Na tej samej zakładce konfiguruję połączenie z bazą danych. Pobieram sterownik JDBC MySQL Connector.

W sekcji Database wybieram typ bazy danych jako
MySQL (InnoDB)
oraz tworze nowy profil połączenia z bazą. Jako
Profile Type
wybieram
MySQL


Klikam
Next >
i definiuję nowy sterownik


Na zakładce
Jar List
wskazuję plik jar który znajdował się w paczce z pobranym sterownikiem JDBC.


Podaję nazwę bazy danych oraz nazwę użytkownika i hasło do bazy


Od razu tworzę taką bazę wydając polecenie w konsoli mysql

create database test default character set utf8;

a w Eclipsie klikam
Finish.

Komputer chwilkę pomyśli, po czym stworzy 4 projekty:
seamApplication z aplikacją webową
seamApplication-ear z deskryptorem aplikacji EAR
seamApplication-ejb z aplikacją EJB
seamApplication-test z testami aplikacji

Startuję serwer aby sprawdzić, czy aplikacja się uruchamia.
Po wpisaniu w przeglądarkę adresu:
http://localhost:8080/seamApplication/
moim oczom ukazuje się aplikacja:


So far, so good...

Jako, że mamy do stworzenia aplikację CRUD, trzeba by stworzyć encję, którą będziemy mogli edytować. Tworzę więc klasę
pl.matt.model.Employee:


package pl.matt.model;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;

import org.hibernate.validator.Email;
import org.hibernate.validator.Length;
import org.hibernate.validator.Max;
import org.hibernate.validator.Min;
import org.hibernate.validator.NotNull;

@Entity
@Table(name="employees")
public class Employee {

@Id
@GeneratedValue(strategy=GenerationType.IDENTITY)
private int id;

@Length(max=128)
@NotNull
private String name;

@Column(length=128)
@NotNull
private String surname;

@Email
@NotNull
@Column(length=128)
private String email;

@Min(0)
@Max(130)
@NotNull
private int age;

public int getId() {
return id;
}

public void setId(int id) {
this.id = id;
}

public String getName() {
return name;
}

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

public String getSurname() {
return surname;
}

public void setSurname(String surname) {
this.surname = surname;
}

public String getEmail() {
return email;
}

public void setEmail(String email) {
this.email = email;
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}

}


Następnie generuję do niej interfejs użytkownika. Klikam prawym przyciskiem myszy na projekcie, wybieram
New... -> Other
i
Seam Generate Entities


Zaznaczam opcję

Use existing entities

i klikam
Finish

Restartuję JBossa i aplikacja gotowa.

Pojawiła się lista pracowników, ich wyszukiwarka oraz strony umożliwiające ich dodawanie, kasowania oraz aktualizację.


Do tego działa logowanie użytkowników, ajaxowa walidacja adresu email, wieku i wymagalności pól.


Żyć nie umierać.
Kod źródłowy przykładu zamieszczam tutaj, tu, tu i tu.

niedziela, 14 czerwca 2009

Filmy się obrobiły

Stało się.

Wszystkie filmy z warszawskiego Eclipse Demo Campu pojawiły się na stronie:
http://javatv.pl/.

Muszę przyznać, że jakoś jest niezła, a i 15 minutowe nagrania ogląda się przyjemniej niż 2 godzinne.

Oto moje wystąpienie:

poniedziałek, 8 czerwca 2009

Garść wrażeń po warszawskim Eclipse DemoCamp Galileo 2009

W ubiegły wtorek, 2giego czerwca w Warszawie w budynku MIMUW przy ul. Banacha 2 odbyły się warsztaty poświęcone najnowszemu wydaniu Eclipse IDE (jeszcze w wersji RC) Eclipse DemoCamp Galileo 2009.

Warsztaty poprzedził poczęstunek z pizzy, co okazało się sprawką patrona Code-House.org.

Formuła imprezy była dość nietypowa, przynajmniej jak na warszawskie spotkania. Warsztaty składały się z 6ciu 15 minutowych wystąpień. Jednym z nich, było wystąpienie "Wtyczka do Eclipse w 5 ... no może 15 minut" autorstwa... mojego autorstwa.

Zaczęło się całkiem nietypowo, bo od kłopotów technicznych. Projektor odmówił współpracy z moim Ubuntu w rozdzielczości wyższej niż 800x600. A że prezentacja trwała 15 minut... to i skończyło się nietypowo, ponieważ tworząc wtyczkę nie mogłem zaznaczyć odpowiednich opcji konfiguracyjnych. Nie mieściły się one po prostu na ekranie. I tak zamiast odtwarzacza mp3jek Eclipse zamienił się tylko w prymitywny manager plików.

Jeżeli ktoś miałby ochotę zobaczyć co chciałem osiągnąć, a nie tylko to co mi się udało "programując przez dziurkę od klucza" jak to określił słuchacz w pierwszym rzędzie, kod wtyczki odtwarzającej pliki MP3 zamieszczam tutaj.

Niedługo możecie spodziewać się krótkiego poradnika, a ja nie mogę doczekać się nagrania z konferencji.

piątek, 17 kwietnia 2009

Pudelek Eclipse Plug-in

Zastanawiałeś się jak zwiększyć wydajność pracy Java Teamu w Twojej firmie? Wiesz ile czasu tracą programiści przełączając się pomiędzy Eclipsem a przeglądarką internetową, żeby przejrzeć najnowsze wiadomości?
To na szczęście już przeszłość.

Dzięki wtyczce Pudelek Eclipse Plug-in zintegrujesz swoje ulubione środowisko deweloperskie, z najpopularniejszym serwisem plotkarskim. Dzięki intuicyjnemu interfejsowi użytkownika (artykuły widziane są jako pliki .java), od razu połapiesz się co i jak.



I to wszystko za darmo.
Zapraszam do instalacji, używania, komentowania i rozpowszechniania wtyczki.
Strona domowa projektu:
http://sites.google.com/site/pudelekeclipseplugin/

środa, 15 kwietnia 2009

Jazoon09 rozkład jazdy

Całkiem niedawno pojawił się plan konferencji Jazoon09 (czerwiec 2009).
I tak w
poniedziałek dominuje GlassFish i OSGi,
we wtorek kupę smakołyków. Między innymi Spring 3, Glassfish 3 i JBoss AS 5.0
w środę między innymi Jazoon Rookie, Java FX
we czwartek oprócz lunchu dopieszczanie GC i co nieco o JPA 2.0.
a w piątek rządzić będzie bezpieczeństwo.
Zapowiada się bardzo interesująco, tylko gdzie impreza integracyjna?

niedziela, 12 kwietnia 2009

leniwe ładowanie właściwości w Hibernate

Święta, święta i... jeszcze został jeden dzień obżarstwa. Tu barszczyk, tam kiełbaska, mięsko, sałatka, mazurek. Wszystkiego trzeba spróbować. A gdyby tak jeść tylko to, co jest w danej chwili niezbędne? Po prostu nie przejadać się. Chyba byłoby zdrowiej...

To takie moje świąteczne przemyślenia. W programowaniu jednak jest trochę jak w życiu. Często mamy do czynienia z podobnymi sytuacjami. Nie zjadamy co prawda w naszych programach wszystkiego, co jest pod ręką, ale wyciągając z bazy danych obiekty przy użyciu narzędzi ORM (Object Relational Mapping), pobieramy je wyciągając wszystkie kolumny z bazy danych. Inicjujemy wszystkie, nawet te niepotrzebne w danej chwili właściwości obiektów.

O ile zazwyczaj nikomu to nie przeszkadza, o tyle w przypadku pobierania właściwości o dużych rozmiarach (np. duże obiekty binarne, tekstowe) może to się odbić czkawką i negatywnie wpłynąć na wydajność aplikacji.

Na szczęście Hibernate, będący chyba najpopularniejszym narzędziem mapowania obiektowo relacyjnego, umożliwia selektywne pobieranie właściwości obiektów. Wyboru, które kolumny pobieramy wyciągając obiekt z relacyjnej bazy danych dokonujemy za pomocą parametru fetch adnotacji @Basic. Jeżeli konfigurację przejścia obiektowo relacyjnego przechowujemy w plikach XML, za to zachowanie odpowiedzialny jest atrybut lazy znacznika property.

Domyślnie pobierane są wszystkie atrybuty obiektów, nie będące kolekcjami, co odpowiada ustawieniom:

@Basic(fetch = FetchType.EAGER)
oraz

<property lazy="false" />

Sprawdźmy więc w akcji, jak działa konfiguracja leniwego pobierania wybranych właściwości obiektów w Hibernate. Mój przykładowy program powstał na podstawie przykładów ze strony projektu Hibernate.

Pobierał będę encję Message

package pl.matt.model;

public class Message {
private Long id;
private String text;

Message() {}

public Message(String text) {
this.text = text;
}

public Long getId() {
return id;
}
private void setId(Long id) {
this.id = id;
}

public String getText() {
return text;
}
public void setText(String text) {
this.text = text;
}
}


konfigurację przejścia obiektowo relacyjnego zawarłem w pliku XML Message.hbm.xml


<?xml version="1.0"?>
<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">

<hibernate-mapping>

<class
name="pl.matt.model.Message"
table="MESSAGES">

<id
name="id"
column="MESSAGE_ID"
>
<generator class="native"/>
</id>

<property
name="text"
column="MESSAGE_TEXT"
lazy="false"
/>

</class>

</hibernate-mapping>


Na prosty program pobierający obiekt z bazy danych składa klasa Main

package pl.matt.main;

import org.hibernate.Session;
import org.hibernate.Transaction;

import pl.matt.model.Message;
import pl.matt.utils.HibernateUtil;

public class Main {

public static void main(String[] args) {
Session session = HibernateUtil.getSessionFactory().openSession();
Transaction tx = session.beginTransaction();

Message message = (Message) session.get(Message.class, 1l);
System.out.println(message.getId());

tx.commit();
session.close();

HibernateUtil.shutdown();
}
}


Klasa narzędziowa HibernateUtil zawiera kilka metod ułatwiających pracę z Hibernate:

package pl.matt.utils;

import org.hibernate.*;
import org.hibernate.cfg.*;

/**
* Startup Hibernate and provide access to the singleton SessionFactory
*/
public class HibernateUtil {

private static SessionFactory sessionFactory;

static {
try {
sessionFactory = new Configuration().configure().buildSessionFactory();
} catch (Throwable ex) {
throw new ExceptionInInitializerError(ex);
}
}

public static SessionFactory getSessionFactory() {
// Alternatively, we could look up in JNDI here
return sessionFactory;
}

public static void shutdown() {
// Close caches and connection pools
getSessionFactory().close();
}
}


W pliku hibernate.cfg.xml konfiguruję połączenie z bazą danych HSQL oraz ustawiam wartość true parametru show_sql który umożliwia podglądanie zapytań SQL wykonywanych przez Hibernate.


<?xml version='1.0' encoding='utf-8'?> <!DOCTYPE hibernate-configuration
PUBLIC "-//Hibernate/Hibernate Configuration DTD//EN"
"http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd">

<hibernate-configuration>
<session-factory>

<property name="hibernate.connection.driver_class">org.hsqldb.jdbcDriver</property>
<property name="hibernate.connection.url">jdbc:hsqldb:hsql://localhost</property>
<property name="hibernate.connection.username">sa</property>

<!-- SQL to stdout logging -->
<property name="show_sql">true</property>
<property name="format_sql">true</property>
<property name="use_sql_comments">true</property>

<property name="hibernate.hbm2ddl.auto">update</property>

<property name="dialect">org.hibernate.dialect.HSQLDialect</property>

<mapping resource="pl/matt/model/Message.hbm.xml"/>

</session-factory>
</hibernate-configuration>


Uruchamiam program i zgodnie z oczekiwaniami widzę zapytanie pobierające 2 właściwości z tabeli MESSAGES:

Hibernate:
/* load pl.matt.model.Message */ select
message0_.MESSAGE_ID as MESSAGE1_0_0_,
message0_.MESSAGE_TEXT as MESSAGE2_0_0_
from
MESSAGES message0_
where
message0_.MESSAGE_ID=?


zmieniam w pliku Message.hbm.xml wartość atrybutu lazy na true. Uruchamiam ponownie program i tym razem niezgodnie z oczekiwaniami widzę ponownie:

Hibernate:
/* load pl.matt.model.Message */ select
message0_.MESSAGE_ID as MESSAGE1_0_0_,
message0_.MESSAGE_TEXT as MESSAGE2_0_0_
from
MESSAGES message0_
where
message0_.MESSAGE_ID=?


Hmm... słabo.
Zagłębiłem się w dokumentację i już widzę, że nie będzie łatwo:

Note

To enable property level lazy fetching, your classes have to be instrumented: bytecode is added to the original one to enable such feature, please refer to the Hibernate reference documentation. If your classes are not instrumented, property level lazy loading is silently ignored.

Nawet nie wiedziałem, że jest taki czasownik instrument. W każdym razie moje ustawienia zostały po cichu zignorowane, jak to ładnie określiła dokumentacja Hibernate. Trzeba by to zmienić. Szperam więc dalej w poszukiwaniu sposobu zinstrumetowania klas.

Wykonać ten proces można antowym zadaniem:

<target name="instrument" depends="compile">
<taskdef name="instrument" classname="org.hibernate.tool.instrument.cglib.InstrumentTask">
<classpath path="${build.dir}"/>
<classpath refid="project.classpath"/>
</taskdef>

<instrument verbose="true">
<fileset dir="${build.dir}/pl/matt/model">
<include name="*.class"/>
</fileset>
</instrument>
</target>


W dokumentacji Hibernate jest mały błąd, gdyż klasa zadania instrument InstrumentTask nie znajduje się w pakiecie org.hibernate.tool.instrument tylko w org.hibernate.tool.instrument.cglib.

Jako, że powyższe zadanie zmienia kod plików *.class, muszę uruchamiać te skompilowane i zmienione przez ANTa pliki. Posłużę się do tego celu poniższym plikiem build.xml:

<project name="hibernateLazyBasic" default="compile" basedir=".">

<!-- Name of project and version -->
<property name="proj.name" value="hibernateLazyBasic"/>
<property name="proj.shortname" value="hibernateLazyBasic"/>
<property name="version" value="1.0"/>

<!-- Global properties for this build -->
<property name="database.dir" value="database"/>
<property name="src.java.dir" value="src/java"/>
<property name="lib.dir" value="lib"/>
<property name="build.dir" value="build"/>

<!-- Classpath declaration -->
<path id="project.classpath">
<fileset dir="${lib.dir}">
<include name="**/*.jar"/>
<include name="**/*.zip"/>
</fileset>
</path>

<!-- Useful shortcuts -->
<patternset id="meta.files">
<include name="**/*.xml"/>
<include name="**/*.properties"/>
</patternset>

<!-- Clean up -->
<target name="clean" description="Clean the build directory">
<delete dir="${build.dir}"/>
<mkdir dir="${build.dir}"/>
</target>

<!-- Compile Java source -->
<target name="compile">
<mkdir dir="${build.dir}"/>
<javac srcdir="${src.java.dir}"
destdir="${build.dir}"
classpathref="project.classpath"/>
</target>

<!-- Copy metadata to build classpath -->
<target name="copymetafiles">
<mkdir dir="${build.dir}"/>
<copy todir="${build.dir}">
<fileset dir="${src.java.dir}">
<patternset refid="meta.files"/>
</fileset>
</copy>
</target>

<target name="run-instrumented" depends="clean, instrument, copymetafiles">
<java fork="true"
classname="pl.matt.main.Main"
classpathref="project.classpath">
<classpath path="${build.dir}"/>
</java>
</target>

<target name="run" depends="clean, compile, copymetafiles">
<java fork="true"
classname="pl.matt.main.Main"
classpathref="project.classpath">
<classpath path="${build.dir}"/>
</java>
</target>

<target name="instrument" depends="compile">
<taskdef name="instrument" classname="org.hibernate.tool.instrument.cglib.InstrumentTask">
<classpath path="${build.dir}"/>
<classpath refid="project.classpath"/>
</taskdef>
<instrument verbose="true">
<fileset dir="${build.dir}/pl/matt/model">
<include name="*.class"/>
</fileset>
</instrument>
</target>

</project>


zadanie o nazwie run-instrumented kompiluje, przeprowadza instrumentację klas z pakiedu pl.matt.model i uruchamia program.

Po jego wykonaniu, otrzymuję na konsoli:

[java] Hibernate:
[java] /* load pl.matt.model.Message */ select
[java] message0_.MESSAGE_ID as MESSAGE1_0_0_
[java] from
[java] MESSAGES message0_
[java] where
[java] message0_.MESSAGE_ID=?


Ładowane jest tylko pole ID z tabeli MESSAGES, czyli zgodnie z oczekiwaniami. Kolumna MESSAGE_TEXT pobierana jest z bazy osobnym zapytaniem:

[java] Hibernate:
[java] /* sequential select
[java] pl.matt.model.Message */ select
[java] message_.MESSAGE_TEXT as MESSAGE2_0_
[java] from
[java] MESSAGES message_
[java] where
[java] message_.MESSAGE_ID=?

dopiero wtedy, kiedy będzie potrzebna.

Generalnie wszystko działa, tylko trzeba odrobinę zmienić proces kompilacji programu.
Kod źródłowy przedstawiający powyższe rozwiązanie znajdziesz tutaj

a że święta już w połowie, pozostaje mi życzyć Wam mokrego dyngusa.

wtorek, 31 marca 2009

Jazoon Cutting Edge

Jeżeli zawsze chciałeś opowiedzieć coś ciekawego programistom Java, świetnie trafiłeś. Masz pomysł na prezentację, ale boisz się, że nie będziesz w stanie zająć 60, czy 90 minut?

Jazoon Cutting Edge - These 20 minutes talks address the most recent developments in the Java industry.
Call for papers: 15 April 2009
Submission deadline: 15 May 2009

i wszystko wiadomo.

Do zgarnięcia darmowa wejściówka czerwcowy Jazoon i 50% zniżki dla osoby towarzyszącej (również w prezentacji).
Miło byłoby usłyszeć kogoś z Polski.

Może Jacek się zgłosi, ze swoimi zabawkami? Kto nie był dziś na spotkaniu warszawskiego JUGa niech żałuje.

niedziela, 22 marca 2009

Ziarna sesyjne - czar prysł.

Dopiero co zachwycałem się nad lokalnym i zdalnym sposobem wywoływania usług ziaren EJB. Przyszła wiosna, śnieg stopniał i czar prysł. Otóż pomiędzy wywołaniem danej metody poprzez interfejs oznaczony adnotacją @Local a wywołaniem za pomocą interfejsu @Remote mogą być subtelne różnice. Bierze się to stąd, że parametry metod wywoływanych lokalnie (@Local) przekazywane są poprzez referencje, parametry metod zdalnych (@Remote) przez wartość.

Zobaczmy o co chodzi na podstawie prostego przykładu. Stworzę z tej okazji w NetBeans IDE 6.5.1 projekt ziarna EJB oraz projekt klienta EJB.

Zaczynam od aplikacji EJB:
Z menu File wybieram pozycję New Project:


następnie wybieram kategorię Java EE i projekt EJB Module
klikam Next, po czym na zakładce Server and Settings dodaję nową konfigurację serwera, klikając przycisk Add...

Przykład będę uruchamiał na serwerze JBoss 4.2.2.GA


więc wskazuję jego położenie na dysku:


klikam Next> i Finish. Projekt gotowy.

W projekcie stworzę proste ziarno, które będzie udostępniało metodę dodającą podany jako parametr element do podanej jako parametr kolekcji:


public Collection add(Collection collection, Object element) {
collection.add(element);
return collection;
}


Ziarno będzie implementowało interfejs lokalny oraz zdalny. Ziarno oraz interfejsy przedstawiają się następująco:

Kod ziarna StatelessBean:

package pl.matt.session;

import java.util.Collection;
import javax.ejb.Local;
import javax.ejb.Remote;


@javax.ejb.Stateless
@Remote(StatelessRemote.class)
@Local(StatelessLocal.class)
public class StatelessBean implements StatelessRemote, StatelessLocal {

public Collection add(Collection collection, Object element) {
collection.add(element);
return collection;
}

}


interfejs zdalny StatelessRemote:

package pl.matt.session;

import java.util.Collection;
import javax.ejb.Remote;

public interface StatelessRemote {

Collection add(Collection collection, Object element);

}


i identyczny interfejs lokalny StatelessLocal:

package pl.matt.session;

import java.util.Collection;
import javax.ejb.Remote;

public interface StatelessLocal {

Collection add(Collection collection, Object element);

}


Całość uzupełnia ziarno testujące dodawanie elementów do kolekcji, które próbuje zarówno zdalnie jak i lokalnie dodać 3 napisy do listy:

interfejs TestRemote:

package pl.matt.session;

public interface TestRemote {
void test();
}


oraz jego implementacja:


package pl.matt.session;

import java.util.ArrayList;
import java.util.List;
import javax.ejb.Remote;
import javax.naming.Context;
import javax.naming.InitialContext;

@javax.ejb.Stateless
@Remote(TestRemote.class)
public class TestBean implements TestRemote {

public void test() {
try {
Context context = new InitialContext();
StatelessLocal statelessLocal = (StatelessLocal) context.lookup("StatelessBean/local");
StatelessRemote statelessRemote = (StatelessRemote) context.lookup("StatelessBean/remote");
List<String> localList = new ArrayList();
statelessLocal.add(localList, "1");
statelessLocal.add(localList, "2");
statelessLocal.add(localList, "3");
System.out.println("local list: " + localList);

List<String> remoteList = new ArrayList();
statelessRemote.add(remoteList, "1");
statelessRemote.add(remoteList, "2");
statelessRemote.add(remoteList, "3");
System.out.println("remote list: " + remoteList);
} catch (Exception e) {
e.printStackTrace();
}
}
}


Do wywołania metody ziarna TestBean potrzebuję klienta EJB. Tworzę go wybierając ponownie pozycję New Project... z menu File. Następnie z kategorii Java EE wybieram projekt Enterprise Application Client:

Do tak stworzonego projektu dodaję jako zależność stworzony uprzednio projekt EJB


Dzięki temu, będę w nim mógł uzyskać dostęp do interfejsu testującego. Całość kodu klienta zawarta jest w metodzie main():


package pl.matt;

import javax.naming.Context;
import javax.naming.InitialContext;
import pl.matt.session.TestRemote;

public class Main {

public static void main(String[] args) {
try {
System.out.println("start");
Context context = new InitialContext();
TestRemote test = (TestRemote) context.lookup("TestBean/remote");
test.test();
System.out.println("finished");
} catch (Exception e) {
e.printStackTrace();
}

}
}


Pozostało uruchomienie i przekonanie się, jak to wszystko będzie działać. Klikam prawym przyciskiem myszy na projekcie EJB, wybieram opcję Run. Następnie to samo robię na projekcie klienta EJB. JBoss się uruchamia, po chwili widzę w konsoli:


22:27:55,969 INFO [STDOUT] local list: [1, 2, 3]
22:27:55,974 INFO [STDOUT] remote list: []


2 razy wywołałem tę samą metodę, raz przez interfejs @Remote, raz przez @Local i wynik jej działania jest inny. Do kolekcji, przekazanej przez wartość elementy nie zostały dodane (tzn. zostały, ale do kopii tej kolekcji, przekazanej do ziarna EJB).

Trzeba uważać.

środa, 25 lutego 2009

Jazoon Rookie

Jesteś młody, żądny przygód? Masz mniej niż 26 lat? Masz coś ciekawego do powiedzenia o Javie? Doskonale się składa. Zawody Jazoon Rookie są właśnie dla Ciebie.
Jesteś młody, żądny przygód? Masz mniej niż 26 lat? Nie masz nic ciekawego do powiedzenia o Javie? Jeszcze lepiej. Zawody Jazoon Rookie są właśnie dla Ciebie.

Jazoon Rookie to zawody, w których zmierzy się trzech prelegentów wraz ze swoimi 20 minutowymi prelekcjami. Aby stać się jednym z nich, wystarczy przygotować abstrakt 20 minutowej prezentacji i zgłosić się do 15 marca 2008. Termin ten był już 2 razy przekładany, więc zgłoszeń nie ma chyba zbyt dużo. Warto więc spróbować.

Spośród zgłoszeń zostanie wybranych dziesięciu uczestników. Nagrają oni fragment swoich prelekcji. Spośród tych nagrań zostanie wyłoniona trójka finalistów. Każdy z nich zaprezentuje się na konferencji Jazoon'09. Do tego otrzyma darmową wejściówkę na konferencje, transport i zakwaterowanie.

Szczegółowe informacje znajdziesz na stronie Jazoon Rookie.

Zgłoś się koniecznie. Najwyższy czas, żeby ktoś z Polski pokazał, kto rządzi w Javie.

czwartek, 5 lutego 2009

aplikacja JSF i EJB 3.0 w JBoss Tools

W dzisiejszym odcinku, stworzymy aplikację webową (a jakże) korzystającą z EJB 3.0 i JSF. Ktoś może się zapytać po co? Przecież mamy znakomity szkielet na literę S... Owszem mamy, ale może nie znamy, albo może nie potrzebujemy jego możliwości... Różnie to w życiu bywa. Poza tym modułowa budowa, którą narzuca nam podział aplikacji na webową i biznesową wydaje się całkiem rozsądnym rozwiązaniem.
Zobaczmy zatem, co i jak... Do pracy nad aplikacją użyję środowiska Eclipse 3.3 Europa z zainstalowaną wtyczką JBoss Tools 2.1.2.GA (ostatnia finalna wersja). Jako środowisko uruchomieniowe posłuży mi kontener JBoss AS 4.2.0.GA. Bazą danych będzie MySQL 5.0.
Strukturę aplikacji tworzy archiwum EAR (Enterprise Application Archive) zawierające aplikację EJB (archiwum ejb-jar) oraz aplikację webową (archiwum war).


Ten na pozór skomplikowany podział może okazać się bardzo przydatny w większych, żeby nie powiedzieć korporacyjnych rozwiązaniach. Z warstwy usług zaimplementowanej w aplikacji EJB korzystać może nie tylko nasza aplikacja JSF, ale także inne, niekoniecznie nawet webowe aplikacje.

Stworzona aplikacja, będzie typową aplikacją CRUD, umożliwiającą tworzenie, odczyt, aktualizację i kasowanie pracowników. Potrzebować zatem będziemy 3 projekty. Projekt ejb, jsf oraz zawierający je projekt aplikacji ear.
Zacznijmy od EJB. Klikam File / New / Other...


i z listy wybieram EJB Project.


Podaję nazwę projektu ejbProject i klikam Next > Na następnej zakładce zaznaczam EJB module Java i Java Persistence


klikam 2 razy Next >, odznaczam create orm.xml (będę używał adnotacji) i klikam Finish


Tworzę w projekcie encję Employee

package pl.matt.model;

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

@NamedQueries({
@NamedQuery(name = "Employee.findAllOrderByName", query = "FROM Employee e ORDER BY e.lastName, e.firstName")
})
@Entity
public class Employee {

private int id;
private String password;
private String username;
private String firstName;
private String lastName;

@Id
@GeneratedValue
public int getId() {
return id;
}

public void setId(int id) {
this.id = id;
}

@Column(nullable = false)
public String getPassword() {
return password;
}

public void setPassword(String password) {
this.password = password;
}

@Column(nullable = false)
public String getUsername() {
return username;
}

public void setUsername(String username) {
this.username = username;
}

@Column(nullable = false)
public String getFirstName() {
return firstName;
}

public void setFirstName(String firstName) {
this.firstName = firstName;
}

@Column(nullable = false)
public String getLastName() {
return lastName;
}

public void setLastName(String lastName) {
this.lastName = lastName;
}

}



warstwę dostępu do danych (DAO):


package pl.matt.dao;

import java.util.List;

import pl.matt.model.Employee;

public interface EmployeeDao {

/**
* @return
*/
public List<Employee> getAllOrderByName();
/**
* @param employee
* @return
*/
public Employee create(Employee employee);

/**
* @param employee
*/
public void update(Employee employee);

/**
* @param employeeId
* @return
*/
public Employee load(int employeeId);

/**
* @param employeeId
* @return
*/
public void delete(Employee employee);

}



package pl.matt.dao.impl;

import java.util.List;

import javax.ejb.Stateless;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;

import pl.matt.dao.EmployeeDao;
import pl.matt.model.Employee;

@Stateless
public class JpaEmployeeDao implements EmployeeDao {

@PersistenceContext
private EntityManager entityManager;

@SuppressWarnings("unchecked")
public List<Employee> getAllOrderByName() {
return entityManager.createNamedQuery("Employee.findAllOrderByName").getResultList();
}

public Employee create(Employee employee) {
entityManager.persist(employee);
return employee;
}

public void update(Employee employee) {
entityManager.merge(employee);
}

public Employee load(int employeeId) {
return entityManager.find(Employee.class, employeeId);
}

public void delete(Employee employee) {
entityManager.remove(employee);

}

}


i może trochę na wyrost warstwę usług biznesowych

package pl.matt.service;

import java.util.List;

import pl.matt.model.Employee;

public interface EmployeeService {
/**
* @return
*/
public List<Employee> getAllOrderByName();
/**
* @param employee
* @return
*/
public Employee create(Employee employee);

/**
* @param employee
*/
public void update(Employee employee);

/**
* @param employeeId
* @return
*/
public Employee load(int employeeId);

/**
* @param employeeId
*/
public void delete(int employeeId);
}



package pl.matt.service.impl;

import java.util.List;

import javax.ejb.EJB;
import javax.ejb.Stateless;

import pl.matt.dao.EmployeeDao;
import pl.matt.model.Employee;
import pl.matt.service.EmployeeService;

@Stateless
public class EmployeeServiceImpl implements EmployeeService {

@EJB
private EmployeeDao employeeDao;

public Employee create(Employee employee) {
return employeeDao.create(employee);
}

public List<Employee> getAllOrderByName() {
return employeeDao.getAllOrderByName();
}

public void update(Employee employee) {
employeeDao.update(employee);

}

public Employee load(int employeeId) {
return employeeDao.load(employeeId);
}

public void delete(int employeeId) {
Employee employee = load(employeeId);
if (employee != null) {
employeeDao.delete(employee);
}
}

}


Ponieważ korzystam z bazy danych za pośrednictwem JPA, nie obejdzie się bez pliku META-INF/persistence.xml:

<?xml version="1.0" encoding="UTF-8"?>
<persistence xmlns="http://java.sun.com/xml/ns/persistence"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.0"
xsi:schemaLocation="http://java.sun.com/xml/ns/persistence
http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd">


<persistence-unit name="seam_war_project" transaction-type="JTA">
<provider>org.hibernate.ejb.HibernatePersistence</provider>
<jta-data-source>java:/empAppDatasource</jta-data-source>
<properties>
<property name="hibernate.dialect" value="org.hibernate.dialect.MySQLDialect"/>
<property name="hibernate.hbm2ddl.auto" value="update"/>
<property name="hibernate.show_sql" value="true"/>
</properties>
</persistence-unit>

</persistence>


potrzebuję też źródła danych (Data Source) empAppDatasource. Tworzę ją w pliku empApp-ds.xml

<?xml version="1.0" encoding="UTF-8"?>

<!DOCTYPE datasources
PUBLIC "-//JBoss//DTD JBOSS JCA Config 1.5//EN"
"http://www.jboss.org/j2ee/dtd/jboss-ds_1_5.dtd">

<datasources>

<local-tx-datasource>
<jndi-name>empAppDatasource</jndi-name>
<connection-url>jdbc:mysql://localhost:3306/jpabasics?characterEncoding=latin2</connection-url>
<driver-class>com.mysql.jdbc.Driver</driver-class>
<user-name>root</user-name>
<password>root</password>
</local-tx-datasource>

</datasources>

który umieszczam w katalogu server/default/deploy/ JBossa.

Przejdźmy do aplikacji JSF. Tworzę ją wybierając z menu Eclipse File / New / Other... / JSF Project



Jako środowisko JSF ustawiam JSF 1.2 z Faceletami, podaję nazwę projektu jsfModule i klikam Finish.

W projekcie tworzę prosty szablon stron WebContent/templates/common.xhtml

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core">

<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title><ui:insert name="pageTitle">Page Title</ui:insert></title>
<style type="text/css">
body {
font-family: Verdana, Arial, Helvetica, sans-serif;
font-size: 14px;
}

.header {
font-family: Verdana, Arial, Helvetica, sans-serif;
font-size: 18px;
}

.bottom {
font-family: Verdana, Arial, Helvetica, sans-serif;
font-size: 9px;
text-align: center;
vertical-align: middle;
color: #8E969D;
}

td.column1 {
width: 15%;
}

td.column2 {
width: 25%;
}

td.column3 {
width: 60%;
}
</style>
</head>

<body bgcolor="#ffffff">
<table style="border: 1px solid #CAD6E0" align="center" cellpadding="0"
cellspacing="0" border="0" width="400">
<tbody>

<tr>
<td class="header" height="42" align="center" valign="middle"
width="100%" bgcolor="#E4EBEB"><ui:insert name="pageHeader">Page Header</ui:insert>
</td>
</tr>
<tr>
<td height="1" width="100%" bgcolor="#CAD6E0"></td>
</tr>

<tr>
<td width="100%" colspan="2">
<table width="100%" style="height: 150px" align="left"
cellpadding="0" cellspacing="0" border="0">
<tbody>
<tr>
<td align="left" width="800px" valign="middle"><ui:insert
name="body">Page Body</ui:insert></td>
</tr>
</tbody>
</table>
</td>
</tr>

<tr>
<td colspan="2" valign="bottom" height="1" width="100%"
bgcolor="#CAD6E0"></td>
</tr>
</tbody>
</table>
</body>

</html>


plik wyświetlający listę pracowników WebContent/pages/employeeList.xhtml

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:c="http://java.sun.com/jstl/core">

<f:loadBundle basename="resources" var="msg" />

<ui:composition template="/templates/common.xhtml">

<ui:define name="pageTitle">Lista pracowników</ui:define>

<ui:define name="pageHeader">Lista pracowników</ui:define>

<ui:define name="body">
<div style="text-align: center"><h:form>
<h:commandLink action="ADD_EMP" value="Dodaj pracownika" />
<br />
<br />
<ui:repeat value="#{employeeBean.employees}" var="emp">
<h:outputLink value="editEmployee.jsf?employeeId=#{emp.id}">
<h:outputText value="#{emp.firstName} #{emp.lastName}" />
</h:outputLink>
<h:outputText value=" " />
<h:commandLink action="#{employeeBean.delete}" value="[usuń]">
<f:param name="toDelete" value="#{emp.id}" />
</h:commandLink>
<br />
</ui:repeat>

</h:form></div>

</ui:define>
</ui:composition>
</html>


oraz plik umożliwiający edycję pojedynczego pracownika WebContent/pages/editEmployee.xhtml

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:c="http://java.sun.com/jstl/core">

<f:loadBundle basename="resources" var="msg" />

<ui:composition template="/templates/common.xhtml">

<ui:define name="pageTitle">Edycja</ui:define>

<ui:define name="pageHeader">Edycja pracownika</ui:define>

<ui:define name="body">
<h:outputLink value="employeeList.jsf">Powrót</h:outputLink>
<h:form id="employeeForm">
<h:inputHidden id="employeeId" value="#{employeeBean.employee.id}" />
<h:panelGrid columns="3" width="800"
columnClasses="column1,column2,column3">
<h:outputText value="Imię" />
<h:inputText value="#{employeeBean.employee.firstName}"
id="firstName" required="true" />
<h:message for="firstName" style="color:red;" />
<h:outputText value="Nazwisko" />
<h:inputText value="#{employeeBean.employee.lastName}" id="lastName"
required="true" />
<h:message for="lastName" style="color:red;" />
<h:outputText value="Login" />
<h:inputText value="#{employeeBean.employee.username}" id="username"
required="true" />
<h:message for="username" style="color:red;" />
<h:outputText value="haslo" />
<h:inputSecret id="password"
value="#{employeeBean.employee.password}" required="true" redisplay="true" />
<h:message for="password" style="color:red;" />
<h:commandButton action="#{employeeBean.save}" value="zapisz" />
</h:panelGrid>
<h:outputLink value="employeeList.jsf">Powrót</h:outputLink>
</h:form>
</ui:define>
</ui:composition>
</html>


Potrzebne będzie też ziarno zarządzane JSF

package pl.matt.view;

import java.util.List;

import javax.ejb.EJB;
import javax.faces.context.FacesContext;

import pl.matt.model.Employee;
import pl.matt.service.EmployeeService;

public class EmployeeBean {

private List<Employee> employees;
private Employee employee;
private Integer employeeId;

@EJB(name="eeProject/EmployeeServiceImpl/local")
private EmployeeService employeeService;

public List<Employee> getEmployees() {
if (employees == null) {
employees = employeeService.getAllOrderByName();
}
return employees;
}

public Employee getEmployee() {
if (employee == null) {
if (employeeId != null) {
employee = employeeService.load(employeeId);
} else {
employee = new Employee();
}
}
return employee;
}

public void setEmployee(Employee employee) {
this.employee = employee;
}

public String save() {
if (employee.getId() > 0) {
employeeService.update(employee);
} else {
employeeService.create(employee);
}
employees = null;
return "LIST_EMP";
}

public Integer getEmployeeId() {
return employeeId;
}

public void setEmployeeId(Integer employeeId) {
this.employeeId = employeeId;
}

public String delete() {
System.out.println("EmployeeBean.delete()");
Integer id = Integer.valueOf(
FacesContext.getCurrentInstance().getExternalContext().getRequestParameterMap().get("toDelete"));
if (id != null) {
employeeService.delete(id);
}
employees = null;
return "LIST_EMP";
}

}



JBoss umieszcza w interfejsie JNDI ziarna EJB korzystając z trochę dziwnej konwencji:
nazwaModułu/klasaZiarna/local|remote stąd konieczność podania parametru name
@EJB(name="eeProject/EmployeeServiceImpl/local")

przy adnotacji @EJB.
Jak będzie wyglądała sytuacja w momencie, kiedy ziarno EJB będzie implementowało kilka interfejsów zdalnych lub lokalnych? Niestety nie mam pojęca.

Do kompletu brakuje jeszcze pliku WebContent/WEB-INF/faces-config.xml

<?xml version="1.0" encoding="UTF-8"?>
<faces-config version="1.2" xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xi="http://www.w3.org/2001/XInclude"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-facesconfig_1_2.xsd">
<managed-bean>
<managed-bean-name>employeeBean</managed-bean-name>
<managed-bean-class>pl.matt.view.EmployeeBean</managed-bean-class>
<managed-bean-scope>request</managed-bean-scope>
<managed-property>
<property-name>employeeId</property-name>
<value>#{param.employeeId}</value>
</managed-property>
</managed-bean>
<navigation-rule>
<navigation-case>
<from-outcome>ADD_EMP</from-outcome>
<to-view-id>/pages/editEmployee.xhtml</to-view-id>
</navigation-case>
<navigation-case>
<from-outcome>LIST_EMP</from-outcome>
<to-view-id>/pages/employeeList.xhtml</to-view-id>
</navigation-case>
</navigation-rule>
<application>
<view-handler>com.sun.facelets.FaceletViewHandler</view-handler>
</application>
</faces-config>


oraz WebContent/WEB-INF/web.xml

<?xml version="1.0"?>
<web-app version="2.5" 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 http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
<description>Facelets StarterKit</description>
<display-name>jsfModule</display-name>
<context-param>
<param-name>javax.faces.DEFAULT_SUFFIX</param-name>
<param-value>.xhtml</param-value>
</context-param>
<context-param>
<param-name>facelets.REFRESH_PERIOD</param-name>
<param-value>2</param-value>
</context-param>
<context-param>
<param-name>facelets.DEVELOPMENT</param-name>
<param-value>true</param-value>
</context-param>
<context-param>
<param-name>javax.faces.STATE_SAVING_METHOD</param-name>
<param-value>client</param-value>
</context-param>
<context-param>
<param-name>com.sun.faces.validateXml</param-name>
<param-value>true</param-value>
</context-param>
<context-param>
<param-name>com.sun.faces.verifyObjects</param-name>
<param-value>true</param-value>
</context-param>
<servlet>
<servlet-name>Faces Servlet</servlet-name>
<servlet-class>javax.faces.webapp.FacesServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>Faces Servlet</servlet-name>
<url-pattern>*.jsf</url-pattern>
</servlet-mapping>
<login-config>
<auth-method>BASIC</auth-method>
</login-config>
</web-app>


Aplikacja JSF gotowa. Aplikacja EJB gotowa. Czas je wyswatać i pożenić. Pomoże nam w tym projekt EAR. Z menu kontekstowego Eclipse wybieram zatem File / New / Other... / Enterprise Application Project.


Podaję nazwę projektu eeProject klikam 2 razy Next > i wybieram oba stworzone uprzednio moduły. Będą to składowe naszej aplikacji. Dodatkowo zaznaczam opcję Generate Deployment Descriptor i klikam Finish.



W tym projekcie znajduje się tylko plik EarContent/META-INF/application.xml.


<?xml version="1.0" encoding="UTF-8"?>
<application xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:application="http://java.sun.com/xml/ns/javaee/application_5.xsd" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/application_5.xsd" id="Application_ID" version="5">
<module>
<ejb>ejbProject.jar</ejb>
</module>
<module>
<web>
<web-uri>jsfModule.war</web-uri>
<context-root>jsfModule</context-root>
</web>
</module>
</application>


Plik ten opisuje moduły naszej aplikacji. Moduł webowy jsfModule i moduł EJB ejbProject.jar. Wdrożenie aplikacji pozostawiam wtyczce JBoss Tools. Startuję więc serwer i... aplikacja działa.



Kod źródłowy powyższej aplikacji dostępny jest tutaj.

Cóż, pora jechać na narty...