Donnerstag, 26. August 2010

Time for REST or work as usual?

Im Java EE Web Services Umfeld hat sich besonders in Bezug auf RESTful Web Services sehr viel getan. Java EE 5 platziert REST neben SOAP basierten Web Services ohne die Tiefe der Entwicklung von SOAP und dem I-Stack erreicht zu haben.

REST ist eine interessante Web Services Technologie, die altbewährte Konzepte des WWW und das Transportprotokoll HTTP nutzt. REST Web Services sind leichtgewichtig und unabhängig von XML, sodass auch einfache Texte, JSON-Daten und die vielfältigen anderen MIME-Typen als Payload ohne den Overhead eines XML-spezifischen SOAP-Envelopes transportiert werden können.

Das Formular eines einfachen HTML-Dokumentes, eine AJAX oder DWR Routine, eine Business Komponente oder ein Rich Client können als REST-Clients fungieren. REST Web Services sind, wie gewöhnliche HTML-Seiten, mit einer URI adressierbar, vor haltbar wie HTML-Seiten (Caching), mit einer einheitlichen Schnittstelle (GET, POST, PUT, DELETE) versehen und statuslos. REST nutzt alle diejenigen Konzepte die das WWW erfolgreich gemacht haben.

RESTful Web Services in Java EE 5 sind mit @WebServiceProvider zu annotieren und müssen die Provider-Schnittstelle implementieren.Clientseitig kann der RESTful Web Service mit der generischen Dispatch-Schnittstelle aufgerufen werden. Eine standardisierte REST-Programmierschnittstelle wie für SOAP-basierte Web Services (JAX-WS) ist in Java EE 5 nicht enthalten.

Exemplarisches Beispiel eines Session Beans als RESTful Web Service Endpoint in Java EE 5:

@Stateless
@WebServiceProvider
@ServiceMode(value = Service.Mode.PAYLOAD)
@BindingType(value = HTTPBinding.HTTP_BINDING)
public class WSRestBean implements Provider<Source> {
   
    enum HTTP_VERB {
       
        GET("GET","Payload for GET-Response"),
        POST("POST","Payload for POST-Response"),
        DELETE("DELETE","Payload for DELETE-Response"),
        PUT("PUT","Payload for PUT-Response");
       
        private final String name;
        private final String response;
       
        private HTTP_VERB(String name, String response) {
           
            this.name = name;
            this.response = response;               
        }
    }
       
    @Resource
    private WebServiceContext wsCtx;
   
    @Override
    public Source invoke(Source source) {
       
        final String httpVerb = getHttpVerbFromMessageContext();
       
        if(httpVerb.equals(HTTP_VERB.GET.name)) {
                       
            return buildResponseStreamSource(HTTP_VERB.GET);
        }
        else if(httpVerb.equals(HTTP_VERB.POST.name)) {
                       
            return buildResponseStreamSource(HTTP_VERB.POST);
           
        }       
        else if(httpVerb.equals(HTTP_VERB.DELETE.name)) {
                       
            return buildResponseStreamSource(HTTP_VERB.DELETE);
        }
        else if(httpVerb.equals(HTTP_VERB.PUT.name)) {
                       
            return buildResponseStreamSource(HTTP_VERB.PUT);
        }
       
        throw new HTTPException(405);
    }
   
    private String getHttpVerbFromMessageContext() {
      
        final MessageContext ctx = wsCtx.getMessageContext();
        String httpVerb = (String) ctx.get(MessageContext.HTTP_REQUEST_METHOD);      
        httpVerb = httpVerb.trim().toUpperCase();
      
        return httpVerb;
    }
 
    private StreamSource buildResponseStreamSource(HTTP_VERB httpVerb) {
       
        printMethod(httpVerb);

        final String xmlMessage = getXmlResponseMessageByHttpVerb(httpVerb);       
        final ByteArrayInputStream stream = new ByteArrayInputStream(xmlMessage.getBytes());
   
        return new StreamSource(stream);
     }

