I will list here the common how to-s for the new Annotation based Spring MVC. Please note that I am using Spring version 3.0.0.RELEASE. This is a bit, only a small bit, different from the version 2.5.x. So, if you are using 2.5.x, you have to tweak things a bit before you get the same effect.
Consider the following screen:
The fantatstic thing about Spring MVC is it lets you take an Object Oriented approach while designing the Model (Form Bean) and lets you bind that seamlessly to the View (JSP Form). For the above scenario, we can break down the top section into a list of rows. I would denote the row with a class having select, itemName, price... attributes, as shown below (this is an inner class in my ItemBean class, which is my Form Bean):
public static class ItemRow {
private boolean selected;
private String itemName;
private float price;
private int quantity;
...
}
Then, in my ItemBean class, I can have a List of ItemRow, as shown:
public class ItemBean {
private List items;
private float totalPrice;
private Date expectedDelivery;
...
}
This is how the View (Item.jsp) looks like:
Total Price:
Especially note the
path="items[${count.index}].selected"
in the form:checkbox tag. This is the key. It binds a particular index of the items in the ItemBean class to the JSP.
After the introduction of Annotation @Controller, all Spring MVC controllers have become like MultiActionController, that is, you can handle multiple requests in the same controller. Mapping a url is very simple: you should annotate any public method with the @RequestMapping("/DesiredUrl.do"). Of course, you should make sure:
1.> The class is annotated with @Controller
2.> The following line is present in the Spring context file:
The method can have a motley combination of parameters and return type. Read the javadocs for details. This annotation takes an optional parameter method, which can be any enum RequestMethod type. If you do not specify anything, your method will handle all types of requests like GET, POST, DELETE, etc.
In Spring MVC, the framework creates an instance of the Form Bean and binds it to the fields in JSP in case of a POST. In many cases, I would like to customise the Form Bean before it starts binding to the JSP fields. In older versions of Spring MVC, this was done by overriding the formBackingObject() method in AbstractFormController.
With annotations, you do it by specifying the @ModelAttribute before the FormBean attribute in the request handler method and then again on a public method returning the customised Form Bean as shown below:
@RequestMapping(value = "/checkout.htm", method = RequestMethod.POST)
public ModelAndView checkout(
@ModelAttribute("postBean") ItemBean formBean,
BindingResult errors) {
ModelAndView model = new ModelAndView();
model.addObject("command", formBean);
...
return model;
}
@ModelAttribute("postBean")
public ItemBean initBeanForPost() {
ItemBean bean = new ItemBean();
populateBean(bean, true);
return bean;
}
Note that its always a good practice to specify the name in the @ModelAttribute annotation. This way you can pre-load different beans for different methods. As shown in the example above, the name specified in the @ModelAttribute annotation in the request handler method checkout() and that in the method initBeanForPost() should be an exact match. By the way, this works for GET as well as POST or any other request type.
Spring version 3.x onwards, doing validations have become very easy. I will describe this in the following few steps:
Your validator should implement the Validator interface. It has two methods that you need to override. The supports() method makes sure that the Validator can validate a given FormBean. The validate() method does the actual job of validating. A typical Validator will look like this:
class ItemValidator implements Validator {
@Override
public void validate(Object target, Errors errors) {
ItemBean bean = (ItemBean) target;
if (bean.getTotalPrice() == 0) {
errors.rejectValue("totalPrice", "noItemsSelected");
}
}
@Override
public boolean supports(Class> clazz) {
return clazz == ItemBean.class;
}
}
You do this in a public method annotated with @InitBinder in the controller.
@InitBinder
public void initBinder(WebDataBinder binder, WebRequest webRequest) {
if (binder.getTarget() instanceof ItemBean) {
binder.setValidator(new ItemValidator());
}
}
I am setting the validator inside the if (binder.getTarget() instanceof ItemBean) block as otherwise I get an ugly exception in case there are any validation errors. I am putting the stack trace below:
java.lang.IllegalStateException: Invalid target for Validator [com.swayam.demo.web.controller.ItemController$1@11e7cc6]: org.springframework.validation.BeanPropertyBindingResult: 1 errors
Field error in object 'postBean' on field 'totalPrice': rejected value [0.0]; codes [noItemsSelected.postBean.totalPrice,noItemsSelected.totalPrice,noItemsSelected.float,noItemsSelected]; arguments []; default message [null]] with root cause
java.lang.IllegalStateException: Invalid target for Validator [com.swayam.demo.web.controller.ItemController$1@11e7cc6]: org.springframework.validation.BeanPropertyBindingResult: 1 errors
Field error in object 'postBean' on field 'totalPrice': rejected value [0.0]; codes [noItemsSelected.postBean.totalPrice,noItemsSelected.totalPrice,noItemsSelected.float,noItemsSelected]; arguments []; default message [null]
at org.springframework.validation.DataBinder.setValidator(DataBinder.java:472)
at com.swayam.demo.web.controller.ItemController.initBinder(ItemController.java:63)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
at java.lang.reflect.Method.invoke(Method.java:597)
at org.springframework.web.bind.annotation.support.HandlerMethodInvoker.doInvokeMethod(HandlerMethodInvoker.java:710)
at org.springframework.web.bind.annotation.support.HandlerMethodInvoker.initBinder(HandlerMethodInvoker.java:329)
at org.springframework.web.bind.annotation.support.HandlerMethodInvoker.updateModelAttributes(HandlerMethodInvoker.java:691)
at org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter.invokeHandlerMethod(AnnotationMethodHandlerAdapter.java:417)
at org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter.handle(AnnotationMethodHandlerAdapter.java:402)
at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:771)
This is a new feature added in release 3.x. Now the validation happens with JSR-303 Bean Validation API. You can find the jar here. If you are using Maven, you can add the following dependency:
javax.validation
validation-api
1.0.0.GA
This is how you use it:
@RequestMapping(value = "/checkout.htm", method = RequestMethod.POST)
public ModelAndView checkout(
@Valid ItemBean formBean,
BindingResult errors) {
ModelAndView model = new ModelAndView();
...
return model;
}
The Form Bean is validated with the set Validator by the framework. Note that for Spring 2.5.x, you need to make the call to validator manually inside the handler method:
...
Validator validator = new ItemValidator();
validator.validate(formBean, errors);
...
Lets say, my properties file is Messages.properties located at /com/swayam/demo/web/res/. The contents are:
noItemsSelected=You have not selected any items
I have to create an instance of MessageSource in my Spring conf file as shown:
com.swayam.demo.web.res.Messages
Now I can use the property key in my validate() method as shown:
@Override
public void validate(Object target, Errors errors) {
ItemBean bean = (ItemBean) target;
if (bean.getTotalPrice() == 0) {
errors.rejectValue("totalPrice", "noItemsSelected");
}
}
First, from the controller, you have to pass an instance of BindingResult to the JSP.
@RequestMapping(value = "/checkout.htm", method = RequestMethod.POST)
public ModelAndView checkout(
@Valid ItemBean formBean,
BindingResult errors) {
ModelAndView model = new ModelAndView();
model.addObject("command", formBean);
if (errors.hasErrors()) {
model.setViewName("Item");
model.addObject("errors", errors);
} else {
model.setViewName("Checkout");
}
return model;
}
And this is how the JSP looks like:
Often we have to display a complex object as text. The best example I can take is that of a Date. Its a java.util.Date in the model. How do I map this to a text field in my JSP? You have to extend the PropertyEditorSupport and override the following methods:
1. getAsText(): Converts an Object to its String representation. Used for displaying a model object in the JSP.
2. setAsText(String text): Converts the text from the input field to the complex object that the model understands.
A typical implementation would look like this:
class DateEditorSupport extends PropertyEditorSupport {
private static final Logger LOG = Logger.getLogger(DateEditorSupport.class);
private final Format formatter;
DateEditorSupport(String dateFormat) {
formatter = new SimpleDateFormat(dateFormat);
}
public String getAsText() {
String date = null;
Object value = getValue();
if (value instanceof Date) {
date = formatter.format(value);
} else {
throw new java.lang.IllegalArgumentException("Expecting a "
+ Date.class.getName() + " class, got "
+ value.getClass().getName());
}
return date;
}
public void setAsText(String text) {
try {
Date date = (Date) formatter.parseObject(text);
setValue(date);
} catch (ParseException e) {
LOG.fatal("error setting date for String: " + text, e);
}
}
}
Then, in the @InitBinder method, you need to register this against the class:
@InitBinder
public void initBinder(WebDataBinder binder, WebRequest webRequest) {
binder.registerCustomEditor(Date.class, new DateEditorSupport(
"dd/MM/yyyy"));
}
You will find the sources here. Its an Eclipse project, using Maven. In order to get all the libraries, install Maven and run:
mvn eclipse:clean eclipse:eclipse -DdownloadSources=true -DdownloadJavadocs=true -Dwtpversion=2.0
You can also run the war file directly and go to http://localhost:8080/SpringMVC/ to see it in action.