czwartek, 5 lutego 2009

aplikacja JSF i EJB 3.0 w JBoss Tools

W dzisiejszym odcinku, stworzymy aplikację webową (a jakże) korzystającą z EJB 3.0 i JSF. Ktoś może się zapytać po co? Przecież mamy znakomity szkielet na literę S... Owszem mamy, ale może nie znamy, albo może nie potrzebujemy jego możliwości... Różnie to w życiu bywa. Poza tym modułowa budowa, którą narzuca nam podział aplikacji na webową i biznesową wydaje się całkiem rozsądnym rozwiązaniem.
Zobaczmy zatem, co i jak... Do pracy nad aplikacją użyję środowiska Eclipse 3.3 Europa z zainstalowaną wtyczką JBoss Tools 2.1.2.GA (ostatnia finalna wersja). Jako środowisko uruchomieniowe posłuży mi kontener JBoss AS 4.2.0.GA. Bazą danych będzie MySQL 5.0.
Strukturę aplikacji tworzy archiwum EAR (Enterprise Application Archive) zawierające aplikację EJB (archiwum ejb-jar) oraz aplikację webową (archiwum war).


Ten na pozór skomplikowany podział może okazać się bardzo przydatny w większych, żeby nie powiedzieć korporacyjnych rozwiązaniach. Z warstwy usług zaimplementowanej w aplikacji EJB korzystać może nie tylko nasza aplikacja JSF, ale także inne, niekoniecznie nawet webowe aplikacje.

Stworzona aplikacja, będzie typową aplikacją CRUD, umożliwiającą tworzenie, odczyt, aktualizację i kasowanie pracowników. Potrzebować zatem będziemy 3 projekty. Projekt ejb, jsf oraz zawierający je projekt aplikacji ear.
Zacznijmy od EJB. Klikam File / New / Other...


i z listy wybieram EJB Project.


Podaję nazwę projektu ejbProject i klikam Next > Na następnej zakładce zaznaczam EJB module Java i Java Persistence


klikam 2 razy Next >, odznaczam create orm.xml (będę używał adnotacji) i klikam Finish


Tworzę w projekcie encję Employee

package pl.matt.model;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.NamedQueries;
import javax.persistence.NamedQuery;

@NamedQueries({
@NamedQuery(name = "Employee.findAllOrderByName", query = "FROM Employee e ORDER BY e.lastName, e.firstName")
})
@Entity
public class Employee {

private int id;
private String password;
private String username;
private String firstName;
private String lastName;

@Id
@GeneratedValue
public int getId() {
return id;
}

public void setId(int id) {
this.id = id;
}

@Column(nullable = false)
public String getPassword() {
return password;
}

public void setPassword(String password) {
this.password = password;
}

@Column(nullable = false)
public String getUsername() {
return username;
}

public void setUsername(String username) {
this.username = username;
}

@Column(nullable = false)
public String getFirstName() {
return firstName;
}

public void setFirstName(String firstName) {
this.firstName = firstName;
}

@Column(nullable = false)
public String getLastName() {
return lastName;
}

public void setLastName(String lastName) {
this.lastName = lastName;
}

}



warstwę dostępu do danych (DAO):


package pl.matt.dao;

import java.util.List;

import pl.matt.model.Employee;

public interface EmployeeDao {

/**
* @return
*/
public List<Employee> getAllOrderByName();
/**
* @param employee
* @return
*/
public Employee create(Employee employee);

/**
* @param employee
*/
public void update(Employee employee);

/**
* @param employeeId
* @return
*/
public Employee load(int employeeId);

/**
* @param employeeId
* @return
*/
public void delete(Employee employee);

}



package pl.matt.dao.impl;

import java.util.List;

import javax.ejb.Stateless;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;

import pl.matt.dao.EmployeeDao;
import pl.matt.model.Employee;

@Stateless
public class JpaEmployeeDao implements EmployeeDao {

@PersistenceContext
private EntityManager entityManager;

@SuppressWarnings("unchecked")
public List<Employee> getAllOrderByName() {
return entityManager.createNamedQuery("Employee.findAllOrderByName").getResultList();
}

public Employee create(Employee employee) {
entityManager.persist(employee);
return employee;
}

public void update(Employee employee) {
entityManager.merge(employee);
}

public Employee load(int employeeId) {
return entityManager.find(Employee.class, employeeId);
}

public void delete(Employee employee) {
entityManager.remove(employee);

}

}


i może trochę na wyrost warstwę usług biznesowych

package pl.matt.service;

import java.util.List;

import pl.matt.model.Employee;

