Getting REST Right in Spring 3.0
Join the DZone community and get the full member experience.
Join For FreeI've been working on getting a REST + Hibernate application using the goodies found in Spring 3.0 for my upcoming book - Spring Persistence with Hibernate. I'm unfortunately slightly disappointed at the current state of affairs (sorry Arjen and Chris). Spring 3.0 is in RC2, meaning it's had 2 Release Candidates, but the REST stuff is still not 100% right. I was able to work around issues, but I shouldn't have had to deal with the issues I dealt with had Spring REST been a battle tested in production. I'm going to levarage my work show you how to get a basic application up and running so that you can see exactly what I mean. My application is going to use maven, Spring 3.0, Spring MVC (REST), Spring OXM, Jetty, Hibernate and an in-memory database using H2. You can download a ZIP file: gallery.zip.
Maven is the logical first place to start. If you don't have maven installed, then do it. Choose a top level folder for your project (mine is c:\dev\springpersitence\gallery) and run mvn archetype:create -DgroupId=com.smartpants.artwork -DartifactId=gallery to create the project. (You can switch your groupId to match com.yourcompany.project). Update the pom.xml to import a whole bunch of dependencies from a whole bunch of repositories: We're also going to set up an embedded Jetty instance:
<?xml version="1.0" encoding="UTF-8"?><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/maven-v4_0_0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.smartpants.artwork</groupId> <artifactId>gallery</artifactId> <packaging>war</packaging> <name>Artwork Gallery App</name> <version>0.1</version> <description /> <build> <finalName>gallery-web-app</finalName> <plugins> <plugin> <artifactId>maven-compiler-plugin</artifactId> <configuration> <source>1.6</source> <target>1.6</target> </configuration> </plugin> <plugin> <groupId>org.mortbay.jetty</groupId> <artifactId>maven-jetty-plugin</artifactId> <version>6.1.15</version> <configuration> <!-- By default the artifactId is taken, override it with something simple --> <contextPath>/</contextPath> </configuration> </plugin> </plugins> </build> <properties> <spring.framework.version>3.0.0.RC2</spring.framework.version> <org.slf4j.version>1.5.2</org.slf4j.version> </properties> <dependencies> <!-- Spring --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-beans</artifactId> <version>${spring.framework.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context-support</artifactId> <version>${spring.framework.version}</version> <exclusions> <exclusion> <groupId>quartz</groupId> <artifactId>quartz</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-core</artifactId> <version>${spring.framework.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>${spring.framework.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-test</artifactId> <version>${spring.framework.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-orm</artifactId> <version>${spring.framework.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-oxm</artifactId> <version>${spring.framework.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-tx</artifactId> <version>${spring.framework.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-web</artifactId> <version>${spring.framework.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>${spring.framework.version}</version> </dependency> <dependency> <groupId>cglib</groupId> <artifactId>cglib-nodep</artifactId> <version>2.1_3</version> </dependency> <dependency> <groupId>javax.xml.bind</groupId> <artifactId>jaxb-api</artifactId> <version>2.0</version> </dependency> <!-- Hibernate --> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-annotations</artifactId> <version>3.4.0.GA</version> </dependency> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate</artifactId> <version>3.2.6.ga</version> </dependency> <dependency> <groupId>javax.persistence</groupId> <artifactId>persistence-api</artifactId> <version>1.0</version> </dependency> <dependency> <groupId>javassist</groupId> <artifactId>javassist</artifactId> <version>3.11.0.GA</version> </dependency> <!-- Database --> <dependency> <groupId>commons-dbcp</groupId> <artifactId>commons-dbcp</artifactId> <version>1.2.1</version> </dependency> <dependency> <groupId>commons-collections</groupId> <artifactId>commons-collections</artifactId> <version>3.2.1</version> </dependency> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <version>1.2.122</version> </dependency> <dependency> <groupId>javax.transaction</groupId> <artifactId>jta</artifactId> <version>1.1</version> </dependency> <!-- JUnit --> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.5</version> <scope>test</scope> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>${org.slf4j.version}</version> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-simple</artifactId> <version>${org.slf4j.version}</version> </dependency> <dependency> <groupId>org.mortbay.jetty</groupId> <artifactId>maven-jetty-plugin</artifactId> <version>6.1.15</version> </dependency> </dependencies> <repositories> <repository> <id>ibiblio mirror</id> <url>http://mirrors.ibiblio.org/pub/mirrors/maven2/</url> </repository> <repository> <id>Springframework milestone</id> <url>http://maven.springframework.org/milestone</url> </repository> <repository> <id>jboss</id> <url>http://repository.jboss.org/maven2</url> </repository> <repository> <id>java.net</id> <url>https://maven-repository.dev.java.net/nonav/repository</url> <layout>legacy</layout> </repository> <repository> <id>codehaus</id> <url>http://repository.codehaus.org</url> </repository> <repository> <id>atlassian</id> <url>http://repository.atlassian.com/maven2</url> </repository> <repository> <id>maven2-repository.dev.java.net</id> <name>Java.net Repository for Maven</name> <url>http://download.java.net/maven/2/</url> <layout>default</layout> </repository> </repositories> <pluginRepositories> <pluginRepository> <id>maven2-repository.dev.java.net</id> <url>http://download.java.net/maven/2</url> </pluginRepository> <pluginRepository> <id>maven-repository.dev.java.net</id> <name>Java.net Maven 1 Repository (legacy)</name> <url>http://download.java.net/maven/1</url> <layout>legacy</layout> </pluginRepository> </pluginRepositories></project>
Spring XML Configuration
Next, you'll need a couple of Spring configuration files. We'll put those files in src/resources. The first file, spring-context is going to be to set up hibernate (including classpath scanning for @Entity object), create the database (using the new jdbc namespace), setup Spring classpath scanning (for Spring beans annotated with @Controller, @Repository & etc.) and setup transaction management:
<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:tx="http://www.springframework.org/schema/tx" xmlns:jdbc="http://www.springframework.org/schema/jdbc" xmlns:p="http://www.springframework.org/schema/p" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.0.xsd http://www.springframework.org/schema/jdbchttp://www.springframework.org/schema/jdbc/spring-jdbc-3.0.xsd"> <context:component-scan base-package="com.smartpants.artwork" /> <jdbc:embedded-database id="dataSource" type="H2" /> <bean id="sessionFactory" class="org.springframework.orm.hibernate3.annotation.AnnotationSessionFactoryBean" p:dataSource-ref="dataSource" p:lobHandler-ref="defaultLobHandler" p:packagesToScan="com.smartpants.artwork.domain"> <property name="hibernateProperties"> <value> hibernate.dialect=org.hibernate.dialect.H2Dialect hibernate.show_sql=true hibernate.hbm2ddl.auto=create </value> </property> </bean> <bean id="defaultLobHandler" class="org.springframework.jdbc.support.lob.DefaultLobHandler" /> <bean id="transactionManager" class="org.springframework.orm.hibernate3.HibernateTransactionManager" p:sessionFactory-ref="sessionFactory" /> <tx:annotation-driven proxy-target-class="true" /></beans>
We'll also create another Spring XML file which is more geared towards the REST aspects of our projects. We're going to setup Spring @MVC annotation processing, and XML processing through JAXB.
<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:aop="http://www.springframework.org/schema/aop" xmlns:p="http://www.springframework.org/schema/p" xmlns:tx="http://www.springframework.org/schema/tx" xmlns:jdbc="http://www.springframework.org/schema/jdbc" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.0.xsd http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc-3.0.xsd"> <bean class="org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping" /> <bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter"> <property name="messageConverters"> <list> <ref bean="marshallingHttpMessageConverter"/> </list> </property> </bean> <bean id="marshallingHttpMessageConverter" class="org.springframework.http.converter.xml.MarshallingHttpMessageConverter" p:marshaller-ref="jaxb2Marshaller" p:unmarshaller-ref="jaxb2Marshaller" /> <bean id="jaxb2Marshaller" class="com.smartpants.artwork.controllers.MyJaxb2Marshaller"> <property name="classesToBeBound"> <list> <value>com.smartpants.artwork.domain.Person</value> <value>com.smartpants.artwork.domain.People</value> </list> </property> </bean> <context:component-scan base-package="com.smartpants.artwork.controllers" /> <tx:annotation-driven proxy-target-class="true" /></beans>
There are two things I don't like about this:
1.) Notice that I had to create com.smartpants.artwork.controllers.MyJaxb2Marshaller. That's to get around a problem that OXM has when working with Hibernate proxied objects, which IMHO is a fundamental test case for a Java REST system). Essentially, Spring OXM/JAXB only checks for the @XMLRootElement only on the class itself, and not on the parent classes (like the JAX-RS implementation other Spring subsystems). The hibernate proxy process subclasses your base object (in my case Person) and doesn't add the @XMLRootElement annotation to the subclass. My workaround:
package com.smartpants.artwork.controllers;import javax.xml.bind.annotation.XmlRootElement;import org.springframework.core.annotation.AnnotationUtils;import org.springframework.oxm.jaxb.Jaxb2Marshaller;public class MyJaxb2Marshaller extends Jaxb2Marshaller { @Override public boolean supports(Class clazz) { return super.supports(clazz) ? true : AnnotationUtils.findAnnotation(clazz, XmlRootElement.class) != null; }}
The Spring framework comes with some great annotation processing utilities that Spring OXM doesn't use... Like I said earlier, this doesn't seem battle tested to me.
2.) Why the heck do I need to create the "classesToBeBound" list? The JAX-RS frameworks don't need anything like that... My guess is that this is a legacy from OXM being a WS-* based system.
Domain objects
My single domain object is pretty simple. I need a Person table/entity in my system. It represents a logical user of my system. It plays the dual role of Hibernate object and an object for my REST interface.
package com.smartpants.artwork.domain;import java.io.Serializable;import javax.persistence.Entity;import javax.persistence.GeneratedValue;import javax.persistence.Id;import javax.persistence.Version;import javax.xml.bind.annotation.XmlRootElement;@Entity@XmlRootElementpublic class Person implements Serializable { private static final long serialVersionUID = 1L; private Long id; private String firstName; private String lastName; private String username; private String password; private int roleLevel; private Integer version; public Person() { } public Person(String firstName, String lastName, String username, String password, int roleLevel) { this.firstName = firstName; this.lastName = lastName; this.username = username; this.password = password; this.roleLevel = roleLevel; } @Id @GeneratedValue public Long getId() { return id; } public void setId(Long id) { this.id = id; } 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 getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public int getRoleLevel() { return roleLevel; } public void setRoleLevel(int roleLevel) { this.roleLevel = roleLevel; } @Version public Integer getVersion() { System.out.println("getting version " + getLastName()); return version == null ? 1 : version; } public void setVersion(Integer version) { this.version = version; } public enum RoleLevel { ADMIN(1), GUEST(2), PUBLIC(3); private final int level; RoleLevel(int value) { this.level = value; } public static RoleLevel getLevel(String roleName) { return RoleLevel.valueOf(roleName); } public int getLevel() { return this.level; } }}
Using JAXB generally requires a wrapper object, so I decided to create a People JAXB object:
package com.smartpants.artwork.domain;import java.io.Serializable;import java.util.List;import javax.xml.bind.annotation.XmlRootElement;@XmlRootElementpublic class People implements Serializable { private static final long serialVersionUID = 1L; private List<Person> person; public People() { // empty constructor required for JAXB } public People(List person) { this.person = person; } public List<Person> getPerson() { return person; } public void setPerson(List person) { this.person = person; }}
DAOs
The DAOs aren't exactly new technology, but I'll add them for completeness. Like a good boy, I created a PersonDao interface and PersonDaoHibernate implementation. I've gotten accustomed to Grails/GORM's niceties over the last few months, and the interface kind of bothered me... but that's a post for another time. The PersonDao:
package com.smartpants.artwork.dao;import java.util.List;import com.smartpants.artwork.domain.Person;import com.smartpants.artwork.exception.AuthenticationException;public interface PersonDao { public Person getPerson(Long personId); public void savePerson(Person person); public List<Person> getPeople(); public Person getPersonByUsername(String username); public Person authenticatePerson(String user, String password) throws AuthenticationException;}
The hibernate implementation (notice the super.setupSessionFactory() hack I had to do to get @Autowired to work):
package com.smartpants.artwork.dao.hibernate;import static java.lang.String.format;import java.util.List;import org.hibernate.SessionFactory;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.dao.DataAccessException;import org.springframework.orm.hibernate3.support.HibernateDaoSupport;import org.springframework.stereotype.Repository;import org.springframework.transaction.annotation.Propagation;import org.springframework.transaction.annotation.Transactional;import org.springframework.util.CollectionUtils;import com.smartpants.artwork.dao.PersonDao;import com.smartpants.artwork.domain.Person;import com.smartpants.artwork.exception.AuthenticationException;@Repository@Transactional@SuppressWarnings("unchecked")public class PersonDaoHibernate extends HibernateDaoSupport implements PersonDao { @Autowired public void setupSessionFactory(SessionFactory sessionFactory) { this.setSessionFactory(sessionFactory); } public Person getPerson(Long personId) throws DataAccessException { return this.getHibernateTemplate().load(Person.class, personId); } @Transactional(readOnly = false, propagation = Propagation.REQUIRED) public void savePerson(Person person) throws DataAccessException { this.getHibernateTemplate().saveOrUpdate(person); } public List getPeople() throws DataAccessException { return this.getHibernateTemplate().find("select people from Person people"); } public Person getPersonByUsername(String username) { List people = this.getHibernateTemplate().findByNamedParam( "select people from Person people " + "where people.username = :username", "username", username); Person person = getFirst(people); if (person != null) getHibernateTemplate().evict(person); return person; } public Person authenticatePerson(String username, String password) throws AuthenticationException { List validUsers = this.getHibernateTemplate().findByNamedParam( "select people from Person people " + "where people.username = :username " + "and people.password = :password", new String[] { "username", "password" }, new String[] { username, password }); if (validUsers.isEmpty()) throw new AuthenticationException(format("Could not authenticate %s", username)); return getFirst(validUsers); } private static <T> T getFirst(List<T> list) { return CollectionUtils.isEmpty(list) ? null : list.get(0); }}
Controller
I wanted to build a really simple PersonController that did a POST for adding a new Person at "/people", with a GET at "/people" returning all Person objects (wrapped in a People object), and a GET at "/people/{id}". It's pretty straight forward, but is a bit more verbose that a JAX-RS equivalent:
package com.smartpants.artwork.controllers;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Controller;import org.springframework.transaction.annotation.Transactional;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;import org.springframework.web.servlet.View;import org.springframework.web.servlet.view.RedirectView;import com.smartpants.artwork.dao.PersonDao;import com.smartpants.artwork.domain.People;import com.smartpants.artwork.domain.Person;@Controller@RequestMapping("/people")@Transactional()public class PersonController { private PersonDao personDao; @Autowired public void setPersonDao(PersonDao personDao) { this.personDao = personDao; } @RequestMapping(method = RequestMethod.GET) public People getAll() { return new People(personDao.getPeople()); } @RequestMapping(value = "/{id}", method = RequestMethod.GET) @ResponseBody public Person getPerson(@PathVariable("id") Long personId) { return personDao.getPerson(personId); } @RequestMapping(method = RequestMethod.POST) @Transactional(readOnly = false) public View savePerson(@RequestBody Person person) { personDao.savePerson(person); return new RedirectView("/people/" + person.getId()); }}
web.xml
The web.xml is pretty standard fare for Spring MVC:
<?xml version="1.0" encoding="ISO-8859-1"?><web-app xmlns="http://java.sun.com/xml/ns/j2ee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd" version="2.4"> <display-name>ArtworkWeb</display-name> <description>ArtWork</description> <distributable /> <context-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:spring-context.xml</param-value> </context-param> <filter> <filter-name>hibernateFilter</filter-name> <filter-class>org.springframework.orm.hibernate3.support.OpenSessionInViewFilter</filter-class> </filter> <listener> <listener-class>org.springframework.web.context.ContextLoaderListener </listener-class> </listener> <servlet> <servlet-name>artworkWeb</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:spring-dispatcher.xml</param-value> </init-param> <load-on-startup>2</load-on-startup> </servlet> <!-- - Dispatcher servlet mapping for the web user interface, - refering to the "image" servlet above. --> <servlet-mapping> <servlet-name>artworkWeb</servlet-name> <url-pattern>/*</url-pattern> </servlet-mapping> <filter-mapping> <filter-name>hibernateFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping></web-app>
Jetty & RESTClient
I use RESTClient to test out HTTP. To run our new application, run mvn jetty:run in a command line in your project (I can't seem to get my Eclipse plugins to execute this for me... but theoretically that should work as well). If you'd like to debug your application, set a system variable MAVEN_OPTS to -Xdebug -Xnoagent -Djava.compiler=NONE -Xrunjdwp:transport=dt_socket,address=4000,server=y,suspend=n.
You should get a long log that ends with statements that your /person/ mappings are mapped to PersonController methods. From there you can open up RESTClient, and open up a file with the following contents that set up a POST to /person:
<?xml version="1.0" encoding="UTF-8"?><rest-client version="2.3"><request><http-version>1.1</http-version><URL>http://localhost:8080/people</URL><method>POST</method><headers><header key="ACCEPT" value="application/xml"/></headers><body content-type="application/xml" charset="UTF-8"><person> <firstName>Solomon</firstName> <lastName>Solomon</lastName> <username>Solomon</username> <password>foo</password></person></body></request></rest-client>
Conclusion
I couldn't find much documentation, and had to ask the Spring guys and they were pretty helpful. I think that the Spring Framework's REST development approach is sound, but it doesn't seem to be battle tested yet. Meaning, it seems to be missing features that should exist if this subsystem were broadly deployed. It took me way too long to set up this simple test case, and I had to do a relatively ugly workaround for something that should have been there already. I like Spring's approach, but I'd be hesitant to use it on a big scale app, since you usually end up needing a some feature late in the game... the kind of features that don't exist in less mature platforms
Opinions expressed by DZone contributors are their own.
Comments