    private String getXmlResponseMessageByHttpVerb(HTTP_VERB httpVerb) {
               
        switch(httpVerb) {
       
          case GET:
             
              return getXmlResponseMessage(HTTP_VERB.GET);
             
          case POST:
             
              return getXmlResponseMessage(HTTP_VERB.POST);
             
          case DELETE:
             
              return getXmlResponseMessage(HTTP_VERB.DELETE);
             
          case PUT:
             
              return getXmlResponseMessage(HTTP_VERB.PUT);             
        }
       
        throw new HTTPException(405);
    }

    private String getXmlResponseMessage(HTTP_VERB httpVerb) {
       
        return "<rest:response xmlns:rest='urn:rest'>"
             + httpVerb.response
             + "</rest:response>";
    }
   
    private void printMethod(HTTP_VERB httpVerb) {
       
        System.out.println("REST Service received - " + httpVerb.name);       
    }
}

Der Einfachheit und Leistungsfähigkeit von RESTful Web Services ist es zu verdanken, dass sich neben JAX-WS in Java EE 6 ein weiterer Standard JAX-RS gesellt. Die JAX-RS Implementierung RESTEasy, die von Bill Burke entwickelt wurde, ist Teil des JBoss Stacks. Bill Burke hat im Rahmen der JAX-RS Expert Group die JAX-RS Spezifikation mitentwickelt.

Sein Buch RESTful Java with JAX-RS beschreibt detailliert die JAX-RS Schnittstelle und Programmierung von RESTful Web Services mit der Programmiersprache Java. Das Buch beinhaltet auch eine Art Workbook in dem die JAX-RS Beispiele der RESTEasy Distribution im Detail erläutert  werden. Entwickler, die sich für JAX-RS und RESTful Web Services interessieren sollten sich die RESTEasy Beispiele und Dokumentation ansehen. Das Buch von Bill Burke ist als zusätzliche Referenz geeignet und ausnahmslos zu empfehlen.

Durch JAX-RS wird die Entwicklung von RESTful Web Services durch die standardisierte Programmierschnittstelle nochmals erleichtert.Deshalb soll in dem Blogbeitrag ein JAX-RS WebService auf Basis von RESTEasy programmiert werden.

Als Programmierumgebung dient JBoss Developer Studio 3.0 GA, der JBoss AS 5.1 GA und die RESTEasy 1.2 GA (resteasy-jaxrs-1.2.GA-all.zip) Distribution. Im JBoss ist eine Datasource zu einer installierten relationalen Datenbank anzulegen. 

Implementiert wird ein RESTful Web Service, der Daten über ein einfaches HTML-Frontend in eine Datenbank schreiben und Daten lesen kann.Die Funktionalität des REST Web Services soll ein rudimentärer Blog sein.

Beim erste Schritt der Implementierung ist ein Workspace mit dem Namen "blog" anzulegen. Im Workspace wird ein "Enterprise Application Project" mit dem Namen Blog-Project angelegt.Das Projekt mündet in eine EAR-Datei, die zwei Teilprojekte "BlogProjectEJB" (JAR) und "BlogProjectWeb" (WAR) beinhaltet.

Die nächsten Konfigurationsschritte beinhalten, dass in das META-INF Verzeichnis des Projektes "BlogProjectEJB" die Datei persistence.xml hinterlegt wird.

Der Aufbau der persistence.xml sieht folgendermaßen aus:

<persistence xmlns="http://java.sun.com/xml/ns/persistence" version="1.0">
       <persistence-unit name="EmployeeService">
           <jta-data-source>java:/DataSourceName</jta-data-source>
           <properties>
               <property name="hibernate.hbm2ddl.auto"
                   value="create-drop" />
           </properties>
       </persistence-unit>  
</persistence>

