czwartek, 22 października 2009

Oracle + daty + JDBC

Ostatnio musiałem się zmierzyć z problemem związanym z obsługą dat w ORACLE 10. Wprowadzenie do tego problemu można znaleźć tutaj. W dużym skrócie: w przypadku insert do kolumny DATE wartości typu java.sql.Timestamp, czyli z poziomu Hibernate zapisujemy property typu java.sql.Timestamp lub java.util.Calendar/java.util.Date z annotacją @Temporal(TemporalType.TIMESTAMP) ), do bazy trafia zapisana przez nas data z czasem, ale bez milisekund - co jest zrozumiałe, bo DATE nie trzyma milisekund. Jeśli jednak spróbujemy odczytać zapisaną przez nas uprzednio krotkę, podając w warunku where zapisaną przed chwilą wartość - wynik jest pusty!
Pewnym popularnym rozwiązaniem, jest ustawienie specjalnego property dla ORACLE 9/10 JDBC driver: oracle.jdbc.V8Compatible=true. Konsekwencje użycia tego ustawienia można znaleźć tu.
Wyżej wymieniony problem można samemu zdiagnozować za pomocą testu.
Środowisko testowe korzysta z Oracle 10 g (10.2.0), JDBC Driver 10.2.0.4 a same testy są uruchamiane w JUNIT 4 za pomocą Spring TestContext Framework.
Encja, która będzie służyć do testów:

