Статьи

Spring 3.0 Portlet MVC – Часть II (Аннотации)

This article is taken from the book Portlets in Action. This is the 2nd article in the 3-part series of articles focusing on Spring 3.0 Portlet MVC framework. In the previous article, available here, we discussed how a simple Hello World portlet can be created using Spring Portlet MVC framework. In this article, we look at creating a Book Catalog multi-page portlet using annotations.

Our first order or business is to understand the requirements of Book Catalog portlet. Once we accomplish that, we will delve into the details of Spring Portlet MVC annotations.

NOTE

The example portlet in this article doesn’t use any database; therefore, you don’t need to install a database to get the portlet working. The basic setup that you did in Part I of this article will be used to deploy and run the example portlet.

 

Book Catalog Portlet: Requirements

Figure 1 shows the book catalog portlet’s home page, which shows the list of books in the catalog.

Figure 1 Book catalog portlet’s home page showing list of books in the catalog. The page gives the option to edit or remove an existing book or to add new book to the catalog.

‘Add New Book’ option allows you to add a new book to the catalog, as shown in figure 2.

Figure 2 Book name, author and ISBN number information must be supplied to add a new book to the catalog. The ‘Home’ link takes user to the home page of the portlet.

Figure 2 shows that book name, author and ISBN number fields are mandatory. If a user attempts to save book information without entering any of the mandatory fields or the entered value of ISBN number is not numeric, then an error message is shown to the user, as shown in figure 3.

Figure 3 Error messages are shown if the required fields are not entered by the user or the ISBN number is non-numeric, while editing an existing book or adding a new book to the catalog.

As each book in the catalog has a unique ISBN number, an attempt to add a book with an existing ISBN number will show the following error message:

“A book with the same ISBN number already exists. Please enter a different ISBN number”

When the book information is successfully added then the user is taken to the home page (shown in figure 1) of the Book Catalog portlet. ‘Edit’ and ‘Remove’ options in figure 1 allow editing and removing the selected book, respectively, from the catalog. ‘Edit’ option shows a page similar to the one shown for ‘Add Book’ option (refer figure 2), with the selected book information shown in an editable form. Saving the edited book information takes the user to the home page (shown in figure 1) of the portlet. While deleting a book, the user is asked to confirm delete action, before permanently removing the book from the catalog.

It’s recommended that at this time you download the source code from the following location: http://code.google.com/p/portletsinaction/downloads/list and import it into your Eclipse IDE for reference.

Spring Portlet MVC Annotations

In Spring 2.5, annotations were introduced to create controllers that support form handling/command object functionality, without requiring controllers to extend or implement framework-specific classes or interfaces. These new breed of controllers (referred to as ‘annotated controllers’) make use of annotations at the type (which is at class or interface level), method and method parameter levels to provide the controller functionality. We will look at all the annotations that will help us in developing our Book Catalog portlet.

Let’s look at each of these annotations in detail.

Identifying your controllers with @Controller
@Controller is a class level annotation, which indicates that the annotated class is a Spring component of type ‘controller’. Listing 1 shows an example usage of @Controller annotation.

Listing 1 @Controller annotation — AddBookController

import org.springframework.stereotype.Controller;@Controller(value="addBookController")                                   #1public class AddBookController {  ....  public String showAddBookForm(..) {                                       ....  }  public void addBook(..) {                                                 ....  }  ....}

#1 @Controller annotated Spring handler

At #1, @Controller class-level annotation accepts an optional value element which specifies a logical name of the controller component. The value element is same as the id attribute of bean element, which we used to define our objects in web application context XML file, which we saw in Part I of this article series. The above @Controller annotation is same as the following definition in the web application context XML file:

<bean id=”addBookController”     class=”chapter08.code.listing.base.AddBookController”/>

Classpath Scanning and Auto-Detection
The @Controller annotation is typically used along with classpath scanning feature of Spring to allow auto-detection of controller classes. The classpath scanning feature scans for Spring components in the classpath, that is, components that are annotated with @Component, @Service, @Controller or @Repository annotations, and auto-detects, that is, registers them with the web application context. This effectively means that if you are using classpath scanning feature along with Spring’s component annotations, you don’t need to explicitly declare beans in the web application context XML file.

To enable classpath scanning for a portlet, you must declare the following entry in the web application context XML file of the portlet:




<context:component-scan base-package="sample.bookcatalog.code.listing"/>

The component-scan element of spring-context schema is used to enable classpath scanning for Spring components. The base-package attribute specifies comma-separated names of the packages under which the Spring container will look for Spring components.

Now, if you have defined a BookService bean with @Service annotation, as shown below, then it will be registered in the web application context by the Spring framework with id as ‘myBookService’.

