July 6, 2009

REST-Anwendungen mit Spring 3.0

Die Interoperabilität zwischen heterogenen Softwaressystemen ist eine interessante Aufgabe. Zur Realisierung stehen heutzutage Web-Services über SOAP oder die Verwendung von REST zur Verfügung. Web-Services über SOAP sind unabhängig vom Übertragungsprotokoll und damit flexibler als REST, wobei HTTP verwendet wird. Außerdem sind für Web-Services ausführliche Definitionen der Schnittstellen (WSDL) und beim Austausch von Daten ein großer Anteil von Verwaltungsinformationen innerhalb der ausgetauschten XML-Nachrichten erforderlich. Das hat auch alles seine Vorteile, aber für eine einfache Anwendung, die lediglich Dienste/Informationen für andere Software zur Verfügung stellen möchte, scheint derzeit mit REST und der Verwendung des verbreiteten HTTP der pragmatischere Ansatz vorzuliegen.

In diesem Beitrag soll es aber nicht über die Unterschiede oder Vor- und Nachteile von Web-Services über SOAP gegenüber REST gehen, sondern die einfache Implementierung von Funktionalität über REST mit Spring 3.0 aufgezeigt werden. Anzumerken in diesem Zusammenhang sei noch, dass zum jetzigen Zeitpunkt Spring 3.0 noch nicht fertiggestellt ist und der Milestone 3 Verwendung findet. Allerdings sollte sich an der Anwendung nichts Grundlegendes ändern, da das Release von Spring 3.0 für das zweite Halbjahr 2009 angekündigt ist.

Zur Demonstration zeige ich in diesem und den folgenen Beiträgen die Implementierung einer rudimentären Webanwendung mit Sping MVC. Diese soll lediglich eine einfaches Ticketssystem abbilden, wobei lediglich Tickets angezeigt und die CRUD-Operationen unterstützt werden.
  1. REST-Anwendungen mit Spring 3.0
    1. Spring MVC Anwendung
      1. Entwicklungsumgebung
      2. Step-by-Step Beschreibung
        1. Eclipse Webprojekt anlegen
        2. Geschäftsklassen
        3. Controller
        4. Darstellung
        5. Konfiguration
          1. Webserver-Konfiguration web.xml
          2. Spring Anwendungskontext
          3. Servletkontext
        6. Ergebnis
      3. Appendix
        1. DAOMock-Implementierung
        2. Projektstruktur

Spring MVC Anwendung

  • Entwicklung innerhalb von Eclipse und Ausführung innerhalb der WTP (Apache 5.5)
  • einfaches Ticketssystem mit per Annotation definierte Controller und URI-Mappings über Annotations (adaptiert vom PetClinic-Beispiel)
  • einfache Mock-Implementierung des Data Access Objects, dass ggf. einfach um “echte” Persistenz erweitert werden kann

Entwicklungsumgebung

  • Java 6 auf Mac OSX
  • Eclipse 3.5
  • Eclipse-Plugins:
    • WTP
    • Subversive + Connectors
  • Spring 3.0M3
  • Jakarta Standard Library
  • Apache Tomcat 5.5

Step-by-Step Beschreibung

Eclipse Webprojekt anlegen

  • neues ‘Dynamic Web Projekt’ erstellen (Wizard)
    • Schritt 1: Projektname eintragen
    • Schritt 2:‘ Default output folder’ auf webapp/WEB-INF/classes setzen
    • Schritt 3: ‘Content directory’ auf webapp setzen

Geschäftsklassen

  • Klasse Ticket und eine Enumeration TicketStatus anlegen
  • Attribute eines Tickets anlegen (» nächstes Listing) und die Getter und Setter erstellen (lassen), die Enumeration can beliebige Enum-Literals enthalten (z. B. OPEN, INPROGRESS, REOPENED, RESOLVED, CLOSED, WONTFIX )
public class Ticket {
private long id;
private String name;
private String description;
private String reporter;
private String assignedTo;
private TicketStatus status;

/** Helper method to differentiate between new and existing tickets. */
public boolean isNew() {
return (this.id < 1);
}
}