@Entity
@Table(name = "TestTable")
@SequenceGenerator(name = "seq_id", sequenceName = "SQ_TT")
public class TestEntity {
@Id
@GeneratedValue(generator = "seq_id")
Long i;

@Column
String state;

@Column(columnDefinition = "date")
@Temporal(TemporalType.TIMESTAMP)
Date d;

public TestEntity() {
}

public TestEntity( String state, Date d ) {
this.state = state;
this.d = d;
}

Test ukazujący w/w problem

@Test
public void test1(){
TestEntity t = new TestEntity("foo",new Date());
sessionFactory.getCurrentSession().save( t );
sessionFactory.getCurrentSession().flush();
sessionFactory.getCurrentSession().clear();
t = (TestEntity) sessionFactory.getCurrentSession().createQuery( "from TestEntity where d=?").setTimestamp( 0, t.getD()).uniqueResult();
assertThat( t, notNullValue() );
}

Test ten kończy się failure.
Można to łatwo poprawić, gdy "wyzerujemy" milisekundy

TestEntity t = new TestEntity("foo",new Date());
sessionFactory.getCurrentSession().save( t );
sessionFactory.getCurrentSession().flush();
sessionFactory.getCurrentSession().clear();
Calendar cal = Calendar.getInstance();
cal.setTime( t.getD() );
cal.set( Calendar.MILLISECOND, 0 );
t = (TestEntity) sessionFactory.getCurrentSession().createQuery( " from TestEntity where d=?").setTimestamp( 0, cal.getTime()).uniqueResult();
assertThat( t, notNullValue() );

Wykonanie tego testu kończy się sukcesem.
Na tym niestety problem się nie kończy. Pomimo tego, że udało nam się uzyskać poprawny wynik, to potrzeba "zerowania" milisekund jest strasznie upierdliwe, ale co gorsze zapytanie takie nie wykorzystują indeksu, który byłyby założone na kolumnie typu DATE. Można się o tym przekonać wykonując następujące polecenia (użytkownik system) :

select sql_id, child_number,sql_text from V$SQL where sql_text like '%from TestEntity%' order by last_load_time desc;

W wyniku dostaniemy sql_id oraz child_number, obie wartości służą jako input dla polecenia:

SELECT * FROM table(DBMS_XPLAN.display_cursor('<sql_id >',<child_number>));

W przypadku gdy mielibyśmy stworzony indeks np. na 2 kolumnach (jedna z nich będzie typu DATE), to zapytania z warunkiem where, które teoretycznie mogłyby wykorzystywać indeks (, np status=? and d>=? and d mogą się bardzo długo wykonywać.
Jeśli nawet ORACLE użyje indeksu to może zrobić to bardzo nieefektywnie.
Jest to spowodowane tym, że ORACLE będzie "promował" daty w indeksie do typu TIMESTAMP. Taka "promocja" powoduję, że przeglądnięcie indeksu jest nieefektywne. Widać to w planie zapytania:
|*  6 |      INDEX RANGE SCAN         | MOJ_INDEX |   741 |       | |                                                                                                                                                                            
--------------------------------------------------------------------------------------------------------

6 - access("state"=:3)
filter((INTERNAL_FUNCTION("D")>=:1 AND INTERNAL_FUNCTION("D")<:2))

"Promocja" typu jest prezentowana jako INTERNAL_FUNCTION
Rozwiązaniem problemu (brak użycia indeksu oraz potrzeba "zerowania" milisekund) byłoby "wypchnięcie" do bazy z poziomu JDBC wartości typu DATE a nie TIMESTAMP.
Można to zrobić poprzez:
  • używanie funkcji to_date - wymaga to ręcznego wprowadzania daty w postaci String

    @Test
    public void testToDate(){
    TestEntity t = new TestEntity("foo",new Date());
    sessionFactory.getCurrentSession().save( t );
    sessionFactory.getCurrentSession().flush();
    sessionFactory.getCurrentSession().clear();
    String dateInStringFormat = createDateFormat( "yyyy-MM-dd HH:mm:ss" ).format( t.getD() );
    t = (TestEntity) sessionFactory.getCurrentSession().createQuery( " from TestEntity where d=to_date(?,'YYYY-MM-DD HH24:MI:SS')").setString( 0, dateInStringFormat ).uniqueResult();
    assertThat( t, notNullValue() );
    }
  • użycie funkcji cast as
    - cast z TIMESTAMP na DATE nie tylko "rzutuje" jeden typ na drugi, ale także dokonuję zaokrąglenia:

    @Test
    public void testCastMilis500andAbove(){
    Calendar calendar = Calendar.getInstance();
    calendar.setTime( new Date() );
    calendar.set( Calendar.MILLISECOND, 500 );
    TestEntity t = new TestEntity("foo",calendar.getTime());
    sessionFactory.getCurrentSession().save( t );
    sessionFactory.getCurrentSession().flush();
    sessionFactory.getCurrentSession().clear();
    t = (TestEntity) sessionFactory.getCurrentSession().createQuery( " from TestEntity where d= cast (? as date)").setTimestamp( 0, calendar.getTime()).uniqueResult();
    assertThat( t, nullValue() );
    }

    @Test
    public void testCastMillisBelow500(){
    Calendar calendar = Calendar.getInstance();
    calendar.setTime( new Date() );
    calendar.set( Calendar.MILLISECOND, 499 );
    TestEntity t = new TestEntity("foo",calendar.getTime());
    sessionFactory.getCurrentSession().save( t );
    sessionFactory.getCurrentSession().flush();
    sessionFactory.getCurrentSession().clear();
    t = (TestEntity) sessionFactory.getCurrentSession().createQuery( " from TestEntity where d= cast (? as date)").setTimestamp( 0, calendar.getTime()).uniqueResult();
    assertThat( t, notNullValue() );
    }
    Nie objedzie się w takim przypadku od "zerowania" milisekund. W przypadku Criteria API nie wiem w jaki sposób out-of -box używać wywołań funkcji. Chyba trzeba by stworzyć własnego Criterion
  • bezpośrednie użycie typu oracle.sql.DATE:

    @Test
    public void test3ImplicitOracleDateJDBC() throws SQLException{
    TestEntity t = new TestEntity("foo",new Date());
    sessionFactory.getCurrentSession().save( t );
    sessionFactory.getCurrentSession().flush();
    sessionFactory.getCurrentSession().clear();
    SimpleJdbcTemplate jdbcTemplate = new SimpleJdbcTemplate(dataSource);
    Timestamp ts = new Timestamp(t.getD().getTime());
    oracle.sql.DATE d = new oracle.sql.DATE(ts);
    long queryForLong = jdbcTemplate.queryForLong( "select count(* ) from TestTable where d=?", d );
    assertThat( queryForLong, equalTo( 1l ) );
    }

    Skuteczne, ale niewygodne. W przypadku HQL trzeba by pewnie stworzyć custom type, który byłby użyty przy bindowaniu parametrów przy pomocy wywołania

    Query setParameter(int position, Object val, Type type);

    W przypadku użycia Criteria API chyba najwygodniejsze byłoby stworzenie własnego Criterion
  • przy użyciu driveraJDBC w wersji 11.1.0.7.0 zadziała taka konstrukcja:

    @Test
    public void testImplicitType() throws SQLException{
    TestEntity t = new TestEntity("foo",new Date());
    sessionFactory.getCurrentSession().save( t );
    sessionFactory.getCurrentSession().flush();
    sessionFactory.getCurrentSession().clear();
    Connection connection = sessionFactory.getCurrentSession().connection();
    PreparedStatement preparedStatement = connection.prepareStatement( "select count(*) from TestTable where d=?");
    preparedStatement.setObject( 1,new Timestamp(t.getD().getTime()) ,java.sql.Types.DATE );
    assertThat( preparedStatement.executeUpdate(), equalTo( 1 ));
    }
  • Oracle JDBC 9/10 (dla 9 nie sprawdzałem) driver undocumented "feature":

    @Test
    public void testSetTimeMilis500andAbove(){
    Calendar calendar = Calendar.getInstance();
    calendar.setTime( new Date() );
    calendar.set( Calendar.MILLISECOND, 500 );
    TestEntity t = new TestEntity("foo",calendar.getTime());
    sessionFactory.getCurrentSession().save( t );
    sessionFactory.getCurrentSession().flush();
    sessionFactory.getCurrentSession().clear();
    t = (TestEntity) sessionFactory.getCurrentSession().createQuery( " from TestEntity where d= ? ").setTime( 0, calendar.getTime()).uniqueResult();
    assertThat( t, notNullValue() );
    }

    @Test
    public void testSetTimeMilisBelow500(){
    Calendar calendar = Calendar.getInstance();
    calendar.setTime( new Date() );
    calendar.set( Calendar.MILLISECOND, 499 );
    TestEntity t = new TestEntity("foo",calendar.getTime());
    sessionFactory.getCurrentSession().save( t );
    sessionFactory.getCurrentSession().flush();
    sessionFactory.getCurrentSession().clear();
    t = (TestEntity) sessionFactory.getCurrentSession().createQuery( " from TestEntity where d=?").setTime( 0, calendar.getTime()).uniqueResult();
    assertThat( t, notNullValue() );;
    }

    Zgodnie z dokumentacją setTime() powinno zostać skonwertowane na TIME, ale konwersja jest robiona na DATE! Dla Criteria API trzeba by stworzyć własne Criterion, aby móc wymusić na Hibernate użycie setTime() - Hibernate out-of-box decyduje jaką metodę wywołac na PreparedStatement na podstawie typu/annotacji property na której ustawiamy warunek.

sobota, 17 października 2009

JDD 09

“Jak usprawnić model domeny wykorzystując jBPM?”

Pierwsza prezentacja dotyczyła jBPM. Dotychczas nie miałem do czynienia z takiego typu narzędziami i uważałem je bardziej za sztukę dla sztuki, które tak na prawdę nadają się tylko na prezentację. Pomimo uprzedzeń, prezentacje uważam za bardzo udaną, zaczęła się od wyjaśnienia znaczenia podstawowych, groźnych skrótów: BPM, BPEL, PDL. Po tym nastąpiło przedstawienie konkretnego narzędzia: JBoss jBPM. Parę rzeczy wyglądało na prawdę obiecująco: procesy można modelować w przeglądarce i pod Eclipse. Szczególnie ta pierwsza opcja może być ciekawa dla analityków - "wyklikają" proces w przeglądarce a później developer może zaimportować go w Eclipse. Sam jBPM może być uruchamiany w trybie standalone lub embedded. Moduł jBPM Console pozwala monitorować i śledzić wykonanie proces w postaci wykresów/tabelek i innych wodotrysków w przeglądarce. Zachwyciłoby to nie jednego managera. Dodatkowo jBPM Console pozwala w łatwy sposób na tworzenie prototypów aplikacji wykorzystujących jBPM, np. zasymulowanie wysłania SMS można zrealizować poprzez utworzenie prostego formularza HTML, którego wysłanie wygeneruje input dla kolejnego etapu procesu.


“Sztuka messagingu"

Pierwsza prezentacja jednej z gwiazd JDD Marka Richardsa, autora Java Message Service. Mark starał się przekonać nas (i mnie osobiście przekonał), że pomimo tego, że JMS API nie zmieniło się od 2002, JMS jest cały czas bardzo użyteczną technologią, a jej umiejętne wykorzystanie pozwala nam tworzyć wydajne/skalowalne/niezawodne i potencjalnie heterogeniczne systemy. Tu znajduje się zapowiedz prezentacji Marka, która pasuję bardzo do obu jego prezentacji na JDD. Na początku prezentacji wiało trochę nudą, i nie pomogło nawet odebranie wiadomości w Groovym w typowym przykładzie send/receive. O wiele ciekawsze okazało się omówienie podstawowych przypadków użycia JMS:

  • integracja poprzez JMS pomiędzy aplikacjami/modułami napisanymi w potencjalnie różnych językach (nie chodzi to bynajmniej o wspomniane powyżej Groovy), np JAVA i .NET
  • skalowalność - tu szczególnie leży siła rozwiązań opartych na JMS. W przypadku komunikacji 2 komponentów: pierwszy wysyła komunikat do kolejki, a drugi komponent przetwarza ten komunikat. Dzięki takiemu podejściu można bardzo swobodnie i łatwo sterować ilością komponentów (listnerów) przetwarzających komunikaty
  • asynchroniczność - możliwość asynchonicznego powiadamiania zainteresowanych komponentów

Dalej pojawiło się parę slajdów związanych z 2 modelami JMS: point-to-point or publish-subscibe, dostępne typy wiadomości (tu pojawiła się uwaga: aby zapewnić interoperability można zapomnieć o ObjectMessage) oraz budowa wiadomości. Mark zwrócił także dodatkowo na ważną cechę związaną z używaniem JMS API: w odróżnieniu od JDBC, transakcyjna jest obiekt session a nie connection. Zazwyczaj optymalnym rozwiązaniem ma być użycie pojedynczego connection i puli obiektów session. Po prezentacji rozmawiałem z Markiem i uświadomił mnie ,że 1 MessageListener= 1 session = 1 TCP connection (to ostanie sam zamierzam sprawdzić ). Oznacza to, że nie można przesadzać z liczbą równolegle odpalonych listnerów - Mark stwierdził. że dla mocno obciążonych systemów ma byc to około 20. Jeśli chodzi o technikalia to polecił używanie Jencks wraz z ActiveMQ lub CachingConnectionFactory.
Bardzo podobał mi się slajdy przedstawiające różne considerations związane z wykorzystywaniem JMS:

  • trwałość komunikatów - wiadomości mają byc domyślnie persystentne co oznaczą, że są trzymane w persystentnym storage. Ma to znaczący wpływ na wydajność - przedstawił wykres prezentujący 4 000 persystentych komunikatów na sekundę do 11 000 nie persystentnych. JMSDeliveryMode można ustawić na MessageProducer lub bezpośrednio na Message
  • użycie pojedynczej kolejki dla całego przetwarzania - architektura w której to wszystkie komponenty wrzucają swoje komunikaty do pojedynczej kolejki. Wiadomości z kolejki są odczytywane przez pojedynczy komponent pełniący rolę routera, który na podstawie "magicznego dyskryminatora" przechowanego w property wiadomości (a nie w treści wiadomości) deleguję wywołanie (wywołuję metodę) odpowiedniego komponentu. Problem w takim podejściu polega na tym, że pewne komponenty (potencjalnie o różnym znaczeniu) mogą wrzucać z różną częstotliwością swoje wiadomości (potencjalnie o różnej charakterystyce przetwarzania) do wspólnej kolejki. Może to bardzo negatywnie wpływać na response time oraz dodatkowo utrudni skalowania takiego systemu - nie za wiele pomogą tutaj triki z ustawieniami różnych priorytetów dla wiadomości, tym bardziej, że nie można wywłaszczać aktualnie przetwarzanych komunikatów. Idąc dalej, Mark stwierdził, że nie jest niczym egzotycznym definiowanie więcej niż 1 kolejki dla tych samych wiadomości - by zapewnić QoS


Na końcu tej części stanowczo stwierdził, że JMS bardzo dobrze się nadaje gdy mówimy o technologiach związanych z integracją szczególnie pod kątem interoperability. Stwierdził, że dla rozwiązań działających w środowisku "inside firewall" JMS nadaję się o wiele bardziej niż web services, głównie ze względu na większą możliwość tuningu, niezawodność i mniejszą złożoność tworzenia takich systemów (?).
Na końcu pojawiła się wzmianka o REST w świecie JMS i pewna technlogiczno-syntaktyczna niespójność: co powinna robić metoda GET ? czy ma pobierać wiadomość z kolejki ? A jeśli tak to pobranie wiadomości jednocześnie usuwa ją z kolejki, a przecież wywołanie GET powinno być safe. Z tego co zapamiętałem to Active MQ oraz Websphere MQ mają posiadać pseudo REST API do swoich systemów

“Obsługa sytuacji wyjątkowych w systemach budowanych w technologii JEE”

Prezentacja sponsora konferencji, na szczęści wyglądało to duże lepiej niż rok temu. Zaprezentowany materiał całkiem fajny, oparty na doświadczeniu, ale trochę mało świeży, szczególnie gdy ktoś ma do czynienia z lekkimi frameworkami, które stawiają na unchecked exceptions. Oprócz podstawowych informacji o tym jakie wyjątki logować, w jaki sposób logować, co logować itd mnie bardziej zainteresowały następujące zagadnienia:

  • transakcyjność a pamięć podręczna - w sytuacji gdy nasz cache nie jest transakcyjny (czyli zawsze, chyba że się mylę ) to trzeba zadbać aby w przypadku rollback wycofać to co w ramach transakcji zmodyfikowaliśmy w cache. Sprawa wydaję się w ogólności dość skomplikowana , a z tego co się orientuję to w hibernate 2nd level cache potrafi sobie z czymś takim radzić out-of-box
  • transakcyjność a sesja - w przypadku gdy nasze przetwarzanie na serwerze zakończy się wyjątkiem - sesja może być niespójna - wyjątek mógł polecieć po tym jak coś z sesji zmodyfikowaliśmy. Rozwiązanie ma być oparte o filtr opakowywujący oryginalną sesje w wrapper, dostępny dla aplikacje, a faktyczny zapis do sesji jest realizowany w tym samym filtrze po zakończeniu przetwarzania
  • W przypadku standardowego logowania SQLException stacktrace/message wyjątku nie zawierają ani SQLState ani ErrorCode
  • W przypadku ServletException nie jest logowany faktyczny powód wystąpienia, który można zobaczyć dobierając się do rootCause
  • naruszenie więzów integralności na bazie nie koniecznie jest błędem typu unrecover , a może być po prostu błędem biznesowym - próba stworzenia 2 userów o tym samym login. który to musi być unikalny. W takim przypadku można taki wyjątek złapać i poprawnie obsłużyć.

“Asynchroniczność, współbieżność i rozproszone przetwarzanie w Java EE – przykłady z użyciem technologii middleware Oracle: WebLogic Server, EclipseLink/TopLink JPA i Coherence"

Prezentacja Waldka Kota miała pierwotnie składać się z 2 części: omówienie WorkManager API a później przedstawienie Oracle Coherence. Jednak ze względu na to, że widownia miała problemy z zebraniem się z obiadu oraz dużą liczbą pytań o WorkManager API część druga w ogóle się nie odbyła, a Waldek obiecał, że opiszę to co miał przygotowanie na temat Oracle Coherence na swoim blogu. Prezentacja zaczęła się od przypomnienia/uświadomienia czemu nie powinno się tworzyć wątków w środowisku JEE. Z WorkManager API miałem styczność jakiś czas temu przy tworzeniu aplikacji JEE uruchamianych na "ulubionym" IBM Websphere. Pierwotnie API to powstała w ramach JSR 236, które aktualnie zostało zaniechane i z tego co opowiadał Waldek są jakieś zawirowania w tej sprawie w związku z JEE 6. Do tego czasu Websphere(min 6.0, w wersji 5.0 ma podobny mechanizm Asynchronous Beans)oraz Weblogic (min 9.0) wspierają CommonJ Timer and Work Manager for Application Servers (oprócz Work Manager API mamy do dyspozycji Timer API). Jest to na prawdę silna broń, ale nie do końca wierzę aby nadawała się do tworzenia aplikacji batchowych. Niespecjalnie wgłębiałem się w dostępne opcje konfiguracyjne WorkManager na Websphere, ale to co pokazywał Waldek na przykładzie serwera Weblogic robi naprawdę wrażenie: przypięcie WorkManager do wielu aplikacji, pojedynczej aplikacji, servletu, ziarna EJB, czy nawet metody EJB, określanie parametrów definiujących min/max ilość wątków, ustawienie stuck time i jeszcze pewnie parę innych.
Co ważne podkreślenia WorkManager API uruchamia zadania w kontekście JEE: same zadanie jest wykonywane w kontekście (security, classpath, naming) w jakim zostało ono zlecone do wykonania.
Specyfikacja wspomina także o Remoteable WorkManager i RemoteWorkItem - delegowanie zadań do zdalnych węzłów w klastrze. Websphere nie wspiera takiej funkcjonalności, jestem ciekaw jak to jest w Weblogic.


Efektywne przeglądy kodu dla developerów Java używających metodologii z rodziny agile

Bardzo ciekawa prezentacja, na temat code review. Wojtek na podstawie własnych doświadczeń, przestawił nie tylko na czym polega nowoczesny proces code review, ale co najważniejsze wiele wskazówek związanych z wprowadzaniem code review do organizacji - nic na siłę, metoda małych kroczków, trzeba dostosowac proces do zespołu (zmotywowany i samo-organizujący się zespół :) ), sposób wybieranie reviewerów. Pomimo wielu zasadniczych zalet: mentoring mniej doświadczonych/nowych członków zespołu, budowanie bazy wiedzy, zwiększenie collective ownership kodu, ewentualnie wykrywanie błędów, wprowadzenie code review niestety daje mało mierzalne rezultaty - co często może powodować opór managementu. Code review nie powinno w żadnym przypadku zastępowac statycznej analizy kodu, które to powinna odbywać sie przed review w celu "wyczyszczenia" kodu z naruszeń. Code review, które miałoby się odbywać "tradycyjnie" w postaci spotkania, na którym obecny miałby być cały zespól przeglądający wydrukowany kod, ma być mało skuteczne i problematyczne w sensie logistycznym, o wiele lepiej wykorzystać do tego narzędzia ,np. Crucible. Przyznam, że narzędzie zrobiło na mnie duże wrażenie - wszystko wyglądało bardzo intuicyjnie i prosto, a jednocześnie dobrze dostosowane na potrzeby developerów. Bardzo ciekawie wyglądało porównanie code review i pair programming- obie techniki dotyczą pracy 2 osób nad kawałkiem kodu, jednak tak na prawdę więcej je od siebie różni niż łączy... Z projektów darmowych podobno warto sprawdzić: rietveld, reviewClipse a sam kiedyś instalowałem jupiter


“Testowanie z Groovy”

Prezentacja kolejnej z gwiazd JDD Scotta Davisa. Prezentacja mnie rozczarowała, po standardowej opowieści o tym, że na JVM można uruchamiać aplikacje napisane w: JRuby, JavaScript, JPython,Java FX, Scala oraz kilu żartach prowadzącego, zostało kilkanaście minut na wpomnienie o tym czym jest BDD i pokazanie trywialnego kodu w Groovy z wykorzystaniem paradygmatu: given... : when... : then ... and ...


Architektura Resource-Oriented (ROA) i REST

ostatnia prezentacja JDD. Tym razem Scott opowiadał o REST i ROA. Zaczęło sie od typowych żartów na temat SOAP, które miało umożliwiać szybkie i wygodne tworzenie interoperable web services. Po tym jak wylano wiadro pomyj na SOAP, Scott pokazał z jaką łatwością konsumuje się REST webservices z poziomu Groovy, szczególnie gdy korzystamy z XMLSlurper. Po przedstawieniu podstawowych pojęć związanych z REST, Scott przedstawił wiele przykładów wykorzystania ROA: Ebay, Amazon, Twitter, Yahoo, GData. Szczególnie polecał przyjrzenie się Google Calendar Data API, które uważa za modelowe przykład wykorzystania ROA, szczególnie gdy sami będziemy projektować REST based systems.

poniedziałek, 12 października 2009

Dostęp do Websphere Remote EJB 2.1 z JAVA SE

Potrzebowałem stworzyć małą aplikację J2SE do wołania zdalnych sesyjnych bezstanowych EJB uruchomionych na Websphere 6.1.0.19. W mojej poprzedniej pracy juz robiłem coś takiego, ale wtedy aplikacja działała na SUN JRE. Tym razem podstawowym wymaganiem było użycie IBM JRE. Skoro serwera aplikacyjny oraz JRE są od tego samego dostawcy to sprawa powinna być prosta.... tymbardziej, że aplikacja była trywialnana a dodatkowo nie miałem potrzeby korzystać z żadnych z enterprise features jak bezpieczeństwo czy transakcyjność.
Dodatkowo EAR zawierający EJB, do którego trzeba się było dobierać nie był mojego autorstwa i nie dostarczono dla niego jara klienckiego - ze stubami, ale te można samemu wygenerować za pomocą ejbdeploy.
Za wszelką cene chciałem uniknąć sytuacji, w której to musiałbym uruchamiać moją aplikację za pomocą client container albo wykonywać jakies inne, dziwne kroki w fazie developmentu/deploymentu.
Po przeczytaniu dokumentacji wyglądało to wszystko extra skomplikowanie. Tymbardziej, że Weblogic już od dawna udostępniał swoim użytkownikom lekkiego client.jar (czy jakoś tak) do takich celów.
Czemu prosta rzecz nie może byc prosta?
Po wielu próbach i błędach udało mi się w końcu przygotować aplikację. Oprócz jara ze stubami, classpath zawiera następujące pozycje:

ecore-2.3.0-v200706262000.jar
emf-2.1.0.jar
com.ibm.ws.webservices.thinclient_6.1.0.jar
com.ibm.ws.runtime_6.1.0.jar


ecore-2.3.0-v200706262000.jar jest do ściągnięcia z mvnrepository, pozostałe 3 są skopiowane z WebSphere.
Dodatkowo bardzo ważna jest kolejność na classpath: com.ibm.ws.webservices.thinclient_6.1.0.jar musi byc przed com.ibm.ws.runtime_6.1.0.jar, ze względu na to, że klasa com.ibm.ejs.ras.Tr znajduję się w tych obu jarach.

Odwołanie do EJB jest realizowane w następujacy sposób:

Context initialContext = new InitialContext();
StringBuilder builder = new StringBuilder("corbaname:iiop:");
builder.append(serverIp);
builder.append(":");
builder.append(serverPort);
builder.append("#");
builder.append(ejbJNDIName);
Object lookUpResult = initialContext.lookup(builder.toString());
HomeInterface home = (HomeInterface) javax.rmi.PortableRemoteObject.narrow(
lookUpResult, HomeInterface.class);

serverIp - adres serwera na którym jest zdeployowany EJB
serverPort - port do komunikacji RMI (domyslnie 2809)
ejbJNDIName - nazwa JNDI zdalnego EJB

Dodatkowo dla serwera, którego EJB będziemy wywoływać należy dodać parę:adres ip, nazwa serwera do etc\hosts.

Jedyne co mi się nie podoba w przygotowanym przeze mnie rozwiązaniu to potrzeba umieszczenia na classpath w/w bibliotek z Websphere (sam com.ibm.ws.runtime_6.1.0.jar zajmuję prawie 60 MB).
Podobno w Websphere 7.0 został przygotowany jar kliencki, ale osobiście nie mam jak na razie dostępu do tej wersji.

UPDATE: przeglądając materiały związane z classloadingiem dla WAS 6.1 natknąłem się na info dotyczące "thin client library" dla WAS 6.1. Składają się na to 2 jary: com.ibm.ws.admin.client_6.1.0.jar (34 Kb) oraz com.ibm.ws.webservices.thinclient_6.1.0.jar (15 Kb) siedzące w <INSTALL_ROOT>/runtimes. Po usunięciu z classpath jarów wymienionych na początku posta (ecore-2.3.0-v200706262000.jar, emf-2.1.0.jar, com.ibm.ws.webservices.thinclient_6.1.0.jar, com.ibm.ws.runtime_6.1.0.jar) i podegraniu na ich miejsce tych z <INSTALL_ROOT>/runtimes wszystko działa bez zmian.

środa, 16 września 2009

Websphere 6.1 remote SLSB między 2 EAR

W aplikacji EAR na websphere 6.1 stanęliśmy przed problemem integracji z pewnym zewnętrznym modułem. Początkowo planowałem owy moduł w postaci zbioru jarów wrzucić bezpośrednio do aplikacji i korzystać z niego poprzez przygotwaną facade. Jednak gdy otrzymałem już moduł to wiedziałem ,że takie podejście może się nie sprawdzić. Moduł ważył ponad 44 MB, a dodatkowo zawierał takie jary jak: servlet.api, jta.jar oraz parę innych z j2ee, ale także pewne biblioteki (m.in stax,jaxb,spring, hibernate, xerces, log4j) w innych wersjach niż te z których do teraz korzystałem... bez OSGI sądzę, że nie ma szans pogodzić ze sobą mojej aplikacji i modułu w ramach pojedynczego EAR.
Jedyne co udało się wykombinować to wrzucić nowy moduł jako osobny EAR, na ten sam serwer, na którym jest zdeployowana aplikacja. Oczywiście trzeba było obmyślić w jaki sposób aplikacja będzie mogła się teraz komunikowac z modułem. Najprostsze wydawało się stworzenie remote stateless session EJB 2.1 (2.1, bo nasz serwer nie ma zainstalowanego packa do EJB 3.0).
Skoro i tak juz został utworzony interfejs POJO, który miał wystawiać wszystkie usługi to wystarczyło stworzyć (wyklikać) EJB, którego remote interfejs pokrywałby się z tym uprzednio przygotowanym (z jedyną modyfikacją, że każda z metod musi deklarowac RemoteException).
Dostawca modułu wszystko ładnie przygotował - otrzymałem client jar z interfejsami oraz stubami, a także dodatkowo dostarczono nawet klienta testowego. Wszystko było super do momentu kiedy spróbowałem wywołać z mojej aplikacji metodę interfejsu modułu. Skoro funkcjonalność modułu jest dostępna przez remote EJB to odwołanie do takiego komponentu wydaję się bezproblemowe z poziomu Spring:

<xml:namespace prefix = jee /><?xml:namespace prefix = jee />
<jee:remote-slsb id="modulFacade" name="ejb/ModulFacade" ref="false" interface="xxx.yyy.zzz.ModuleFacadePojo"/>


Konsumpcja remote stateless session EJB w taki sposób ma parę zasadniczych zalet:

