Integracja JSF + Facelets + Spring + JPA + Tomahawk

napisane przez wiktor, 14:40 08-05-2007

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:

JAVA:
  1. @NamedQueries({
  2. @NamedQuery(name = "findAllUsers", query = "SELECT user FROM User user"),
  3. @NamedQuery(name = "findUserByLogin", query = "SELECT user FROM User user WHERE user.login = ?1")
  4.         })
  5. public class User {
  6.     @Id
  7.     @GeneratedValue(strategy = GenerationType.AUTO)
  8.     private Integer id;
  9.  
  10.     private String login;
  11.     private String password;
  12.  
  13.     @OneToMany(cascade = {CascadeType.MERGE, CascadeType.PERSIST}, fetch = FetchType.EAGER)
  14.     private List<Role> roles;
  15.  
  16.     // gettery i settery dla id, login i password.
  17.  
  18.     // przesłonienie metod hashCode() i equals()
  19.  
  20.     // taka mała sztuczka, bez której t:selectManyCheckbox nie chce działać (opakowanie w tablicę)
  21.     public Role[] getRolesAsArray() {
  22.         return roles == null ? null : roles.toArray(new Role[0]);
  23.     }
  24.  
  25.     public void setRolesAsArray(Role[] roles) {
  26.         // błąd Toplinka: https://glassfish.dev.java.net/issues/show_bug.cgi?id=556
  27.         this.roles = new ArrayList(Arrays.asList(roles));
  28.     }
  29. }

Natomiast rola jest również prosta i jest następująca:

JAVA:
  1. @NamedQueries({
  2. @NamedQuery(name = "findAllRoles", query = "SELECT role FROM Role role"),
  3. @NamedQuery(name = "findRoleByRoleName", query = "SELECT role FROM Role role WHERE role.roleName = ?1")
  4.         })
  5. public class Role {
  6.  
  7.     @Id
  8.     @GeneratedValue(strategy = GenerationType.AUTO)
  9.     private Integer id;
  10.  
  11.     private String roleName;
  12.  
  13.     // gettery i settery
  14.  
  15.     // przesłonięcie hashCode() i equals()
  16. }

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).

JAVA:
  1. public interface UserDao {
  2.     public List<User> findAll();
  3.     public User findById(Integer id);
  4.     public User findByLogin(String login);
  5.     public void save(User user);
  6.     public void delete(User user);
  7. }
  8.  
  9. public interface RoleDao {
  10.     public List<Role> findAll();
  11.     public Role findById(Integer id);
  12.     public Role findByRoleName(String roleName);
  13.     public void save(Role role);
  14.     public void delete(Role role);    
  15. }

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).

JAVA:
  1. public class JpaUserDao extends JpaDaoSupport implements UserDao {
  2.  
  3.     public User findByLogin(String login) {
  4.         List<User> users = getJpaTemplate().findByNamedQuery("findUserByLogin", login);
  5.         if (users.size()> 1) {
  6.             throw new DataIntegrityViolationException("More than one user with the same login.");
  7.         }
  8.         return users.size() == 0 ? null : users.get(0);
  9.     }
  10.  
  11.     public List<User> findAll() {
  12.         return getJpaTemplate().findByNamedQuery("findAllUsers");
  13.     }
  14.  
  15.     @Transactional(readOnly = false, propagation = Propagation.REQUIRES_NEW)
  16.     public void save(User user) {
  17.         getJpaTemplate().merge(user);
  18.     }
  19.  
  20.     @Transactional(readOnly = false, propagation = Propagation.REQUIRES_NEW)
  21.     public void delete(User user) {
  22.         getJpaTemplate().remove(getJpaTemplate().merge(user));
  23.     }
  24.  
  25.     public User findById(Integer id) {
  26.         return getJpaTemplate().find(User.class, id);
  27.     }
  28.  
  29. }
  30.  
  31. public class JpaRoleDao extends JpaDaoSupport implements RoleDao {
  32.     // implementację można znaleźć w repozytorium lub w załączonym kodzie źródłowym    
  33. }

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.

XML:
  1. <!-- Deklaracja beana do obsługi użytkowników -->
  2. <bean id="userDao" class="com.mocnakawa.jsfcrud.data.dao.jpa.JpaUserDao">
  3.    <property name="entityManagerFactory" ref="entityManagerFactory"/>
  4. </bean>
  5.  
  6. <!-- Deklaracja beana do obsługi ról -->
  7. <bean id="roleDao" class="com.mocnakawa.jsfcrud.data.dao.jpa.JpaRoleDao">
  8.    <property name="entityManagerFactory" ref="entityManagerFactory"/>
  9. </bean>
  10.  
  11. <!-- Deklaracja EntityManagera, Spring domyślnie szuka pliku META-INF/persistence.xml i z niego bierze dane -->
  12. <bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalEntityManagerFactoryBean">
  13.    <property name="persistenceUnitName" value="JsfCrudUnit"/>
  14. </bean>
  15.  
  16. <!-- Do obsługi transakcji -->
  17. <bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager">
  18.    <property name="entityManagerFactory" ref="entityManagerFactory"/>
  19. </bean>
  20.  
  21. <!-- Transakcje będą deklarowane przez adnotacje -->
  22. <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.

