Spring MVC Exception Handling using @ExceptionHandler with AngularJS GUI

Good exception handling is an essential part of any well developed Application Framework and Spring MVC is no exception — pardon the pun. Spring MVC provides several different ways to handle exceptions in our applications. In this tutorial, we will cover Controller Based Exception Handling using the @ExceptionHandler annotation above the method that will handle it.

For this tutorial we will be creating a custom exception class which we will thrown when a book order comes in from the client that exceeds the stock in hand. Our exception handler will catch these exceptions, generate the appropriate ModelandView and our View will display the appropriate message to our audience.

Defining the Exception Handler using @ExceptionHandler

In this example I create a method and simply annotate it with @ExceptionHandler and pass it the exception class as an argument. In my case, we are trapping the custom exception called LimitedQuantityException. Now that we have registered this exception with Spring using the above noted annotation any exceptions of this type will be handled by this method. Controllers can have multiple exception handlers each handling different exceptions. Now, when exceptions do occur the appropriate exception handler will get called.

@ExceptionHandler(LimitedQuantityException.class)
public ModelAndView handleLimitedQuantityException(LimitedQuantityException LQEx) {
  ModelAndView model = new ModelAndView("error");
  model.addObject("exception", LQEx);

  return model;
}

Libraries Required

For this project I am using Maven for Spring Framework for my builds, but for those of you that prefer to simply copy the required libraries into your webapp/WEB-INF/lib I am including the image below for your reference. In this REST web service I am using JSON so you will notice that a few of the Jackson libraries have been included in additional to the standard Spring MVC library dependencies.

spring exception dependencies

Our final project structure looks like this image below. We will cover all the components in this tutorial.

spring mvc exception

Spring Maven Dependencies (pom.xml)

As mentioned earlier, we will need to add the Jackson library dependency for JSON into our existing pom.xml file.

