Entwicklung einer JSON basierten REST Schnittstelle mit Spring Web MVC

veröffentlicht am 05. April 2012

Momentan erfreuen sich JSON basierte REST Schnittstellen großer Beliebtheit. Zur Entwicklung solcher Schnittstellen kommen im Java Bereich meist Frameworks wie  RESTlet oder Jersey zum Einsatz. Aber auch mit Spring Web MVC lassen sich REST Schnittstellen im Handumdrehen implementieren, was dieser Artikel anhand eines einfachen Beispiels erläutert.

Zunächst wird eine Reihe an Bibliotheken benötigt. Das Projekt habe ich mit Unterstützung des Eclipse Plugins Maven Integration for Eclipse WTP eingerichtet. Die pom.xml für das Projekt sieht wie folgt aus. Hier fällt auf, dass der Jackson Mapper eingebunden wird, wodurch es Spring Web MVC ermöglicht wird, auch Daten im JSON Format zu verarbeiten.

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<groupId>de.patrickgotthard</groupId>
	<artifactId>springmvc-json-rest</artifactId>
	<version>1.0.0-SNAPSHOT</version>
	<packaging>war</packaging>
	<properties>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
	</properties>
	<build>
		<plugins>
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-compiler-plugin</artifactId>
				<version>2.3.2</version>
				<configuration>
					<source>1.6</source>
					<target>1.6</target>
				</configuration>
			</plugin>
		</plugins>
	</build>
	<dependencies>
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-webmvc</artifactId>
			<version>3.1.1.RELEASE</version>
		</dependency>
		<dependency>
			<groupId>org.codehaus.jackson</groupId>
			<artifactId>jackson-mapper-asl</artifactId>
			<version>1.9.6</version>
		</dependency>
	</dependencies>
</project>

Sobald alle Bibliotheken eingebunden sind, wird in der web.xml der obligatorische Spring Context hochgezogen und das Spring Web MVC DispatcherServlet initialisiert.

<?xml version="1.0" encoding="UTF-8"?>
<web-app 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_3_0.xsd"
	version="3.0">

	<context-param>
		<param-name>contextConfigLocation</param-name>
		<param-value>WEB-INF/applicationContext.xml</param-value>
	</context-param>

	<listener>
		<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
	</listener>

	<servlet>
		<servlet-name>json</servlet-name>
		<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
		<load-on-startup>1</load-on-startup>
	</servlet>

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

</web-app>

Die Spring Konfiguration erfolgt dann in der Datei applicationContext.xml. Hier wird zunächst das Package festgelegt, in dem Spring automatisch nach annotierten Klassen suchen soll. Gleich danach wird die Annotation Unterstützung für Spring Web MVC aktiviert und das DispatcherServlet für alle URLs eingerichtet.

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns:context="http://www.springframework.org/schema/context"
	xmlns:mvc="http://www.springframework.org/schema/mvc"
	xsi:schemaLocation="
		http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.1.xsd
		http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.1.xsd
		http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-3.1.xsd">

	<context:component-scan base-package="de.patrickgotthard.springmvcjsonrest" />

	<mvc:annotation-driven />
	<mvc:default-servlet-handler />

</beans>

In der web.xml haben wir dem DispatcherServlet den Namen json gegeben. Ohne weitere Konfiguration erwartet Spring nun eine Datei nach folgendem Namensschema: [servletName]-servlet.xml – in diesem Fall also json-servlet.xml. In dieser Datei können bereits im Standardcontext deklarierte Beans überschrieben als auch neue Beans angelegt werden. Dies ist in diesem Beispiel nicht erforderlich, sodass die Datei lediglich den Minimalumfang enthält:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="
		http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

</beans>

Für dieses Beispiel habe ich die einfache Klasse Person erstellt, die lediglich eine ID, einen Vornamen und einen Nachnamen beinhaltet:

package de.patrickgotthard.springmvcjsonrest;

public class Person {

	private Integer id;
	private String firstName;
	private String lastName;

	public Integer getId() {
		return id;
	}

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

	public String getFirstName() {
		return firstName;
	}

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

	public String getLastName() {
		return lastName;
	}

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

}

Zur Vereinfachung wird in diesem Beispiel außerdem auf eine Datenbank verzichtet und stattdessen eine HashMap als Datenspeicher verwendet. Im Realfall würde man z.B. auf Hibernate zurückgreifen. Über die Annotation @Component wird Spring signalisiert, dass diese Bean auch in anderen Klassen eingefügt werden können soll.

package de.patrickgotthard.springmvcjsonrest;

import java.util.Collection;
import java.util.HashMap;
import java.util.Map;

import org.springframework.stereotype.Component;

@Component
public class PersonPersistence {

	private final Map<Integer, Person> persons = new HashMap<Integer, Person>();
	private int id = 0;

	public void create(final Person person) {
		person.setId(id);
		persons.put(id, person);
		id++;
	}

	public Collection<Person> list() {
		return persons.values();
	}

	public Person read(final int id) {
		return persons.get(id);
	}