public interface EmployeeService {
/**
* @return
*/
public List<Employee> getAllOrderByName();
/**
* @param employee
* @return
*/
public Employee create(Employee employee);

/**
* @param employee
*/
public void update(Employee employee);

/**
* @param employeeId
* @return
*/
public Employee load(int employeeId);

/**
* @param employeeId
*/
public void delete(int employeeId);
}



package pl.matt.service.impl;

import java.util.List;

import javax.ejb.EJB;
import javax.ejb.Stateless;

import pl.matt.dao.EmployeeDao;
import pl.matt.model.Employee;
import pl.matt.service.EmployeeService;

@Stateless
public class EmployeeServiceImpl implements EmployeeService {

@EJB
private EmployeeDao employeeDao;

public Employee create(Employee employee) {
return employeeDao.create(employee);
}

public List<Employee> getAllOrderByName() {
return employeeDao.getAllOrderByName();
}

public void update(Employee employee) {
employeeDao.update(employee);

}

public Employee load(int employeeId) {
return employeeDao.load(employeeId);
}

public void delete(int employeeId) {
Employee employee = load(employeeId);
if (employee != null) {
employeeDao.delete(employee);
}
}

}


Ponieważ korzystam z bazy danych za pośrednictwem JPA, nie obejdzie się bez pliku META-INF/persistence.xml:

<?xml version="1.0" encoding="UTF-8"?>
<persistence xmlns="http://java.sun.com/xml/ns/persistence"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.0"
xsi:schemaLocation="http://java.sun.com/xml/ns/persistence
http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd">


<persistence-unit name="seam_war_project" transaction-type="JTA">
<provider>org.hibernate.ejb.HibernatePersistence</provider>
<jta-data-source>java:/empAppDatasource</jta-data-source>
<properties>
<property name="hibernate.dialect" value="org.hibernate.dialect.MySQLDialect"/>
<property name="hibernate.hbm2ddl.auto" value="update"/>
<property name="hibernate.show_sql" value="true"/>
</properties>
</persistence-unit>

</persistence>


potrzebuję też źródła danych (Data Source) empAppDatasource. Tworzę ją w pliku empApp-ds.xml

<?xml version="1.0" encoding="UTF-8"?>

<!DOCTYPE datasources
PUBLIC "-//JBoss//DTD JBOSS JCA Config 1.5//EN"
"http://www.jboss.org/j2ee/dtd/jboss-ds_1_5.dtd">

<datasources>

<local-tx-datasource>
<jndi-name>empAppDatasource</jndi-name>
<connection-url>jdbc:mysql://localhost:3306/jpabasics?characterEncoding=latin2</connection-url>
<driver-class>com.mysql.jdbc.Driver</driver-class>
<user-name>root</user-name>
<password>root</password>
</local-tx-datasource>

</datasources>

który umieszczam w katalogu server/default/deploy/ JBossa.

Przejdźmy do aplikacji JSF. Tworzę ją wybierając z menu Eclipse File / New / Other... / JSF Project



Jako środowisko JSF ustawiam JSF 1.2 z Faceletami, podaję nazwę projektu jsfModule i klikam Finish.

W projekcie tworzę prosty szablon stron WebContent/templates/common.xhtml

<!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:ui="http://java.sun.com/jsf/facelets"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core">

<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title><ui:insert name="pageTitle">Page Title</ui:insert></title>
<style type="text/css">
body {
font-family: Verdana, Arial, Helvetica, sans-serif;
font-size: 14px;
}

.header {
font-family: Verdana, Arial, Helvetica, sans-serif;
font-size: 18px;
}

.bottom {
font-family: Verdana, Arial, Helvetica, sans-serif;
font-size: 9px;
text-align: center;
vertical-align: middle;
color: #8E969D;
}

td.column1 {
width: 15%;
}

td.column2 {
width: 25%;
}

td.column3 {
width: 60%;
}
</style>
</head>

<body bgcolor="#ffffff">
<table style="border: 1px solid #CAD6E0" align="center" cellpadding="0"
cellspacing="0" border="0" width="400">
<tbody>

<tr>
<td class="header" height="42" align="center" valign="middle"
width="100%" bgcolor="#E4EBEB"><ui:insert name="pageHeader">Page Header</ui:insert>
</td>
</tr>
<tr>
<td height="1" width="100%" bgcolor="#CAD6E0"></td>
</tr>

<tr>
<td width="100%" colspan="2">
<table width="100%" style="height: 150px" align="left"
cellpadding="0" cellspacing="0" border="0">
<tbody>
<tr>
<td align="left" width="800px" valign="middle"><ui:insert
name="body">Page Body</ui:insert></td>
</tr>
</tbody>
</table>
</td>
</tr>

<tr>
<td colspan="2" valign="bottom" height="1" width="100%"
bgcolor="#CAD6E0"></td>
</tr>
</tbody>
</table>
</body>

</html>


