Sonntag, 19. September 2010

KISS like CDI

Java EE 5 hat die Entwicklung von Webanwendungen mit Java ServerFaces (JSF) standardisiert. Der Web Beans Standard, der durch JBoss Seam positiv beeinflusst wurde, ist in Java EE 5 allerdings noch nicht vollständig umgesetzt. JBoss Seam hat große Popularität im Kreis der Anwendungsentwickler für Websysteme erworben. Wesentlich für den Erfolg von JBoss Seam sind die Integration von JSF und EJB 3, Bijection, deklaratives Statusmanagement, Workspace Management und die Nutzung von Annotationen zur Entwicklung von Webanwendungen. Das Konzept der Konversationen ist in JBoss Seam bereits umgesetzt, sodass die Programmierung von Wizzards stattfinden kann. Bookmarking, verbessertes Session-Handling und nicht zuletzt der Webframework JSF tragen zu dem Erfolg von JBoss Seam bei.

Contexts and Dependency Injection (CDI) ist im JSR 330 mit Relation zum JSR 299 (Web Beans) zusammengefasst und ein Teil des Java EE 6 Standards. CDI verbindet JSF 2 mit der EJB-Schicht. Beide Technologien implementieren "convention over configuration". Durch diesen Grundsatz und neue Navigationsregeln wird die "faces-config.xml" in vielen Fällen überflüssig. Die Implementierung von Webapplikationen ist in Java EE 6 deutlich einfacher als in der Vorgängerversion. JSF 2 beinhaltet Facelets als standardisierte View-Technik. Die Verwaltung der Ressourcen in einer WAR Datei ist auf ein definiertes Ressourceverzeichnis festgelegt. In einer WAR Datei werden im aktuellen Standard EJBs, Managed Beans und XHTML-Seiten untergebracht. Das Verpacken eines WARs und eines EJB-JARs in einem EAR ist deshalb für ein anschliessendes Deployment nicht mehr notwendig.

Java EE 6 Stack mit CDI
Der Blogbeitrag implementiert ein Szenario mit JSF 2, CDI, EJB 3.1 und der JPA. Wie einfach die Programmierung von Konversationen geworden ist und wie wenig Quellcode erstellt wird, zeigt die Implementierung eines Wizzards für eine einfache Teilnehmerregistrierung. Die Teilnehmerregistrierung wird als WAR Datei bereitgestellt und verwendet keine JSF 2 spezifischen Konfigurationsdateien.

Architektur des Wizzards für die Teilnehmerregistrierung

Der Quellcode wird in diesem Beitrag nur auszugsweise vorgestellt, um einen Eindruck über die Leichtigkeit der Implementierung einer Konversation in Form eines Wizzards zu vermitteln. Die Validierungen (JSR 303) sind nur beispielhaft und ohne tieferen fachlichen Sinn in dem Szenario implementiert.

Erste XHTML-Seite des Wizzards

<?xml version='1.0' encoding='UTF-8' ?>
<!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">
    <h:head>
        <title>Attendant Registration</title>
    </h:head>
    <h:body>
        <h3>Enter Attendant Data (Page 1 of 2)</h3>
        <h:form>
            <h:panelGrid columns="2">
                <h:outputLabel for="firstName" value="First Name"/>
                <h:inputText id="firstName" value="#{attendant.firstName}"/>
                <h:outputLabel for="lastName" value="Last Name"/>
                <h:inputText id="lastName" value="#{attendant.lastName}"/>
                <h:panelGroup/>
                <h:commandButton value="Next" action="#{attendantController.navigateToAddressPage}"/>
            </h:panelGrid>
        </h:form>
    </h:body>
</html>

Zweite XHTML-Seite des Wizzards

<?xml version='1.0' encoding='UTF-8' ?>
<!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">
    <h:head>
        <title>Attendant Registration</title>
    </h:head>
    <h:body>
        <h3>Enter Attendant Data (Page 2 of 2)</h3>
        <h:form>
            <h:panelGrid columns="2">
                <h:outputLabel for="zipcode" value="Zipcode"/>
                <h:inputText id="zipcode" value="${attendant.zipcode}"/>               
                <h:outputLabel for="city" value="City"/>
                <h:inputText id="city" value="${attendant.city}"/>
                <h:outputLabel for="street" value="Street"/>
                <h:inputText id="street" value="${attendant.street}"/>              
                <h:commandButton value="Previous" action="#{attendantController.navigateToNamePage}"/>
                <h:commandButton value="Next" action="#{attendantController.navigateToConfirmationPage}"/>
            </h:panelGrid>
        </h:form>
    </h:body>
</html>

Controller des Wizzards

package ccd.jee.jsf.attendant.controller;

import java.io.Serializable;
import javax.ejb.EJB;
import javax.enterprise.context.Conversation;
import javax.enterprise.context.RequestScoped;
import javax.inject.Inject;
import javax.inject.Named;
import ccd.jee.jsf.attendant.model.Attendant;
import ccd.jee.session.bean.attendant.dao.AttendantDAO;

@Named
@RequestScoped
public class AttendantController implements Serializable {

    enum NAVIGATION_RULES {
  
        NAME_PAGE("name-page"),
        ADDRESS_PAGE("address-page"),
        CONFIRMATION_PAGE("confirmation-page");
      
        private final String page;
      
        private NAVIGATION_RULES(String page) {
          
            this.page = page;
        }      
    }
  
