Sunday, June 16, 2013

Spring 4 & REST

In this blog I will show you how to use Spring to implement RESTful web services. As with any other Spring MVC application, you will need a controller class that is used to expose your web service methods.

Here is an example:
package com.noushin.spring.ws.controller;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ExceptionHandler;
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 com.noushin.spring.ws.model.Student;
import com.noushin.spring.ws.service.StudentService;

/**
 * 
 * This class is used to demonstrate how Spring is used to implement RESTful web services.
 * 
 * @author nbashir
 *
 */
@Controller
@RequestMapping("/student")
public class StudentController {

   @Autowired
   StudentService studentService;
   
   /**
    * To test: curl -i -v -X POST -H "Content-Type: application/json;charset=UTF-8" http://localhost:8080/ws/student/add -d '{"name":"John Wait", "grade":1}'
    *  
    * @param student JSON representation of student to be added
    * 
    */
   @RequestMapping(value="/add", method=RequestMethod.POST, consumes="application/json;charset=UTF-8", produces="application/json;charset=UTF-8")
   @ResponseBody
   public Student addStudent(@RequestBody Student student) {
      return studentService.addStudent(student);
   }
   
   /**
    * To test:  curl -i -v -X GET -H "Content-Type: application/json;charset=UTF-8" http://localhost:8080/ws/student/get/16a61b8e-f6ae-459f-bb92-164bd7b72d07
    * 
    * @param id Student ID
    * @return A student in JSON format
    */
   @RequestMapping(value="/get/{id}", method=RequestMethod.GET, produces="application/json;charset=UTF-8")
   @ResponseBody
   public Student getStudent(@PathVariable String id) {
      return studentService.getStudent(id);
   }
   
   /**
    * To test:  curl -i -v -X GET -H "Content-Type: application/json;charset=UTF-8" http://localhost:8080/ws/student/getall
    * 
    * @return A list of all students in JSON format
    */
   @RequestMapping(value="/getall", method=RequestMethod.GET, produces="application/json;charset=UTF-8")
   public @ResponseBody List<Student> getStudents() {
      return studentService.getStudents();
   }
   
   /**
    * To test: curl -i -v -X GET http://localhost:8080/ws/student/testexception
    * Only for demonstration purposes.
    * 
    * @throws Exception 
    */
   @RequestMapping(value="/testexception", method=RequestMethod.GET)
   public void testException() throws Exception {
      System.out.println("Service is available and throws exception.");
      studentService.throwException();
   }
   
   /**
    * Exception Handler for this service.
    * 
    * @param exception Use a different subclass of Exception, if you need a more refined exception handling.
    * @return The same student object populated with info on the exception. 
    */
   @ExceptionHandler(Exception.class)
   public @ResponseBody String handleMyException(Exception exception) {
      return (new String("Error : " + exception.getMessage()));
   }
}
We will need a service class to support our controller methods. Note how service class is accessed in the controller by Spring Autowired feature.
package com.noushin.spring.ws.service;

import java.util.List;
import java.util.UUID;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import com.noushin.spring.ws.dao.StudentDao;
import com.noushin.spring.ws.model.Student;

/**
 * 
 * This class is used to demonstrate how Spring is used to implement RESTful web services.
 * 
 * @author nbashir
 * 
 */
@Component
public class StudentService {

   @Autowired
   StudentDao studentDao;

   public Student addStudent(Student student) {
      student.setId(UUID.randomUUID().toString());
      studentDao.addStudent(student);
      return student;
   }

   public Student getStudent(String id) {
      return studentDao.getStudent(id);
   }

   public List<Student> getStudents() {
      return studentDao.getStudents();
   }

   public void updateStudent(Student student) {
      studentDao.updateStudent(student);
   }

   public void deleteStudent(String id) {
      studentDao.deleteStudent(id);
   }

   public void throwException()  throws Exception {
      throw new Exception("This is an artificial Exception to test WS exception handling.");
   }
}
In a typical application, you would need a data layer that manages your data. Here is a typical Data Access Object (DAO) class. Note in this example, I am just using a local static Map to hold the data. In real world applications, you would be accessing some database or similar technology.
package com.noushin.spring.ws.dao;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;

