Annotation-Driven Spring MVC

This is an email I sent to the developers list, but it seems worth preserving here.
-Djazayeri 15:46, 2 June 2009 (EDT)


Hi All,

I wanted to tell everyone about how cool annotation-driven Spring MVC is. I think it's so cool that I'm going to write a long email about it, with lots of examples.

First off, you need the latest fix (that I just checked in as revisions 8165-8167) to make one small piece of this work, so please do an svn update on 1.4, 1.5, or trunk before trying it out.

Now, let's say you want to do some rapid prototyping of a new UI in a module. So step 1 is to create your module.

Step 2 is to create a moduleApplicationContext.xml in the module's metadata directory. It should look like this:

<?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:context="http://www.springframework.org/schema/context"
       xmlns:util="http://www.springframework.org/schema/util"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
           http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
           http://www.springframework.org/schema/context
           http://www.springframework.org/schema/context/spring-context-2.5.xsd
           http://www.springframework.org/schema/util
           http://www.springframework.org/schema/util/spring-util-2.0.xsd">
    <context:component-scan base-package="@MODULE_PACKAGE@" />
</beans>

Now let's say you want a homepage that says "Welcome, (Your Name)". It's easy. You just need to write a controller and a jsp, but you no longer need to map them in the xml file.

Here's your Java class:

@Controller
public class HomepageController {
    @RequestMapping("/module/yourModuleId/homepage")
    public void showHomepage(ModelMap model) {
        model.addAttribute("authenticatedUser", Context.getAuthenticatedUser());
    }
}

(Note that you need to be including the spring 2.5 jar, not the spring 2.0 jar.)

Here's your JSP, which has to be (by convention) web/module/homepage.jsp.

<%@ include file="/WEB-INF/template/include.jsp"%>
<%@ include file="/WEB-INF/template/header.jsp"%>

<h1>Hello, ${authenticatedUser.personName}!</h1>

<%@ include file="/WEB-INF/template/footer.jsp"%>

So easy! Deploy your module and go to http://localhost:8080/openmrs/module/yourModuleId/homepage.list and voila.

Note: you must browse to a .list or .form extension or else bad things will happen. (If you were to visit homepage.htm the controller will be bypassed and you'll just get the jsp file.)

(If you want to use a different jsp view, e.g. because you want a single controller to have different views depending on some inputs, you may have your controller return a String view name instead of void. You must give the full path for the view, e.g. "/module/yourModule/homepage" .  Returning a null will make Spring show the default view as usual.  Read the Spring documentation on resolving views for more information.)

So, we've now just made a new page with just a java file and a jsp file, without having to muck around with xml deployment descriptors. That's nice, but things get better. Now let's make a page that lets the user retire a location. Let's start with the JSP this time, which we'll put at web/module/manageLocations.jsp.

<%@ include file="/WEB-INF/template/include.jsp"%>
<%@ include file="/WEB-INF/template/header.jsp"%>

<c:forEach var="loc" items="${locations}">
    ${loc.name}
    <a href="retireLocation?locationId=${loc.locationId}">Retire</a>
    <br/>
</c:forEach>

<%@ include file="/WEB-INF/template/footer.jsp"%>

Now we need a java controller that will (1) provide the list of all locations, and (2) allow you to retire one.

@Controller
public class ManageLocationsController {
    /** This method backs the list-all-locations page */
    @RequestMapping("/module/yourModuleId/manageLocations")
    public void listLocations(ModelMap model) {
        model.addAttribute("locations", Context.getLocationService().getAllLocations(false));
    }

    /** This method retires a location */
    @RequestMapping("/module/yourModuleId/retireLocation")
    public String retireLocation(@RequestParam("locationId") Integer locationId) {
        Location loc = Context.getLocationService().getLocation(locationId);
        Context.getLocationService().retireLocation(loc, "Because I want to");
        return "redirect:manageLocations";
    }
}

We've done two things here. First, we're using just a single java class to contain the controllers for both of these functions. This is easier, and I think it's better practice too, to combine related UI functionality. Second, we're letting the framework automatically extract the "locationId" parameter from the request and parse it as an Integer. The framework also has options that will get you parameters out of the session (similar to sessionForm="true" in the old MVC framework), or get you the session itself.

Hopefully you think this is as cool as I do. You can read more about it in the spring documentation here: http://static.springsource.org/spring/docs/3.0.x/spring-framework-reference/html/mvc.html#mvc-controller

You know what would be awesome? If someone followed up this email with simple instructions for how to write a web controller unit test, so we could turn this into a tutorial on test-driven-development.

-Darius