12 stycznia 2010

Każdemu będzie w końcu dane - wreszcie Java EE 5 w projektach!

Nowy rok i nowe projekty, a tak się złożyło, że część z nich nawet rozwojowa - dosłownie i w przenośni. Znalazłem się więc w projekcie, w którym tworzone jest oprogramowanie do synchronizacji danych między bazami danych (tym samym kłaniam się nisko uczestnikom - klimat "Piratów z Karaibów", których oglądałem przez ostatnie 3 dni się udziela :]).

Architektura obejmuje WASv7 z DB2 (centrala) oraz WASCE z DB2 Express (satelitarne, lokalne instalacje). Nareszcie projekt w IBMie, gdzie serwer aplikacyjny wspiera najnowszą specyfikację Java EE 5 (może niekoniecznie najnowszą, bo na dniach wyszła nowiuteńka wersja Java EE 6, ale jej wsparcie nie jest jeszcze na tyle rozpowszechniona, aby nawet próbować się z nią w ramach prac domowych). Przyznaję, że trochę dziwnie mi się pisze oprogramowanie w Java EE 5 z udziałem IBM WebSphere. Jakoś przyzwyczaiłem się do starszych specyfikacji, których unikałem, jak tylko mogłem i większość pracy sprowadzałem do odpowiednich funkcjonalności w RADzie. Udało się i teraz zostało mi to wynagrodzone - wreszcie można oddychać pełną piersią, a jeśli dodać do tego, że teraz wszystkie zabawki IBMa z rodziny WebSphere działają na WASv7 - serwerze aplikacyjnym zgodnym z Java EE 5 i działającym na Java SE 6 - to możecie sobie wyobrazić, jak bardzom z tego rad. Do niedawna jeszcze o Java EE mogłem sobie pomarzyć w cieple domowego kominka, a w pracy...brrr...lepiej nie wspominać, wciąż J2EE. Teraz się odmieniło, bo kiedy się bardzo chce, to się kiedyś ziści. Wiedza, którą zdobywałem w ostatnim roku teraz się przydaje...produkcyjnie. Wreszcie!

Wracając do mojego bieżącego projektu, miałem dzisiaj przygodę z synchronizacją realizowaną przez...stronę JSP. Wszystko było wykonywane z jej poziomu i jej autor zdawał się mieć tego całego programowania w Javie najwyraźniej dosyć (nieprawdaż Paweł?). Szkoda było chłopaka, bo zacny z niego gość, ale brakowało mu wiedzy, ciut więcej i takiej ukierunkowanej na mocne strony Java EE. A skoro już mają Java EE, to nie byłoby dobrze nie skorzystać z dobrodziejstw. Zadanie na rozgrzewkę w sam raz dla mnie - można błysnąć i to przy niewielkim nakładzie pracy. To lubię! ;-)

Przed moimi oczyma jawi się strona JSP, a w niej jeden wielki try/catch z dostępem do bazki, sprawdzeniem tego i owego, aby ostatecznie zbudować odpowiedź XMLową. Dla mnie naturalnym było zaciągnąć @WebService i @Resource do pracy, aby z niecnego JSP zrobić całkiem przyzwoitą klaskę, powtórzę, zwykłą klaskę z kilkoma "wskazaniami" dla serwera, aby traktował ją specjalnie.

Wystarczyło więc, uruchomić NetBeans IDE 6.8.1, zaciągnąć aktualne źródła aplikacji webowej z repozytorium CVS (brrr, że też wciąż tego ustrojstwa się używa) i dodać tę oto klaskę (niektóre elementy usunięte dla poprawienia czytelności):
package pl.tufirma;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.text.MessageFormat;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.Resource;
import javax.jws.WebParam;
import javax.jws.WebService;
import javax.sql.DataSource;