import org.springframework.stereotype.Component;

import com.noushin.spring.ws.model.Student;

/**
 * 
 * This class is used to demonstrate how Spring is used to implement RESTful web services.
 * 
 * @author nbashir
 * 
 */
@Component
public class StudentDao {

   private static Map<String, Student> students = new HashMap<String, Student>();

   public void addStudent(Student student) {
      students.put(student.getId(), student);
   }

   public Student getStudent(String id) {
      return students.get(id);
   }

   public List<Student> getStudents() {
      return new ArrayList<Student>(students.values());
   }

   public void updateStudent(Student student) {
      deleteStudent(student.getId());
      addStudent(student);
   }

   public void deleteStudent(String id) {
      students.remove(id);
   }
}
And finally the business object you are trying to manage:
package com.noushin.spring.ws.model;

/**
 * 
 * This class is used to demonstrate how Spring is used to implement RESTful web services.
 * 
 * @author nbashir
 * 
 */
public class Student {

   private String id;
   private int grade;
   private String name;

   /**
    * Need this constructor for JSON mapping
    */
   public Student() {
      super();
   }

   public Student(int grade, String name) {
      super();
      this.grade = grade;
      this.name = name;
   }

   public Student(String id, int grade, String name) {
      super();
      this.id = id;
      this.grade = grade;
      this.name = name;
   }

   public String getId() {
      return id;
   }

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

   public int getGrade() {
      return grade;
   }

   public void setGrade(int grade) {
      this.grade = grade;
   }

   public String getName() {
      return name;
   }

   public void setName(String name) {
      this.name = name;
   }
}

To run your application as a web based application, you'd need a web.xml and servlet configuration file. Add the following files to your application WEB-INF directory. Example:

~/workspace/ws/src/main/webapp/WEB-INF/web.xml
~/workspace/ws/src/main/webapp/WEB-INF/ws-servlet.xml
web.xml:
<?xml version="1.0" encoding="ISO-8859-1"?>
<web-app version="3.0" metadata-complete="true"
    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">

    <display-name>Spring WS Demo</display-name>
    <description>Spring WS Demo</description>

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

    <servlet-mapping>
        <servlet-name>ws</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>
    
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>/WEB-INF/ws-servlet.xml</param-value>
    </context-param>
 
     <context-param>
        <param-name>log4jConfigLocation</param-name>
        <param-value>/WEB-INF/classes/log4j.properties</param-value>
    </context-param>
    
    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>
   
</web-app>
ws-servlet.xml:
<?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:p="http://www.springframework.org/schema/p"
    xmlns:jms="http://www.springframework.org/schema/jms"
    xmlns:amq="http://activemq.apache.org/schema/core"
    xmlns:mvc="http://www.springframework.org/schema/mvc"
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.2.xsd
                        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.2.xsd
                        http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-3.2.xsd">


    <context:component-scan base-package="com.noushin.spring.ws" />
    <mvc:annotation-driven />
       
</beans>
Since I am using Maven to manage my application build and dependecies, here is the pom.xml you need:
<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>com.noushin.spring</groupId>
    <artifactId>ws</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>war</packaging>
    <name>ws</name>
    <url>http://maven.apache.org</url>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <junit.version>4.10</junit.version>
        <log4j.version>1.2.17</log4j.version>
        <spring.version>4.1.0.RELEASE</spring.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>${junit.version}</version>
            <scope>test</scope>
        </dependency>
        
        <!-- Logging -->
        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>${log4j.version}</version>
        </dependency>
        
        <!-- Spring Test -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-test</artifactId>
            <version>${spring.version}</version>
            <scope>test</scope>
        </dependency>
        
        <!-- Spring web -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-core</artifactId>
            <version>${spring.version}</version>
        </dependency>

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-web</artifactId>
            <version>${spring.version}</version>
        </dependency>

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-webmvc</artifactId>
            <version>${spring.version}</version>
        </dependency>

        <!-- Jackson mapper is used by spring to convert Java POJOs to JSON strings. -->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.2.3</version>
        </dependency>
        
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-war-plugin</artifactId>
                <version>2.1.1</version>
                <configuration>
                    <failOnMissingWebXml>false</failOnMissingWebXml>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-dependency-plugin</artifactId>
                <version>2.1</version>
                <executions>
                    <execution>
                        <phase>validate</phase>
                        <goals>
                            <goal>copy</goal>
                        </goals>
                        <configuration>
                            <outputDirectory>${endorsed.dir}</outputDirectory>
                            <silent>true</silent>
                            <artifactItems>
                                <artifactItem>
                                    <groupId>javax</groupId>
                                    <artifactId>javaee-endorsed-api</artifactId>
                                    <version>6.0</version>
                                    <type>jar</type>
                                </artifactItem>
                            </artifactItems>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

