Spring MVC provides several approaches to exception handling. In Spring, one of the main exception handling constructs is the HandlerExceptionResolver
(org.springframework.web.servlet.HandlerExceptionResolver
) interface. Any objects that implement this interface can resolve exceptions thrown during Controller mapping or execution. HandlerExceptionResolver
implementers are typically registered as beans in the web application context.
Spring MVC creates two such HandlerExceptionResolver
implementations by default to facilitate exception handling:
ResponseStatusExceptionResolver
is created to support the @ResponseStatus
annotationExceptionHandlerExceptionResolver
is created to support the @ExceptionHandler
annotationWe will look at them one by one. First, the @ResponseStatus
(org.springframework.web.bind.annotation.ResponseStatus
) annotation; in
Chapter 3, Control Your Store with Controllers we created a request mapping method to show products by category under the URI template: http://localhost:8080/webstore/market/products/{category}
. If no products were found under the given category, we showed an empty web page, which is not correct semantically as we should show a HTTP status error to indicate that no products exist under the given category. Let's see how to do that with the help of the @ResponseStatus
annotation:
NoProductsFoundUnderCategoryException
under the com.packt.webstore.exception
package in the src/main/java
source folder. Now add the following code to it:package com.packt.webstore.exception; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation .ResponseStatus; @ResponseStatus(value=HttpStatus.NOT_FOUND, reason="No products found under this category") public class NoProductsFoundUnderCategoryException extends RuntimeException{ private static final long serialVersionUID = 3935230281455340039L; }
ProductController
class and modify the getProductsByCategory
method as follows: @RequestMapping("/products/{category}")
public String getProductsByCategory(Model model,
@PathVariable("category") String category) {
List<Product> products =
productService.getProductsByCategory(category);
if (products == null || products.isEmpty()) {
throw new NoProductsFoundUnderCategoryException();
}
model.addAttribute("products", products);
return "products";
}
http://localhost:8080/webstore/market/products/chargers
. You will see a HTTP status error saying No products found under this category, as shown here:In step 1, we created a runtime exception called NoProductsFoundUnderCategoryException
to indicate that no products were found under the given category. One of the important constructs in the NoProductsFoundUnderCategoryException
class is the @ResponseStatus
annotation, which instructs the Spring MVC to return a specific HTTP status if this exception has been thrown from a request mapping method.
We can configure which HTTP status needs to be returned via the value
attribute of the @ResponseStatus
annotation; in our case we configured HttpStatus.NOT_FOUND
(org.springframework.http.HttpStatus
), which displays the familiar HTTP 404 response. The second attribute, reason
, denotes the reason for the HTTP response error.
In step 2, we just modified the getProductsByCategory
method in the ProductController
class to check whether the product list for the given category is empty. If so, we simply throw the exception we created in step 1, which returns a HTTP Status 404 error to the client saying No products found under this category.
So finally in step 3, we fired the web request http://localhost:8080/webstore/market/products/chargers
, which would try to look for products under the category chargers
but, since we didn't have any products under the chargers
category, we got the HTTP Status 404 error.
It's good that we can show the HTTP status error for products not found under a given category, but sometimes you may wish to have an error page where you want to show your error message in a more detailed manner.
For example, run our application and enter http://localhost:8080/webstore/market/product?id=P1234
. You will be able to see a detailed View of the iPhone 6s—now change the product ID in the URL to an invalid one such as http://localhost:8080/webstore/market/product?id=P1000
, and you will see an error page.
We should show a nice error message saying No products found with the given product ID, so let's do that with the help of @ExceptionHandler
:
ProductNotFoundException
under the com.packt.webstore.exception
package in the src/main/java
source folder. Add the following code to it:package com.packt.webstore.exception; public class ProductNotFoundException extends RuntimeException{ private static final long serialVersionUID = -694354952032299587L; private String productId; public ProductNotFoundException(String productId) { this.productId = productId; } public String getProductId() { return productId; } }
InMemoryProductRepository
class and modify the getProductById
method as follows:@Override public Product getProductById(String productID) { String SQL = "SELECT * FROM PRODUCTS WHERE ID = :id"; Map<String, Object> params = new HashMap<>(); params.put("id", productID); try { return jdbcTemplate.queryForObject(SQL, params, new ProductMapper()); } catch (DataAccessException e) { throw new ProductNotFoundException(productID); } }
@ExceptionHandler
(org.springframework.web.bind.annotation.ExceptionHandler
) annotation, as follows, in the ProductController
class:@ExceptionHandler(ProductNotFoundException.class) public ModelAndView handleError(HttpServletRequest req, ProductNotFoundException exception) { ModelAndView mav = new ModelAndView(); mav.addObject("invalidProductId", exception.getProductId()); mav.addObject("exception", exception); mav.addObject("url", req.getRequestURL()+"?"+req.getQueryString()); mav.setViewName("productNotFound"); return mav; }
productNotFound.jsp
under the src/main/webapp/WEB-INF/views/
directory, add the following code snippets to it, and save it:<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%> <%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1"> <link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.0.0/css /bootstrap.min.css"> <title>Welcome</title> </head> <body> <section> <div class="jumbotron"> <div class="container"> <h1 class="alert alert-danger"> There is no product found with the Product id ${invalidProductId}</h1> </div> </div> </section> <section> <div class="container"> <p>${url}</p> <p>${exception}</p> </div> <div class="container"> <p> <a href="<spring:url value="/market/products" />" class="btn btn-primary"> <span class="glyphicon-hand-left glyphicon"> </span> products </a> </p> </div> </section> </body> </html>
http://localhost:8080/webstore/market/product?id=P1000
. You will see an error page showing There is no product found with the Product id P1000 as follows:We decided to show a custom-made error page instead of showing the raw exception in the case of a product not being found for a given product ID. So, in order to achieve that, in step 1 we just created a runtime exception called ProductNotFoundException
to be thrown when the product is not found for the given product ID.
In step 2, we just modified the getProductById
method of the InMemoryProductRepository
class to check whether any products were found for the given product ID. If not, we simply throw the exception (ProductNotFoundException
) we created in step 1.
In step 3, we added our exception handler method to handle ProductNotFoundException
with the help of the @ExceptionHandler
annotation. Within the handleError
method, we just created a ModelAndView
(org.springframework.web.servlet.ModelAndView
) object and stored the requested invalid product ID, exception, and the requested URL, and returned it with the View name productNotFound
:
@ExceptionHandler(ProductNotFoundException.class)
public ModelAndView handleError(HttpServletRequest req, ProductNotFoundException exception) {
ModelAndView mav = new ModelAndView();
mav.addObject("invalidProductId", exception.getProductId());
mav.addObject("exception", exception);
mav.addObject("url", req.getRequestURL()+"?"+req.getQueryString());
mav.setViewName("productNotFound");
return mav;
}
Since we returned the ModelAndView
object with the View name productNotFound
, we must have a View file with the name productNotFound
. That's why we created this View file (productNotFound.jsp
) in step 4. productNotFound.jsp
just contains a CSS-styled <h1>
tag to show the error message and a link button to the product listing page.
So, whenever we request to show a product with an invalid ID such as http://localhost:8080/webstore/product?id=P1000
, the ProductController
class will throw the ProductNotFoundException
, which will be handled by the handleError
method and will show the custom error page (productNotFound.jsp
).