Der DataSourceName muß mit der konfigurierten Datasource im Applikationsserver übereinstimmen. Das konfigurierte Hibernate-Property besagt, dass die DB-Struktur aus den Entitäten erzeugt wird. Dabei wird die Datenbankstruktur bei jedem Serverstart angelegt, bereits gespeicherte Daten bleiben deshalb nicht persistent erhalten.Obwohl wir JPA zum DB-Zugriff verwenden ist das Hibernate-Property korrekt, weil Hibernate der JPA-Provider des JBoss AS ist.

In das WEB-INF/lib Verzeichnis des "BlogProjectWeb" Projektes sind aus dem /lib Verzeichnis der RESTEasy Distribution alle JARs zu kopieren. Danach ändert man die web.xml.

Die web.xml muss folgende RESTEasy-Einträge beinhalten:

<web-app>
    <context-param>
        <param-name>resteasy.servlet.mapping.prefix</param-name>
        <param-value>/rest</param-value>
    </context-param>
    <servlet>
        <servlet-name>Resteasy</servlet-name>
        <servlet-class>
            org.jboss.resteasy.plugins.server.servlet.HttpServletDispatcher
        </servlet-class>
        <init-param>
            <param-name>javax.ws.rs.Application</param-name>
            <param-value>ccd.jee.ws.rest.application.ServiceApplication
            </param-value>
        </init-param>
    </servlet>
    <servlet-mapping>
        <servlet-name>Resteasy</servlet-name>
        <url-pattern>/rest/*</url-pattern>
    </servlet-mapping>
</web-app>

Im Quellordner des Projektes "BlogProjectWeb" legt man die Datei jndi.properties an. 

java.naming.factory.initial=org.jnp.interfaces.NamingContextFactory
java.naming.factory.url.pkgs=org.jboss.naming:org.jnp.interfaces
java.naming.provider.url=jnp\://localhost\:1099

Die Einträge der Datei jndi.properties werden für das Lookup der Web Service Facade (BlogServiceBean) im EJB-Container benötigt. Der RESTful Web Service läuft im Web-Container und greift über die Remote-Schnittstelle auf die Web Service Facade in der EJB-Schicht zu. Die strikte Trennung des Web Service von  der Facade hat den Vorteil, dass man den Web Service (bei entsprechendem Bedarf ) später auch in einem externen Web-Container laufen lassen kann.

Im Quellordner des Projektes "BlogProjectWeb" wird nun das Package ccd.jee.ws.rest.service mit der Klasse BlogService angelegt.

Danach legt man das Package ccd.jee.ws.rest.application an und erzeugt in dem Package die Klasse ServiceApplication.

Die Klasse ServiceApplication hat folgenden Inhalt:

import java.util.HashSet;
import java.util.Set;
import javax.ws.rs.core.Application;
import ccd.jee.ws.rest.service.BlogService;

public class ServiceApplication extends Application {

    private Set<Object> singletons = new HashSet<Object>();
    private Set<Class<?>> empty = new HashSet<Class<?>>();

    public ServiceApplication() {
        singletons.add(new BlogService());
    }

    @Override
    public Set<Class<?>> getClasses() {
        return empty;
    }

    @Override
    public Set<Object> getSingletons() {
        return singletons;
    }
}

Jetzt wechselt man in den Quellordner (ejbModule) des Projektes "BlogProjectEJB" und legt das Package ccd.jee.ws.domain an. In dem Package erzeugt man die Entity-Klasse BlogEntry.

Die Klasse BlogEntry hat folgenden Aufbau:

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;

@Entity
public class BlogEntry {

    @Id
    @GeneratedValue
    private int id;
  
    private String title;
    private String entry;
  
    public int getId() {
        return id;
    }
  
    public String getTitle() {
        return title;
    }
  
    public String getEntry() {
        return entry;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public void setEntry(String entry) {
        this.entry = entry;
    }

    @Override
    public String toString() {
        return "BlogEntry [entry=" + entry + ", title=" + title + "]";
    }
}

Nun legt man das Package ccd.jee.ws.domain.dao an und erzeugt in dem Package die lokale Schnittstelle und die Klasse eines einfachen Data Access Objects (DAO).

Die lokale Schnittstelle des DAOs hat folgenden Aufbau:

import javax.ejb.Local;
import ccd.jee.ws.domain.BlogEntry;

@Local
public interface BlogServiceDao {

    public void store(String title, String entry);
    public BlogEntry getById(Integer id);
}

Das DAO hat folgenden Aufbau:

import javax.ejb.Stateless;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import ccd.jee.ws.domain.BlogEntry;

@Stateless
public class BlogServiceDaoBean implements BlogServiceDao {

   @PersistenceContext
   private EntityManager em;

   public void store(String title, String entry) {

       BlogEntry blogEntry = new BlogEntry();
       blogEntry.setTitle(title);
       blogEntry.setEntry(entry);

       em.persist(blogEntry);
    }

    public BlogEntry getById(Integer id) {

       return em.find(BlogEntry.class, id);
    }
}

Das einfache Domain-Modell wurde angelegt, sodass nun ein weiteres Package im Quellordner mit dem Namen ccd.jee.ws.session anzulegen ist. Im soeben neu angelegten Package legt man die Remote-Schnittstelle  BlogServiceRemote an.

Remote-Schnittstelle des Blog Service (die auch als Facade Interface dient):

import javax.ejb.Remote;
import org.jboss.ejb3.annotation.RemoteBinding;

@Remote
@RemoteBinding(jndiBinding = "ejb/BlogService")
public interface BlogServiceRemote {

    public String getBlogEntryById(Integer id);
    public void storeBlogEntry(String title, String entry);
}

Das statuslose Session Bean legt man im Package ccd.jee.ws.session an:

import javax.ejb.EJB;
import javax.ejb.Stateless;
import ccd.jee.ws.domain.BlogEntry;
import ccd.jee.ws.domain.dao.BlogServiceDao;

@Stateless
public class BlogServiceBean implements BlogServiceRemote {

    @EJB private BlogServiceDao blogServiceDao;
   
    public String getBlogEntryById(Integer id) {
       
        final BlogEntry blogEntry = blogServiceDao.getById(id);
       
        return blogEntry.toString();
    }

    public void storeBlogEntry(String title, String entry) {
       
        blogServiceDao.store(title, entry);       
    }
}

Die Quellen, die im EJB-Container laufen sollen, sind nun erzeugt, sodass wir nun wieder zurück zum Projekt "BlogProjectWeb" gehen können. Dort legen wir zunächst ein Package ccd.jee.ws.rest.helper für Helper-Klassen an. Die Helper Klassen kümmern sich um das Lookup der entfernten Schnittstelle des statuslosen Session Beans, welches im EJB-Container läuft. Nachfolgend sind die Helper-Klassen gelistet, die zu erstellen sind.

BlogServiceReferenceException

public class BlogServiceReferenceException extends RuntimeException {

    public BlogServiceReferenceException(Exception ex) {
        super(ex);
    }
}

BlogServiceReference

import javax.naming.InitialContext;
import javax.naming.NamingException;
import ccd.jee.ws.session.BlogServiceRemote;

class BlogServiceReference {
   
    private final InitialContext context;
   
    BlogServiceReference() {
       
        try {
           
            context = new InitialContext();
           
        }
        catch (NamingException ex) {
           
            throw new BlogServiceReferenceException(ex);
        }
    }
   
    public BlogServiceRemote getRemoteBlogService() {
       
        try {
           
            return (BlogServiceRemote) context.lookup("ejb/BlogService");
        }
        catch (NamingException ex) {
           
            throw new BlogServiceReferenceException(ex);
        }
    }
}

BlogServiceLocator

import ccd.jee.ws.session.BlogServiceRemote;

public class BlogServiceLocator {
   
    private BlogServiceLocator() {}
   
    private static final class DemandHolder {
       
        private static final BlogServiceReference resource = new BlogServiceReference();
    }
   
    public static BlogServiceRemote getInstance() {
       
        return(DemandHolder.resource.getRemoteBlogService());
    }
}

Damit die Remote-Schnittstelle im Projekt "BlogProjectWeb" gefunden werden kann, ist das Projekt "BlogProjectEJB" in den Java EE Module Dependencies (Alt-Enter) bei den JEE Modules einzubinden (Häkchen bei JAR/Module setzen). Normalerweise würde die Remote-Schnittstelle und Helper-Klassen in einem eigenen JAR untergebracht werden, der einfachheithalber wird allerdings auf diesen Schritt verzichtet.

Zum Abschluss ist noch folgender Quellcode in der angelegten BlogService Klasse zu erstellen:

import java.net.URI;
import javax.ws.rs.FormParam;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Response;
import ccd.jee.ws.rest.helper.BlogServiceLocator;
import ccd.jee.ws.session.BlogServiceRemote;

@Path("/blog")
public class BlogService {
   
    @GET
    @Produces("text/html")
    @Path("{id}")
    public Response getBlogEntry(@PathParam("id") Integer id) {
       
        BlogServiceRemote blogService = BlogServiceLocator.getInstance();
        final String blogEntry = blogService.getBlogEntryById(id);
       
        return Response.created(URI.create("/blog/")).
                status(200).entity(blogEntry).build();           
    }
   
    @POST
    @Produces("text/html")
    public Response putBlogEntry(@FormParam("title") String title,
                                                 @FormParam("entry") String entry) {
       
        BlogServiceRemote blogService = BlogServiceLocator.getInstance();
        blogService.storeBlogEntry(title, entry);
       
        return Response.created(URI.create("/blog/")).status(200).
                entity("Der Blog-Eintrag ist erfolgreich angelegt worden!").build();
    }
}

Als letzter Schritt ist noch eine einfache HTML-Datei mit folgendem Aufbau zu erstellen:

<html>
<head><title>REST-Blog</title></head>
<body>
<form action="/BlogProjectWeb/rest/blog" method = "post">
<table>
<tr>
<td style="FONT-WEIGHT: bold;"><u style="BACKGROUND-COLOR: #ff8000;">REST-Blog</u>
</td>
<td style="BACKGROUND-COLOR: #ff8000; COLOR: #ff8000;">
</td>
</tr>
<tr>
<td>Blogtitel:</td>
<td><input type = "text" name = "title" style=" width : 457px;"/>
</td>
</tr>
<tr>
<td>Blogeintrag:</td><td><textarea name = "entry" style=" width : 457px;"></textarea>
</td>
</tr>
<tr>
<td></td>
<td style=" width : 141px;"><input type = "submit" value = "Blogeintrag senden" style=" height : 24px;"/>
</td>
</tr>
</table>
</form>
</body>
</html>

Die HTML-Datei ist im Projekt "BlogProjectWeb"in den WebContent-Ordner zu legen.

Der Blog Service ist eine Full Stack Java EE Anwendung und kann nun in die JBoss AS 5.1 Laufzeitumgebung deployed werden. Wenn alles glatt gelaufen ist, startet der JBoss AS ohne Deployment-Fehler. Die Anwendung wartet nun darauf verwendet zu werden. Nachfolgend ist die Anwendung im Browser-Frontend zu sehen.

HTML-Formular des Blog Services zum Anlegen eines Blogeintrages
Abfrage eines Blogeintrages mit der URI: http://localhost:<PORT>/BlogProjectWeb/rest/blog/1
Der Eintrag <PORT> ist entsprechend den lokalen Porteinstellungen des JBoss AS zu ändern!

Projektstruktur im JBoss Developer Studio 3.0

































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