Integracja JSF + Facelets + Spring + JPA + Tomahawk
Dość sporo tych Trzy Literowych Skrótów :). Wszystkie wymienione technologie zostaną połączone w prostej aplikacji typu CRUD.
W tym artykule pokaże jak zintegrować następujące technologie:
- JavaServer Faces 1.1 - będę wykorzystywał implementację Apache MyFaces - jako warstwa prezentacji,
- Facelets - są one wspaniałym kompanem dla JSF, będę korzystał tylko z szablonów, choć Facelets mają dużo więcej możliwości,
- Spring 2 - kontener IoC, będzie on wstrzykiwał beany obsługujące encje JPA (czyli DAO) do JSF (cudowna integracja) oraz obsługiwał transakcje,
- Java Persistence API - implementacja Toplink - będę wykorzystywał JPA do mapowania obiektowo-relacyjnego,
- Tomahawk - zestaw komponentów JSF ze stajni Apache.
Do artykułu dołączony jest kod źródłowy całej aplikacji. Można go otworzyć od razu w IntelliJ IDEA, a jeśli używasz innego edytora to musisz jakoś zaimportować projekt :).
Możesz także przejrzeć pełen kod w subversion pod adresem: http://svn.mocna-kawa.com/jsfcrud/.
Zacznijmy od przygotowania modelu. Mamy tylko jedną relację jeden do wielu. Jedna osoba może mieć przypisanych wiele ról. Zatem użytkownik wygląda następująco:
-
@NamedQueries({
-
@NamedQuery(name = "findAllUsers", query = "SELECT user FROM User user"),
-
@NamedQuery(name = "findUserByLogin", query = "SELECT user FROM User user WHERE user.login = ?1")
-
})
-
public class User {
-
@Id
-
@GeneratedValue(strategy = GenerationType.AUTO)
-
-
-
@OneToMany(cascade = {CascadeType.MERGE, CascadeType.PERSIST}, fetch = FetchType.EAGER)
-
private List<Role> roles;
-
-
// gettery i settery dla id, login i password.
-
-
// przesłonienie metod hashCode() i equals()
-
-
// taka mała sztuczka, bez której t:selectManyCheckbox nie chce działać (opakowanie w tablicę)
-
public Role[] getRolesAsArray() {
-
return roles == null ? null : roles.toArray(new Role[0]);
-
}
-
-
public void setRolesAsArray(Role[] roles) {
-
// błąd Toplinka: https://glassfish.dev.java.net/issues/show_bug.cgi?id=556
-
}
-
}
Natomiast rola jest również prosta i jest następująca:
-
@NamedQueries({
-
@NamedQuery(name = "findAllRoles", query = "SELECT role FROM Role role"),
-
@NamedQuery(name = "findRoleByRoleName", query = "SELECT role FROM Role role WHERE role.roleName = ?1")
-
})
-
public class Role {
-
-
@Id
-
@GeneratedValue(strategy = GenerationType.AUTO)
-
-
-
// gettery i settery
-
-
// przesłonięcie hashCode() i equals()
-
}
Dobrą praktyką jest używanie nazwanych zapytań. W JPA zapisuje się je za pomocą adnotacji @NamedQueries i odpowiednio @NamedQuery.
Teraz zdefiniujmy interfejs do komunikacji między aplikacją a źródłem danych (czyli DAO).
-
public interface UserDao {
-
public List<User> findAll();
-
public void save(User user);
-
public void delete(User user);
-
}
-
-
public interface RoleDao {
-
public List<Role> findAll();
-
public void save(Role role);
-
public void delete(Role role);
-
}
Teraz implementacja powyższych interfejsów. Będą one dziedziczyć po JpaDaoSupport, która to klasa dostarczona przez Springa zajmuje się komunikacją z EntityManagerem i pozwala na tzw. "jedno-linijkowce" (ang. one-liner).
-
public class JpaUserDao extends JpaDaoSupport implements UserDao {
-
-
List<User> users = getJpaTemplate().findByNamedQuery("findUserByLogin", login);
-
if (users.size()> 1) {
-
throw new DataIntegrityViolationException("More than one user with the same login.");
-
}
-
return users.size() == 0 ? null : users.get(0);
-
}
-
-
public List<User> findAll() {
-
return getJpaTemplate().findByNamedQuery("findAllUsers");
-
}
-
-
@Transactional(readOnly = false, propagation = Propagation.REQUIRES_NEW)
-
public void save(User user) {
-
getJpaTemplate().merge(user);
-
}
-
-
@Transactional(readOnly = false, propagation = Propagation.REQUIRES_NEW)
-
public void delete(User user) {
-
getJpaTemplate().remove(getJpaTemplate().merge(user));
-
}
-
-
return getJpaTemplate().find(User.class, id);
-
}
-
-
}
-
-
public class JpaRoleDao extends JpaDaoSupport implements RoleDao {
-
// implementację można znaleźć w repozytorium lub w załączonym kodzie źródłowym
-
}
Jak widać z powyższego kodu, transakcje są deklarowane w adnotacjach, co czyni je dość przyjemnymi. Teraz trochę konfiguracji: persistence.xml dla JPA oraz applicationContext.xml dla Springa. Beany Springowe będą widoczne w kontekście JSF i będzie można je także używać w stronach JSF.
-
<!-- Deklaracja beana do obsługi użytkowników -->
-
<bean id="userDao" class="com.mocnakawa.jsfcrud.data.dao.jpa.JpaUserDao">
-
<property name="entityManagerFactory" ref="entityManagerFactory"/>
-
</bean>
-
-
<!-- Deklaracja beana do obsługi ról -->
-
<bean id="roleDao" class="com.mocnakawa.jsfcrud.data.dao.jpa.JpaRoleDao">
-
<property name="entityManagerFactory" ref="entityManagerFactory"/>
-
</bean>
-
-
<!-- Deklaracja EntityManagera, Spring domyślnie szuka pliku META-INF/persistence.xml i z niego bierze dane -->
-
<bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalEntityManagerFactoryBean">
-
<property name="persistenceUnitName" value="JsfCrudUnit"/>
-
</bean>
-
-
<!-- Do obsługi transakcji -->
-
<bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager">
-
<property name="entityManagerFactory" ref="entityManagerFactory"/>
-
</bean>
-
-
<!-- Transakcje będą deklarowane przez adnotacje -->
-
<tx:annotation-driven transaction-manager="transactionManager"/>
Zauważmy, że poprzez zastosowanie kontenera wstrzyknięć nie jesteśmy związani z JPA. Interfejs DAO jest niezależny od zastsowanej technologii ORM. Bez problemu moglibyśmy podmienić JPA na Hibernate'a lub iBatis. Teraz persistence.xml.
-
<persistence xmlns="http://java.sun.com/xml/ns/persistence" version="1.0">
-
<persistence-unit name="JsfCrudUnit" transaction-type="RESOURCE_LOCAL">
-
<provider>oracle.toplink.essentials.ejb.cmp3.EntityManagerFactoryProvider</provider>
-
-
<class>com.mocnakawa.jsfcrud.data.domain.Role</class>
-
<class>com.mocnakawa.jsfcrud.data.domain.User</class>
-
-
<properties>
-
<property name="toplink.logging.level" value="OFF"/>
-
<property name="toplink.jdbc.url" value="jdbc:mysql://localhost/jsfcrud"/>
-
<property name="toplink.jdbc.driver" value="com.mysql.jdbc.Driver"/>
-
<property name="toplink.jdbc.user" value="root"/>
-
<property name="toplink.jdbc.password" value="Twoje Hasło"/>
-
<property name="toplink.ddl-generation" value="create-tables"/>
-
</properties>
-
</persistence-unit>
-
</persistence>
Teraz już możemy przejść do warstwy prezentacji, czyli czas na JSF. Czas na to, co tygryski lubią najbardziej, czyli od kontrolera (a dokładnie managed beana JSF).
-
// Klasa typowa dla akcji typu CRUD
-
// Jest to dość proste, więc mam nadzieje, że kod się sam dokumentuje
-
public class UserController {
-
-
// wstrzykniete przez Springa
-
private UserDao userDao;
-
-
private User user;
-
private boolean editMode = false;
-
-
public List<User> getUsers() {
-
return userDao.findAll();
-
}
-
-
user = FacesUtils.getActionAttribute(event, "user", User.class);
-
editMode = true;
-
}
-
-
userDao.save(user);
-
return Constants.USER_AND_ROLE_LIST;
-
}
-
-
user = new User();
-
editMode = false;
-
return Constants.USER_FORM;
-
}
-
-
User userToCreate = userDao.findByLogin(user.getLogin());
-
if (userToCreate == null) {
-
userDao.save(user);
-
return Constants.USER_AND_ROLE_LIST;
-
} else {
-
FacesUtils.addErrorMessage("Login already exists");
-
return null;
-
}
-
}
-
-
userDao.delete(FacesUtils.getActionAttribute(event, "user", User.class));
-
}
-
-
// gettery i settery
-
}
Kontrolera dla ról celowo pominąłem, można go znaleźć w załączonym kodzie źródłowym. Pora na web.xml.
-
<!-- Listner dla Springa -->
-
<listener>
-
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
-
</listener>
-
-
<!-- Konfiguracja Facelets -->
-
<context-param>
-
<param-name>facelets.REFRESH_PERIOD</param-name>
-
<param-value>2</param-value>
-
</context-param>
-
-
<context-param>
-
<param-name>facelets.DEVELOPMENT</param-name>
-
<param-value>true</param-value>
-
</context-param>
-
-
<context-param>
-
<param-name>javax.faces.DEFAULT_SUFFIX</param-name>
-
<param-value>.xhtml</param-value>
-
</context-param>
-
-
<!-- Konfiguracja Springa -->
-
<context-param>
-
<param-name>contextConfigLocation</param-name>
-
<param-value>/WEB-INF/applicationContext*.xml</param-value>
-
</context-param>
-
-
<filter>
-
<filter-name>RequestContextFilter</filter-name>
-
<filter-class>org.springframework.web.filter.RequestContextFilter</filter-class>
-
</filter>
-
-
<filter-mapping>
-
<filter-name>RequestContextFilter</filter-name>
-
<url-pattern>*.jsf</url-pattern>
-
</filter-mapping>
-
-
<!-- Konfiguracja kontrolera JSF -->
-
<servlet>
-
<servlet-name>Faces Servlet</servlet-name>
-
<servlet-class>javax.faces.webapp.FacesServlet</servlet-class>
-
<load-on-startup>1</load-on-startup>
-
</servlet>
-
-
<servlet-mapping>
-
<servlet-name>Faces Servlet</servlet-name>
-
<url-pattern>*.jsf</url-pattern>
-
</servlet-mapping>
Należy nie zapominać, aby podpiąć Springa. Parametr facelets.DEVELOPMENT w Facelets jest bardzo użytecznym podczas pisania aplikacji. Prezentuje on błędy występujące w bindingach JSF lub o rzucanych wyjątkach w bardzo przystępny sposób.
Czas na zaprezentowanie potęgi Facelets. Zadeklarujmy szablon dla naszych stron:
-
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
-
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
-
<html xmlns="http://www.w3.org/1999/xhtml"
-
xmlns:h="http://java.sun.com/jsf/html"
-
xmlns:f="http://java.sun.com/jsf/core"
-
xmlns:ui="http://java.sun.com/jsf/facelets">
-
-
<head>
-
<title>
-
<ui:insert name="title">Default title</ui:insert>
-
</title>
-
</head>
-
<body>
-
-
<h1>
-
<ui:insert name="title">Default title</ui:insert>
-
</h1>
-
-
<ui:insert name="content"/>
-
<ui:include src="footer.xhtml"/>
-
-
</body>
-
</html>
Tak ui:include oraz ui:insert są raczej zrozumiałe. Szablony Facelets są tak bardzo proste, jak powinny być właśnie. Są dużo bardziej przyjazne od Tilesów. Czas na wypisanie wszystkich użytkowników.
-
<!-- Wskazanie szablonu, z którego będziemy korzystać -->
-
<ui:composition template="/pages/layout.xhtml">
-
-
<!-- definiowanie tytułu, który będzie później wstawiony za pomocą ui:insert -->
-
<ui:define name="title">Users CRUD</ui:define>
-
-
<!-- definiowanie zawartości strony -->
-
<ui:define name="content">
-
<h:form>
-
<h:dataTable value="#{userController.users}" var="user">
-
<h:column>
-
<f:facet name="header">Login</f:facet>
-
#{user.login}
-
</h:column>
-
<h:column>
-
<f:facet name="header">Password</f:facet>