  • nie musimy pisać kodu, który robi lookup, wywołuję create(), cachuje home

  • nasz kod korzystający z modulFacade nie musi zajmować się obsługą RemoteException


Jednak po wywołaniu motody na beanie modulFacade leciały wyjątki: ClassCastException lub NoSuchMethodException. Szczególnie ten drugi wyjątek dał mi do myślenia i zasugerował, że wyjątki te mogą wynikac z moich ulubionych problemów związanych z classloadingiem. Przygotowana przez dostawce modułu aplikacja testowa w postacie war działała poprawnie, jednak tam dostęp do EJB był zrealizowany poprzez wygenerowaną (pewnie z RAD) klasę Util, która zajmowała się lookup(), PortableRemoteObject.narrow(), create(). Na potrzeby testu, spróbowałem wykorzystać tą klasę w mojej aplikacji i okazało się ,że tym razem wszystko działa bez problemu...



W takim razie problem leżał w Spring lub w niewłaściwej konfiguracji elementu jee:remote-slsb.Po chwili googlowania udało mi się znaleźć odpowiedni post na forum, wraz z rozwiązaniem.


Chodzi o ustawienie atrybutu home-interface:


<xml:namespace prefix = jee /><?xml:namespace prefix = jee /><jee:remote-slsb id="modulFacade" name="ejb/ModulFacade" ref="false" interface="xxx.yyy.zzz.ModuleFacadePojo" home-interface="xxx.yyy.zzz.ModulFacadeHome"/>

Pobrany z JNDI objekt będący home remote SLSB aplikacji-modułu jest stubem, który został załadowany przez classloader aplikacji-modułu. Dalsze wywołania na tym obiekcie skutkują tworzeniem obiektów, których klasy są ładowane tym samym classloaderem. W ten sposób dojdziemy do momentu, w którym to dana metod biznesowe zwraca nam obiekt, którego klasa znajduję się w client jar, ale został załadowany przez classloader aplikacji-modułu.
Teraz jeśli w naszej aplikacji zrobiony będzie cast to dostaniemy ClassCastException.
Podobnie to wszystko wygląda gdy nasza metoda biznesowa, będzie brała jako parametr obiekt klasy znajdujący się w client jar. W takim przypadku sygnatura metody nie będzie się zgadzać bo 2 identyczne klasy załadowane przez 2 różne classloadery są niezgodne.
Całość bardzo ładnie widać w trybie debug:

protected Object lookup() throws NamingException {
Object homeObject = super.lookup();
if (this.homeInterface != null) {
try {
homeObject = PortableRemoteObject.narrow(homeObject, this.homeInterface);
}catch (ClassCastException ex) {
throw new RemoteLookupFailureException(
Could not narrow EJB home stub to home interface [" + this.homeInterface.getName() + "]", ex);
}
}
return homeObject;
}

Wywołanie lookup() zwraca nam objekt będacy interfejsem home EJB.
Jednak gdy podejrzymy: homeObject.getClass().getClassLoader() to zobaczymy classloader, który załadował aplikacje modułu. Dopiero wywołanie PortableRemoteObject.narrow spowoduję, że wartością homeObject.getClass().getClassLoader() będzie classloader naszej aplikacji. Z tego co się doczytałem zachowanie takie jest spowodowane tym, że Websphere optymalizuję wywołanie przez zdalne EJB w ramach dwóch aplikacji znajdujących się w tym samym JVM. Jak będę miał chwilę to faktycznie można to sprawdzić: należałoby wrzucic client jar do shared-library i zobaczyć czy faktycznie uda się uzyskać pass-by-reference.
Dodatkowo gdy każda z tych aplikacji byłaby uruchamiana na różnych JVM to podawanie home-interface ma także być zbędne, czyli bez wywołania PortableRemoteObject.narrow wszystko ma działać...to też można by sprawdzić.

Update: przypadkowo trafiłem na całkiem sensowny opis mechanizmu/rozwiązań: http://fixunix.com/websphere/344774-local-interfaces-same-jvm-but-different-ear-issue.html

wtorek, 14 lipca 2009

Websphere 6.1, SWS i MTOM

Websphere w wersji 6.1 obsługuje jedynie SAAJ w wersji 1.2. Aby móc korzystac z MTOM potrzebny jest SAAJ 1.3. Z tego co znalazłem w sieci jest dostępny odpowiedni feature pack, który ma obsługiwac SAAJ 1.3, MTOM oraz wiele więcej. Jednak instalowanie feature packów od IBM często kończy się różnie (tzn czasami nie działają, a jeszcze częściej trzeba instalowac kolejne packi/łaty itd). Dlatego zdecydowałem się na "własną instalację" SAAJ 1.3.
Na podstawie moich wcześniejszych walk z SWS pod Websphere, wszystko okazało się całkiem proste.
Do jarów wchodzących w skład shared-library (definujących biblioteki nowego classloadera)dodajemy dodatkowo:

Dodatkowo pozwalamy SWS na automatyczne wykrycie implementacji javax.xml.soap.MessageFactory - czyli z naszej konfiguracji springowej wywalamy bean "messageFactory".

Alternatywnie, MTOM równie działa, gdy wymienione wyżej biblioteki wrzucimy do WEB-INF/lib oraz ustawimy ładowanie klas z bibliotek aplikacji przed tymi z bibliotek serwera (PARENT-LAST dla modułu webowego).

W przypadku ustawienia PARENT_LAST i używania JAXB 2 pojawiają się jednak pewne problemy, które objawiają się wyjątkiem:


Caused by: java.lang.VerifyError
at com.sun.xml.bind.v2.model.impl.RuntimeBuiltinLeafInfoImpl.<clinit>(RuntimeBuiltinLeafInfoImpl.java:224)
at java.lang.J9VMInternals.initializeImpl(Native Method)
at java.lang.J9VMInternals.initialize(J9VMInternals.java:194)
at com.sun.xml.bind.v2.model.impl.RuntimeTypeInfoSetImpl.<init>(RuntimeTypeInfoSetImpl.java:61)
at com.sun.xml.bind.v2.model.impl.RuntimeModelBuilder.createTypeInfoSet(RuntimeModelBuilder.java:127)
at com.sun.xml.bind.v2.model.impl.RuntimeModelBuilder.createTypeInfoSet(RuntimeModelBuilder.java:79)
at com.sun.xml.bind.v2.model.impl.ModelBuilder.<init>(ModelBuilder.java:151)
at com.sun.xml.bind.v2.model.impl.RuntimeModelBuilder.<init>(RuntimeModelBuilder.java:87)
at com.sun.xml.bind.v2.runtime.JAXBContextImpl.getTypeInfoSet(JAXBContextImpl.java:422)
at com.sun.xml.bind.v2.runtime.JAXBContextImpl..<init.>(JAXBContextImpl.java:286)
at com.sun.xml.bind.v2.ContextFactory.createContext(ContextFactory.java:139)
at com.sun.xml.bind.v2.ContextFactory.createContext(ContextFactory.java:117)
at com.sun.xml.bind.v2.ContextFactory.createContext(ContextFactory.java:188)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:79)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:618)
at javax.xml.bind.ContextFinder.newInstance(ContextFinder.java:133)
at javax.xml.bind.ContextFinder.find(ContextFinder.java:286)
at javax.xml.bind.JAXBContext.newInstance(JAXBContext.java:372)
at javax.xml.bind.JAXBContext.newInstance(JAXBContext.java:337)
at org.springframework.oxm.jaxb.Jaxb2Marshaller.createJaxbContextFromContextPath(Jaxb2Marshaller.java:311)