    private final NAVIGATION_RULES navRuleName = NAVIGATION_RULES.NAME_PAGE;
    private final NAVIGATION_RULES navRuleAddress = NAVIGATION_RULES.ADDRESS_PAGE;
    private final NAVIGATION_RULES navRuleConfirmation = NAVIGATION_RULES.CONFIRMATION_PAGE;
  
    @EJB
    private AttendantDAO attendantDAO;

    @Inject
    private Conversation conversation;
  
    @Inject
    private Attendant attendant;

    public String attendantConversation() {
      
        startConversation();     
        return navRuleName.page;
    }

    public String navigateToNamePage() {     
   
        return navRuleName.page;
    }

    public String navigateToAddressPage() {      
      
        return navRuleAddress.page;
    }

    public String navigateToConfirmationPage() {

        endConversation();              
        return navRuleConfirmation.page;
    }
  
    private void startConversation() {
  
        conversation.begin();
    }
  
    private void endConversation() {
      
        attendantDAO.store(attendant);
        conversation.end();
    }
}

Managed Bean des Wizzards

package ccd.jee.jsf.attendant.model;

import java.io.Serializable;
import javax.enterprise.context.ConversationScoped;
import javax.inject.Named;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;

@Named
@ConversationScoped
@Entity
public class Attendant implements Serializable {
   
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private int id;
   
    @NotNull(message="First name must be typed!")
    @Size(min=2, max=40, message="First name between 2 and 40 characters!")
    private String firstName;
      
    @NotNull(message="Last name must be typed!")
    @Size(min=2, max=40, message="Last name between 2 and 40 characters!")
    private String lastName;
   
    private String zipcode;
   
    @NotNull(message="City must be typed!")
    @Size(min=2, max=40, message="City between 2 and 40 characters!")
    private String city;   
   
    @NotNull(message="Street must be typed!")
    @Size(min=2, max=40, message="Street between 2 and 40 characters!")   
    private String street;
   
    public String getCity() {
        return city;
    }

    public void setCity(String city) {
        this.city = city;
    }

    public String getFirstName() {
        return firstName;
    }

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

    public String getLastName() {
        return lastName;
    }

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

    public void setZipcode(String zipcode) {
        this.zipcode = zipcode;
    }

    public String getStreet() {
        return street;
    }

    public void setStreet(String street) {
        this.street = street;
    }
   
    public int getId() {
        return id;
    }
}

DAO-Schnittstelle des Wizzards

package ccd.jee.session.bean.attendant.dao;

import javax.ejb.Local;
import ccd.jee.jsf.attendant.model.Attendant;

@Local
public interface AttendantDAO {

     public void store(Attendant attendant);
}

DAO-Implementierung des Wizzards

package ccd.jee.session.bean.attendant.dao;

import javax.ejb.Stateless;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import ccd.jee.jsf.attendant.model.Attendant;

@Stateless
public class AttendantDAOBean implements AttendantDAO {

    @PersistenceContext
    private EntityManager em;

    @Override
    public void store(Attendant attendant) {
       
        final Attendant newAttendant = assembleAttendant(attendant);       
        storeAttendant(newAttendant);           
    }

    private void storeAttendant(final Attendant newAttendant) {
       
        em.persist(newAttendant);      
    }

    private Attendant assembleAttendant(Attendant attendant) {
       
        final Attendant newAttendant = new Attendant();
        newAttendant.setFirstName(attendant.getFirstName());
        newAttendant.setLastName(attendant.getLastName());
        newAttendant.setZipcode(attendant.getZipcode());
        newAttendant.setStreet(attendant.getStreet());
        newAttendant.setCity(attendant.getCity());
       
        return newAttendant;
    }
}

Interessant bei der Implementierung des Wizzards ist der Controller in dem die Konversation geöffnet und geschlossen wird, sowie das Managed Bean. Das Managed Bean wird per CDI in den Controller injiziert und in den XHTML-Seiten mit den Feldern des Beans verknüpft. Die Felder des Managed Beans werden während der Konversation mit Daten befüllt und am Ende per DAO in die Datenbank geschrieben. Die Annotierung des Managed Beans als Entity stellt kein Problem dar und reduziert den Implementierungsaufwand. CDI wirkt sich deshalb sehr positiv auf das DRY-Prinzip aus, weil keine Redundanzen zu programmieren sind. In Vorgängerversionen des Java Enterprise Standards musste man noch mit Value Objects (bzw. Data Transfer Objects) arbeiten. Zur Assemblierung wurde ein Value Object Assembler eingesetzt, dass Lookup der Beans war aufwendiger als Annotierungen zu verwenden und Konversationen konnten nur sehr viel mühsamer mit dem erweiterten Persistenzkontext programmiert werden.

In der Summe vereinfacht der Java EE 6 Standard die Programmierung von Webapplikationen mit JSF 2, CDI und EJB 3.1 deutlich, sodass man sich bewusst die Frage stellen darf, was in der nächsten Version des Standards noch erreicht werden soll. Der gute Eindruck von Java EE 5 wird durch die Java EE 6 Technologien nochmals verbessert, sodass man dem aktuellen Standard einen hohen Reifegrad zuordnen kann.

CDI is a KISS Framework and amazingly simple!


Der Rechtshinweis des Java Blog für Clean Code Developer ist bei der Verwendung und Weiterentwicklung des Quellcodes des Blogeintrages zu beachten.