@WebService
public class Synchronizator {

private final String AUTH_SQL = "SELECT ID FROM AUTHORIZE WHERE AUTH = ?";

@Resource(name = "jdbc/mak")
DataSource makDS;

public String synchronizacjaPoczatkowa(@WebParam(name = "key") String klucz) {
return synchronizeFirst(klucz);
}

protected String synchronizeFirst(String key) {
try {
String id_authorize = pobierzIdAuthorize(key);
MessageFormat SQLQUERY = new MessageFormat("WITH ...");

StringBuffer queryParam = new StringBuffer(id_authorize);
String query = SQLQUERY.format(new Object[]{queryParam.toString()});

System.out.println("SQLQUERY: " + query);
Connection conn = makDS.getConnection();
PreparedStatement stmt = conn.prepareStatement(query);
ResultSet rs = stmt.executeQuery();
String XMLResult = "";
if (rs.next()) {
XMLResult = rs.getString(1);
}
return XMLResult;
} catch (SQLException ex) {
Logger.getLogger(Synchronizator.class.getName()).log(Level.SEVERE, null, ex);
throw new IllegalStateException("Problemy z bazka", ex);
}
}

protected String pobierzIdAuthorize(String key) {
try {
Connection conn = makDS.getConnection();
PreparedStatement authStmt = conn.prepareStatement(AUTH_SQL);
authStmt.setString(1, key);
ResultSet authRs = authStmt.executeQuery();
if (!authRs.next()) {
throw new IllegalArgumentException("NIe odnaleziono klucza dla " + key);
}
return authRs.getString(1);
} catch (SQLException ex) {
Logger.getLogger(Synchronizator.class.getName()).log(Level.SEVERE, null, ex);
throw new IllegalArgumentException("NIe odnaleziono klucza dla " + key, ex);
}
}
}
Jak widać wiele jeszcze do zrobienia, ale sam szkielet klasy już jest odpowiednio dopasowany. Propozycje dalszych zmian mile widziane. Mamy wystawioną usługę jako usługę sieciową za pomocą @WebService, a dostęp do bazy danych kontrolowany jest przez serwer aplikacyjny i dostępny klasie przez @Resource. Czyż nie ładniej, chciałoby się zapytać? Teraz pozostaje obsłużyć właściwe tworzenie pliku XML (pewnie JAXB, ale nie będę ukrywał, że aż mnie rwie do Groovy), klient po stronie bazy lokalnej, który czyta odpowiedź od usługi, zapis do lokalnej bazy danych i powinno być całkiem cacy. Terapia szokowa w postaci Groovy i Grails czeka w kolejce (jeszcze im nic nie mówiłem, nawet nie wspominałem, więc prośba o dochowanie tajemnicy, zgoda?).

Jeśli są zainteresowani, aby dowiedzieć się więcej o szczegółach działania tej klasy, wystarczy się odezwać w komentarzu, np. tak: "Wyjaśnień potrzebowałbym więcej...". Chętni? Artykuł czy skrinkast*?

Ach i przy okazji tych doświadczeń z adnotacjami Java EE 5, sprawdziłem jeszcze jedno połączenie, tym razem NetBeans IDE z Eclipse. Sam projekt tworzony był w Eclipse IDE i tak został zapisany w repozytorium. Ja wystartowałem z NetBeans, więc konieczny był import, który sądziłem początkowo, że trochę pozamienia w projekcie. Okazało się, że nie tylko nie pozamieniał, ale moja zmiana była widoczna w Eclipse bez jakichkolwiek specjalnych kroków poza cvs update. Nie było mi dane ukryć mojego zdumienia, że tak łatwo poszło. Miałem przez moment stracha, że może się nie udać, ale, jak to mówią, diabeł ma wielkie oczy.

[*] Z tym "skrinkast" to przegiąłem, co?