<?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.avaldes</groupId>
  <artifactId>tutorial</artifactId>
  <name>SpringExceptionHandlingExample</name>
  <packaging>war</packaging>
  <version>1.0.0-BUILD-SNAPSHOT</version>
  <properties>
    <java-version>1.6</java-version>
    <org.springframework-version>3.1.1.RELEASE</org.springframework-version>
    <org.aspectj-version>1.6.10</org.aspectj-version>
    <org.slf4j-version>1.6.3</org.slf4j-version>
    <org.jackson-version>1.9.4</org.jackson-version>
  </properties>
  <dependencies>
    <!-- Spring -->
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-context</artifactId>
      <version>${org.springframework-version}</version>
      <exclusions>
        <!-- Exclude Commons Logging in favor of SLF4j -->
        <exclusion>
          <groupId>commons-logging</groupId>
          <artifactId>commons-logging</artifactId>
         </exclusion>
      </exclusions>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-webmvc</artifactId>
      <version>${org.springframework-version}</version>
    </dependency>
        
    <!-- AspectJ -->
    <dependency>
      <groupId>org.aspectj</groupId>
      <artifactId>aspectjrt</artifactId>
      <version>${org.aspectj-version}</version>
    </dependency> 
    
    <!-- Jackson  -->
    <dependency>
      <groupId>org.codehaus.jackson</groupId>
      <artifactId>jackson-mapper-asl</artifactId>
      <version>${org.jackson-version}</version>
    </dependency>
    
    <!-- Logging -->
    <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>slf4j-api</artifactId>
      <version>${org.slf4j-version}</version>
    </dependency>
    <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>jcl-over-slf4j</artifactId>
      <version>${org.slf4j-version}</version>
      <scope>runtime</scope>
    </dependency>
    <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>slf4j-log4j12</artifactId>
      <version>${org.slf4j-version}</version>
      <scope>runtime</scope>
    </dependency>
    <dependency>
      <groupId>log4j</groupId>
      <artifactId>log4j</artifactId>
      <version>1.2.15</version>
      <exclusions>
        <exclusion>
          <groupId>javax.mail</groupId>
          <artifactId>mail</artifactId>
        </exclusion>
        <exclusion>
          <groupId>javax.jms</groupId>
          <artifactId>jms</artifactId>
        </exclusion>
        <exclusion>
          <groupId>com.sun.jdmk</groupId>
          <artifactId>jmxtools</artifactId>
        </exclusion>
        <exclusion>
          <groupId>com.sun.jmx</groupId>
          <artifactId>jmxri</artifactId>
        </exclusion>
      </exclusions>
      <scope>runtime</scope>
    </dependency>

    <!-- Servlet -->
    <dependency>
      <groupId>javax.servlet</groupId>
      <artifactId>servlet-api</artifactId>
      <version>2.5</version>
      <scope>provided</scope>
    </dependency>
    <dependency>
      <groupId>javax.servlet.jsp</groupId>
      <artifactId>jsp-api</artifactId>
      <version>2.1</version>
      <scope>provided</scope>
    </dependency>
    <dependency>
      <groupId>javax.servlet</groupId>
      <artifactId>jstl</artifactId>
      <version>1.2</version>
    </dependency>
  
  </dependencies>
    <build>
        <plugins>
            <plugin>
                <artifactId>maven-eclipse-plugin</artifactId>
                <version>2.9</version>
                <configuration>
                    <additionalProjectnatures>
                        <projectnature>org.springframework.ide.eclipse.core.springnature</projectnature>
                    </additionalProjectnatures>
                    <additionalBuildcommands>
                        <buildcommand>org.springframework.ide.eclipse.core.springbuilder</buildcommand>
                    </additionalBuildcommands>
                    <downloadSources>true</downloadSources>
                    <downloadJavadocs>true</downloadJavadocs>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>2.5.1</version>
                <configuration>
                    <source>1.6</source>
                    <target>1.6</target>
                    <compilerArgument>-Xlint:all</compilerArgument>
                    <showWarnings>true</showWarnings>
                    <showDeprecation>true</showDeprecation>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.codehaus.mojo</groupId>
                <artifactId>exec-maven-plugin</artifactId>
                <version>1.2.1</version>
                <configuration>
                    <mainClass>org.test.int1.Main</mainClass>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

Web Deployment Descriptor

Our web.xml is quite straight forward. Here we define our DispatcherServlet servlet, define our servlet’s application context and define what our URL pattern is going to be for the dispatcher.

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

  <!-- The definition of the Root Spring Container shared by all Servlets and Filters -->
  <context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>/WEB-INF/root-context.xml</param-value>
  </context-param>
  
  <!-- Creates the Spring Container shared by all Servlets and Filters -->
  <listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
  </listener>

  <!-- Processes application requests -->
  <servlet>
    <servlet-name>dispatcher</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <init-param>
      <param-name>contextConfigLocation</param-name>
      <param-value>/WEB-INF/servlet-context.xml</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
  </servlet>
    
  <servlet-mapping>
    <servlet-name>dispatcher</servlet-name>
    <url-pattern>/</url-pattern>
  </servlet-mapping>

</web-app>

Servlet Configuration

In our servlet-context.xml file we ensure we have our annotation-driven tag so that we can use our annotations: @Controller, @RequestMapping, @ResponseBody, etc.

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

  <!-- DispatcherServlet Context: defines this servlet's request-processing infrastructure -->
  
  <!-- Enables the Spring MVC @Controller programming model -->
  <annotation-driven />

  <!-- Handles HTTP GET requests for /resources/** by efficiently serving up static resources in the ${webappRoot}/resources directory -->
  <resources mapping="/resources/**" location="/resources/" />

  <!-- Resolves views selected for rendering by @Controllers to .jsp resources in the /WEB-INF/views directory -->
  <beans:bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
    <beans:property name="prefix" value="/WEB-INF/views/" />
    <beans:property name="suffix" value=".jsp" />
  </beans:bean>
  
  <context:component-scan base-package="com.avaldes.tutorial" />
  
</beans:beans>

Controller Class with our Exception Handler (RestController.java)

Our RestController class is the main class that contains all web service mapping end points. The @Controller annotation indicates that this particular class is playing the role of a Spring MVC controller.

Note

In this class is where we have defined our Controller Based Exception Handler. It is also here that we are throwing our custom exception should an order arrive with a book quantity that exceeds our stock on hand.

package com.avaldes.tutorial;

import java.io.IOException;
import java.text.DateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;

import org.codehaus.jackson.JsonGenerationException;
import org.codehaus.jackson.map.JsonMappingException;
import org.codehaus.jackson.map.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.servlet.ModelAndView;

import com.avaldes.tutorial.exception.LimitedQuantityException;
import com.avaldes.tutorial.model.Book;
import com.avaldes.tutorial.model.BookOrder;

/**
 * Handles requests for the application home page.
 */