Po dłuższej chwili debugowania problemem okazała się klasa javax.xml.namespace.QName.
JAXB 2 przy starcie dobiera się do stałych klasy javax.xml.datatype.DatatypeConstants.
Klasa ta sama w sobie jest ładowana z Websphere(../runtimes/base_v61/java/jre/lib/xml.jar), a typem tych stałych jest klasa javax.xml.namespace.QName, która też znajdują się w Webpshere (../runtimes/base_v61/lib/j2ee.jar). Czyli przy odwołaniu się do do klasy javax.xml.datatype.DatatypeConstants, zostanie ona wraz ze swoimi zależnosciami ( m.in. klasą javax.xml.namespace.QName ) załadowana przez classloader Websphere.
Dodatkowo klasa javax.xml.namespace.QName znajduje się w Jsr173_api-1.0.jar/stax-api-1.0.1.jar, a przy ustawieniu Parent-Last nasza aplikacja najpierw załaduje ją właśnie stamtąd. Dojdzie do sytuacji, w której "skontatkują" się ze sobą 2 klasy (javax.xml.namespace.QName)o tej samej nazwie, o potencjalnie różnych definicjach, załadowane przez 2 różne classloadery.
Rozwiązaniem okazuję się wywalenie z naszej aplikacji biblioteki Jsr173_api-1.0.jar/stax-api-1.0.1.jar i podegranie na jej miejsce stax-api-1.0-2.jar(ta nie zawiera w sobie żadnych klas z pakietu jaxax.xml.namespace)

UPDATE: na jednym ze środowisk pomimo w/w ustawień leciały wyjątki:

Caused by:
java.lang.NoClassDefFoundError: javax.xml.transform.stax.StAXResult
at com.sun.org.apache.xalan.internal.xsltc.trax.TransformerImpl.getOutputHandler(TransformerImpl.java:416)
at com.sun.org.apache.xalan.internal.xsltc.trax.TransformerImpl.transform(TransformerImpl.java:334)
at org.springframework.xml.transform.TransformerObjectSupport.transform(TransformerObjectSupport.java:71)
at org.springframework.ws.wsdl.wsdl11.provider.InliningXsdSchemaTypesProvider.getSchemaElement(InliningXsdSchemaTypesProvider.java:113)
at org.springframework.ws.wsdl.wsdl11.provider.InliningXsdSchemaTypesProvider.addTypes(InliningXsdSchemaTypesProvider.java:101)
at org.springframework.ws.wsdl.wsdl11.ProviderBasedWsdl4jDefinition.afterPropertiesSet(ProviderBasedWsdl4jDefinition.java:233)
at org.springframework.ws.wsdl.wsdl11.DefaultWsdl11Definition.afterPropertiesSet(DefaultWsdl11Definition.java:170)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1369)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1335)
... 39 more
Caused by:
java.lang.ClassNotFoundException: javax.xml.transform.stax.StAXResult
at java.net.URLClassLoader.findClass(URLClassLoader.java:496)
at com.ibm.ws.bootstrap.ExtClassLoader.findClass(ExtClassLoader.java:132)
at java.lang.ClassLoader.loadClass(ClassLoader.java:631)
at com.ibm.ws.bootstrap.ExtClassLoader.loadClass(ExtClassLoader.java:87)
at java.lang.ClassLoader.loadClass(ClassLoader.java:597)
at com.ibm.ws.classloader.ProtectionClassLoader.loadClass(ProtectionClassLoader.java:58)
at com.ibm.ws.classloader.ProtectionClassLoader.loadClass(ProtectionClassLoader.java:54)
at com.ibm.ws.classloader.CompoundClassLoader.loadClass(CompoundClassLoader.java:394)
at java.lang.ClassLoader.loadClass(ClassLoader.java:597)
... 48 more

Klasa org.springframework.xml.transform.TransformerObjectSupport korzysta z wywołania javax.xml.transform.TransformerFactory.newInstance(), przy czym faktyczna implementacja jest określona za pomocą mechanizmu ServiceLoader API. Okazało się, że w obu jarach: jaxp-ri-1.4.2.jar oraz xalan-2.7.0.jar, istnieją wpisy definiujące implementacje dla interfejsu javax.xml.transform.TransformerFactory. Dotychczas "szczęśliwie" wczytywana była implementacja org.apache.xalan.processor.TransformerFactoryImpl z xalan-2.7.0.jar, ale na jednym ze środowisk zaczytano com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl z jaxp-ri-1.4.2.jar. Nie miałem zbytnio ochoty wgłębiać się w ten temat (można poprzez system property wymusić konkretną implementację , może można określić kolejność ładowania przez ServiceLoader API) usunąłem plik META-INF\services\javax.xml.transform.TransformerFactory z jaxp-ri-1.4.2.jar i po kłopocie :)

poniedziałek, 15 czerwca 2009

Oracle i Hibernate