@Service(value=”myBookService”)public class BookServiceImpl implements BookService { ....}

Dependency Injection Using @Autowired Annotation
The @Autowired annotation is used with a field, setter method or constructor to instruct the Spring container to inject the dependency. For instance, the AddBookController handler bean makes use of @Autowired annotation to instruct Spring container to inject the BookSerivce bean, as shown here:

@Controller(value="addBookController")                                   public class AddBookController {    @Autowired    private BookService bookService;    }

@Autowired annotation resolves dependencies ‘by type’ and not ‘by name’. To resolve dependencies ‘by name’, AddBookController makes use of @Autowired annotation along with @Qualifier annotation. The @Qualifier annotation in its simplest form, accepts the name (or you can say id) of the bean which needs to be injected. The following code snippet shows how to resolve dependencies ‘by name’:

@Controller(value="addBookController")                                   public class AddBookController {    @Autowired    @Qualifier(“myBookService”)    private BookService bookService;    }

In the above code @Qualifier accepts a String element which corresponds to the name of the BookService bean which needs to be injected into AddBookController.

Map request to handlers using @RequestMapping

The @RequestMapping annotation maps portlet requests to appropriate handlers and handler methods. @RequestMapping at class level maps portlet request to an appropriate handler based on current portlet mode and request parameters. Listing 2 shows that request in VIEW portlet mode can be handled by AddBookController.

Listing 2 @RequestMapping annotation — AddBookController

import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.RequestMapping;@RequestMapping(value = "VIEW")                                          #1@Controller(value="addBookController")                                   public class AddBookController {  @Autowired  @Qualifier(“myBookService”)  private BookService bookService;    public String showAddBookForm(..) {                                       ....  }  public void addBook(..) {                                                 ....  }  ....}

#1 @RequestMapping annotation

In Listing 2, the value element of @RequestMapping annotation specifies the portlet mode which is supported by AddBookController handler.

Identify render methods with @RenderMapping

@RenderMapping is a method level annotation which is used to map render requests to render methods of handler class. Listing 3 shows how @RenderMapping annotation is used by AddBookController.

Listing 3 @RenderMapping annotation — AddBookController