@Controller
public class RestController {
  
  private static final Logger logger = LoggerFactory.getLogger(RestController.class);
  private Map<String, Book> books = new HashMap<String, Book>();
  
  public RestController() {
    // HashMap of sample data to simulate data store
    books.put("978-0312367558", new Book("978-0312367558", "A Wrinkle in Time", "Madeleine L'Engle", "Square Fish", 10, 6.99));
    books.put("978-0486407883", new Book("978-0486407883", "Black Beauty","Anna Sewell", "Dover Publications", 8, 4.50));
    books.put("978-0064400558", new Book("978-0064400558", "Charlotte's Web", "E. B. White", "Harper Collins", 6, 6.29));
    books.put("978-0810993136", new Book("978-0810993136", "Diary of a Wimpy Kid", "Jeff Kinney", "Amulet Books", 15, 7.88));
    books.put("978-0763642648", new Book("978-0763642648", "Guess How Much I Love You", "Sam McBratney", "Candlewick", 12, 4.54));
    books.put("978-0385732550", new Book("978-0385732550", "The Giver", "Lois Lowry", "Ember", 5, 3.99));
    books.put("978-0553609417", new Book("978-0553609417", "Anne of Green Gables", "L.M. Montgomery", "Starfire", 3, 6.95));
    books.put("978-0439023528", new Book("978-0439023528", "The Hunger Games", "Suzanne Collins", "Scholastic Press", 54, 7.53));
  }
  
  @RequestMapping(value="/books", method=RequestMethod.GET)
  @ResponseBody
  public Map<String, Book> getAllBooks() {
    logger.info("Inside getAllBooks() method...");
    
    return books;
  }

  @RequestMapping(value="/books/{isbn}", method=RequestMethod.GET)
  @ResponseBody
  public Book getBookByIsbn(@PathVariable("isbn") String isbn) {
    
    Book myBook = books.get(isbn); 
    
    if (myBook != null) {
      logger.info("Inside getBookByIsbn, returned: " + myBook.toString());
    } else {
      logger.info("Inside getBookByIsbn, isbn: " + isbn+ ", NOT FOUND!");
    }
    return myBook; 
  }
  
  @RequestMapping(value="/books/submitOrder", method=RequestMethod.POST)
  @ResponseBody
  public BookOrder submitOrder(@RequestParam("ISBN") String ISBN, @RequestParam("quantity") int quantity) 
      throws LimitedQuantityException {
    BookOrder order = null;
    
    Book myBook = books.get(ISBN); 
    if (myBook != null) {
      if (quantity > myBook.getStockQty()) {
        logger.info("Inside submitOrder, quantity exceeds Stock on Hand...");
        String msg = String.format("Book(%s) ISBN(%s): Quantity %d exceeds stock on hand of %d books", 
            myBook.getName(), myBook.getISBN(), quantity, myBook.getStockQty());
        throw new LimitedQuantityException(msg);
      } else {
        order = new BookOrder();
        order.setISBN(myBook.getISBN());
        order.setName(myBook.getName());
        order.setAuthor(myBook.getAuthor());
        order.setPublisher(myBook.getPublisher());
        order.setQuantity(quantity);
        order.setPrice(myBook.getPrice());
        double subtotal = quantity*myBook.getPrice();
        String subtot = String.format("%13.2f", subtotal);
        order.setSubtotal(new Double(subtot).doubleValue());
        logger.info("Inside submitOrder, adding: " + order.toString());
      }
    } else {
      logger.info("Inside submitOrder, unable to find book with ISBN [ " + ISBN +" ]");
    }
    return order;
  }