W 2 ostatnich projektach dość intensywnie wykorzystujemy Hibernate jako ORM dla Oracle 10.2.0.1.0. (sterownik JDBC w wersji 10.2.0.4.0.) Takie rozwiązanie nie zawsze okazało się bezproblemowe.
  • "Wrong column type in {nazwa schematu.nazwa kolumny} for column {nazwa kolumny}. Found: date, expected: timestamp". Walidacja schematu przez Hibernate(hibernate.hbm2ddl.auto=validate) wyrzuca wyjątek przy mapowaniu
    @Temporal(TemporalType.TIMESTAMP)
    private java.util.Date published;

    gdy kolumna w bazie jest typu DATE.
    Oracle w wersji 9.2 wprowadził nowy typ danych TIMESTAMP, ale jednocześnie wprowadził mapowanie DATE =>java.sql.Date (
    javax.persistence.TemporalType.Date)
    . Typ java.sql.Date posiada jedynie dane o dacie bez info o czasie, przy czym Oraclowy DATE przetrzymuje dane zarówno o dacie jak i o czasie. Sprawdzenie przez drvier JDBC czy DATE mapuje się na java.sql.TimeStamp(
    javax.persistence.TemporalType.TIMESTAMP)
    jest false i rzucany jest wyjątek. Rozwiązań tego problemu jest kilka, przy czym większość z nich polega na modyfikacji definicji kolumn i/lub zmian w kodzie. Na nasze potrzeby najsensowniejsze okazało się wprowadzenie property (jako property połączenia lub property systemowe) oracle.jdbc.V8Compatible=true. Oznacza to powrót do mapowania między DATE a java.sql.TimeStamp zgodnie z tym jak to było dla Oracle 8i (który nie miał typu TIMESTAMP). W przypadku konfiguracji Spring dla c3p0 definicja DataSource wygląda mniej więcej następująco:

    <bean class="com.mchange.v2.c3p0.ComboPooledDataSource" id="dataSourcePool" method="close">
    <property name="properties">
    <props>
    <prop key="oracle.jdbc.V8Compatible">true</prop>
    </props>
    </property>
    <property name="driverClass" value="${db.driver}"/>
    <property name="jdbcUrl" value="${db.url}"/>
    <property name="user" value="${db.username}"/>
    <property name="password" value="${db.pwd}"/>
    ...
    </bean>

    Ważne jest zachowanie kolejności: najpierw property properties wraz z entry oracle.jdbc.V8Compatible ustawione na true, a dopiero później pozostałe propertiesy.
  • Zgodnie z FAQ oracle.jdbc.V8Compatible zostało wprowadzone aby zapewnić kompatybilność z Oracle 8.1, który nie posiadał typu Timestamp. Oznacza to, że przy przekazywaniu do bazy danych obiektu typu java.sql.Timestamp (z poziomu Hibernate w postaci property typu java.util.Timestamp lub property typu java.util.Calendar/ java.util.Date z annotacją @Temporal(TemporalType.TIMESTAMP) ) zostanie on przekonwertowany na typ bazodanowe DATE , który nie przechowuje milisekund. Czyli, jeśli mamy w bazie kolumne ts typu TIMESTAMP to pomimo mapowania:

    @Column(name="ts)
    java.sql.Timestamp ts;
    lub

    @Column(name="ts)
    @Temporal(TemporalType.TIMESTAMP)
    java.util.Date/java.util.Calendar ts;
    insert/update property ts zapisze do bazy danych date z czasem ale bez milisekund.
    Co ciekawsze przy odczycie, property ts posiada milisekundy !!! (zakres milisekund zależy od tego czy zamapowaliśmy na javowe Calendar, Date czy Timestamp) . Jeśli chcemy z poziomu JDBC przechowywać w Oracle daty wraz z czasem z milisekundami nie można użyć oracle.jdbc.V8Compatible=true. Aby móc jednocześnie walidować schemat wystarczy wskazać typ kolumny jako wartość columnDefinition w annotacji @Column:

    @Column(columnDefinition="date")
    @Temporal(TemporalType.TIMESTAMP)
    private java.util.Date d;
    Dodatkowo warto pamiętac, że w przypadku zapisywania do kolumny DATE wartości typu java.sql.Timestamp (czyli z poziomu Hibernate property typu java.sql.Timestamp lub java.util.Calendar/java.util.Date z annotacją @Temporal(TemporalType.TIMESTAMP) ) do bazy zawsze trafi data z czasem bez milisekund. Jest to logiczne gdyż DATE nie trzyma milisekund. Jednak gdy najpierw zapiszemy do bazy wiersz, którego składową jest kolumna DATE wypełniana z pozimou javy wartością typu java.sql.Timestamp ( z poziomu Hibernate property typu java.sql.Timestamp lub java.util.Calendar/java.util.Date z annotacją @Temporal(TemporalType.TIMESTAMP) ), a następnie będziemy chcieli tą samą wartość wykorzystać w warunku where, może się okazać, że nie znajdziemy już takiej krotki. ORACLE najpierw dokona "promocji" wartości w kolumnie DATE do typu parametru wejściowego. W naszym przypadku wartości w kolumnie typu DATE zostaną "wypromowane" do typu TIMESTAMP, ponieważ nasz wejściowy parametr typu java.sql.Timestamp zostanie zamapowany po stronie bazy na typ TIMESTAMP, przy czym proces "promowanie" dodatkowo uzupełni milisekund zerami. Następnie dopiero zostanie wykonane porównywanie. Nawet jeśli ręcznie "wyzerujemy" milisekundy naszego wejściowego java.sql.Timestamp i w ten sposób znajdziemy żądany wiersz, to trzeba pamiętac, że w zapytaniu ORACLE nie użyje indeksu założonego na kolumnie typu DATE- indeks jest założony na typie DATE a nie TIMESTAMP. W przypadku gdy mielibyśmy włączone oracle.jdbc.V8Compatible takich problemów udałoby się uniknąć, ponieważ sterownik JDBC zamapuje nam typ java.sql.Timestamp na DATE. Więcej na ten temat możemy znaleźć tu i tu
  • "Wrong column type in {nazwa schematu.nazwa kolumny} for column {nazwa kolumny}. Found: char, expected: varchar2(255 char)". Walidacja schematu przez Hibernate(hibernate.hbm2ddl.auto=validate) wyrzuca wyjątek przy mapowaniu:

    @Column(name="name")
    private String name;
    gdy kolumna w bazie jest typu char(x) gdzie x>1.
    Problem wynika z błędu w Hibernate, który mapuje bazodanowy typ char na javowy Character.
    Gdy zmienimy mapowanie na

    @Column(name="name")
    private char name;

    wszystko jest oki, ale będziemy ograniczeni do przechowywania/odczytywania pojedyńczego znaku...
    Aby typ char(x) mapował się na String trzeba albo załadować patch opisany powyżej w zgłoszeniu, albo też wskazać konkretny typ bazdanowy na jaki będzie zamapowane nasze property.
    W tym drugim przypadku wystarczy dopisać columnDefinition

    @Column(name="name", columnDefinition="char")
    private String name;

  • Criterion uwzględniające escapowanie znaków specjalnych do wyszukiwania za pomocą like: http://levanhuy.wordpress.com/2009/02/19/providing-an-escape-sequence-for-criteria-queries/
    Trzeba jednak kod trochę zmodyfikować aby escapował znak służący jako escape character, czyli w tym przypadku trzeba escapowac znak '\'.
  • Wyszukiwanie Clob po zawartości. Posiadając mapowanie
    @Lob
    @Column(name = "CONTENT", nullable = false)
    private char[] content;
    do typu bazodanowego CLOB można wyszukiwać Cloby po zawartości. Nie chcę tutaj pisać na temat wydajności takiego rozwiązania, ale ogólnie jest to możliwe i działa. Wyszukiwanie przy pomocy like wygląda standardowo:
    List list = session.createQuery(" from Obj where content like ?").setParameter(0, "%Ala%".toCharArray()).list();

    W przypadku używanie Criteria najlepiej posłużyc się zmodyfikowaną klasą CriteriaEscape predstawioną powyżej. Dla Clob, które są zamapowane do char[] trzeba dokonać małej poprawki:
    public TypedValue[] getTypedValues(Criteria criteria,
    CriteriaQuery criteriaQuery) throws HibernateException {
    return new TypedValue[]{criteriaQuery.getTypedValue(criteria, propertyName, ("%" + value + "%").toCharArray())};
    }

    Idealnie wydaję się być usunięcie manualnej konkatenacji Stringów i użycie org.hibernate.criterion.MatchMode.toMatchString(). Nie tylko będzie to bardziej eleganckie, ale też bardziej funkcjonalne - można by przekazać implementacje org.hibernate.criterion.MatchMode w konstruktorze klasy i dzięki temu możemy łatwo sterować czy znak '%' ma być na początku/końcu czy i na początku i na końcu szukanej frazy.
    Trochę inaczej to wygląda gdy spróbujemy wyszukiwać Cloby za pomocą operatora równości. Próba wykonania kodu:
    Object object = currentSession.createQuery(" from Obj where content=?" ).setParameter(0, "ipsum".toCharArray()).setMaxResults(1).uniqueResult();

    kończy się wyjątkiem: java.sql.SQLException: ORA-00932: inconsistent datatypes: expected - got CLOB. Nie da się ukryć, że typy faktycznie nie pasują. Można się posiłkować Oraclową funkcją TO_CHAR. W Oracle 10.1.0.4.2 takie zapytanie przejdzie:

    Object object = currentSession.createQuery(" from Obj where to_char(content)=?" ).setParameter(0, "ipsum".toCharArray()).setMaxResults(1).uniqueResult();

    Oracle "po cichu" obetnie CLOB do pierwszych 4000 znaków i wtedy dokona porównania.
    Od wersji 10.2.0.1.0 jednak w takim przypadku dostaniemy błąd:
    SQL Error: ORA-22835: Buffer too small for CLOB to CHAR or BLOB to RAW conversion (actual: {wartość powyżej 4000}, maximum: 4000). Oracle tym razem już nie obetnie "po cichu" CLOB do 4000 znaków, ale można takie zachowanie na wymusić:
    Object object = currentSession.createQuery(" from Obj where to_char(substr(CONTENT,1,4000))=?" ).setParameter(0, "ipsum".toCharArray()).setMaxResults(1).uniqueResult();

  • Przy okazji udało się natrafić na parę "niedoskonałości" Hibernate, które zostały już jakiś czas temu znaleziono, ale nie poprawione

    • Zdefiniowanie Criteria na klasie, która nie jest zmapowana, nie powoduje wystąpienia żadnego wyjątku. Jest to dziwne biorąc pod uwagę, że w HQL coś takiego nie przejdzie. Problem został już dawno zgłoszony
    • Hibernate generuje nie poprawne zapytanie w przypadku projekcji definiującej alias, który jest taki sam jak nazwa property a jednocześnie zdefinujemy restrykcji na tym property. Objawia się to wyjątkiem java.sql.SQLException: ORA-00904: "Y0_": invalid identifier. Dotyczy to jedynie aliasów dla root entity. Problem juz został dawno zgłoszony, przygotowano dla niego patch, ale bezpieczniej jest prefixowanie wszystkich restrykcji na root entity za pomocą aliasu"this"
      ProjectionList projectionList = Projections.projectionList().add( Projections.id().as( "id" ) ).add( Projections
      .property( "name" ).as( "name" ) ).add( Projections.property( "surname" ).as( "surname" ) );
      List result = currentSession.createCriteria(Author.class)
      .setProjection( projectionList )
      .add( Restrictions.eq( "this.surname", "Borchardt") )
      .setResultTransformer( new AliasToBeanResultTransformer(Author.class) )
      .list();
      albo korzystanie z podlasy AliasedProjection napisanej przez Chris Federowicza i Kevin Schmidta

      ProjectionList projectionList = Projections.projectionList().add( new CustomPropertyAliasProjection("id","id"))
      .add( new CustomPropertyAliasProjection("name", "name")).add( new CustomPropertyAliasProjection("surname","surname" ));
      List result = currentSession.createCriteria(Author.class)
      .setProjection( projectionList )
      .add( Restrictions.like( "surname", "Borchardt") )
      .setResultTransformer( new AliasToBeanResultTransformer(Author.class) )
      .list();

      Problem ten nie dotyczy wszystkich baz, np HSQLDB radzi sobie bez problemu gdy warunek w where jest definiowany na aliasie a nie na kolumnie, czyli bład ten tam nie występuję. Ogólnie jest to kolejny przykład "rozjazdu" kiedy system pracuję z bazą A a na testy podpinamy bazę B.


sobota, 6 czerwca 2009

java encoding

Wpis został stworzony na podstawie prezentacji Dawida Weissa "The beauty of debugging".
  • Kodowanie plików źródłowych.
    javac zamienia kod źródłowy na bytecode rozumiany przez JVM. W ten sposób z pliku .java powstaje nam plik .class. W ramach tego procesu literały zawarte w pliku źródłowym trafiają do pliku wynikowego, a dokładnie do constant pool table i zostają zakodowane za pomocą UTF-8 (nie jest to 100% prawdą ale możemy tak przyjąć). javac domyślnie zakłada, że nasz plik źródłowy ma kodowanie zgodne z domyślnym kodowaniem platformy - czyli na moim Windows jest to cp1250. Jeśli mamy w takim razie plik źródłowy a w nim następujący kod:
    String str="żółć";

    to w przypadku gdy kodowanie tego pliku jest cp1250 (czyli zgodne z domyślnym kodowaniem platformy)to podglądając ten plik w edytorze hex, żółć to następujący łańcuch bajtów (hex): BF F3 B3 E6. Kompilacji pliku źródłowego, tworzy plik .class, w którym to łańcuch żółć zostaje zakodowany do postaci UTF-8(hex) : C5BC C3B3 C582 C487. Wszystko super, ale gdy nasz plik źródłowy zostanie zakodowany w UTF8 i skompilujemy go tak jak poprzednio, plik wynikowy będzie tym razem zawierał "bezsensowne krzaki". W pliku źródłowym żółć zostanie zakodowana w UTF8 jako:C5BC C3B3 C582 C487, ale javac domyślnie potraktuje ten plik jako zakodowany w Cp1250 i zostanie przeprowadzona konwersja literałów do UTF8. W ten sposób żółć zakodowana w UTF8 zostaje potraktowana jako ciąg bajtów w cp1250, dla którego będzie robiona konwersja do UTF8. W wyniku tego plik wynikowy zakoduje żółć jako następujący ciąg bajtów(hex):C4B9 C4BD C482 C582 C4B9 E280 9AC3 84E2 80A1. Mechanizm jest następujący: C4B9 w UTF8 jest to kod znaku, który odpowiada znakowi C5 w cp1250, C4BD w UTF8 odpowiada BC z cp120 itd. Trzeba pamiętać aby przy kompilacji wskazać kodowanie plików źródłowych: javac -encoding ... , ponieważ nie zawsze musi być/jest zgodne z kodowaniem domyślnym platformy.
  • Kodowanie znaków dla operacji I/O
    W Java typ char jest reprezentowany jako znak Unicode (Unicode code point). Nie oznacza to jednak, że java działa tylko na systemach z charset Unicode. Java domyślnie dokonuje konwersji do i z Unicode, ale ta domyślna konwersja zadziała poprawnie gdy to co chcemy skonwertować lub to, na co chcemy skonwertować jest kodowane za pomocą domyślnego kodowania znaków na naszym systemie. W Java API jest wiele fragmentów, których działanie jest uzależnione od domyślnego kodowania, np. używanie FileWriter, FileReader, wywołanie getBytes() na obiekcie typu String. Oznacza to, że używanie tych konstrukcji może zupełnie inaczej się zachowywać w zależności od systemu, na którym uruchomimy aplikacje. Następujący kod:
    String str = "żółć";
    FileWriter fileWriter = new FileWriter("plik.txt");
    fileWriter.write(str);
    fileWriter.flush();
    fileWriter.close);

    zapisuje do pliku plik.txt tekst "żółć". Uruchamiam aplikacje pod Windows i za pomocą edytora hex oglądam zawartość pliku plik.txt:AF F3 B3 E6. Domyślne kodowanie na moim komputerze to Cp1250. Można to sprawdzić odpytując system property: file.encoding. W przypadku uruchomienia aplikacji na Linux (tam file.encoding to UTF-8) plik plik.txt zawiera tekst "żółć", ale zakodowany w UTF8 czyli w edytorze hex zobaczymy następujący łańcuch bajtów: C5 BC C3 B3 C5 82 C4 87. Jak widać wynik działania zależy od platformy, na której uruchomimy aplikacje ...Przy uruchomieniu aplikacji można wymusić domyślne kodowanie znaków za pomocą -Dfile.encoding=, lub też (co wydaje się lepsze) unikać korzystania z elementów API, które wykorzystują domyślne kodowanie
W przypadku gdy będziemy chcieli "wypchnąć" znak Unicode, dla którego nie istnieje reprezentacja w kodowaniu, w którym będziemy chcieli ten znak reprezentować, to zobaczymy znak '?'.
char s = '\u01fe';
FileWriter fileWriter = new FileWriter("foo.bar");
fileWriter.write(s);
fileWriter.flush();
fileWriter.close();
System.out.println(s);

Uruchomienie powyższego kodu na moim komputerze z Windows (bez dodawania -Dfile.encoding) skutkuje utworzeniem pliku foo.bar oraz wypisaniem znaku na konsole.
Z racji tego, że znak 'Ǿ' nie jest w żaden sposób reprezentowany w moim domyślnym kodowaniu(cp1250) na konsoli oraz w pliku znajdzie się znak '?' (w hex 3f).
W przypadku gdy przy uruchomieniu aplikacji dodam -Dfile.encoding=UTF8 w pliku zobaczę poprawną reprezentację znaku w kodowaniu UTF8: C7 BE , a na konoli 2 znaczki: Çľ, czyli reprezentaja znaków o kodach odpowiednio C7 oraz BE w kodowaniu cp1250.
Więcej na ten temat tu

W przypadku gdy potrzebujemy sprawdzić dany znak, szczególnie dotyczy to znaków "wklejonych z nieznanych źródeł" najlepiej wyświetlić każdy znak jako code point oraz jego reprezentacje w wybranym kodowaniu, np UTF-8.
    
    int i = 8211;
    printRepresentation(i);  

    char c='ą'
    printRepresentation(c);  

    char c2 = '\u1FB4';
    printRepresentation(c2);  

   private void printRepresentation(int i) throws UnsupportedEncodingException {
        System.out.println((char)i + " = " +i +" " + unicodeCodePoint(i) + " '" + utf8HexRepresentation((char)i)+"'");
    }

   private String unicodeCodePoint(int i) {
        return String.format("U+%04X", i);
    }

    public static String utf8HexRepresentation(char c) throws UnsupportedEncodingException {
        byte[] bytes = (""+c).getBytes("UTF-8");
        return hexString(bytes);
    }
    private static String hexString(byte[] data) {
        StringBuffer s = new StringBuffer();
        for (int i = 0; i < data.length; i++) {
            int high_nibble = (data[i] & 0xf0) >>> 4;
            int low_nibble = (data[i] & 0x0f);
            s.append(hex_table[high_nibble]);
            s.append(hex_table[low_nibble]);
            s.append(" ");
        }
        return s.toString().trim();
    }

    private static char[] hex_table = { '0', '1', '2', '3', '4', '5', '6', '7',
            '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' };

Wyszukanie "problematycznego" znaku w pliku/plikach można zrobić za pomocą grepa:
 
grep --colour $'\xe2\x80\x93' *

Jeśli chodzi o kodowanie znaków w J2EE to warto zajrzeć tu

wtorek, 2 czerwca 2009

HamCrest 1.2

Nie dawno pojawił się Hamcrest 1.2. Bardzo mi się podoba idea pisania assercji przy wykorzystaniu
Assert.assertThat(T arg0, Matcher<T> arg1)

Pozwala to nie tylko poprawić czytelność kodu, ale także go znacznie skrócić.
Na stronie domowej projektu jest parę przykładów użycia, ale lista ta jest trochę uboga i najlepiej samemu zacząć zabawę.

Sama konfiguracja pod Eclipse IDE zapowiadała się dość banalnie: wrzucamy hamcrest-all-1.2.jar do classpath i wszystko. Jednak czekała na mnie niemiła niespodzianka i zobaczyłem:java.lang.NoSuchMethodError. Okazuję się, jednak że Junit 4.4 (ten co działa z Spring TestContext Framework) ma sam w sobie okrojonego hamcresta...
Istenieje na szczęście "goły" JUnit4.4: junit-dep-4.4.jar. W takim razie usunąłem z classpath junit-4.4.jar a na jego miejsce podgrałem junit-dep-4.4.jar.
Od tego momentu pod Eclipse wszystko działa jak należy.

Jednak próba uruchomienia, a nawet kompilacji testów z wyrażeniami hamcrest nie działa.
Problem dotyczy Java generics i pojawiał się już wcześniej.
Następujący kod kompiluje się w IDE (jdk1.5.0_12)
Assert.assertThat(4, either(equalTo(4)).or(equalTo(3)));

Jednak odpalenie javac (uruchamiane z pełnej ścieżki do jdk1.5.0_12), wywala następujący błąd:
or(org.hamcrest.Matcher) in org.hamcrest.core.CombinableMatcher<java.lang.Object> cannot be applied to (org.hamcrest.Matcher<java.lang.Integer>)
Assert.assertThat(4, either(equalTo(4)).or(equalTo(3)));

Zgłosiłem issue, ale odpowiedz jest dość standardowa ze strony twórców projektu: "flaky Java generics". W takim razie pozostaję nam jedynie znaleźć workaround i pomóc kompilatorowi...
Assert.assertThat(4, Matchers.<Integer>either(equalTo(4)).or(equalTo(3)));


No i miało być tak pięknie, ale zaczęło się sypać uruchamianie testów z mavena.
Po wywaleniu junit-4.4 i dodaniu zależności do junit4.4-dep oraz hamcrest 1.2:

junit
junit-dep
4.4
true
test


org.hamcrest
hamcrest-core




org.hamcrest
hamcrest-all
1.2
test


SureFire uruchamiał testy nie jako testy JUnit4 a jako zwykłe POJO-tests.
Niestety dość długo zajęło znalezienie przyczyny, i nie obyło się bez. Na szczęście
szybciej poszło znalezienie rozwiązania

wtorek, 12 maja 2009

Hibernate i Spring DM Server

Spring Dm Server w wersji 1.0 odpaliłem już prawie rok temu. Udało mi się odpalić aplikację web, która pod spodem korzysta z Hibernate. Nie ukrywam, że nie poszło to gładko, ale w końcu się udało. Niestety rok po tym, chciałem pobawić się jeszcze raz, tym razem pod STS 2.0, cała wiedza wyparowała i musiałem jeszcze raz się przebijac i oglądać wyjątki w stylu java.lang.NoClassDefFoundError. W końcu zdecydowałem się to spisać, aby w przyszłości zaoszczędzic sobie ponownej straty czasu.
Aplikacja składa się z następujących projektów:
  • web - Spring DM web bundle
  • domain - obiekty domeny wykorzystywane przez moduł web oraz moduł api, korzysta z hsqlbd oraz commons dbcp
  • api - definiuje interfejs- kontrakt między warstwą web a warstwą dao
  • dao - implementuję interfejs zdefiniowany przez moduł api, dostarcza implementacji usługi OSGI

  1. No PAR deployment- aplikacja składa się z luźno wrzucanych bundli.
    • konfiguracja ORM określona za pomocą annotacji JPA (AnnotationSessionFactoryBean):
      • dao
        Manifest-Version: 1.0
        Bundle-Version: 1.0.0
        Bundle-Name: HibernateTest_dao Bundle
        Bundle-ManifestVersion: 2
        Bundle-SymbolicName: foo.bar.HibernateTest_dao
        Import-Package: foo.bar.dao.api,
        foo.bar.domain,
        javax.sql;version="[0.0.0,0.0.0]"
        Import-Bundle: com.springsource.org.apache.commons.dbcp,
        com.springsource.org.hsqldb,
        com.springsource.org.hibernate;version="[3.2.6.ga,3.2.6.ga]"
        Import-Library: org.springframework.spring.instrumented
        jak się można było spodziewać, importujemy bundle Hibernate

      • domain
        Manifest-Version: 1.0
        Bundle-Version: 1.0.0
        Bundle-Name: HibernateTest_domain Bundle
        Bundle-ManifestVersion: 2
        Bundle-SymbolicName: foo.bar.HibernateTest_domain
        Export-Package: foo.bar.domain;version="1.0.0"
        Import-Bundle: com.springsource.javax.persistence;version="[1.0.0,1.0.0]",
        com.springsource.org.hibernate;version="[3.2.6.ga,3.2.6.ga]"

        pomimo, że nie używamy annotacji Hibernate (a jedynie JPA) bundle Hibernate musi zostac zaimportowany - w przeciwnym przypadku przy starcie bundle dao dostaniemy wyjątek: java.lang.NoClassDefFoundError: org/hibernate/proxy/HibernateProxy

    • konfiguracja ORM określona w plikach konfiguracyjnych xml (LocalSessionFactoryBean):
      • dao
        Manifest-Version: 1.0
        Bundle-Version: 1.0.0
        Bundle-Name: HibernateTest_dao Bundle
        Bundle-ManifestVersion: 2
        Bundle-SymbolicName: foo.bar.HibernateTest_dao
        Import-Package: foo.bar.dao.api,
        foo.bar.domain,
        javax.sql;version="[0.0.0,0.0.0]"
        Import-Bundle: com.springsource.org.apache.commons.dbcp,
        com.springsource.org.hsqldb,
        com.springsource.org.hibernate;version="[3.2.6.ga,3.2.6.ga]"
        Import-Library: org.springframework.spring.instrumented
        bez zmian
      • domain
        Manifest-Version: 1.0
        Bundle-Version: 1.0.0
        Bundle-Name: HibernateTest_domain Bundle
        Bundle-ManifestVersion: 2
        Bundle-SymbolicName: foo.bar.HibernateTest_domain
        Export-Package: foo.bar.domain;version="1.0.0"
        tym razem bundle domain nie potrzebuje żadnego importu: ani JPA ani Hibernate

    Aby moc korzystać z HQL to dla No-PAR deployment wymagane jest zaimportowanie Hibernate dla bundle web.
    Manifest-Version: 1.0
    Web-DispatcherServletUrlPatterns: *.html
    Module-Type: Web
    Bundle-Version: 1.0.0
    Bundle-Name: HibernateTest_web Bundle
    Bundle-ManifestVersion: 2
    Web-ContextPath: HibernateTest
    Bundle-SymbolicName: foo.bar.HibernateTest_web
    Import-Package: foo.bar.dao.api,
    foo.bar.domain;version="[1.0.0,1.0.0]"
    Import-Bundle: com.springsource.org.apache.taglibs.standard;version="[1.1.2,1.1.2]",
    com.springsource.org.hibernate;version="[3.2.6.ga,3.2.6.ga]"
    Import-Library: org.springframework.spring.instrumented

    W innym przypadku, przy próbie wykonania zapytania HQL zobaczymy: org.hibernate.QueryException: ClassNotFoundException: org.hibernate.hql.ast.HqlToken. Hibernate ładuję cześć klas za pomocą Thread context classloadera, czyli w naszym przypadku za pomocą ClassLoadera, który załadował moduł web. Najciekawsze jest to, że zapytania korzystające z Criteria API, operacje get/load działają bez tego importu. Jest on jedna wymagany do odpalania zapytań w HQL.

  2. PAR deployment - nasze bundle zostają zapakowane w archiwum PAR
    • konfiguracja ORM określona za pomocą annotacji JPA (AnnotationSessionFactoryBean):
      • dao
        Manifest-Version: 1.0
        Bundle-Version: 1.0.0
        Bundle-Name: HibernateTest_dao Bundle
        Bundle-ManifestVersion: 2
        Bundle-SymbolicName: foo.bar.HibernateTest_dao
        Import-Package: foo.bar.dao.api,
        foo.bar.domain,
        javax.sql;version="[0.0.0,0.0.0]"
        Import-Bundle: com.springsource.org.apache.commons.dbcp,
        com.springsource.org.hsqldb,
        com.springsource.org.hibernate;version="[3.2.6.ga,3.2.6.ga]";import-scope:=application
        Import-Library: org.springframework.spring.instrumented
        tym razem importujemy bundle Hibernate z dodatkowym atrybutem import-scope:=application, który jest pewnym rozszerzeniem Spring DM Server

      • domain
        Manifest-Version: 1.0
        Bundle-Version: 1.0.0
        Bundle-Name: HibernateTest_domain Bundle
        Bundle-ManifestVersion: 2
        Bundle-SymbolicName: foo.bar.HibernateTest_domain
        Export-Package: foo.bar.domain;version="1.0.0"
        Import-Bundle: com.springsource.javax.persistence;version="[1.0.0,1.0.0]"

        używamy jedynie annotacji JPA, nie musimy importować bundle Hibernate

    • konfiguracja ORM określona w plikach konfiguracyjnych xml (LocalSessionFactoryBean):
      • dao
        Manifest-Version: 1.0
        Bundle-Version: 1.0.0
        Bundle-Name: HibernateTest_dao Bundle
        Bundle-ManifestVersion: 2
        Bundle-SymbolicName: foo.bar.HibernateTest_dao
        Import-Package: foo.bar.dao.api,
        foo.bar.domain,
        javax.sql;version="[0.0.0,0.0.0]"
        Import-Bundle: com.springsource.org.apache.commons.dbcp,
        com.springsource.org.hsqldb,
        com.springsource.org.hibernate;version="[3.2.6.ga,3.2.6.ga]";import-scope:=application
        Import-Library: org.springframework.spring.instrumented
        bez zmian
      • domain

        Manifest-Version: 1.0
        Bundle-Version: 1.0.0
        Bundle-Name: HibernateTest_domain Bundle
        Bundle-ManifestVersion: 2
        Bundle-SymbolicName: foo.bar.HibernateTest_domain
        Export-Package: foo.bar.domain;version="1.0.0"
        skoro tym razem nie używamy JPA to nie potrzebujemy importu bundle JPA

    Tym razem do bundle web nie musimy importować bundle Hibernate aby móc korzystać z HQL

    Manifest-Version: 1.0
    Web-DispatcherServletUrlPatterns: *.html
    Module-Type: Web
    Bundle-Version: 1.0.0
    Bundle-Name: HibernateTest_web Bundle
    Bundle-ManifestVersion: 2
    Web-ContextPath: HibernateTest
    Bundle-SymbolicName: foo.bar.HibernateTest_web
    Import-Package: foo.bar.dao.api,
    foo.bar.domain;version="[1.0.0,1.0.0]"
    Import-Bundle: com.springsource.org.apache.taglibs.standard;version="[1.1.2,1.1.2]"
    Import-Library: org.springframework.spring.instrumented

    Dzięki importowaniu bundle Hibernate z atrybutem import-scope:=application nie ma potrzeby importowania Hibernate ani w bundle web ani w bundle domain. Po podłączeniu się za pomocą telnet do konsoli Equinox możemy wyświetlić nagłówki bundli wchodzące w skład naszego PAR, w ten sposób zobaczymy że, każdy z nich "automagicznie" importuje pakiety Hibernate.

GeeCon dzień 2

Java FX For Developers Not Designers
Poprzedniego wieczora staraliśmy się trochę zintegrować i chyba dlatego prezentacja Adam Biena nie zbyt zapadła mi w pamięć. Z tego co pamiętam to zaczęło się od omówień zmian w JRE: JRE kernel, lepsza komunikacja na linii JavaScript-Applet, możliwość wyspecyfikowania per-applet wymagań co do pamięci, specyfikowanie per-applet wymagań co do wersji JRE. Następnie Adam przeszedł do JavaFX. Było to mniej więcej wszystko to samo co na pozostałych prezentacjach na ten temat. Jedyne co mnie "obudziło" to pisanie testów JUnit w JavaFX. Pojawiło się pytanie czy można to robić w stylu JUnit 4.X, czyli za pomocą annotacji. Odpowiedz było prosta: można użyć JUnit w wersji 4.X, ale nie można korzystać z annotacji, ponieważ JavaFX nie wspiera annotacji!.

RESTful Web Services with Spring 3.0
Znużony trochę prezentacją na temat JavaFX na drugie pół godziny przeniosłem się do sali obok aby posłuchać Arjena Poutsmy. Było to praktycznie to samo co Arjen zaprezentował jakiś czas temu w Warszawie. Arjen oprócz tego, starał się wytłumaczyć czym Spring Framework REST support różni się od implementacji JAX-RS: ten pierwszy dotyczy interfejsu human to machine a ten drugi machine to machine. Fuknkcjonalności w stylu: filtr, który obchodzi ograniczenie przeglądarki do wysyłania jesdynie żądan POST i GET, używanie file extensions w celu określenia żądanej reprezentacji (kolejne obejście przeglądarki) mają ułatwić tworzenie aplikacji zgodnych z paradygmatem REST. Jednak jak to później Arjen w rozmowie uściślił @MVC/@REST ma duże zastosowanie dla web development, a JAX-RS dla RESTful services, czyli ma stanowić konkurencje dla SOAP WebServices. Arjen wspomniał także o RestTemplate czyli o REST po stronie klienta. Na koniec pojawiło się pytanie związane z security w świecie REST. Po stronie serwera nic się nie zmienia i standardowo używać form-based authentication oraz SpringSecurity(jest w stanie sprawdzać prawo do wywołania zasobu także na podstawie użytej metody HTTP), a dla RestTemplate używać basic/digest authentication i przesyłać nasze credentials przy każdym żądaniu.

Glassfish
Dodatkowa, nieplanowana prezentacja przeprowadzona przez Antonio Goncalves na temat GlassFish. Antonio zaprezentował jak zainstalować, skonfigurować i używać GlassFish. Wyglądało to jako typowy step-by-step turtorial - od pobierania instalki poprzez uruchamiania serwera i jego konfiguracje każdy etap został zaprezentowany z uprzednio przygotowanych filmików flashowych- nic nie miało prawo nie działać :). Filmiki zostały stworzone przez Antonio na podstawie kroków opisanych w GlassFish Quick Start Guides.
Serwer można zainstalować w 3 trybach: "development", "cluster", ""enterprise". Nie obyło się bez pokazania 2 alternatywnych interfejsów dostępu do serwera: Admin Console i asadmin oraz narzędzie (stworzone w swingu) do instalowania/aktualizowania komponentów samego serwera Update Center. Bardzo mi się spodobało, że Antionio miał przygotowane slajdy omawiające architekturę samego serwera i na tej podstawie wyjaśniał poszczególne pojęcia: domain, administration server, node agent, cental repository, cluster, server instance. Na końcu jeszcze krótka informacja na temat Glassfish V3 , jako referencyjnej implementacji JEE6, z modularną architekturą opartą na OSGI. Rozmawiając na temat OSGI w kontekście V3 z Antionio, usłyszałem, że jak na razie nie ma żadnych planów aby serwer mógł hostować aplikację użytkownika oparte na OSGI.