XML:
  1. <persistence xmlns="http://java.sun.com/xml/ns/persistence" version="1.0">
  2.     <persistence-unit name="JsfCrudUnit" transaction-type="RESOURCE_LOCAL">
  3.         <provider>oracle.toplink.essentials.ejb.cmp3.EntityManagerFactoryProvider</provider>
  4.  
  5.         <class>com.mocnakawa.jsfcrud.data.domain.Role</class>
  6.         <class>com.mocnakawa.jsfcrud.data.domain.User</class>
  7.  
  8.         <properties>
  9.             <property name="toplink.logging.level" value="OFF"/>
  10.             <property name="toplink.jdbc.url" value="jdbc:mysql://localhost/jsfcrud"/>
  11.             <property name="toplink.jdbc.driver" value="com.mysql.jdbc.Driver"/>
  12.             <property name="toplink.jdbc.user" value="root"/>
  13.             <property name="toplink.jdbc.password" value="Twoje Hasło"/>
  14.             <property name="toplink.ddl-generation" value="create-tables"/>
  15.         </properties>
  16.     </persistence-unit>
  17. </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).

JAVA:
  1. // Klasa typowa dla akcji typu CRUD
  2. // Jest to dość proste, więc mam nadzieje, że kod się sam dokumentuje
  3. public class UserController {
  4.  
  5.     // wstrzykniete przez Springa
  6.     private UserDao userDao;
  7.  
  8.     private User user;
  9.     private boolean editMode = false;
  10.  
  11.     public List<User> getUsers() {
  12.         return userDao.findAll();
  13.     }
  14.  
  15.     public void editSetup(ActionEvent event) {
  16.         user = FacesUtils.getActionAttribute(event, "user", User.class);
  17.         editMode = true;
  18.     }
  19.  
  20.     public String update() {
  21.         userDao.save(user);
  22.         return Constants.USER_AND_ROLE_LIST;
  23.     }
  24.  
  25.     public String createSetup() {
  26.         user = new User();
  27.         editMode = false;
  28.         return Constants.USER_FORM;
  29.     }
  30.  
  31.     public String create() {
  32.         User userToCreate = userDao.findByLogin(user.getLogin());
  33.         if (userToCreate == null) {
  34.             userDao.save(user);
  35.             return Constants.USER_AND_ROLE_LIST;
  36.         } else {
  37.             FacesUtils.addErrorMessage("Login already exists");
  38.             return null;
  39.         }
  40.     }
  41.  
  42.     public void delete(ActionEvent event) {
  43.         userDao.delete(FacesUtils.getActionAttribute(event, "user", User.class));
  44.     }
  45.    
  46.     // gettery i settery
  47. }

Kontrolera dla ról celowo pominąłem, można go znaleźć w załączonym kodzie źródłowym. Pora na web.xml.

XML:
  1. <!-- Listner dla Springa -->
  2. <listener>
  3.     <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
  4. </listener>
  5.  
  6. <!-- Konfiguracja Facelets -->
  7. <context-param>
  8.     <param-name>facelets.REFRESH_PERIOD</param-name>
  9.     <param-value>2</param-value>
  10. </context-param>
  11.  
  12. <context-param>
  13.     <param-name>facelets.DEVELOPMENT</param-name>
  14.     <param-value>true</param-value>
  15. </context-param>
  16.  
  17. <context-param>
  18.     <param-name>javax.faces.DEFAULT_SUFFIX</param-name>
  19.     <param-value>.xhtml</param-value>
  20. </context-param>
  21.  
  22.  <!-- Konfiguracja Springa -->
  23. <context-param>
  24.     <param-name>contextConfigLocation</param-name>
  25.     <param-value>/WEB-INF/applicationContext*.xml</param-value>
  26. </context-param>
  27.  
  28. <filter>
  29.     <filter-name>RequestContextFilter</filter-name>
  30.     <filter-class>org.springframework.web.filter.RequestContextFilter</filter-class>
  31. </filter>
  32.  
  33. <filter-mapping>
  34.     <filter-name>RequestContextFilter</filter-name>
  35.     <url-pattern>*.jsf</url-pattern>
  36. </filter-mapping>
  37.  
  38.  <!-- Konfiguracja kontrolera JSF -->
  39. <servlet>
  40.     <servlet-name>Faces Servlet</servlet-name>
  41.     <servlet-class>javax.faces.webapp.FacesServlet</servlet-class>
  42.     <load-on-startup>1</load-on-startup>
  43. </servlet>
  44.  
  45. <servlet-mapping>
  46.     <servlet-name>Faces Servlet</servlet-name>
  47.     <url-pattern>*.jsf</url-pattern>
  48. </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:

XML:
  1. <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
  2.        "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
  3. <html xmlns="http://www.w3.org/1999/xhtml"
  4.       xmlns:h="http://java.sun.com/jsf/html"
  5.       xmlns:f="http://java.sun.com/jsf/core"
  6.       xmlns:ui="http://java.sun.com/jsf/facelets">
  7.  
  8. <head>
  9.     <title>
  10.         <ui:insert name="title">Default title</ui:insert>
  11.     </title>
  12. </head>
  13. <body>
  14.  
  15. <h1>
  16.     <ui:insert name="title">Default title</ui:insert>
  17. </h1>
  18.  
  19. <ui:insert name="content"/>
  20. <ui:include src="footer.xhtml"/>
  21.  
  22. </body>
  23. </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.

XML:
  1. <!-- Wskazanie szablonu, z którego będziemy korzystać -->
  2. <ui:composition template="/pages/layout.xhtml">
  3.  
  4.     <!-- definiowanie tytułu, który będzie później wstawiony za pomocą ui:insert -->
  5.     <ui:define name="title">Users CRUD</ui:define>
  6.  
  7.     <!-- definiowanie zawartości strony -->
  8.     <ui:define name="content">
  9.         <h:form>
  10.             <h:dataTable value="#{userController.users}" var="user">
  11.                 <h:column>
  12.                     <f:facet name="header">Login</f:facet>
  13.                     #{user.login}
  14.                 </h:column>
  15.                 <h:column>
  16.                     <f:facet name="header">Password</f:facet>