19 komentarzy:

  1. Zastanawia mnie brak w try-catch obecności connection.close() - przecież bez zamykania połączeń, czyli de facto zwracania ich do puli, bardzo szybko wyczerpiesz tę pulę.
    No chyba że WAS posiada niezwykle inteligentne zarządzanie pulami połączeń, albo ten kod jest wywoływany mniej razy niż liczba połączeń w puli.

    OdpowiedzUsuń
  2. Już miałem się z Tobą zgodzić, kiedy po chwili przypomniałem sobie, że...

    connection.close() nie zwraca połączenia do puli, a (sugeruje, że) zamyka połączenie, które w przypadku puli połączeń...nie robi nic. Tak działa pula, że trzyma pootwierane połączenia i są one tak trzymane do czasu ich zamknięcia przy zamykaniu puli połączeń lub serwera, albo w przypadku stwierdzenia niepoprawnego stanu. Wystarczy więc wyjść poza zasięg widoczności zmiennej lokalnej conn i połączenie wraca do puli.

    Kiedy to napisałem zacząłem się zastanawiać, jak kod zarządzający pulą miałby się dowiedzieć o fakcie, że połączenie nie jest już wykorzystywane. Samo zdeaktualizowanie zmiennej to za mało, bo przypadłby się jeszcze jakieś zdarzenie, którego przechwycenie byłoby sygnałem końca używania. Pora poczytać, chyba, że są zainteresowani wyprowadzić nas/mnie z błędu i wyjaśnić szczegóły.

    OdpowiedzUsuń
  3. Ja zawsze myślałem, że Connection.close() zwraca połączenie do puli. Co więcej, nadal tak myślę ;) Pewnie różne są pule, ale jak inaczej miałoby być ono zwracane? Przy garbage collection? Wydaje mi się to słabym rozwiązaniem.

    OdpowiedzUsuń
  4. Jeśli się nie mylę to zwalnianiem połączeń zajmuje się "pool maintenance thread" i poczytać o nim można tutaj: http://publib.boulder.ibm.com/infocenter/wasinfo/v5r0/index.jsp?topic=/com.ibm.websphere.base.doc/info/aes/ae/udat_conpoolset.html

    Jak dokładnie jest to zrealizowane/zaimplementowane to nie wiem.

    OdpowiedzUsuń
  5. Wiesz Jacku,

    Osoby z tego projektu też czytają czasem Twojego bloga. Więc już widzę przerażenie na ich twarzach, jak kolejny raz widzą nazwę Groovy :P

    Raz już próbowałeś, ale teraz przynajmniej nie mi przyjdzie pisać dla nich kod :D

    OdpowiedzUsuń
  6. Fajny kod...

    Pomijając już wszystkie wytyczne odnośnie nazwy zmiennych. Poprzez mieszanie angielskiego i polskiego w sposób dość komiczny ;-)

    1. Fragment odpowiedzialny za budowę "query":
    String id_authorize = pobierzIdAuthorize(key);
    MessageFormat SQLQUERY = new MessageFormat("WITH ...");

    StringBuffer queryParam = new StringBuffer(id_authorize);
    String query = SQLQUERY.format(new Object[]{queryParam.toString()});

    System.out.println("SQLQUERY: " + query);

    po wykonaniu tego kodu query = "WITH ...".
    Chyba przy anonimizacji firmowego kody trochę za dużo poleciało. Szczerze mówiąc nie rozumiem tego fragmentu kodu

    2. Używasz równolegle sysout-a i logera. Po co ?

    3. Catch w metodzie pobierzIdAuthorize, rzuca wyjątek:
    new IllegalArgumentException("NIe odnaleziono klucza dla " + key, ex). W ramach konsekwencji powinien rzucać:
    throw new IllegalStateException("Problemy z bazka", ex);
    lub rzucać wyjątek wyżej (tam i tak go łapiesz).

    Popraw go lepiej przed commitem, jest zdecydowanie lepszy niż jsp, ale czy to kod który chciałbyś znaleźć w projekcie ?

    OdpowiedzUsuń
  7. Po pierwsze duże brawa za odwagę pokazania "takiego" kodu ;-)

    A teraz do uwag:
    1) linia 23 - synchronizacjaPoczatkowa() nie robi nic, kod synchronizeFirst() powinien być przesunięty do synchronizacjaPoczatkowa()
    2) mieszasz logikę aplikacji z obsługą wyjątków, powinno się to rozdzielić
    3) zasięg metod protected czemu nie private ?
    4) jak logujesz wyjątek to go nie wyrzucaj dalej - tylko jedno albo logujesz albo wyrzucasz dalej
    5) brak konsekwencji w nazwach - jeśli nie ma sensownych nazw to chociaż komentarz pasuje dodać

    OdpowiedzUsuń
  8. W moim przypadku oddawanie połączeń do puli działa w przypadku użycia:

    ...
    Connection connection;
    ...
    try {
    connection = datasource.getConnection();
    ...
    } finally {
    if(connection!=null) {
    connection.close();
    connection=null;
    }
    }

    W innych przypadkach połączenia nie wracają do puli, lub wracają za późno powodując jej wyczerpanie. No ale to Tomcat z DBCP, więc może Twoje środowisko jest nieco bardziej dopracowane pod tym względem.

    OdpowiedzUsuń
  9. A dlaczego użyłeś WebService a nie EJB bezstanowego ?:>

    OdpowiedzUsuń
  10. Ten komentarz został usunięty przez administratora bloga.

    OdpowiedzUsuń
  11. @Artur Karazniewicz
    Co się tak pienisz? Jak masz takie problemy że "Skóra mi cierpnie i zęby bolą" to idź do lekarza jak najszybciej bo z czasem może być tylko gorzej… Poza tym to nie jest miejsce do opisywania swoich problemów zdrowotnych.
    Twój komentarze w przeciwieństwie do poprzedników nic nie wnosi do sprawy.

    Moim zdaniem Jacek chciał napisać że jest to pierwszy krok do tego aby aplikacja była lepsza. tzn. przeniósł logikę biznesową ze strony JSP do WS. Zrobił pierwszy krok dalej będzie refactoring. Kolejne iteracje sprawią że kod będzie bardziej elastyczny, zgodny z konwencją i wzorcami ( i bóg wie czym jeszcze). Mając nadzieje na to że to my jako czytający bloga pomożemy mu w realizacji tego zadania.

    OdpowiedzUsuń
  12. ale przecież to nie jest Jacka kod, napisał przecież, że to jakiegoś współpracownika.

    OdpowiedzUsuń
  13. @geronimo

    Faktycznie - mea culpa - to było nie fair - z lekkim wstydem usuwam go:>. Odrabiam również niniejszym pracę domową i wnoszę kilka konstruktywnych uwag :>

    Jacek

    Jako blog Projektanta JEE powinien promować dobre zasady, jakość oraz najlepsze wzorce. Niestety tym razem nie wyszło :> Zgadzam się w pełni z uwagami Łukasz Ł i Łukasza Z a szczególnie razi po oczach brak trzymania się konwencji:

    1. Mieszanie konwencji językowych
    2. Nie trzymanie się konwencji JCS
    3. Strasznie również razi budowanie samego zapytania

    Jeśli to odziedziczony kod to powinien być poddany refaktoryzacji, jako lekcja poglądowa jak rozwijać odziedziczony kod. Ten kod po prostu jest niepoprawny. Np. fragment:

    String XMLResult = "";
    if (rs.next()) {
    XMLResult = rs.getString(1);
    }

    Dodatkowo warto również promować najlepsze praktyki przy opisie zastosowanych technologii. To co pokazałeś pozwala rzeczywiście wystawić web serwis. Niemniej takie podejście prowadzi w pierwszym rzędzie to ściśle powiązanych, wrażliwych na modyfikacje mechanizmów integracyjnych. Refaktoryzacja kodu w Javie spowoduje zmianę interfejsu web service. Jeśli prezentujesz przykład zastosowania nowej technologii to warto zrobić to tak jak nakazują najlepsze praktyki. Przede wszystkim:

    1. Odseparowanie warstwy logikii, dostępu do bazy w odpowiednie izolowane komponenty
    2. Po drugie uniezależnienie samego WebService od implementacji endpointu w java (zastosowanie SEI oraz nazwanie poszczególnych elementów WSDL w adnotacjach WebService, WebMethod itd.).

    OdpowiedzUsuń
  14. @Artur Karazniewicz

    Może to już późna pora, ale co w tym fragmencie jest niepoprawne (lub co rozumiesz w tym wypadku poprzez niepoprawne)?

    String XMLResult = "";
    if (rs.next()) {
    XMLResult = rs.getString(1);
    }

    Jeżeli założymy że zapytanie się wykonało i zwróciło chociaż jeden rekord z jedną kolumną.

    OdpowiedzUsuń
  15. Chyba komentujecie inny kod, nie ma tam takiego fragmentu:

    String XMLResult = "";

    tylko

    String XMLResult = "< find>< /find>";
    if (rs.next()) {
    XMLResult = rs.getString(1);
    }
    return XMLResult;

    i jest to chyba najlepszy kawałek kodu - poza nazwą zmiennej - w całej klasie ;-)

    OdpowiedzUsuń
  16. Komentujemy ten sam, tylko blogger był uprzejmy wyciąć tagi xmlowe.

    Ten fragment mógłby być potencjalnie poprawny gdyby to nie był... web service ;). Zauważ, że kiedy w bazie danych nie zostanie odnaleziony rekord wówczas silnik JAX-WS zwróci (domyślnie) escapeowany ciąg: "& amp; find>& amp; /find>" - a zapewne nie to autor miał na myśli ;). Zwracanie XMLa w postaci ciągów znaków, w przypadku JAXB jest bardzo, bardzo słabym pomysłem ;). Generalnie trudno mi po tym fragmencie wymyślić co autor mógłby mieć na myśli? Dlaczego w ogóle w przypadku braku rezultatu zwracać XML, a w przeciwnym przypadku liczbę - tego nie wiem - nawet gdyby to było poprawne;)

    OdpowiedzUsuń
  17. Przepraszam w poprzednim komentarzu powinno być '& lt;' a nie '& amp;' :-)

    OdpowiedzUsuń
  18. "Dlaczego w ogóle w przypadku braku rezultatu zwracać XML, a w przeciwnym przypadku liczbę - tego nie wiem - nawet gdyby to było poprawne;)"
    w/g mnie w obu przypadkach zwraca String'a. Sugerując się nazwą zmiennej zaryzykowałbym że każdorazowo chodzi o jakiegoś xml'a. Być może autor właśnie tego xml'a chciał przekazać w inne miejsce (z wcześniejszej części wpisu o stronie jsp, to chyba wynika). Co do samego pomysły przekazywania xml'a ciągu znaków przez w-s nie będę się wypowiadał bo nie znam kontekstu.

    Zastanawiam się za to jak jest przechowywany ten xml w bazce ? Czyżby jako zwykły ciąg znaków ?

    OdpowiedzUsuń
  19. String id_authorize = pobierzIdAuthorize(key);
    MessageFormat SQLQUERY = new MessageFormat("WITH ...");
    StringBuffer queryParam = new StringBuffer(id_authorize);
    String query = SQLQUERY.format(new Object[]{queryParam.toString()});

    Po pierwsze w przypadku braku rywalizacji międzywątkowej lepiej użyć StringBuilder, choć sprytny kompilator powinien i tak to podmienić. Ale czy ten StringBuffer queryParam cokolwiek tu wnosi?

    OdpowiedzUsuń