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.
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.
- 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
- Klasse
Ticket
und eine EnumerationTicketStatus
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); } }
TicketApplicationController
, der alsMultiActionController
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
undEditTicketForm
alsSimpleFormController
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" %>
- Einbindung des Spring-Kontextes (
ContextLoaderListener
) DispatcherServlet
mit entsprechendem Mapping
- definiert einen eigene Anwendungskontext, der in
${servletname}-servlet.xml
zu definieren ist
- definiert einen eigene Anwendungskontext, der in
HiddenHttpMethodFilter
der dazu dient, dass neben GET- und POST-Requests auch PUT- und DELETE-Anfragen über HTML-Formulare möglich sind
- dazu wird der Parameter
_method
verwendet (siehe Bsp. Darstellung)
- dazu wird der Parameter
<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>
- Einstellungen zur Persistenz der Anwendung
- hier nur die Definition der Bean
tickettracker
, die in den Controllern perAutowired
als DAO eingebunden wird (siehe Controller)
<bean id="tickettracker" class="de.mmrotzek.spring.rest.simplerest.TicketTrackerMockImpl"/>
- 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"/>
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); } } }
- 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
',- I am really thankful to this topic because it really gives up to date information ~',
ReplyDelete