  @RequestMapping(value="/books/addOrder", method=RequestMethod.GET)
  public ModelAndView addOrder() {
    ModelAndView model = new ModelAndView("bookOrder", "command", new Book());
    ArrayList<Book> bookList = new ArrayList<Book>(books.values());
    ObjectMapper mapper = new ObjectMapper();
    try {
      model.addObject("jsonbooks", mapper.writeValueAsString(bookList));
    } catch (JsonGenerationException e) {
      e.printStackTrace();
    } catch (JsonMappingException e) {
      e.printStackTrace();
    } catch (IOException e) {
      e.printStackTrace();
    }
    return model;
  }
  
  @ExceptionHandler(LimitedQuantityException.class)
    public ModelAndView handleLimitedQuantityException(LimitedQuantityException LQEx) {
    ModelAndView model = new ModelAndView("error");
    model.addObject("exception", LQEx);

    return model;
  }
}

Model Classes

For this tutorial, I defined two model classes one for Book which defines the books we have in our virtual bookstore and the other class called BookOrder which will handle the incoming book order request from our simple web application. These classes will be transmitted as JSON objects from our REST web service back to our client.

Book.java

package com.avaldes.tutorial.model;

public class Book { 
  private String ISBN;
  private String name;
  private String author;
  private String publisher;
  private int stockQty;
  private double price;
  
  public Book() {}
  
  public Book(String ISBN, String name, String author, 
                           String publisher, int qty, double price) {
    this.ISBN = ISBN;
    this.name = name;
    this.author = author;
    this.publisher = publisher;
    this.stockQty = qty;
    this.price = price;
  }
  
  public String getISBN() {
    return ISBN;
  }
  
  public void setISBN(String ISBN) {
    this.ISBN= ISBN;
  }
  
  public String getName() {
    return name;
  }
  
  public void setName(String name) {
    this.name = name;
  }
  
  public String getAuthor() {
    return author;
  }
  
  public void setAuthor(String author) {
    this.author = author;
  }
  
  public String getPublisher() {
    return publisher;
  }
  
  public void setPublisher(String publisher) {
    this.publisher = publisher;
  }
  
  public int getStockQty() {
    return stockQty;
  }

  public void setStockQty(int stockQty) {
    this.stockQty = stockQty;
  }

  public double getPrice() {
    return price;
  }

  public void setPrice(double price) {
    this.price = price;
  }

  @Override
  public String toString() {
    return "Book [ISBN=" + ISBN+ ", name=" + name + ", author=" + author
        + ", publisher=" + publisher + ", stockQty=" + stockQty 
        + ", price=" + price + "]";
  } 
}

BookOrder.java

package com.avaldes.tutorial.model;

public class BookOrder {
  private String ISBN;
  private String name;
  private String author;
  private String publisher;
  private int quantity;
  private double price;
  private double subtotal;   // not really needed, but ...   
  
  public BookOrder() {}
  
  public BookOrder(String ISBN, String name, String author, String publisher, 
                                int quantity, double price, double subtotal) {
    this.ISBN= ISBN;
    this.name = name;
    this.author = author;
    this.publisher = publisher;
    this.quantity = quantity;
    this.price = price;
    this.subtotal = subtotal;
  }
  
  public String getISBISBNN() {
    return ISBN;
  }
  
  public void setISBN(String ISBN) {
    this.ISBN= ISBN;
  }
  
  public String getName() {
    return name;
  }
  
  public void setName(String name) {
    this.name = name;
  }
  