What's new in Java EE 6
Antonio Goncalves tym razem opowiadał na temat JEE6. Same wydanie specyfikacji jest dość mocno opóźnione z tym co pierwotnie zakładano, a dodatkowo wokół paru specyfikacji zrobiło się dość dużo szumu i wiele emocji :). W JEE 6 pewne specyfikacje zostały "wycięte" m.in.: JAX-RPC, EJB 2.1 entity beans, JSR 88, JAXR. Wprowadzono w końcu pojęcie profili, czyli JEE ma przestać być jednym wielkim monolitycznym kolosem w stylu one size fits all. Aktualnie w przygotowaniu są 3 profile, przy czym jakie specyfikacje będą należeć do jakiego profilu jeszcze nie jest w 100 % pewne.
JEE6 wprowadza wiele zmian i to, w każdej warstwie JEE:
  • Servlets 3.0 (annotacje, fragments, możliwośc rejestrowania/odrejestrowywania servletów, przetwarzanie asynchroniczne), JSF 2.0, AJAX support, JSP jako technologia niszowa
  • JAX-WS 2.2, JAX-RS(nie wspiera client-side REST)
  • EJB 3.1(optional local intefaces), @Singleton (per application per JVM), @ConcurrencyManagement, EJB Lite, EJB asynchronous methods ( zwracją null lub Future, domyślnie dla nich Propagation.Requires_NEW), możliwość pakowania EJB w WAR, globalne ( przenośne) nazwy JNDI, Timer Services API( prawie jak Cron), Embeddable EJB Container
  • JPA 2.0 (m.in. mapowanie kolekcji komponentów, rozbudowa JPQL, dodatnie Criteria API, nowe mechanizmy lockowania)
  • Web Beans przemianowane na JDCI