Controller

  • TicketApplicationController, der als MultiActionController fungiert; behandelt Anfragen an:
    • / - Initiale Anfrage (Startseite)
    • /tickets - Liste von Tickets
    • /tickets/{ticketId} - Detailansicht eines Tickets
@Controller
public class TicketTrackerController {

/**
* Data Access Object.
*/
private TicketTracker tickettracker;

/**
* Autowire Data Acces Object with id 'tickettracker' (defined in
* application-context.xml)
*
* @param tickettracker
*/
@Autowired
public TicketTrackerController(TicketTracker tickettracker) {
this.tickettracker = tickettracker;
}

/**
* Custom handler for applications initial request.
*
* Relies on the RequestToViewNameTranslator to determine the logical view
* name based on the request URL.
*
* @return
*/
@RequestMapping("/")
public String indexHandler() {
return "index";
}

/**
* Custom handler for displaying tickets.
*
* @return a ModelMap with the model attributes for the view
*/
@RequestMapping("/tickets")
public ModelAndView ticketsHandler() {
ModelAndView mav = new ModelAndView("tickets/list");
mav.addObject("tickets", tickettracker.findTickets());
return mav;
}

/**
* Custom handler for displaying an ticket.
*
* @param ticketId the ID of the ticket to display
* @return a ModelMap with the model attributes for the view
*/
@RequestMapping("/tickets/{ticketId}")
public ModelAndView ticketHandler(@PathVariable("ticketId") int ticketId) {
ModelAndView mav = new ModelAndView("tickets/show");
mav.addObject(this.tickettracker.findTicket(ticketId));
return mav;
}
}
  • AddTicketForm und EditTicketForm als SimpleFormController behandeln Anfragen an:
    • /tickets/new - GET liefert Eingabeformular / POST speichert das neue Ticket
    • /tickets/{ticketId}/edit - GET liefert Bearbeitungsformular / PUT aktualisiert das Ticket
@Controller
@RequestMapping("/tickets/new")
public class AddTicketForm {

/**
* Data Access Object.
*/
private TicketTracker tickettracker;

/**
* Autowire Data Acces Object with id 'tickettracker' (defined in application-context.xml)
*
* @param tickettracker
*/
@Autowired
public AddTicketForm(TicketTracker tickettracker) {
this.tickettracker = tickettracker;
}

/**
* Initializes the form.
*
* @param model
* @return View path.
*/
@RequestMapping(method = RequestMethod.GET)
public String setupForm(Model model) {
Ticket ticket = new Ticket();
model.addAttribute(ticket);
return "tickets/form";
}

/**
* Handles form submits, to save the new Ticket.
*
* @param ticket
* @param result
* @param status
* @return View path.
*/
@RequestMapping(method = RequestMethod.POST)
public String processSubmit(@ModelAttribute Ticket ticket, BindingResult result, SessionStatus status) {

this.tickettracker.saveOrUpdateTicket(ticket);
status.setComplete();
return "redirect:/tickets/" + ticket.getId();
}
}

@Controller
@RequestMapping("/tickets/{ticketId}/edit")
public class EditTicketForm {

// ... Autowired Data Access Object injection (see AddTicketForm)

@RequestMapping(method = RequestMethod.GET)
public String setupForm(@PathVariable("ticketId") int ticketId, Model model) {
Ticket ticket = this.tickettracker.findTicket(ticketId);
model.addAttribute(ticket);
return "tickets/form";
}

@RequestMapping(method = RequestMethod.PUT)
public String processSubmit(@ModelAttribute Ticket ticket,
BindingResult result, SessionStatus status) {

this.tickettracker.saveOrUpdateTicket(ticket);
status.setComplete();
return "redirect:/tickets/" + ticket.getId();
}
}

Darstellung

  • die Ausgabe wird mittels JSPs beschreiben, die unter webapp/WEB-INF/jsp/ zu finden sind (siehe Projektstruktur)
  • Exemplarisch sei hier die Darstellung aller Tickets ( list.jsp) angeführt:
<%@ include file="/WEB-INF/jsp/includes.jsp" %>;
<%@ include file="/WEB-INF/jsp/header.jsp" %>
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>

<h2>Tickets:</h2>

<table>
<thead>
<th>#</th>
<th>Name</th>
<th>Reporter</th>
<th>Assigned To</th>
<th>Status</th>
</thead>
<c:forEach var="ticket" items="${tickets}">
<tr>
<td>
<spring:url value="/tickets/{ticketId}/edit" var="editUrl">
<spring:param name="ticketId" value="${ticket.id}"/>
</spring:url>

<spring:url value="/tickets/{ticketId}" var="ticketUrl">
<spring:param name="ticketId" value="${ticket.id}"/>
</spring:url>

${ticket.id} 
<a href="${fn:escapeXml(editUrl)}">Edit</a> 
<a href="${fn:escapeXml(ticketUrl)}">Show</a>
</td>
<td>${ticket.name}</td>
<td>${ticket.reporter}</td>
<td>${ticket.assignedTo}</td>
<td>${ticket.status}</td>
</tr>
</c:forEach>
<tr>
<td colspan="5">
<a href="<spring:url value="/tickets/new" htmlEscape="true" />">New Ticket</a>
</td>
</tr>
</table>

<%@ include file="/WEB-INF/jsp/footer.jsp" %>

Konfiguration

Webserver-Konfiguration web.xml
  • Einbindung des Spring-Kontextes ( ContextLoaderListener)
  • DispatcherServlet mit entsprechendem Mapping
    • definiert einen eigene Anwendungskontext, der in ${servletname}-servlet.xml zu definieren ist
  • HiddenHttpMethodFilter der dazu dient, dass neben GET- und POST-Requests auch PUT- und DELETE-Anfragen über HTML-Formulare möglich sind
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