  public String getAuthor() {
    return author;
  }
  
  public void setAuthor(String author) {
    this.author = author;
  }
  
  public String getPublisher() {
    return publisher;
  }
  
  public void setPublisher(String publisher) {
    this.publisher = publisher;
  }
  
  public int getQuantity() {
    return quantity;
  }

  public void setQuantity(int quantity) {
    this.quantity = quantity;
  }

  public double getPrice() {
    return price;
  }

  public void setPrice(double price) {
    this.price = price;
  }

  public double getSubtotal() {
    return subtotal;
  }

  public void setSubtotal(double subtotal) {
    this.subtotal = subtotal;
  }

  @Override
  public String toString() {
    return "BookOrder [ISBN=" + ISBN + ", name=" + name + ", author="
        + author + ", publisher=" + publisher + ", quantity="
        + quantity + ", price=" + price + ", subtotal=" + subtotal
        + "]";
  } 
}

Custom Exception Class (LimitedQuantityException.java)

package com.avaldes.tutorial.exception;

public class LimitedQuantityException extends Exception {

  private static final long serialVersionUID = -3712981290802922344L;
  private String message;
  
  public LimitedQuantityException(String message) {
    super();
    this.message = message;
  }

  public String getMessage() {
    return message;
  }

  public void setMessage(String message) {
    this.message = message;
  }
}

View (BookOrder.jsp)

<%@ taglib prefix="f" uri="http://www.springframework.org/tags/form" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>

<html>
 <head>
   <title>Book Order Form</title>
   <link href="<c:url value="/resources/include/styles.css"/>" rel="stylesheet">
    <script type="text/javascript" 
     src="http://ajax.googleapis.com/ajax/libs/angularjs/1.2.1/angular.min.js">
   </script>
   <script>
   function bookController($scope) {
       $scope.optionsGroup = <% String json = 
                    (String) request.getAttribute("jsonbooks");
       out.println(json);%>;
   }
   </script>
 </head>
 <body ng-controller="bookController" ng-app>
      <h1>Book Order Form</h1>
      <div>
        <h2>Order Price: {{book.price*quantity | currency}}</h2>
      </div>
    <form method="POST" action="/SpringExceptionHandling/books/submitOrder">
    <table>
      <tbody>
        <tr>
          <td><label for="name">Book Name:</label></td>
          <td><select ng-options="value.name for value in optionsGroup" 
               ng-model="book"></select>
          </td>
        </tr>
        <tr>
          <td><label for="name">Book Name:</label></td>
          <td>{{book.isbn}}</td>
        </tr>
        <tr>
          <td><label for="author">Book Author:</label></td>
          <td>{{book.author}}</td>
        </tr>
        <tr>
          <td><label for="publisher">Publisher:</label></td>
          <td>{{book.publisher}}</td>
        </tr>
        <tr>
          <td><label for="price">price:</label></td>
          <td>{{book.price | currency}}</td>
        </tr>
        <tr>
          <td>Quantity:</td>
          <td><input type="text" ng-model="quantity"/></td>
       </tr>
       <tr>
         <td colspan="2">
          <input type="hidden" name="ISBN" id="ISBN" value="{{book.isbn}}">
          <input type="hidden" name="quantity" id="quantity" value="{{quantity}}">
          <input type="submit" value="Submit Order" class="button">
         </td>
       </tr>
     </tbody>
    </table>
    </form> 
  </body>
</html>

Output

book order form

In the next screenshot a book order has come in that has exceeded our stock on hand allowing us to throw our custom exception. As you can see from the screenshot below, the exception was handled by our exception handler and sent to the appropriate view.

book order exception

Download the Code

That’s It

Hopefully, you will found this tutorial helpful. Please like us on social media so we can continue bringing you additional tutorials like this one.

spring mvc angularjs

[sc:spring_mvc ]

Please Share Us on Social Media

Facebooktwitterredditpinterestlinkedinmail

Leave a Reply

Your email address will not be published. Required fields are marked *