import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.portlet.bind.annotation.RenderMapping;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.beans.factory.annotation.Qualifier;@RequestMapping(value = "VIEW")                                          @Controller(value="addBookController")                                   public class AddBookController {  @Autowired  @Qualifier(“myBookService”)  private BookService bookService;     @RenderMapping(params=”myaction=addBookForm”)                          #1  public String showAddBookForm(RenderResponse response) {                                       return “addBookForm”;  }  public void addBook(..) {                                                 ....  }  ....}

#1 @RenderMapping annotation

At #1, @RenderMapping annotation marks showAddBookForm method as a render method of AddBookController handler. The params element specifies request parameter name-value pairs, which should be present in the portlet request to invoke the annotated method. For instance, showAddBookForm method is invoked when the current portlet mode is VIEW (refer @RequestMapping class-level annotation of AddBookController) and the value of request parameter ‘myaction’ is ‘addBookForm’.

In listing 3, showAddBookForm method is a render method but it has not been defined to accept RenderRequest and RenderResponse objects. This is because annotated controllers allow you to have flexible signatures for your render / action methods, that is, you can pass arguments and define return type appropriate for your request handling method.

Identify action methods with @ActionMapping

@ActionMapping is a method level annotation which maps action requests to handler methods. Listing 4 shows how @ActionMapping annotation is used by AddBookController.

Listing 4 @ActionMapping annotation — AddBookController

import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.portlet.bind.annotation.*;import org.springframework.beans.factory.annotation.*;@RequestMapping(value = "VIEW")                                          @Controller(value="addBookController")                                   public class AddBookController {  @Autowired  @Qualifier(“myBookService”)  private BookService bookService;     @RenderMapping(params=”myaction=addBookForm”)                            public String showAddBookForm(RenderResponse response) {                                       return “addBookForm”;  }  @ActionMapping(params = "myaction=addBook")                            #1  public void addBook(..) {                                                 ....  }  ....}

#1 @ActionMapping annotation

At #1, @ActionMapping annotation marks addBook method as an action method of handler. The value element (not used in the code listing) specifies the value of the request parameter javax.portlet.action. The params element specifies request parameter name-value pairs, which should be present in the portlet request to invoke addBook method. For instance, if the request is in VIEW portlet mode and the value of ‘myaction’ request parameter is ‘addBook’, then addBook handler method is invoked.

Pass request parameters to action/render methods using @RequestParam
@RequestParam is a method parameter level annotation, meant for binding request parameter to a method argument. It is useful when you want to pass a request parameter value as an argument to your handler method.

Listing 5 shows how @RequestParam is used in EditBookController.

Listing 5 @RequestParam annotation — EditBookController

import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.RequestMapping;@RequestMapping(value = "VIEW")                                          @Controller(value="editBookController")                                   public class EditBookController {  @Autowired  @Qualifier(“myBookService”)  private BookService bookService;  ....  @ModelAttribute(“book”)                                                #1  public Book getBook(@RequestParam Long isbnNumber) {                   #2                                                return bookService.getBook(isbnNumber);  }  ....}

#1 @ModelAttribute annotation
#2 @RequestParam annotation

In listing 5, @RequestParam has been used to pass the request parameter isbnNumber to getBook method. The @RequestParam annotation precedes the method argument whose value comes from request parameter. The @RequestParam is also responsible for type conversion of request parameter value from String to the method argument type, as shown in the code above. Listing 5 also shows that getBook method is annotated with @ModelAttribute annotation, which we’ll discuss next.

Identify model attributes with @ModelAttribute
In Part I of this article series, we programmatically added model attributes to the Spring’s Model object. You can achieve the same functionality in annotated controllers using @ModelAttribute annotation. The @ModelAttribute annotation is responsible for adding/retrieving model attributes to/from the Model object. @ModelAttribute is a method as well as a method parameter level annotation. If used at method level, it is meant to bind return value of the method to a model attribute. If used at method parameter level it is meant to bind a model attribute to a method argument.

Model attributes in Spring consists of command objects and reference data (if any) that is used by the controller / handler to fulfill portlet requests. Views (like JSP pages) in Spring Portlet MVC usually get their data from model attributes, therefore, to show the Book Catalog portlet’s home page as shown in figure 1, you need to obtain the list of books from the data source and store it as a model attribute.

Listing 6 shows how BookController retrieves books from catalog and stores them as a model attribute.

Listing 6 @ModelAttribute annotation — BookController

import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.RequestMapping;@RequestMapping(value = "VIEW")                                          @Controller(value="bookController")                                   public class BookController {  @Autowired  @Qualifier(“myBookService”)  private BookService bookService;  ....  @ModelAttribute(“books”)                                               #1  public Book getBooks() {                                                                   return bookService.getBooks();  }  ....}

#1 @ModelAttribute annotation

In the above code, the list of books obtained by invoking the getBooks method of BookService is stored as model attribute with name books. If a handler class contains @ModelAttribute annotated methods then these methods are called before the render/action method of the handler.

You can also create your command objects using @ModelAttribute annotated methods. Figure 2 shows the ‘Add Book’ form, which consists of multiple entry fields. You can write a @ModelAttribute annotated method to create a ‘Book’ command object and add it to the model. Request parameters will be bound to the ‘Book’ command object when the ‘Add Book’ form is submitted by clicking the ‘Add Book’ button, as shown in figure 2. Listing 7 shows how you can create ‘Book’ command object using @ModelAttribute annotated method:

Listing 7 @ModelAttribute annotation — AddBookController

import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.RequestMapping;@RequestMapping(value = "VIEW")                                          @Controller(value="addBookController")                                   public class AddBookController {  @Autowired  @Qualifier(“myBookService”)  private BookService bookService;     @RenderMapping(params=”myaction=addBookForm”)                            public String showAddBookForm(RenderResponse response) {                                       return “addBookForm”;  }  @ActionMapping(params = "myaction=addBook")                              public void addBook(..) {                                                 ....  }  @ModelAttribute("book")                                                #1  public Book getCommandObject() {    return new Book();  }  ....}

#1 @ModelAttribute annotation

In the above code, getCommandObject is invoked before the render/action method; therefore, it becomes an ideal place to create command objects.

@ModelAttribute annotation at method parameter level is used to bind a model attribute to the method argument, that is, the model attribute is passed as an argument to the method, as shown in listing 8.

Listing 8 @ModeAttribute annotation — AddBookController

import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.RequestMapping;@RequestMapping(value = "VIEW")                                          @Controller(value="addBookController")public class AddBookController {  @Autowired  @Qualifier(“myBookService”)  private BookService bookService;     @RenderMapping(params=”myaction=addBookForm”)                            public String showAddBookForm(RenderResponse response) {                                       return “addBookForm”;  }  @ActionMapping(params = "myaction=addBook")                              public void addBook(@ModelAttribute(value="book")Book book, ..) {      #1     ....                                           bookService.addBook(book);  }  @ModelAttribute("book")                                                #1                                     public Book getCommandObject() {    return new Book();  }  ....}

#1 @ModelAttribute annotation

In the above code @ModelAttribute(“book”) annotation in addBook method retrieves model attribute named ‘book’ and binds it with the book argument of addBook method.

Methods annotated with @ModelAttribute annotation have the same flexibility in their signature as the methods with @RenderMapping/@ActionMapping annotation.

Initialize WebDataBinder with @InitBinder
Figure 3 shows binding and validation errors that occur when ‘Add Book’ form is submitted. The WebDataBinder object in Spring framework binds request parameters to controller’s command object (which is model attributes in annotated controller) and specifies the validator to be used for validating the command object/model attribute. If binding or validation errors occur during request processing, then they are stored into Errors object of Spring framework. The Errors object is then used by the JSP page to show appropriate binding or validation error messages.

@InitBinder is a method level annotation which is used to initialize WebDataBinder object. Listing 9 shows how AddBookController makes use of @InitBinder annotation.

Listing9 @InitBinder annotation — AddBookController

import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.RequestMapping;@RequestMapping(value = "VIEW")                                          @Controller(value="addBookController")public class AddBookController {  @Autowired  @Qualifier(“myBookService”)  private BookService bookService;     @RenderMapping(params=”myaction=addBookForm”)                            public String showAddBookForm(RenderResponse response) {                                       return “addBookForm”;  }  @ActionMapping(params = "myaction=addBook")                              public void addBook(@ModelAttribute(value="book")Book book, ..) {    ....                                                  bookService.addBook(book);  }  @ModelAttribute("book")                                                                                     public Book getCommandObject() {    return new Book();  }  @InitBinder("book")                                                    #1  public void initBinder(WebDataBinder binder) {binder.registerCustomEditor(Long.class, new LongNumberEditor());  }  ....}

#1 @InitBinder annotation

The @InitBinder(«book») annotation means that the initBinder method is meant to initialize the WebDataBinder for the AddBookController handler and it applies to the book model attribute. The registerCustomEditor registers LongNumberEditor custom property editor for Long type properties in the book model attribute. The LongNumberEditor is useful in converting the ISBN number, entered by the user on ‘Add Book’ form, to Long type in the Book object.

Validation

Book Catalog portlet uses Spring’s Validator framework for validating data entered during editing and adding book. Listing 10 shows AddBookValidator class which implements Spring’s Validator interface.

Listing 10 AddBookValidator Spring validator

@Component("myAddBookValidator")  public class AddBookValidator implements Validator {                      #1       @Autowired    @Qualifier("myBookService")    private BookService bookService;                                        #2 public boolean supports(Class<?> klass) {                               #3      return Book.class.isAssignableFrom(klass);    }     public void validate(Object target, Errors errors) {                    #4    Book book = (Book)target;    ValidationUtils.rejectIfEmptyOrWhitespace(errors,      "name", "NotEmpty.book.name");....     if(!bookService.isUniqueISBN(book.getIsbnNumber())) {   errors.rejectValue("isbnNumber",      "unique.constraint.failure");       }  }   }

#1 Implement Spring’s Validator interface
#2 Autowire BookService by name
#3 Validator is for Book type object
#4 Validate Book object





One of the important things to notice in listing 10 is the @Component annotation at the class level. This means you don’t need to explicitly register your Validator in Spring’s web application context XML, if classpath-scanning is being used. Now, AddBookValidator can be easily autowired in the AddBookController class for validation purposes, as shown in listing 11.

Listing 11 Form Validation — AddBookController

import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.RequestMapping;@RequestMapping(value = "VIEW")                                          @Controller(value="addBookController")public class AddBookController {  @Autowired  @Qualifier(“myAddBookValidator”)  private Validator myAddBookValidator;  ....  @ActionMapping(params = "myaction=addBook")                              public void addBook(@ModelAttribute(value="book")Book book,            #1       BindingResult bindingResult,..) {                                 #1    myAddBookValidator.validate(book, bindingResult);                    #2    ....                                                  bookService.addBook(book);  }  ....}

На первом месте объект BindingResult (подкласс объекта Spring Errors) предоставляет результаты привязки данных. Если во время привязки данных возникает ошибка, вы можете проверить ее, вызвав метод hasErrors объекта BindingResult. На # 2 вызывается валидатор метода validate для валидации объекта Book.

Библиотека тегов Spring ‘form’

В примере портлета «Каталог книг» используется библиотека тегов форм Spring для привязки данных (для команд и объектов поддержки форм) и отображения сообщений об ошибках.

Вывод

В этой статье мы создали портлет Каталог книг, используя аннотации. В части 3: Конфигурация и тестирование мы рассмотрим, как мы можем настроить контекст веб-приложения с помощью аннотаций, и протестировать наш портлет Каталог книг, используя фиктивные объекты.