Tak przygotowana specyfikacja ma nawet umożliwiać tworzenie aplikacji batchowych:
Batch processing = EJB Lite + Timer + Asynch calls + Embeddable Container


Automation of functional tests
Tomasza Kaczanowskiego w swojej prezentacji przedstawił nam idee automatyzacji testów funkcjonalnych. W przypadku testów funkcjonalnych pojawia się zawsze problem związany z przygotowaniem środowiska uruchomieniowego dla testów, które powinno jak najbardziej przypominać środowisko produkcyjne. Przygotowanie takiego środowiska oraz mechanizmu automatyzacji testów funkcjonalnych nie zawsze jest łatwe/możliwe do osiągnięcia i często trzeba do tego podejśc pragmatycznie. Tomasz na podstawie własnych doświadczeń przedstawił w jaki sposób przeprowadził automatyczne funkcjonalne testowanie 2 odmiennych systemów: klasyczny system JEE z interfejsem web, system oparty na OSGI z interfejsem SOAP WebSerices. Przygotowanie środowiska dla testów funkcjonalnych zostało rozbite na poszczególne fazy:
  • stworzenie bazy danych -przykładowe narzędzia: hibernate3-maven-plugin, SQL-maven-plugin
  • załadowanie danych - użycie DBUnit (podobno DBUnit ma problemy z ładowaniem duzej ilości kodu napisanego w PL/SQL), SQL-maven-plugin
  • wrzucenia aplikacji - cargo-maven-plugin, capistarano ?
  • uruchomienie testów - selenium-maven-plugin, soapui-maven-plugin
Tomasz ostrzegał, że w/w narzędzia bardzo pomogły mu osiągnąc zamierzony efekt, ale na pewno nie są one "silver bullet" i nie koniecznie muszą działac w innych środowiskach.

SOLID design principles
Super zabawna prezentacja przeprowadzona w typowym włoskim stylu. Prezentacja bazuje na
Object Oriented Design principles zdefiniowanych przez wujka Boba czyli Boba Martina.
Bruno Bussola zaczął od omówienia czemu powinno nam zależeć na posiadaniu "good design" oraz jakie są symptomy, że coś jest nie tak z naszym designem:
  • rigidity - wprowadzenie zmiany jest nieprzewidywalne(bardzo trudne/nie możliwe do oszacowania pod względem czasu i kosztu). Nawet mała zmiana powoduje, że musimy zmieniać coraz to kolejne elementy (change marathon)
  • fragility - wprowadzenie zmiany w jednym elemencie powoduje, że zaczyna "się sypać" w innym teoretycznie nie powiązanym fragmencie
  • immobility - poszczególne elementy są ze sobą tak powiązane, że nie ma szans aby "wyjąc" pojedyńczy element i re-użyć go.
  • viscosity - przy wprowadzeniu zmiany o wiele łatwiej jest napisać "hack"niż zrobić to poprawnie - zgodnie z designem
Jak dobrze wiemy dobry design powinien się cechować przede wszystkim 2 rzeczami: low coupling, high cohession. Oczywiście łatwiej o tym mówić niż zrobić, ale wujek Bob przygotował nam zbiór zasad aby osiągnąć zamierzony cel. SOLID jest to akronim przy czym każda z liter jest pierwszą literą kolejnych akronimów:
  • Single Responsibility Principle - klasa powinna odpowiadać za tylko 1 aspekt(posiadać pojedynczą odpowiedzialność), czyli powinien być tylko pojedynczy powód dla którego mielibyśmy wprowadzać zmiany w klasie. Przykładowo, klasa Employ nie powinna jednocześnie posiadać metod związanych z obliczaniem pensji, persystencją i generowaniem raportu na podstawie danych użytkownika, a interfejs Modem nie powinien posiadać metod związanych z obsługa komunikacji(stop/start) i obsługą komunikatów. Dośc łatwo jest znaleźć klasy, które łamią te zasady: są to zazwyczaj te, które pełnią role facady: *Controller, *Manager
  • Open-Closed Principle: powinniśmy móc dodawać nową funkcjonalność do naszego kodu bez dokonywania zmiany w istniejącym kodzie. W Javie stosunkowo łatwo się implementuję tę zasadę za pomocą polimorfizmu. Oczywiście trzeba starać się/umieć przewidzieć jakie funkcjonalności mogą/mają szansę się zmienić...
  • Liskov Substitution Principle - zasada zastępowania, czyli możliwość przekazania zamiast obiektu danej klasy, obiekt podklasy bez zmieniania żadnych oczekiwanych zachowań. Wszystko to bardzo ładnie przedstawia przykład kwadrat i prostokąt. Czy kwadrat jest prostokątem? Z matematycznego punktu widzenia tak, ale niestety takie myślenie nie jest zgodne z tą zasadą. Prostokąt ma szerokość i wysokość, przy czym obie te wartości są niezależne - możemy zmienić jeden z wymiarów bez zmieniania drugiego z nich. Kwadrat ma tylko 1 wymiar - szerokość, ale skoro widzimy go jako prostokąt to możemy zmieniać jego wysokość i szerokość. Zmiana jakiejkolwiek z tych wartości wpływa bezpośrednio na drugą, czyli łamie kontrakt określony przez prostokąt.
  • Interface Segregation Principle - zasada określająca, że powinniśmy używać interfejsów do definiowania zależności pomiędzy 2 elementami kodu. Jeśli klasa wystawia kilka "grup powiązanych metod", w szczególności gdy każda z tych grup jest wykorzystywana przez innego klienta, zależnośći takie powinno się przedstawić w postaci pojedynczego interfejsu dla każdej z "grup metod"(pojedynczy klient zależy od pojedynczego interefejsu, który enkaspuluje powiązane ze soba metody, a nie od interfejsu zawierającego wszystkie możliwe metody)
  • Dependecy Inversion Principle - Moduły wysokopoziomowe nie powinny zależeć od modułów niskopoziomowych. Obie grupy modułów powinny zależeć od abstrakcji. Abstrakcje nie powinny zależeć od szczegółowych rozwiązań. To szczegółowe rozwiązania powinny zależeć od abstrakcji.

Developing RESTful Web services with Jersey
Prezentacja Jakuba Podlesaka na temat JAX-RS, a w szczególności projektu Jersey. Zaczęło się standardowo od tego czym jest REST: resources, uniform service interface, multiple representations, hypermedia exhchange (boot URI paradigm), stateless. Jersey będący RI JAX-RS może zostać uruchomiony w Java Servlets Container, embedded HTTP Server, JAX-WS Provider. Jesrsey (nie wiem czy jest to w specyfikacji JAX-RS) jest w stanie wygenerować nam WADL, dokument opisujący nasze usługi oraz dodatkowo wprowadza @Consume. Annotacja ta pozwala na bindowanie ciała żądania HTTP do obiektu. Z tego co wiem to jest to coś nad czym aktualnie pracuje Arejn Poustsma w ramach Spring @Rest.
Jeresey out-of-box wspiera JAXB, czyli marshalling/unmarshalling w formatach XML oraz JSON jest zapewniony. Content-negotiation jest określany na podstawie nagłówka Accept oraz rozszerzenia zasobu. Aby wysyłać z poziomu przeglądarki żądania HTTP: PUT i DELETE używany był plugin do firefox - Poster.
Pomimo tego, że JAX-RS dotyczy jedynie warstwy serwerowej Jersej posiada także API dla klientów usług REST.