</project>

And since no application development is ever complete without proper unit testing, here are some basic method level testing of StudentService. Note, I already included the curl commands you need to test your web service methods from command line in Linux in StudentController class.

StudentServiceTest.java:
package com.noushin.spring.ws.service;

import static org.junit.Assert.*;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.UUID;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import com.noushin.spring.ws.model.Student;
import com.noushin.spring.ws.service.StudentService;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
public class StudentServiceTest {

   @Autowired
   StudentService service;

   @Before
   public void setUp() throws Exception {
   }

   @After
   public void tearDown() throws Exception {
   }

   @Test
   public void testAddStudent() {
      Student student = service.addStudent(new Student(5, "John Wait"));
      Student addedStudent = service.getStudent(student.getId());
      assertTrue(addedStudent.getGrade() == student.getGrade());
   }

   @Test
   public void testDeleteStudent() {
      Student student = service.addStudent(new Student(5, "John Wait"));
      student = service.getStudent(student.getId());
      service.deleteStudent(student.getId());
      Student deletedStudent = service.getStudent(student.getId());
      assertTrue(deletedStudent == null);
   }

   @Test
   public void testGetStudents() {
      Student student = new Student(1, "Jane Porter");
      service.addStudent(student);
      
      student = new Student(2, "John Downy");
      service.addStudent(student);

      student = new Student(3, "Kate Summerfield");
      service.addStudent(student);

      ArrayList<Student> students = (ArrayList<Student>) service.getStudents();
      assertTrue(students != null);      
      Iterator<Student> it = (Iterator<Student>) students.iterator();
      while (it.hasNext()) {
         student = (Student) it.next();
         System.out.println(student.getGrade() + ":" + student.getName());
      }
   }

   @Test
   public void testUpdateStudent() {
      Student student = service.addStudent(new Student(1, "Jane Porter"));
      int grade1 = student.getGrade();
      System.out.println("Student >>> " + student.getId() + " - Name : " + student.getName() + " - Grade : " + student.getGrade());
      student.setGrade(2);
      Student updatedStudent = service.getStudent(student.getId());
      System.out.println("updatedStudent >>> " + updatedStudent.getId() + " - Name : " + updatedStudent.getName() + " - Grade : " + updatedStudent.getGrade());
      int grade2 = updatedStudent.getGrade();
      assertTrue(grade1 != grade2);
   }
}

Since I am using Spring Test framework, here is the context file you need to run your tests. Place the following file in
~workspace/ws/src/test/resources/com/noushin/spring/ws/service/StudentServiceTest-context.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:amq="http://activemq.apache.org/schema/core" xmlns:context="http://www.springframework.org/schema/context"
    xmlns:p="http://www.springframework.org/schema/p" 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-2.0.xsd
                        http://activemq.apache.org/schema/core http://activemq.apache.org/schema/core/activemq-core.xsd
                        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">

    <context:component-scan base-package="com.noushin.spring.ws" />
    <context:annotation-config />

</beans>


One more note regarding debugging Spring web services. Add the following lines to your log4j.properties file. They will come handy when you get errors when invoking web service methods:
log4j.category.org.springframework.web.servlet.DispatcherServlet=DEBUG
log4j.category.org.springframework.web.servlet.mvc=TRACE

No comments:

Post a Comment