<servlet-mapping>
<servlet-name>default</servlet-name>
<url-pattern>/static/*</url-pattern>
</servlet-mapping>

<servlet>
<servlet-name>tickettracker</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<load-on-startup>2</load-on-startup>
</servlet>

<servlet-mapping>
<servlet-name>tickettracker</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>

<filter>
<filter-name>httpMethodFilter</filter-name>
<filter-class>org.springframework.web.filter.HiddenHttpMethodFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>httpMethodFilter</filter-name>
<servlet-name>tickettracker</servlet-name>
</filter-mapping>
Spring Anwendungskontext ( applicationContext.xml)
  • Einstellungen zur Persistenz der Anwendung
  • hier nur die Definition der Bean tickettracker, die in den Controllern per Autowired als DAO eingebunden wird (siehe Controller)
<bean id="tickettracker" class="de.mmrotzek.spring.rest.simplerest.TicketTrackerMockImpl"/>
Servletkontext ( ${servletname}-servlet.xml)
  • Definition des Packages in dem sich Klassen mit Controller-Annotation befinden
  • Handler für die Behandlung der RequestMapping-Annotation für Klassen und Methoden
  • ViewResolver zur Bestimmung der JSP, die zur Darstellung verwendet wird
<!-- The controllers are autodetected POJOs labeled with the @Controller annotation. -->
<context:component-scan base-package="de.mmrotzek.spring.rest.simplerest" />

<!-- Annotation Handler for Types and Methods. -->
<bean class="org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping"/>
<bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter"/>

<bean id="internalResourceViewResolver" 
class="org.springframework.web.servlet.view.InternalResourceViewResolver"
p:prefix="/WEB-INF/jsp/" p:suffix=".jsp"/>

Ergebnis

List of tickets
Liste von Tickets

Edit ticket form
Formular zum Bearbeiten

Appendix

DAOMock-Implementierung

Formular zum Bearbeiten
public class TicketTrackerMockImpl implements TicketTracker {

public TicketTrackerMockImpl() {
// add some dummy data
for (int i = 1; i < = 10; i++) {
Ticket t = new Ticket();
t.setId(i);
t.setName("Ticket " + i);
t.setDescription("Description " + i);
t.setReporter(StringGenerator.getUniqueID());
t.setStatus(randomStatus());

ticketsCache.add(t);
}

}

private List ticketsCache = new ArrayList();

public List findTickets() {
return Collections.unmodifiableList(ticketsCache);
}

public Ticket findTicket(long id) {
if (ticketsCache.isEmpty()) {
return null;
}

if (id > 0) {
for (Ticket t : ticketsCache) {
if (t.getId() == id) {
return t;
}
}
}

return null;
}

public boolean saveOrUpdateTicket(Ticket ticket) {
// no tickets exist, so add it
if (ticketsCache.isEmpty()) {
ticket.setId(1);
return ticketsCache.add(ticket);
}

// there are some tickets and the tickets to store has a valid id, try
// to find the ticket to update
final Ticket t = findTicket(ticket.getId());

if(t != null) {
BeanUtils.copyProperties(ticket, t);
return true;
}

// add new ticket
ticket.setId(getMaxId()+1);
return ticketsCache.add(ticket);
}

protected long getMaxId() {
long max = 0;
for (Ticket t : ticketsCache) {
if(t.getId() > max) {
max = t.getId();
}
}

return max;
}

protected TicketStatus randomStatus() {
final int max = TicketStatus.values().length;
final Random r = new Random();
final int rand = r.nextInt(max);

for(TicketStatus ts:TicketStatus.values()) {
if(ts.ordinal() == rand) {
return ts;
}
}
return null;
}

protected static class StringGenerator {
private static final int NUM_CHARS = 6;
private static String chars = "abcdefghijklmonpqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
private static Random r = new Random();

public static String getUniqueID() {
char[] buf = new char[NUM_CHARS];

for (int i = 0; i < buf.length; i++) {
buf[i] = chars.charAt(r.nextInt(chars.length()));
}

return new String(buf);
}
}
}

Projektstruktur

- src
- de
- mmrotzek
- spring
- rest
- simplerest
- TicketTracker.java
- TicketTrackerMockImpl.java
- entity
- Ticket.java
- TicketStatus.java
- web
- AddTicketForm.java
- EditTicketForm.java
- TicketTrackerController.java
- webapp
- WEB-INF
- applicationContext.xml
- tickettracker-servlet.xml
- jsp
- tickets
- form.jsp
- list.jsp
- show.jsp
- footer.jsp
- header.jsp
- includes.jsp
- index.jsp
- styles
- style.css
- lib
- com.springsource.antlr-2.7.7.jar
- com.springsource.org.antlr-3.0.1.jar
- jstl.jar
- org.springframework.aop-3.0.0.M3.jar
- org.springframework.asm-3.0.0.M3.jar
- org.springframework.aspects-3.0.0.M3.jar
- org.springframework.beans-3.0.0.M3.jar
- org.springframework.context-3.0.0.M3.jar
- org.springframework.context.support-3.0.0.M3.jar
- org.springframework.core-3.0.0.M3.jar
- org.springframework.expression-3.0.0.M3.jar
- org.springframework.instrument-3.0.0.M3.jar
- org.springframework.instrument.classloading-3.0.0.M3.jar
- org.springframework.jdbc-3.0.0.M3.jar
- org.springframework.jms-3.0.0.M3.jar
- org.springframework.orm-3.0.0.M3.jar
- org.springframework.oxm-3.0.0.M3.jar
- org.springframework.spring-library-3.0.0.M3.libd
- org.springframework.test-3.0.0.M3.jar
- org.springframework.transaction-3.0.0.M3.jar
- org.springframework.web-3.0.0.M3.jar
- org.springframework.web.portlet-3.0.0.M3.jar
- org.springframework.web.servlet-3.0.0.M3.jar
- standard.jar
- web.xml

1 comment:

  1. ',- I am really thankful to this topic because it really gives up to date information ~',

    ReplyDelete