sobota, 9 maja 2009

GeeCon dzień 1

GeeCon okazał się być największą i najlepszą polską konferencją Javowa na jakiej dotychczas byłem.
JavaFX: The Platform for Rich Internet Applications
Keynote Simona Rittera na temat Java FX. Niezbyt interesuję się technologiami związanymi z UI oraz widziałem Java FX w akcji podczas Devoxx i chyba dlatego nie do końca dałem się wciągnąć w prezentacje... Podstawowa informacja to, że Java FX jest językiem skryptowym działającym na JRE , pod spodem korzysta z dobrodziejstwa Javy i służy jako język do tworzenia UI uruchamianych w różnych środowiskach: przeglądarka, desktop, urządzenia mobilne, oraz inne wynalazki w stylu TV set top box, konsole gier i inne takie.
Siłą tej technologii ma być prostota, czyli szybkość i łatwość tworzenia UI, które mają działać w wielu środowiskach, czyli 1 język do tworzenia GUI na wielu platformach - cross screen functionality.
Ze względu na to, że przez jakiś czas pracowałem jako J2me developer bardziej zaintersowało mnie wykorzystanie Fx w świecie telefonów komórkowych. Jestem ciekaw czy jest to technologia, która poradzi sobie z problemem fragmentation w świecie mobile. Jak na razie podobno nie ma urządzenia które wspierałoby JAVA FX out of box, ale Sun ma rozmawiać na ten temat z vendorami. Teoretycznie do uruchamianie aplikacji FX na telefonie potrzebna będzie końcówka z obsługą MIDP 2.0 + parę dodatkowych jarów. FX wprowadza profile API, czyli dodatkowe API związane z renderowaniem UI oraz innymi możliwościami (np. PIM oraz obsługa SMS dla Mobile profile) związanymi z platformą uruchomieniową.
W świecie UI (i co za tym idzie RIA) szczególnie ważne jest tworzenie "wodotrysków" i tu Java FX może się na prawdę wykazać. Obsługa audio, video, animacji, rich text jest na prawdę bardzo łatwa, a dodatkowo są też pluginy, które umożliwiają integracje projektów tworzonych w PhotoShop i innych dziwactwach do wykorzystania ich w Java FX. Jako, że aplikacja oprócz UI potrzebuje też trochę logiki, FX posiada wsparcie do komunikacji przez WebServices - chyba chodzi o klasyczne SOAP WebServices (z tego co słyszałem to także Asynchronous Web Services) oraz Restful WebServices.

Collaboration, data synchronization and offline capabilities for Rich Internet Applications
Bardzo przyjemna prezentacja na temat Flex. Cornelaiu po omówieniu ogólnej architektury skupił się na zaprezentowaniu 2 aspektów: data synchronization oraz offline. Dodatkowo zaprezentował tour de flex czyli zestaw przykładów-aplikacji dzięki którym można zobaczyć co Flex potrafi. Nie chodzi jedynie o UI, ale także o pewne feature, które jest bardzo ciężko zaimplementować korzystając z "klasycznych technologii webowych". Pokazano typowe przykłady, w których to z poziomu paru okien przeglądarki widać te same dane, a zmiany robione przez użytkownika są jednocześnie propagowane do okien pozostałych użytkowników. W przypadku konfliktów czyli, np. 2 userów jednocześnie robi zmianę tego samego wiersza Flex wykrywa taką sytuację i pozwala nam coś takiego obsłużyć. Z tego co się orientuje jeśli model który oglądamy w UI jest zbindowany do Hibernate to Flex potrafi podpiąc się pod mechanizm optimistic locking, a w innym przypadku jest to zaimplementowane poprzez cachowanie danych zanim zostaną dostarczone klientowi. Dodatkowo mechanizm Data Management umożliwia korzystanie z Hibernate po stronie klienta, tzn. obiekty w ActionScript są zbindowane do obiektów hibernatowych i ich modyfikacja, ale także odczyt asocjacji (lazy associations) jest propagowany na serwer. Dodatkowo Flex obsługuje comet przy czym ma możliwość "dostosowania" się do działającej infrastruktury: próbuje otworzyc dedykowany socket do serwera, a jeśli to się nie uda to wykorzystuję "klasyczne mechanizmy": Long Pooling , Http Streaming. Kolejny aspektem była praca offline. Jeśli nasz klient straci łącznośc z serwerem, możemy cały czas działać - oczywiście do momentu kiedy nie staniemy się znów online, nie widzimy zmian na serewreze ,ale możemy wykonywać pewne operacje i zapisywać je w lokalnym storage . W przypadku aplikacji nie jest uruchamiana w przeglądarce (aplikacja dektopowa) Flex umożliwia nam korzystanie z wbudowanego SQL Lite. Dla Flexa uruchamianego w przeglądarce ten storage jest bardziej ubogi. Został zaprezentowany przykład, w którym to napisany przez nas listener obsługuję logike związana z zapisywaniem i "wypychaniem" danych na serwer jeśli odpowiednio przejdziemy z online od offline i offline do online. Podobno wszystkie przykłady które zostaly zaprezentowane są dostępne na blogu

Liberate your enterprise Java applications from the Operating System (or: about the JVM-level virtualization)
Chyba najciekawsza (przynajmniej dla mnie) prezentacja dnia. Waldek zaczął od omówienie czym jest wirtualizacja i jakie są korzyści z jej stosowania(przykładowo: "be green", lepsze wykorzystanie maszyn, HA, management). Następnie przedstawił 2 ogólne rodzaje infrastruktury wirtualizacji: Host(OS-based) oraz Hypervisor(bare-metal) . W pierwszym przypadku, w istniejącym systemie operacyjnym instalujemy oprogramowanie, które umożliwia nam uruchamianie maszyn wirtualnych. Jest to architektura, w której pracuje VmWare Workstation, VmWare Server czy VirtualBox. W przypadku Hypervisor, oprogramowanie umożliwiające uruchamianie maszyn wirtualnych jest instalowane bezpośrednio na hardware, czyli pomijamy system operacyjny. Tak działa Oravle VM, Vmware ESX/ESXi. Architektura bare-metal ma nie tylko być wydajniejsza , ale także izoluje nas od stabilności(tzn. braku stabilności) systemów operacyjnych, na których instalowane jest oprogramowanie infrastruktury wirtualizacji. Następnie Waldek przedstawił JRockitVE. Produkt najprościej można opisać jako JRockitVE = JRockitVM + "thin kind-of OS layer". Dzięki JRockitVE mamy możliwość bezpośredniego uruchamiania aplikacji Javowych na maszynie wirtualnej bez potrzeby instalowania na niej systemu operacyjnego.
JVM do swojej pracy(szczególnie jeśli mamy od czynienia z serwerem aplikacyjnym) potrzebuje jedynie pewnych funkcjonalności systemu operacyjnego, np. obsługa urządzeń, system I/O.
Dlatego Jrockit VE dostarcza pewną cienką warstwę, która ma emulować te braki. Waldek podkreślał, że ta warstwa powstała od zera (podejście bottom-up) a nie w wyniku usuwanie zbędnych fragmentów jakiegoś istniejącego OS i oczywiście nie wspiera pewnych funkcjonalności: gui, sound, jni. Z punktu widzenia Javy EE braki te mają być niezauważalne. Taka dostarczana przez Jrockit VE OS jest jedno-procesowy (nie ma możliwości uruchomienia aplikacji/serwera aplikacyjnego oraz bazy danych w ramach jednej maszyny wirtualnej chyba, że baza będzie sama w sobie uruchamiania w JVM) , jedno-użytkownikowy oraz jest w stanie korzystać z funkcji znajdującego się pod spodem hypervisora (paravirtualization). Dodatkowo do tak uruchomionej maszyny wirtualnej możemy się podłączyć za pomocą SSH, jest system plików, jest też jakiś ograniczony shell.
Uruchamianie aplikacji bezpośrednio JRockit VE pozwala nam pozbyć się OS i związanego z nim narzutu: OS nie musi zajmować storage, nie zajmuje RAM, nie ma przejść pomiędzy privileged i user mode.
Na koniec najfajniejsze były przykłady, w których to Waldek za pomocą dostarczanego z JRockitVE narzędzia tworzył z aplikacji Javowej i pliku konfiguracyjnego obraz maszyny wirtualnej i wrzucał go do VmWare Workstation. Na potrzeby prezentacji nie było to środowisko 1:1 z tym co opisywał wcześniej, ale i tak robiło to duże wrażenie..

Making EJB 3.1 development a breeze with Apache OpenEJB
Pierwotnie zająłem miejsce w sali obok i chciałem posłuchać o Scala, jednak gdy na jednym z początkowych slajdów zobaczyłem jakieś krzaki, tzn. jakieś wyrażenie w języku funkcyjnym to czym prędzej uciekłem do sali obok. Przypomniały mi się od razu wyrażenia λ (lambda), które starałem się jakoś pojąć na studiach i jakoś zawsze szło mi to opornie.
W 2 sali Jacek Laskowski zaprezentował OpenEJB jako kontener EJB 3.1. Byłem już na kilku prezentacjach, które omawiały JEE 6 i nie były to dla mnie zbytnie nowości. Z 2 strony cały czas mnie ciekawi co Jacek miał na myśli gdy mówił o integracji OpenEJB ze SpringFramework...

Practical Groovy
Prezentacja na temat Groovy przeprowadzona przez pracownika JetBrains, czyli wszystkie przykłady tworzone i uruchamiane w IntelliJ IDEA. Prezentacja dość typowa dla tych omawiających Groovy: zaczyna się od pokazania ile trzeba "naklepać" w Javie oraz jak szybko da się to przerobić na Groovy (za pomocą narzędzia i/lub ręcznie) aby finalnie zobaczyć zdecydowanie mniejszą ilość kodu z w pełni zachowaną funkcjonalności. Wszystko to wygląda bardzo ładnie, ale mnie przede wszystkim przeraża brak silnej kontroli typów w Groovy, co zmusza developerów do pisania jeszcze większej liczby testów aby wyłapać takie bugi. Podobno można dodatkowo się w tym aspekcie wesprzeć narzędziami do analizy kodu. Osobiście spodobała mi się idea pisania testów jednostkowych dla kodu w Javie w Groovym. Wyglądało to na prawdę całkiem zwięźle i prosto. Oczywiście nie obyło się od prezentacji pokazującej modyfikowanie kodu w w runtime co dla Javowca jest zawsze dużym zaskoczeniem. Groovy podobno bardzo ładnie nadaje się do tworzenie skryptów Anta, budowania konfiguracji Springowych oraz można go wykorzystywać do tworzenia różnej maści DSL. Dodawanie "w locie" metod do klas, używanie clousures i paru jeszcze innych wynalazków, których nazw nie zapamiętałem na pewno wpłynie pozytywnie na produktywność . Jeszcze tylko nie wiem czy taki kod nie okaże się bardzo "fragile".

Building Flex applications with Java Google App Engine backends
Kolejna prezentacja z Flex w temacie. Zaczęło się niestety od problemów technicznych z projektorem i niestety większość prezentacji była wyświetlana w wściekle różowych kolorach. Z całej prezentacji nie interesował mnie Flex, a Google App Engine. Google dotychczas na swoim App Engine umożliwiało wrzucanie aplikacji napisanych w Python, od niedawna jednak można hostować tam aplikacje Javowe. Z tego co się orientuję to chodzi głównie o aplikacje webowe oparte na Java Servlets(z tego co pamiętam to Servlets 2.4) oraz JSP. Dodatkowo można korzystać z dostępnych usług: storage za dostępem za pomocą JDO/JPA, dostęp do Memcache, Java Mail, Authentication/Authorization (na podstawie google accounts). Google dostarcza Google App Engine Java SDK, który może zostać zainstalowane standalone lub jako plugin do Eclipse.W ten sposób łatwo możemy tworzyć, budować, testować (za pomocą lokalnej instancji Jetty) i uploadowac aplikacje na Google App Engine. Podobno nie ma problemu aby uruchomić w taki sposób aplikacje napisaną w Groovy lub za korzystającą z Spring Framework. Podobno jest także wersja BlazeDS, która ma działać na GAE. Google dostarcza jednocześnie model biznesowy(czytaj pricing) dla swojego produktu, ale podobno całkiem dużo można się pobawić za free. O zabawach z flexem na GAE można znaleźc tu