plik wyświetlający listę pracowników WebContent/pages/employeeList.xhtml

<!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:ui="http://java.sun.com/jsf/facelets"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:c="http://java.sun.com/jstl/core">

<f:loadBundle basename="resources" var="msg" />

<ui:composition template="/templates/common.xhtml">

<ui:define name="pageTitle">Lista pracowników</ui:define>

<ui:define name="pageHeader">Lista pracowników</ui:define>

<ui:define name="body">
<div style="text-align: center"><h:form>
<h:commandLink action="ADD_EMP" value="Dodaj pracownika" />
<br />
<br />
<ui:repeat value="#{employeeBean.employees}" var="emp">
<h:outputLink value="editEmployee.jsf?employeeId=#{emp.id}">
<h:outputText value="#{emp.firstName} #{emp.lastName}" />
</h:outputLink>
<h:outputText value=" " />
<h:commandLink action="#{employeeBean.delete}" value="[usuń]">
<f:param name="toDelete" value="#{emp.id}" />
</h:commandLink>
<br />
</ui:repeat>

</h:form></div>

</ui:define>
</ui:composition>
</html>


oraz plik umożliwiający edycję pojedynczego pracownika WebContent/pages/editEmployee.xhtml

<!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:ui="http://java.sun.com/jsf/facelets"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:c="http://java.sun.com/jstl/core">

<f:loadBundle basename="resources" var="msg" />

<ui:composition template="/templates/common.xhtml">

<ui:define name="pageTitle">Edycja</ui:define>

<ui:define name="pageHeader">Edycja pracownika</ui:define>

<ui:define name="body">
<h:outputLink value="employeeList.jsf">Powrót</h:outputLink>
<h:form id="employeeForm">
<h:inputHidden id="employeeId" value="#{employeeBean.employee.id}" />
<h:panelGrid columns="3" width="800"
columnClasses="column1,column2,column3">
<h:outputText value="Imię" />
<h:inputText value="#{employeeBean.employee.firstName}"
id="firstName" required="true" />
<h:message for="firstName" style="color:red;" />
<h:outputText value="Nazwisko" />
<h:inputText value="#{employeeBean.employee.lastName}" id="lastName"
required="true" />
<h:message for="lastName" style="color:red;" />
<h:outputText value="Login" />
<h:inputText value="#{employeeBean.employee.username}" id="username"
required="true" />
<h:message for="username" style="color:red;" />
<h:outputText value="haslo" />
<h:inputSecret id="password"
value="#{employeeBean.employee.password}" required="true" redisplay="true" />
<h:message for="password" style="color:red;" />
<h:commandButton action="#{employeeBean.save}" value="zapisz" />
</h:panelGrid>
<h:outputLink value="employeeList.jsf">Powrót</h:outputLink>
</h:form>
</ui:define>
</ui:composition>
</html>


Potrzebne będzie też ziarno zarządzane JSF

package pl.matt.view;

import java.util.List;

import javax.ejb.EJB;
import javax.faces.context.FacesContext;

import pl.matt.model.Employee;
import pl.matt.service.EmployeeService;

public class EmployeeBean {

private List<Employee> employees;
private Employee employee;
private Integer employeeId;

@EJB(name="eeProject/EmployeeServiceImpl/local")
private EmployeeService employeeService;

public List<Employee> getEmployees() {
if (employees == null) {
employees = employeeService.getAllOrderByName();
}
return employees;
}

public Employee getEmployee() {
if (employee == null) {
if (employeeId != null) {
employee = employeeService.load(employeeId);
} else {
employee = new Employee();
}
}
return employee;
}

public void setEmployee(Employee employee) {
this.employee = employee;
}

public String save() {
if (employee.getId() > 0) {
employeeService.update(employee);
} else {
employeeService.create(employee);
}
employees = null;
return "LIST_EMP";
}

public Integer getEmployeeId() {
return employeeId;
}

public void setEmployeeId(Integer employeeId) {
this.employeeId = employeeId;
}

public String delete() {
System.out.println("EmployeeBean.delete()");
Integer id = Integer.valueOf(
FacesContext.getCurrentInstance().getExternalContext().getRequestParameterMap().get("toDelete"));
if (id != null) {
employeeService.delete(id);
}
employees = null;
return "LIST_EMP";
}

}



JBoss umieszcza w interfejsie JNDI ziarna EJB korzystając z trochę dziwnej konwencji:
nazwaModułu/klasaZiarna/local|remote stąd konieczność podania parametru name
@EJB(name="eeProject/EmployeeServiceImpl/local")

przy adnotacji @EJB.
Jak będzie wyglądała sytuacja w momencie, kiedy ziarno EJB będzie implementowało kilka interfejsów zdalnych lub lokalnych? Niestety nie mam pojęca.

