Thursday, October 16th, 2008...1:36 pm
Reusing Annotation Based Controllers in Spring MVC
I was a late comer to the wonderful world of Spring. Seeing that I hate XML, I am very keen on using annotations everywhere! Everything I read said that using annotation based controllers wasn't a good idea because they were hard to re-use due to their very fluid rules for method signatures.
The first thing I did in Spring MVC was to write the perennial CRUD application. After I was done the code was fairly clean but still contained plenty of boilerplate java code. So I wanted to make it generic so I could churn out CRUD's for maintenance tables.
Goals:
- Only write code that's specific to the Entity I am dealing with in the controller.
- Make use of <Generics> so I don't have to cast stuff like a caveman from 1997.
- Have spring figure out URI mapping based on controller names.
- Write zero bean specific xml.
Anotomy of a CRUD Controller.
Your classic CRUD (Create Update Delete) Controller works with one domain type and needs to do a few things:
- List items
- Create items
- Update items
- Delete items
As far as controller methods go, you'll generally need to do a few things:
- Set up your backing domain object for your form on creations and edits
- Validate and save results from the form on submission.
- List items from your data store.
The Old Way
Let's look at how we'd accomplish listing items with Spring MVC.
public ModelMap list(ModelMap model) {
model.addAttribute(dao.selectAll(Employee.class));
return model;
}
So this method uses my dao to run a query. (Your DAO Service implementation will be different depending on how you access your database.)
It then takes the results of that query and throws it in the ModelMap. Recall the ModelMap is smart and will come up with the correct attribute name. So if selectAll returns a List of Employees it will be added to the model as employeeList. Great. Now we use the @RequestMapping annotation to set our URL mapping to employee/list.
The New Way
Let's look at how we could make this list method generic. We've got two things that explicitly reference our Employee domain object. The URL mapping and the .class we pass to our DAO service.
Solving the URL problem
If we define a generic list method in our abstract controller one option for configuring the specific url in the extending class would be something like this.
public ModelMap list(ModelMap map)
{
super(map);
}
I don't like this method, it assumes the person implementing the super class will know that they must override the list method just to set a request mapping. A better method would be if Spring was able to infer the URL map from the implementing controller's class name. ControllerClassNameHandlerMapping to the rescue!
Spring's ControllerClassNameHandlerMapping will take a controller's class name and map it to a URL. I like to mix this with the SimpleUrlHandlerMapping to map views the same way.
Here's how I configure these beans:
class="org.springframework.web.servlet.mvc.support.ControllerClassNameHandlerMapping">
<property name="order" value="1" />
<property name="defaultHandler">
<!--
If no @Controller match, map path to a view to render; e.g. the
"/intro" path would map to the view named "intro"
-->
<bean class="org.springframework.web.servlet.mvc.UrlFilenameViewController" />
</property>
</bean>
<!-- Maps all other request URLs to views -->
<bean id="urlMapping"
class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
<property name="defaultHandler">
<!--
Selects view names to render based on the request URI: e.g. /index
selects "index"
-->
<bean class="org.springframework.web.servlet.mvc.UrlFilenameViewController" />
</property>
<property name="order" value="2" />
</bean>
So what all this means is that if someone requests: /department/list it will automatically map to a Controller called DepartmentController and the @RequestMapping annotated method called list. There's no need to define a URL mapping in our @RequestMapping annotation, we'll just define a request type instead.
Dynamic entity/domain class
There are many ways we can take care of the problem. The coolest way is to use some neat reflection tricks like so:
protected Class<T> getEntityClass()
{
return (Class<T>) ((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()[0];
}
So now we have a generic list method:
public ModelMap list(ModelMap model) {
model.addAttribute(dao.selectAll(getEntityClass()));
return model;
}
Creating/Updating
The normal pattern for doing creating and updating in Spring MVC controllers consists of a few methods.
Methods to display the initial form:
These methods simply show the same form view.
I implemented them like this, we assume the view name from the Domain object (entity)'s name:
public String create(ModelMap model, WebRequest request) {
return ClassUtils.getShortName(getEntityClass()).toLowerCase() + "/form";
}
@RequestMapping(method = RequestMethod.GET)
public String update(ModelMap model) {
return ClassUtils.getShortName(getEntityClass()).toLowerCase() + "/form";
}
Now before the form is submitted we need to tell Spring to bind our form to a @ModelAttribute. This is the same as a FormBackingObject in classic Spring MVC.
The way it works is any method annotated with @ModelAttribute will be executed and the returned object bound to any matching forms in a view.
In this view I do one of two things. If we're going to be doing a create I return a new Domain object. This domain object may have some default values set to get initially bound in to the form. If we're doing an update I need to look up the Domain Object from the DAO Service.
Since our @ModelAttribute annotated method will be called on each request I need a way to tell if this is an update request or a create request. An easy way is just to check if our Primary Key parameter is set in the WebRequest.
Here's what a generic @ModelAttribute method looks like:
So we need another abstract method called getPkParam that returns the String representing the domain property that the Primary Key is stored in.
If on your database the primary key is always called "id" then you could certainly implement getPkParam to return "id" every time in the superclass.
So far we have implemented methods that do the following in our superclass: Forward a request to the correct form view and setup the model object to make it available for automatic binding in our view.
The next step is to define what happens when a form is submitted. When a form is submitted we'll have to do 2 things, weather and update or an insert we need to Validate the form and persist the Domain object to the database if validation passes.
Due to the @ModelAttribute annotation the model object will now be available in your view as {$crudObj}. Ideally we would make it available based on the type name so the view was more readable, but that's one of the limitations we have when using annotations is the inability to add dynamic values as parameters.
Let's look at a generic save method:
public String save(@ModelAttribute("crudObj") T entity, BindingResult bindingResult) {
//validate entity
getValidator().validate(entity, bindingResult);
//return to form if we had errors
if(bindingResult.hasErrors())
return ClassUtils.getShortName(getEntityClass()).toLowerCase() + "/form";
//merge into datasource
dao.save(entity);
//return to list
return "redirect:/" + ClassUtils.getShortName(getEntityClass()).toLowerCase() + "/list.html";
}
We have a new abstract method called getValidator(Object obj,BindingResult res) that will return the validator for your Domain class.
Implementation
We now have all the pieces in place.
Finished CrudController<T>
//Bring in a generic DAO that can handle crud operations.
@Autowired
protected SimpleDao dao;
public CrudController() {
super();
}
abstract protected Validator getValidator();
@SuppressWarnings("unchecked")
protected Class<T> getEntityClass()
{
return (Class<T>) ((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()[0];
}
@SuppressWarnings("unchecked")
protected T getEntityInstance()
{
try {
return (T) Class.forName(getEntityClass().getName()).newInstance();
} catch (InstantiationException e) {
throw new RuntimeException(e);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
}
//assume the primary key property is going to be the Entity Class plus Seq
protected String getPkParam()
{
return ClassUtils.getShortNameAsProperty(getEntityClass()) + "Seq";
}
@RequestMapping(method = RequestMethod.GET)
public ModelMap list(ModelMap model) {
model.addAttribute(dao.selectAll(getEntityClass()));
return model;
}
@RequestMapping(method = RequestMethod.GET)
public String create(ModelMap model, WebRequest request) {
return ClassUtils.getShortName(getEntityClass()).toLowerCase() + "/form";
}
@RequestMapping(method = RequestMethod.GET)
public String update(ModelMap model) {
return ClassUtils.getShortName(getEntityClass()).toLowerCase() + "/form";
}
@RequestMapping(method = RequestMethod.POST)
public String save(@ModelAttribute("crudObj") T entity, BindingResult bindingResult) {
//validate entity
getValidator().validate(entity, bindingResult);
//return to form if we had errors
if(bindingResult.hasErrors())
return ClassUtils.getShortName(getEntityClass()).toLowerCase() + "/form";
//merge into datasource
dao.save(entity);
//return to list
return "redirect:/" + ClassUtils.getShortName(getEntityClass()).toLowerCase() + "/list.html";
}
@RequestMapping(method = RequestMethod.POST)
public String delete(String isInsert, @ModelAttribute("crudObj") T entity, BindingResult bindingResult) {
//delete
dao.delete(entity);
//return to list
return "redirect:/" + ClassUtils.getShortName(getEntityClass()).toLowerCase() + "/list.html";
}
@ModelAttribute("crudObj")
public T setupModel(WebRequest request) {
String pk = request.getParameter(getPkParam());
if(pk == null || org.apache.commons.lang.StringUtils.isEmpty(pk)) {
return getEntityInstance();
} else {
return (T)dao.find(getEntityClass(), Long.parseLong(pk));
}
}
//Set up any custom editors, adds a custom one for java.sql.date by default
@InitBinder
public void initBinder(WebDataBinder binder) {
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
dateFormat.setLenient(false);
binder.registerCustomEditor(Date.class, new CustomSqlDateEditor(dateFormat, false));
}
}
Now if I want to add another CRUD to my application I simply have to do the following:
Implement my abstract CrudController for the Domain class I want to edit. So let's say we wanted a department controller.
It's definition would look like this:
public class DepartmentController extends CrudController<Department> {
@Override
protected Validator getValidator() {
return new DepartmentValidator();
}
}
Next I'd define my validator class, it might look something like this:
public boolean supports(Class departmentClass) {
// TODO Auto-generated method stub
return Department.class.equals(departmentClass);
}
public void validate(Object healthScope, Errors errors) {
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "departmentName","departmentName");
}
}
View set up
If you notice sometimes I pull the view name from the Domain class, this method is obviously assuming a certain directory structure on your view side:
Here's what my view directory looks like:
/jsp
/department
form.jsp
list.jsp
The last thing I'd do is define my jsp's like this: (You can make your jsp's however you want, i'm just showing some example ones for completeness.)
list.jsp
This page just lists our data using the handy DisplayTag library.
<%@ page language="java" contentType="text/html; charset=EUC-KR"
pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@ taglib uri="http://displaytag.sf.net" prefix="display" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<c:import url="/WEB-INF/jsp/header.jsp" />
<body>
<div id="content">
<br />
<h3>Departments</h3>
<span class="pagebanner"><a href="<c:url value="create.html" />">[Create New]</a></span><br />
<display:table name="departmentList" requestURI="list.html" pagesize="25">
<display:column href="update.html" paramId="departmentSeq" paramProperty="departmentSeq"/>
<display:column title="Department Name" property="departmentName"/>
</display:table>
</div>
</body>
</html>
form.jsp
Displays a form for both a new and edit. Notice this pulls from the command object "crudObj" that was set up in the @ModelAttribute("crudObj") annotated method.
pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %>
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<body>
<div id="content">
Create/Edit Department
<form:form action="save.html" commandName="crudObj">
Name: <form:input path="departmentName"/> <form:errors path="departmentName" cssClass="error"/>
<form:hidden path="departmentSeq"/><br />
<input type="submit" value="Submit"/>
</form:form>
</div>
</body>
</html>
Summary
Let's review:
- By using a generic templated base class we reap the benefits of Spring's "configuration by convention" and our controller is more specific and easy to use.
- We take advantage of Configuration by Convention again to automatically have meaningful url's without having to explicitly configure them thanks to Spring's ControllerClassNameHandlerMapping.
The way I created an abstract controller above was just one way to do it, however I believe I've shown that just because your controller classes don't extend an old-style Spring controller class doesn't mean you can't take advantage of inheritance to make your own life easier, even when using annotations.







6 Comments
July 28th, 2009 at 8:40 am
Hi,
Good Article with very good informative information. If you provide us the code, that would be great idea.
December 9th, 2009 at 8:17 pm
Why is update() annotated as a .GET as opposed to .POST?
December 10th, 2009 at 9:03 am
John - it's a GET just because I'm lazy!
February 22nd, 2011 at 10:48 am
thank you for this article
can you put this source code for download please
May 10th, 2011 at 9:53 pm
Thanks
March 10th, 2012 at 5:19 pm
Very nice post!
However, I have a concern about the following;
1. The assumption that Controller deal with one domain object.
2. The controller should not call the DAO layer.
It should call a service layer which may have additional business logic on actions such as "save".
Wonder if you solve these problems in a generic way as well.
Leave a Reply