2. Architektura warstwowa aplikacji Java
Aplikacja Java jest często podzielona na warstwy, z których każda pełni ściśle określoną rolę. Rozważmy typową architekturę, czyli architekturę trójwarstwową:
![]() |
- warstwa [1], zwana tutaj [ui] (interfejs użytkownika), to warstwa, która komunikuje się z użytkownikiem za pośrednictwem graficznego interfejsu Swing, interfejsu konsoli lub interfejsu internetowego. Jej rolą jest przekazywanie danych pochodzących od użytkownika do warstwy [2] lub prezentowanie użytkownikowi danych dostarczonych przez warstwę [2].
- Warstwa [2], zwana tutaj [metier], to warstwa, która stosuje tzw. reguły biznesowe, c.a.d. specyficzna logika aplikacji, bez zwracania uwagi na to, skąd pochodzą przekazane jej dane ani dokąd trafiają generowane przez nią wyniki.
- warstwa [3], zwana tutaj [DAO] (Data Access Object), jest warstwą, która dostarcza warstwie [2] wcześniej zapisane dane (pliki, bazy danych, ...) oraz zapisuje niektóre wyniki dostarczane przez warstwę [2].
Istnieją różne możliwości implementacji warstwy [DAO]. Przyjrzyjmy się kilku z nich:
![]() |
Powyższa warstwa [JDBC] jest standardową warstwą używaną w Javie do uzyskiwania dostępu do baz danych. Izoluje ona warstwę [DAO] od warstwy SGBD, która zarządza bazą danych. Teoretycznie można zmienić warstwę SGBD bez zmiany kodu warstwy [DAO]. Pomimo tej zalety warstwa API JDBC ma pewne wady:
- wszystkie operacje na warstwie SGBD mogą wywołać wyjątek kontrolowany (checked) SQLException. Zmusza to kod wywołujący (w tym przypadku warstwę [DAO]) do otoczenia ich blokami try/catch, co sprawia, że kod staje się dość rozbudowany.
- warstwa [DAO] nie jest całkowicie niezależna od warstwy SGBD. Warstwy te posiadają na przykład własne metody automatycznego generowania wartości kluczy głównych, których warstwa [DAO] nie może zignorować. W związku z tym podczas wstawiania rekordu:
- w przypadku Oracle warstwa [DAO] musi najpierw uzyskać wartość klucza głównego rekordu, a dopiero potem go wstawić.
- w przypadku serwera SQL warstwa [DAO] wstawia rekord, któremu warstwa SGBD automatycznie przypisuje wartość klucza głównego, a następnie przekazuje tę wartość warstwie [DAO].
Różnice te można wyeliminować poprzez zastosowanie procedur przechowywanych. W powyższym przykładzie warstwa [DAO] wywoła procedurę przechowywaną w Oracle lub serwerze SQL, która uwzględni specyfikę warstwy SGBD. Zostaną one ukryte przed warstwą [DAO]. Niemniej jednak, choć zmiana warstwy SGBD nie będzie wiązała się z koniecznością przepisania warstwy [DAO], to jednak wymaga przepisania procedur przechowywanych. Nie należy tego traktować jako przeszkody nie do pokonania.
Podjęto wiele wysiłków, aby oddzielić warstwę [DAO] od zastrzeżonych elementów warstwy SGBD. Rozwiązaniem, które odniosło w ostatnich latach prawdziwy sukces w tej dziedzinie, jest rozwiązanie Hibernate:
![]() |
Warstwa [Hibernate] znajduje się pomiędzy warstwą [DAO] napisaną przez programistę a warstwą [JDBC]. Hibernate to ORM (Object Relational Mapper), czyli narzędzie łączące świat relacyjnych baz danych ze światem obiektów przetwarzanych przez Javę. Programista warstwy [DAO] nie widzi już warstwy [JDBC] ani tabel bazy danych, z których treści chce korzystać. Widzi jedynie obraz obiektowy bazy danych, dostarczany przez warstwę [Hibernate]. Połączenie między tabelami bazy danych a obiektami obsługiwanymi przez warstwę [DAO] realizowane jest głównie na dwa sposoby:
- poprzez pliki konfiguracyjne typu XML
- poprzez adnotacje Java w kodzie, technika dostępna dopiero od wersji JDK 1.5
Warstwa [Hibernate] jest warstwą abstrakcyjną, która ma być jak najbardziej przejrzysta. Idealnym rozwiązaniem jest sytuacja, w której programista warstwy [DAO] może całkowicie zignorować fakt, że pracuje z bazą danych. Jest to możliwe, o ile to nie on sam pisze konfigurację, która stanowi pomost między światem relacyjnym a światem obiektowym. Konfiguracja tego pomostu jest dość skomplikowana i wymaga pewnej wprawy.
Warstwa obiektów [4], będąca odzwierciedleniem warstwy BD, nazywana jest „kontekstem trwałości”. Warstwa [DAO] oparta na Hibernate wykonuje operacje trwałości (CRUD: create – read – update – delete) na obiektach kontekstu trwałości, które są przekształcane przez Hibernate na polecenia SQL wykonywane przez warstwę JDBC. W przypadku operacji zapytań do bazy danych (SQL Select), Hibernate udostępnia programistom język HQL (Hibernate Query Language) do wysyłania zapytań do kontekstu trwałości [4], a nie bezpośrednio do samej bazy BD.
Hibernate jest popularny, ale trudny do opanowania. Krzywa uczenia się, często przedstawiana jako łatwa, jest w rzeczywistości dość stroma. Gdy tylko mamy do czynienia z bazą danych zawierającą tabele o relacjach „jeden do wielu” lub „wiele do wielu”, konfiguracja mostu relacyjno-obiektowego nie jest w zasięgu możliwości zwykłego początkującego. Błędy w konfiguracji mogą prowadzić do niskiej wydajności aplikacji.
W obliczu sukcesu produktów ORM, firma Sun, twórca języka Java, postanowiła ustandaryzować warstwę ORM poprzez specyfikację o nazwie JPA (Java Persistence API), która pojawiła się wraz z Javą 5. Specyfikacja JPA została zaimplementowana w różnych produktach: Hibernate, Toplink, EclipseLink, OpenJpa, ... Wraz z JPA poprzednia architektura zmienia się na następującą:
![]() |
Warstwa [DAO] komunikuje się teraz ze specyfikacją JPA, czyli zestawem interfejsów. Programista zyskał dzięki temu na standaryzacji. Wcześniej, jeśli zmieniał swoją warstwę ORM, musiał również zmienić warstwę [DAO], która została napisana w celu komunikacji z konkretną warstwą ORM. Teraz napisze warstwę [DAO], która będzie współpracować z warstwą JPA. Niezależnie od produktu, który ją implementuje, interfejs warstwy JPA udostępniany warstwie [DAO] pozostaje taki sam.
W niniejszym dokumencie będziemy korzystać z warstwy [DAO] opartej na warstwie JPA/Hibernate lub JPA/EclipseLink. Ponadto do połączenia tych warstw ze sobą wykorzystamy framework Spring 2.8.
![]() |
Dużą zaletą Springa jest to, że umożliwia on łączenie warstw poprzez konfigurację, a nie w kodzie. Jeśli więc implementacja JPA / Hibernate musi zostać zastąpiona implementacją Hibernate bez JPA, ponieważ na przykład aplikacja działa w środowisku JDK 1.4, które nie obsługuje JPA, ta zmiana implementacji warstwy [DAO] nie ma wpływu na kod warstwy [métier]. Należy zmodyfikować jedynie plik konfiguracyjny Springa, który łączy te warstwy ze sobą.
W przypadku Java EE 5 istnieje inne rozwiązanie: zaimplementowanie warstw [metier] i [DAO] przy użyciu EJB3 (Enterprise Java Bean w wersji 3):
![]() |
Zobaczymy, że rozwiązanie to nie różni się zbytnio od tego wykorzystującego Spring. Środowisko Java EE5 jest dostępne na tzw. serwerach aplikacji, takich jak Sun Application Server 9.x (Glassfish), Jboss Application Server, Oracle Container for Java (OC4J) itp. Serwer aplikacji to zasadniczo serwer aplikacji internetowych. Istnieją również środowiska EE 5 tzw. „samodzielne”, c.a.d, które mogą być wykorzystywane poza serwerem aplikacji. Dotyczy to na przykład JBoss, EJB3 lub OpenEJB.
W środowisku EE5 warstwy są implementowane przez obiekty o nazwie EJB (Enterprise Java Bean). W poprzednich wersjach EE obiekty EJB (EJB, 2.x) były uważane za trudne do wdrożenia, testowania, a czasami także za mało wydajne. Rozróżnia się „entity” EJB2.x oraz „session” EJB2.x. Krótko mówiąc, „entity” typu EJB2.x jest odwzorowaniem wiersza tabeli bazy danych, a „session” typu EJB2.x – obiektem wykorzystywanym do implementacji warstw [metier], [DAO] w architekturze wielowarstwowej. Jednym z głównych zarzutów wobec warstw zaimplementowanych przy użyciu EJB jest to, że można z nich korzystać wyłącznie w ramach kontenerów EJB, usługi dostarczanej przez środowisko EE. Środowisko to, którego wdrożenie jest bardziej skomplikowane niż w przypadku środowiska SE (Standard Edition), może zniechęcać programistów do częstego przeprowadzania testów. Istnieją jednak środowiska programistyczne Java, które ułatwiają korzystanie z serwera aplikacji poprzez automatyzację wdrażania EJB na serwerze: Eclipse, NetBeans, JDeveloper, IntelliJ, IDEA. W niniejszym przewodniku wykorzystamy NetBeans 6.8 oraz serwer aplikacji GlassFish v3.
Framework Spring powstał w odpowiedzi na złożoność EJB2. Spring zapewnia w środowisku SE znaczną liczbę usług, które zazwyczaj są dostarczane przez środowiska EE. Tak więc w części „Trwałość danych” Spring zapewnia pule połączeń i menedżery transakcji, których potrzebują aplikacje. Pojawienie się Springa sprzyjało rozwojowi kultury testów jednostkowych, które stały się łatwiejsze do wdrożenia w kontekście SE niż w kontekście EE. Spring umożliwia implementację warstw aplikacji za pomocą klasycznych obiektów Java (POJO, Plain Old/Ordinary Java Object), co pozwala na ich ponowne wykorzystanie w innym kontekście. Ponadto dość przejrzyście integruje wiele narzędzi innych producentów, w szczególności narzędzia do trwałości danych, takie jak Hibernate, EclipseLink, Ibatis...
Java EE5 została zaprojektowana w celu uzupełnienia braków specyfikacji EJB2. EJB i 2.x stały się EJB3. Są to obiekty POJOs oznaczone adnotacjami, które sprawiają, że stają się one obiektami specjalnymi, gdy znajdują się w kontenerze EJB3. W tym kontenerze obiekt EJB3 będzie mógł korzystać z usług kontenera (pula połączeń, menedżer transakcji itp.). Poza kontenerem EJB3 obiekt EJB3 staje się zwykłym obiektem Java. Jego adnotacje EJB są ignorowane.
Powyżej przedstawiliśmy Spring oraz kontener EJB3 jako potencjalną infrastrukturę (framework) naszej architektury wielowarstwowej. To właśnie ta infrastruktura zapewni nam potrzebne usługi: pulę połączeń oraz menedżera transakcji.
- W przypadku Springa warstwy będą zaimplementowane za pomocą obiektów POJOs. Będą one miały dostęp do usług Springa (pula połączeń, menedżer transakcji) poprzez wstrzykiwanie zależności do tych obiektów POJOs: podczas ich tworzenia Spring wstrzykuje do nich odniesienia do usług, których będą potrzebować.
- W przypadku kontenera EJB3 warstwy będą zaimplementowane przy użyciu EJB. Architektura warstwowa zaimplementowana przy użyciu elementów EJB3 niewiele różni się od tej zaimplementowanej przy użyciu elementów POJO instancjonowanych przez Spring. Zauważymy wiele podobieństw.
- Na zakończenie przedstawimy przykład wielowarstwowej aplikacji internetowej:
![]() |






