18. Studium przypadku: Struts 2 / Tiles / Spring / Hibernate / MySQL
Naszą naukę Struts zakończymy studium przypadku. Aby zapewnić realizm, omawiany przykład będzie znacznie bardziej złożony niż te, które analizowaliśmy wcześniej. Dla początkujących z pewnością lepiej będzie pogłębić podstawy Struts 2 na własnych aplikacjach, zanim przystąpią do tego studium przypadku.
Aplikacja będzie wykorzystywać architekturę warstwową:
![]() |
Zestaw warstw [metier], [dao], [jpa/hibernate] zostanie nam dostarczony w postaci archiwum jar, którego funkcjonalność omówimy szczegółowo. Integrację warstw zapewni biblioteka Spring. Warstwa [web] zostanie zaimplementowana przy użyciu Struts 2.
![]() |
18.1. Problem
Zamierzamy stworzyć aplikację internetową umożliwiającą sporządzanie odcinków wypłaty dla opiekunek zatrudnionych przez „Dom Małego Dziecka” w danej gminie.

Niniejsze studium przypadku zostało przedstawione w dokumencie:
Wprowadzenie do języka Java EE 5 dostępnym pod adresem URL [http://tahe.developpez.com/java/javaee]
W niniejszym dokumencie studium przypadku zostało zaimplementowane przy użyciu następującej architektury wielowarstwowej:
![]() |
Warstwa [web] została zaimplementowana przy użyciu frameworka JSF (Java Server Faces). Wykorzystamy tę samą architekturę, implementując warstwę [web] przy użyciu Struts 2. Aby pokazać zalety architektury warstwowej, wykorzystamy archiwum jar warstw [metier, dao, jpa] z wersji JSF i połączymy je z warstwą [web / struts2]:
![]() |
Przedstawimy następujące elementy warstw [metier, dao, jpa]:
- warstwa [web] komunikuje się z interfejsem warstwy [métier]. Przedstawimy ten interfejs.
- warstwa [jpa] uzyskuje dostęp do bazy danych. Przedstawimy ją.
- warstwa [jpa] przekształca wiersze tabel bazy danych w encje JPA, z którymi operują wszystkie warstwy aplikacji. Przedstawimy je.
- Warstwy [metier, dao, jpa] są instancjonowane przez Spring. Przedstawimy plik konfiguracyjny, który realizuje to instancjonowanie i integrację.
18.2. Baza danych
Będziemy korzystać z następującej bazy danych MySQL [dbpam_hibernate]:

- W bazie [1] znajdują się trzy tabele:
- [employes]: tabela rejestrująca pracownice żłobka
- [cotisations]: tabela zawierająca stawki składek na ubezpieczenie społeczne
- [indemnites]: tabela zawierająca informacje umożliwiające obliczenie wynagrodzenia pracownic
Tabela [employes]
![]() |
- w [2] znajduje się tabela pracowników, a w [3] – znaczenie jej pól
Zawartość tabeli mogłaby wyglądać następująco:
Tabela [cotisations]
![]() |
- w [4] – tabela składek, a w [5] – znaczenie jej pól
Zawartość tabeli mogłaby wyglądać następująco:
Tabela [indemnites]
![]() |
- w [6] – tabela świadczeń, a w [7] – znaczenie jej pól
Zawartość tabeli mogłaby wyglądać następująco:
Eksport struktury bazy danych do pliku SQL daje następujący wynik:
18.3. Entities JPA
W poniższej architekturze
![]() |
Warstwa [Jpa] pełni rolę pomostu między obiektami obsługiwanymi przez warstwę [dao] a wierszami tabel bazy danych obsługiwanymi przez sterownik JDBC. Wiersze tabel odczytane z bazy danych są przekształcane w obiekty zwane encjami JPA. Z kolei podczas zapisu encje JPA są przekształcane w wiersze tabel. Encje te są przetwarzane przez wszystkie warstwy, a w szczególności przez warstwę internetową. Musimy więc je poznać:
Entyteta [Employe] reprezentuje wiersz tabeli [Employes]
![]() |
|
Entyteta [Employe] ma następujący wygląd:
package jpa;
...
@Entity
@Table(name="EMPLOYES")
public class Employe implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@Version
@Column(name="VERSION",nullable=false)
private int version;
@Column(name="SS", nullable=false, unique=true, length=15)
private String SS;
@Column(name="NOM", nullable=false, length=30)
private String nom;
@Column(name="PRENOM", nullable=false, length=20)
private String prenom;
@Column(name="ADRESSE", nullable=false, length=50)
private String adresse;
@Column(name="VILLE", nullable=false, length=30)
private String ville;
@Column(name="CP", nullable=false, length=5)
private String codePostal;
@ManyToOne
@JoinColumn(name="INDEMNITE_ID",nullable=false)
private Indemnite indemnite;
public Employe() {
}
public Employe(String SS, String nom, String prenom, String adresse, String ville, String codePostal, Indemnite indemnite){
setSS(SS);
setNom(nom);
setPrenom(prenom);
setAdresse(adresse);
setVille(ville);
setCodePostal(codePostal);
setIndemnite(indemnite);
}
@Override
public String toString() {
return "jpa.Employe[id=" + getId()
+ ",version="+getVersion()
+",SS="+getSS()
+ ",nom="+getNom()
+ ",prenom="+getPrenom()
+ ",adresse="+getAdresse()
+",ville="+getVille()
+",code postal="+getCodePostal()
+",indice="+getIndemnite().getIndice()
+"]";
}
// metody pobierające i ustawiające
...
}
Pominiemy adnotacje @ przeznaczone dla warstwy [Jpa]. Poszczególne pola klasy odzwierciedlają poszczególne kolumny tabeli [EMPLOYES]. Pole „indemnites” (wiersz 29) odzwierciedla fakt, że tabela [EMPLOYES] posiada klucz obcy do tabeli [INDEMNITES]. Podczas edycji danych pracownika edytowane są również jego dodatki.
Entyteta [Indemnite] jest wyrażeniem będącym obiektem wiersza tabeli [INDEMNITES]:
![]() |
|
Entity [Indemnite] ma następującą postać:
package jpa;
...
@Entity
@Table(name="INDEMNITES")
public class Indemnite implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@Version
@Column(name="VERSION",nullable=false)
private int version;
@Column(name="INDICE", nullable=false,unique=true)
private int indice;
@Column(name="BASE_HEURE",nullable=false)
private double baseHeure;
@Column(name="ENTRETIEN_JOUR",nullable=false)
private double entretienJour;
@Column(name="REPAS_JOUR",nullable=false)
private double repasJour;
@Column(name="INDEMNITES_CP",nullable=false)
private double indemnitesCP;
public Indemnite() {
}
public Indemnite(int indice, double baseHeure, double entretienJour, double repasJour, double indemnitesCP){
setIndice(indice);
setBaseHeure(baseHeure);
setEntretienJour(entretienJour);
setRepasJour(repasJour);
setIndemnitesCP(indemnitesCP);
}
@Override
public String toString() {
return "jpa.Indemnite[id=" + getId()
+ ",version="+getVersion()
+",indice="+getIndice()
+",base heure="+getBaseHeure()
+",entretien jour"+getEntretienJour()
+",repas jour="+getRepasJour()
+",indemnités CP="+getIndemnitesCP()
+ "]";
}
// metody pobierające i ustawiające
....
}
Poszczególne pola klasy odzwierciedlają poszczególne kolumny tabeli [INDEMNITES].
Entyteta [Cotisation] jest wyrażeniem będącym obiektem wiersza tabeli [COTISATIONS]:
![]() |
|
Jednostka [Cotisation] ma następujący opis:
package jpa;
...
@Entity
@Table(name="COTISATIONS")
public class Cotisation implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@Version
@Column(name="VERSION",nullable=false)
private int version;
@Column(name="CSGRDS",nullable=false)
private double csgrds;
@Column(name="CSGD",nullable=false)
private double csgd;
@Column(name="SECU",nullable=false)
private double secu;
@Column(name="RETRAITE",nullable=false)
private double retraite;
public Cotisation() {
}
public Cotisation(double csgrds, double csgd, double secu, double retraite){
setCsgrds(csgrds);
setCsgd(csgd);
setSecu(secu);
setRetraite(retraite);
}
@Override
public String toString() {
return "jpa.Cotisation[id=" + getId() + ",version=" + getVersion()+",csgrds="+getCsgrds()+"" +
",csgd="+getCsgd()+",secu="+getSecu()+",retraite="+getRetraite()+"]";
}
// metody pobierające i ustawiające
...
}
Poszczególne pola klasy odzwierciedlają poszczególne kolumny tabeli [INDEMNITES].
18.4. Sposób obliczania wynagrodzenia opiekunki do dzieci
Aplikacja internetowa, którą zamierzamy stworzyć, pozwoli nam obliczyć wynagrodzenie pracownicy na podstawie trzech informacji:
- numer identyfikacyjny pracownicy
- liczba przepracowanych dni
- liczba przepracowanych godzin
Oto zrzut ekranu przedstawiający obliczenie wynagrodzenia:

Poniżej przedstawiamy sposób obliczania miesięcznego wynagrodzenia opiekunki do dzieci. Nie jest to sposób stosowany w rzeczywistości. Jako przykład posłużymy się wynagrodzeniem pani Marie Jouveinal, która w rozliczanym miesiącu przepracowała 150 godzin w ciągu 20 dni.
Uwzględniono następujące elementy: | ||
Wynagrodzenie podstawowe asystentki opiekunki nad dziećmi oblicza się według : | ||
Pewna kwota składek na ubezpieczenie społeczne należy potrącić z tego wynagrodzenia : | ||
Łączna kwota składek na ubezpieczenie społeczne: | ||
Ponadto opiekunka ma prawo, za każdy przepracowany dzień do dodatku na utrzymanie, a także do dodatku posiłku. W związku z tym otrzymuje ona następujące: | ||
Ostatecznie wynagrodzenie netto przysługujące opiekunce wynosi: |
18.5. Interfejs warstwy [métier]
Wróćmy do architektury tworzonej przez nas aplikacji:
![]() |
Warstwa [web / struts 2] komunikuje się z interfejsem warstwy [métier]. Wygląda on następująco:
package metier;
import java.util.List;
import jpa.Employe;
public interface IMetier {
// pobieranie listy płac
FeuilleSalaire calculerFeuilleSalaire(String SS, double nbHeuresTravaillées, int nbJoursTravaillés );
// lista pracowników
List<Employe> findAllEmployes();
}
- wiersz 8: metoda, która pozwoli nam obliczyć wynagrodzenie pracownika
- wiersz 10: metoda, która pozwoli nam wypełnić listę rozwijaną pracowników
Metoda calculerFeuillesalaire zwraca instancję klasy [FeuilleSalaire] o następującej treści:
package metier;
import java.io.Serializable;
import jpa.Cotisation;
import jpa.Employe;
public class FeuilleSalaire implements Serializable {
// pola prywatne
private Employe employe;
private Cotisation cotisation;
private ElementsSalaire elementsSalaire;
// konstruktory
public FeuilleSalaire() {
}
public FeuilleSalaire(Employe employe, Cotisation cotisation,
ElementsSalaire elementsSalaire) {
setEmploye(employe);
setCotisation(cotisation);
setElementsSalaire(elementsSalaire);
}
// toString
@Override
public String toString() {
return "[" + employe + "," + cotisation + ","
+ elementsSalaire + "]";
}
// metody pobierające i ustawiające
...
}
Arkusz płacowy zawiera następujące informacje:
- wiersz 10: informacje o pracowniku, dla którego obliczamy wynagrodzenie
- wiersz 11: różne stawki składek
- wiersz 12: składniki wynagrodzenia
Klasa [ElementsSalaire] ma następujący wygląd:
package metier;
import java.io.Serializable;
public class ElementsSalaire implements Serializable{
// pola prywatne
private double salaireBase;
private double cotisationsSociales;
private double indemnitesEntretien;
private double indemnitesRepas;
private double salaireNet;
// konstruktory
public ElementsSalaire() {
}
public ElementsSalaire(double salaireBase, double cotisationsSociales,
double indemnitesEntretien, double indemnitesRepas,
double salaireNet) {
setSalaireBase(salaireBase);
setCotisationsSociales(cotisationsSociales);
setIndemnitesEntretien(indemnitesEntretien);
setIndemnitesRepas(indemnitesRepas);
setSalaireNet(salaireNet);
}
// toString
@Override
public String toString() {
return "[salaire base=" + salaireBase + ",cotisations sociales=" + cotisationsSociales + ",indemnités d'entretien="
+ indemnitesEntretien + ",indemnités de repas=" + indemnitesRepas + ",salaire net="
+ salaireNet + "]";
}
// metody pobierające i ustawiające
...
}
- wiersze 8–12: składniki wynagrodzenia
18.6. Plik konfiguracyjny Spring
Integrację warstw [métier, dao, jpa] zapewnia następujący plik konfiguracyjny Spring:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-2.0.xsd">
<!-- warstwy aplikacyjne -->
<!-- biznes -->
<bean id="metier" class="metier.Metier">
<property name="employeDao" ref="employeDao"/>
<property name="cotisationDao" ref="cotisationDao"/>
</bean>
<!-- DAO -->
<bean id="employeDao" class="dao.EmployeDao" />
<bean id="indemniteDao" class="dao.IndemniteDao" />
<bean id="cotisationDao" class="dao.CotisationDao" />
<!-- konfiguracja JPA -->
<bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
<property name="dataSource" ref="dataSource" />
<property name="jpaVendorAdapter">
<bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter">
<property name="databasePlatform" value="org.hibernate.dialect.MySQL5InnoDBDialect" />
</bean>
</property>
<property name="loadTimeWeaver">
<bean class="org.springframework.instrument.classloading.InstrumentationLoadTimeWeaver" />
</property>
</bean>
<!-- źródło danych DBCP -->
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
<property name="driverClassName" value="com.mysql.jdbc.Driver" />
<property name="URL" value="jdbc:mysql://localhost:3306/dbpam_hibernate" />
<property name="username" value="root" />
<property name="password" value="" />
</bean>
<!-- menedżer transakcji -->
<tx:annotation-driven transaction-manager="txManager" />
<bean id="txManager" class="org.springframework.orm.jpa.JpaTransactionManager">
<property name="entityManagerFactory" ref="entityManagerFactory" />
</bean>
<!-- przetwarzanie wyjątków -->
<bean class="org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor" />
<!-- trwałość -->
<bean class="org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessor" />
</beans>
Nie będziemy próbować wyjaśniać tej konfiguracji. Jest ona niezbędna do instancjonowania i integracji warstw [métier, dao, jpa]. Nasza aplikacja internetowa, która będzie opierać się na tych warstwach, będzie zatem musiała przejąć tę konfigurację. Należy zauważyć, że wiersze 32–37 konfigurują parametry JDBC bazy danych. Czytelnik, który chciałby zmienić bazę danych, musi zmodyfikować te wiersze.
Więcej informacji na temat tej konfiguracji można znaleźć w dokumencie „Wprowadzenie do Java EE 5”, dostępnym na stronie URL [http://tahe.developpez.com/java/javaee].