Do kompletu brakuje jeszcze pliku WebContent/WEB-INF/faces-config.xml

<?xml version="1.0" encoding="UTF-8"?>
<faces-config version="1.2" xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xi="http://www.w3.org/2001/XInclude"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-facesconfig_1_2.xsd">
<managed-bean>
<managed-bean-name>employeeBean</managed-bean-name>
<managed-bean-class>pl.matt.view.EmployeeBean</managed-bean-class>
<managed-bean-scope>request</managed-bean-scope>
<managed-property>
<property-name>employeeId</property-name>
<value>#{param.employeeId}</value>
</managed-property>
</managed-bean>
<navigation-rule>
<navigation-case>
<from-outcome>ADD_EMP</from-outcome>
<to-view-id>/pages/editEmployee.xhtml</to-view-id>
</navigation-case>
<navigation-case>
<from-outcome>LIST_EMP</from-outcome>
<to-view-id>/pages/employeeList.xhtml</to-view-id>
</navigation-case>
</navigation-rule>
<application>
<view-handler>com.sun.facelets.FaceletViewHandler</view-handler>
</application>
</faces-config>


oraz WebContent/WEB-INF/web.xml

<?xml version="1.0"?>
<web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
<description>Facelets StarterKit</description>
<display-name>jsfModule</display-name>
<context-param>
<param-name>javax.faces.DEFAULT_SUFFIX</param-name>
<param-value>.xhtml</param-value>
</context-param>
<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.STATE_SAVING_METHOD</param-name>
<param-value>client</param-value>
</context-param>
<context-param>
<param-name>com.sun.faces.validateXml</param-name>
<param-value>true</param-value>
</context-param>
<context-param>
<param-name>com.sun.faces.verifyObjects</param-name>
<param-value>true</param-value>
</context-param>
<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>
<login-config>
<auth-method>BASIC</auth-method>
</login-config>
</web-app>


Aplikacja JSF gotowa. Aplikacja EJB gotowa. Czas je wyswatać i pożenić. Pomoże nam w tym projekt EAR. Z menu kontekstowego Eclipse wybieram zatem File / New / Other... / Enterprise Application Project.


Podaję nazwę projektu eeProject klikam 2 razy Next > i wybieram oba stworzone uprzednio moduły. Będą to składowe naszej aplikacji. Dodatkowo zaznaczam opcję Generate Deployment Descriptor i klikam Finish.



W tym projekcie znajduje się tylko plik EarContent/META-INF/application.xml.


<?xml version="1.0" encoding="UTF-8"?>
<application xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:application="http://java.sun.com/xml/ns/javaee/application_5.xsd" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/application_5.xsd" id="Application_ID" version="5">
<module>
<ejb>ejbProject.jar</ejb>
</module>
<module>
<web>
<web-uri>jsfModule.war</web-uri>
<context-root>jsfModule</context-root>
</web>
</module>
</application>


Plik ten opisuje moduły naszej aplikacji. Moduł webowy jsfModule i moduł EJB ejbProject.jar. Wdrożenie aplikacji pozostawiam wtyczce JBoss Tools. Startuję więc serwer i... aplikacja działa.



Kod źródłowy powyższej aplikacji dostępny jest tutaj.

Cóż, pora jechać na narty...

7 komentarzy:

Jacek Laskowski pisze...

Zastanawiam się nad adnotacją @EJB. Potrzebna była deklaracja atrybutu name?

I takie detale - findAllOrderByName - zmieniłbym na findAllOrdersByName oraz EmployeeDao.load na EmployeeDao.read, aby odpowiadało CRUD.

p.s. Jak tam jazda na nartach? Gdzie byłeś?

Jacek

MZ pisze...

potrzebna, gdyż bez niej referencja do ziarna EJB jest NULLem.

Na nartach bardzo fajnie. Byłem w Krynicy, kilka wywrotek było, ale kolejny sezon bez złamań zaliczony.

MZ pisze...

Wersja działająca z JBossem 5.0.1 znajduje się pod adresem:

https://sites.google.com/site/najawie/Home/jsfEjbJBoss5_20090610.zip?attredirects=0

Anonimowy pisze...

Świetny tutorial który bardzo mi pomógł. Dzięki!

Anonimowy pisze...

mi też pomógł w napisaniu pracy inżynierskiej

Michal Panek pisze...

moglbys skomentowac jak wyglada twoja baza danych tych pracownikow? 2 lata 3 lata to kupa czasu, ale moze jeszcze to odczytasz :P

MZ pisze...

Co masz na myśli?

Baza wygląda standardowo. Jest tabela z pracownikami.

Jeśli potrzebujesz szczegółowych informacji to proponuję uruchomić przykładową aplikację i zobaczyć strukturę bazy.