	public boolean update(final Person person) {
		boolean success = false;
		if (persons.containsKey(person.getId())) {
			persons.put(person.getId(), person);
			success = true;
		}
		return success;
	}

	public boolean delete(final int personId) {
		boolean success = false;
		if (persons.containsKey(personId)) {
			persons.remove(personId);
			success = true;
		}
		return success;
	}

}

Jetzt kommt der interessante Teil des Beispiels, die Implementierung der eigentlichen REST Schnittstelle. Diese wird das volle Potential des HTTP Protokolls ausnutzen. Das heißt, dass sowohl die HTTP Methoden GET und POST als auch PUT und DELETE verwendet werden. Je nach später eingesetzten Frameworks, müssen die Methoden PUT und DELETE eventuell über GET und POST nachgebildet werden, da diese nicht immer unterstützt werden.

package de.patrickgotthard.springmvcjsonrest;

import java.util.Collection;
import java.util.HashMap;
import java.util.Map;

import javax.annotation.Resource;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
@RequestMapping("/persons")
public class PersonController {

	@Resource
	private PersonPersistence persistence;

	@ResponseBody
	@RequestMapping(method = RequestMethod.POST)
	public Map<String, Object> create(@RequestBody final Person person) {
		persistence.create(person);
		final Map<String, Object> response = new HashMap<String, Object>();
		response.put("success", "true");
		response.put("id", person.getId());
		return response;
	}

	@ResponseBody
	@RequestMapping(method = RequestMethod.GET)
	public Collection<Person> list() {
		return persistence.list();
	}

	@ResponseBody
	@RequestMapping(value = "/{id}", method = RequestMethod.GET)
	public Person read(@PathVariable final int id) {
		return persistence.read(id);
	}

	@ResponseBody
	@RequestMapping(method = RequestMethod.PUT)
	public Map<String, Object> update(@RequestBody final Person person) {
		final boolean success = persistence.update(person);
		final Map<String, Object> response = new HashMap<String, Object>();
		response.put("success", success);
		return response;
	}

	@ResponseBody
	@RequestMapping(value = "/{id}", method = RequestMethod.DELETE)
	public Map<String, Object> delete(@PathVariable final int id) {
		final boolean success = persistence.delete(id);
		final Map<String, Object> response = new HashMap<String, Object>();
		response.put("success", success);
		return response;
	}

}

Die Annotation @Controller markiert die Klasse zunächst als Spring Web MVC Controller. Direkt danach wird über das @RequestMapping festgelegt, dass der Controller auf den Pfad /persons reagieren soll. Wird diese Angabe weggelassen, werden die an den Methoden definierte Pfade abolut statt relativ behandelt.

Bei den Methoden wird per @RequestMapping ebenfalls die zu verwendende HTTP Methode angegeben. Die teilweise deklarierten (relativen) Pfade in Form von /{id} können als Parameter über @PathVariable ausgelesen werden. Zuletzt sorgt @ResponseBody dafür, dass der Controller Daten im JSON Format zurückliefert, wenn der Client dies unterstützt.

Die Anwendung ist jetzt bereit zur Ausführung in einem Anwendungsserver. Zum Testen der Schnittstelle kann der WizTools.org RESTClient verwendet werden.

Quellen und weiterführende Links

Kommentare

Woher weiß eigentlich Spring, wie zwischen einem POJO der Java-Klasse „Person“ und einer Repräsentation im JSON-Format hin- und herkonvertiert wird?

Kommentar #1 von Eduard am 12. März 2013


Spring selbst kümmert sich nicht um das Mapping zwischen POJO und JSON. Dazu wird der Jackson JSON Processor verwendet: http://wiki.fasterxml.com/JacksonHome

Wie das im einzelnen abläuft, müsste man im Quellcode nachschauen, aber im Prinzip läuft es darauf hinaus, dass sowohl POJOs und JSON ein standardisiertes Format besitzen und dementsprechend zwischen den beiden Formaten übersetzt werden kann.

Kommentar #2 von Patrick am 12. März 2013


Hallo,

kommt bei der Annotation @ResponseBody immer Json raus? Ist das bei Spring so vorgegeben oder muss man den Output Datentyp irgendwo konfigurieren oder festlegen?

Gruß Daniel

Kommentar #3 von Daniel Seelig am 04. September 2013


Hallo Daniel,

beim HTTP Request kann der angeforderte Datentyp angegeben werden, nach dem sich Spring richtet, wenn ein passender „Codec“ zur Verfügung steht. Man kann der Annotation aber auch einen separaten Parameter mitgeben, um den zu erzeugenden Datentyp anzugeben.

Gruß
Patrick

Kommentar #4 von Patrick am 04. September 2013


Hallo Patrick,

das Tutorial ist sehr aufschlussreich und gut formuliert.
Könntest du das Projekt zum import bereitstellen?

Danke Artur

Kommentar #5 von Artur am 21. November 2013


Hallo Artur,

das Projekt habe ich leider nicht mehr vorliegen, aber mit ein bisschen Copy’n’Paste bekommt man das doch schnell zusammengebastelt ;)

Gruß
Patrick

Kommentar #6 von Patrick am 23. November 2013


Hinterlasse einen Kommentar