View rendering in Spring Web MVC

Spring is compatible with a lot of templating engines as Velocity or Freemaker. It also supports the solutions based on native JSP technology. It's more than interesting to see how it handles these different solutions to generate the views.

In the first part of the article we'll try to draw a way traveled by Spring from handling request to generating a view. At the second part we'll implement our custom renderer which will output executable JavaScript code, with extension .exejs.

How Spring generates the views ?

First, let's remind some things about request handling in Spring. If you want to read about it more in details, please go to articles about DispatcherServlet lifecycle. The first step made by Spring with the request consists on retrieving the controller which will handle it. When the controller is found, Spring executes the method matching the request. By doing so, it prepares the next step, view rendering. At the end of controller's method execution, Spring returns an information about the view to use. This information can be a String (par exemple: name of view file without extension) or another "viewable" Object (View or ModelAndView). After that Spring merges Model attributes (for example: the variables passed from controller to view part) and renders the view into final user.

In this simplified schema we saw that Spring makes two things with view part: translates an abstract view name and renders the view to final user. The first operation is made thanks to view resolvers. They decide which template should be used to view rendering. View resolvers implement the org.springframework.web.servlet.ViewResolver interface. It defines only one method, resolveViewName(String viewName, Locale locale). Thanks to this method, Spring can create the object responsible for view rendering. The example of standard view resolver is org.springframework.web.servlet.view.UrlBasedViewResolver those method creating new View instance is called buildView and looks like:

protected AbstractUrlBasedView buildView(String viewName) throws Exception {
  AbstractUrlBasedView view = (AbstractUrlBasedView) BeanUtils.instantiateClass(getViewClass());
  /**
    * This is very important place because it informs which is the path of template view file. 
    * Suppose given configuration:
    * - prefix (specified in this view resolver configuration) : /templates/
    * - suffix (specified in this view resolver configuration) : .jsp
    * - view name (returned dynamically by controller): helloWorld
    * Below setUrl method constructs following path: /templates/helloWorld.jsp. This path represents 
    * the file to render by the view after merging all Model attributes.
    */
  view.setUrl(getPrefix() + viewName + getSuffix());
  String contentType = getContentType();
  if (contentType != null) {
    view.setContentType(contentType);
  }
  view.setRequestContextAttribute(getRequestContextAttribute());
  view.setAttributesMap(getAttributesMap());
  if (this.exposePathVariables != null) {
    view.setExposePathVariables(exposePathVariables);
  }
  return view;
}

Maybe you've already noted that we were talking about view instance in the previous paragraph. This is the second master piece of view part in Spring. View instances are represented by the implementations of org.springframework.web.servlet.View interface. It defines two methods:
- getContentType to return the type of the view
- render(Map model, HttpServletRequest request, HttpServletResponse response) which renders the view to the final user. According to the comment from this interface, the rendering process should be done in 2 steps: the first consisting on preparing the request (passing model attributes to the view) and the second one that consists on rendering the view (for example with RequestDispatcher's include or forward method).

An example of View implementation is org.springframework.web.servlet.view.InternalResourceView which is mostly used to JSP rendering. Its method used to render the view looks like:

@Override
protected void renderMergedOutputModel(
    Map<String, Object> model, HttpServletRequest request, 
    HttpServletResponse response) throws Exception {

  // Determine which request handle to expose to the RequestDispatcher.
  HttpServletRequest requestToExpose = getRequestToExpose(request);

  // Expose the model object as request attributes.
  exposeModelAsRequestAttributes(model, requestToExpose);

  // Expose helpers as request attributes, if any.
  exposeHelpers(requestToExpose);

  // Determine the path for the request dispatcher.
  String dispatcherPath = prepareForRendering(requestToExpose, response);

  // Obtain a RequestDispatcher for the target resource (typically a JSP).
  RequestDispatcher rd = getRequestDispatcher(requestToExpose, dispatcherPath);
  if (rd == null) {
    throw new ServletException("Could not get RequestDispatcher for [" + getUrl() +
        "]: Check that the corresponding file exists within your web application archive!");
  }

  // If already included or response already committed, perform include, else forward.
  if (useInclude(requestToExpose, response)) {
    response.setContentType(getContentType());
    if (logger.isDebugEnabled()) {
      logger.debug("Including resource [" + getUrl() + "] in InternalResourceView '" + getBeanName() + "'");
    }
    rd.include(requestToExpose, response);
  }

  else {
    // Note: The forwarded resource is supposed to determine the content type itself.
    if (logger.isDebugEnabled()) {
      logger.debug("Forwarding to resource [" + getUrl() + "] in InternalResourceView '" + getBeanName() + "'");
    }
    rd.forward(requestToExpose, response);
  }
}

Custom view renderer in Spring

Our custom view renderer will read .exejs files, merge them with Model attributes and sends as dynamic JavaScript response. Before talking Java, we need to define 2 JavaScript files: one with generic template (template.exejs) and another one with the view adapted to controller's result (booking.exejs). Theirs contents look like:

// template.exejs
$(document).ready(function() {
  if (mysite.initialized) {
[[CONTENT_PART]]
  }
});

// booking.exejs
/**
 * Configuration for booked product.
 */
mysite.booking.config = {
  itemId: [[item_id]],
  isBookable: [[is_bookable]]
  successCallback: function() {
    showDialogToUser("[[success_title]]", "[[success_message]]", mysite.gui.config.SUCCESS_KEY);
  },
  errorCallback: function() {
    showDialogToUser("[[error_title]]", "[[error_message]]", mysite.gui.config.ERROR_KEY);
  }
};

// Sends AJAX request to booking the product. If the product can be booked, successCallback is called. Otherwise, errorCallback is called.
sendAjaxRequest('[[booking_url]]', mysite.booking.config.successCallback, mysite.booking.config.errorCallback);

As you can see, the dynamic variables (will be replaced with Model's map values) are defined between [[ and ]]. [[CONTENT_PART]] represents the view returned by controller. In our example, the content of booking.exejs will replace [[CONTENT_PART]] variable. Now, let's define two view resolver beans: one for Velocity and another one for ExeJs:

<bean id="velocityConfig" class="org.springframework.web.servlet.view.velocity.VelocityConfigurer">
  <property name="velocityProperties">
    <props>
      <prop key="resource.loader">file</prop>
      <prop key="file.resource.loader.class">
        org.apache.velocity.runtime.resource.loader.FileResourceLoader
      </prop>
      <prop key="file.resource.loader.path">WEB-INF/velocity</prop>
      <prop key="file.resource.loader.cache">false</prop>
    </props>
  </property>
</bean>
<bean class="org.springframework.web.servlet.view.velocity.VelocityViewResolver">
  <property name="prefix" value=""/>
  <property name="suffix" value=".vm"/>
  <property name="exposeSpringMacroHelpers" value="true"/>
  <property name="order" value="1" />
</bean>
<bean class="com.mysite.view.ExeJsViewResolver">
  <property name="order" value="0" />
</bean>

You can ignore Velocity configuration. Note only that both view resolvers have property called "order". In fact, this property means which view resolver will be launched first, second, third etc. It defines using priority order. The priority is defined in ascendant order, ie. orders with low values will be used first. In our case, ExeJsViewResolver will be used before VelocityViewResolver. Thanks to it we'll able to see how to "jump" from one resolver to another when the view shouldn't be resolved by the first resolver (ie. how to do that ExeJsResolver doesn't resolve Velocity files).

After some setup, we can define controller used to tests. It contains 4 methods: 3 will use .exejs files and one Velocity one. 3 exejs methods will return String, View and ModelAndView instance. It will prove that controller's method can return one of these objects and not only one of them:

@Controller
public class TestController {
        
  @RequestMapping(value = "exeJsTest-string-returned")
  public String testExeJsString(Model model) {
    setTestVariables(model);
    return "booking";
  }

  @RequestMapping(value = "exeJsTest-view-returned")
  public View testExeJsView(Model model) {
    setTestVariables(model);
    ExeJsView result = new ExeJsView();
    result.setViewName("booking"+ExeJsView.EXE_JS_EXT);
    return result;
  }

  @RequestMapping(value = "exeJsTest-modelAndView-returned")
  public ModelAndView testExeJsModelAndView(Model model) {
    setTestVariables(model);
    ModelAndView modelAndView = new ModelAndView();
    modelAndView.setViewName("booking");
    return modelAndView;
  }

  @RequestMapping(value = "velocityResponse")
  public String testVelocityResponse() {
    return "success";
  }
  
  private void setTestVariables(Model model) {
    model.addAttribute("item_id", 394);
    model.addAttribute("is_bookable", false);
    // for messages, you could use MessageSource to get i18n-ed messages - for simplicity, we put the hard-written text
    model.addAttribute("success_title", "Booking successfull !");
    model.addAttribute("success_message", "The item 394 was correctly booked");
    model.addAttribute("error_title", "An error occurred when booking");
    model.addAttribute("error_message", "Item 394 can't be booked because of some technical problems");
    model.addAttribute("booking_url", "/booking/make-book");
  }
}

Two main classes are coming now: view resolver (ExeJsViewResolver) and view (ExeJsView). All mandatory informations to understand them are placed as comments:

/**
 * View resolver for executable JavaScript views. It implements ViewResolver interface 
 * - it's normal. But it has to implement Ordered interface too.
 * 
 * Thanks to Ordered interface, we can define multiple view resolver beans 
 * and set priority execution order to each of them.
 * 
 * @author bartosz
 *
 */
public class ExeJsViewResolver implements ViewResolver, Ordered {
        
  private int order = Integer.MAX_VALUE;
  
  @Override
  public View resolveViewName(String viewName, Locale locale) throws Exception {
    ExeJsView view = new ExeJsView();
    view.setViewName(viewName+ExeJsView.EXE_JS_EXT);
    // We need to check if given .exejs file really exists. Otherwise, 
    // it means that another ViewResolver should be used to handle the generating of 
    // the View returned by the controller.
    if (view.exists()) {
      return view;
    }
    // returning null means "another view resolver must handle it"
    return null;
  }

  public void setOrder(int order) {
    this.order = order;
  }
  
  @Override
  public int getOrder() {
    return this.order;
  }

}

/**
 * View implementation for executable JavaScript returned for some requests. 
 * For the simplicity reasons, EXE_JS_EXT, EXE_JS_TEMPLATE_NAME and CONTENT_PART_ATTR fields
 * are defined in this class. But normally they should be moved from there and definable, 
 * for example, with bean definition. Some other improvement could be 
 * template files caching. But to keep it simple and readable, this class doesn't 
 * implement caching.
 * 
 * @author bartosz
 *
 */
public class ExeJsView extends AbstractView {
        
  public static final String EXE_JS_EXT = ".exejs";
  private static final String EXE_JS_TEMPLATE_NAME = "template.exejs";
  private static final String CONTENT_PART_ATTR = "[[CONTENT_PART]]";
  
  private String viewName;
  private Resource resContentPart = null;
  
  public void setViewName(String viewName) {
          this.viewName = viewName;
  }
  
  /**
    * Creates the response and returns it with HttplServletResponse object.
    */
  @Override
  protected void renderMergedOutputModel(Map<String, Object> model, 
      HttpServletRequest request, HttpServletResponse response) throws Exception {
    Resource template = new ClassPathResource(EXE_JS_TEMPLATE_NAME);
    if (this.resContentPart == null) {
      this.resContentPart = new ClassPathResource(this.viewName);
    }
    String templateContent = FileUtils.readFileToString(template.getFile());
    String viewContent = FileUtils.readFileToString(this.resContentPart.getFile());

    // merge templates with variables defined in the model map
    String fileContent = templateContent.replace(CONTENT_PART_ATTR, viewContent);
    if (model != null && model.size() > 0) {
      for (Map.Entry<String, Object> entry : model.entrySet()) {
        fileContent = fileContent.replaceAll(constructVariable(entry.getKey()), entry.getValue().toString());
      }
    }
    // set response and content-type
    response.setContentType(this.getContentType());
    response.getOutputStream().write(fileContent.getBytes("UTF-8"));
  }
  
  /**
    * This method must always return application/javascript. It's the 
    * reason why we doesn't allow setting content type and 
    * why this return is hard-coded.
    */
  @Override
  public String getContentType() {
    return "application/javascript";
  }
  @Override
  public void setContentType(String contentType) {
    // nothing to do here
  }

  /**
    * Checks if given model exists.
    * 
    * @return True if exists, false otherwise.
    */
  public boolean exists() {
    this.resContentPart = new ClassPathResource(this.viewName);
    try {
      return this.resContentPart.getFile() != null 
        && this.resContentPart.getFile().exists();
    } catch (IOException e) {
      // catch not needed
    }
    return false;
  }
  
  /**
    * Constructs variable name used by .exejs files. The variable name must correspond 
    * to the key of Model map. For example: for Model entry<"name", "My name">,
    * this method will return [[name]].
    * 
    * @param modelAttrName The name from Model map (corresponds to the key of this map)
    * @return Variable recognized by .exejs template.
    */
  private String constructVariable(String modelAttrName) {
    return "\\[\\["+modelAttrName+"\\]\\]";
  }

}

And finally our test case to check 4 methods defined in the controller and view resolving of our dynamic JavaScript files:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations={"applicationContext-test.xml"})
@WebAppConfiguration
public class ViewRenderingReturn {

  @Autowired
  private WebApplicationContext wac;

  private MockMvc mockMvc;

  @Before
  public void setup() {
    this.mockMvc = 
      MockMvcBuilders.webAppContextSetup(this.wac).build();
  }

  @Test
  public void stringReturn() {
    testWithUrl("/exeJsTest-string-returned");
  }
  
  @Test
  public void viewReturn() {
    testWithUrl("/exeJsTest-view-returned");
  }
  
  @Test
  public void modelAndViewReturn() {
    testWithUrl("/exeJsTest-modelAndView-returned");
  }
  
  @Test
  public void velocityResponse() {
    // test if the response is returned by Velocity or ExeJs view resolver
    String hello = "Hello world";
    MvcResult result = this.mockMvc.perform(get("/velocityResponse"))
    .andExpect(status().is(200))
    .andReturn();
    String response = result.getResponse().getContentAsString();
    assertTrue("Response ("+response+") should contain '"+hello+
      "' but it doesn't", response.contains(hello));
  }

  private void testWithUrl(String testUrl) {
    // values expected in the response
    String successTitle = "Booking successfull !";
    String successMsg = "The item 394 was correctly booked";
    String errorTitle ="An error occurred when booking";
    String errorMsg = "Item 394 can't be booked because of some technical problems";
    String itemId = "itemId: 394";
    String isBookable = "isBookable: false";
    String url = "sendAjaxRequest('/booking/make-book'";

    MvcResult result = this.mockMvc.perform(get(testUrl))
    .andExpect(status().is(200))
    .andExpect(content().contentType("application/javascript"))
    .andReturn();
    String response = result.getResponse().getContentAsString();
    assertTrue("Response shouldn't contain variables parts (Strings between [[ and ]]) but it does", 
      !response.contains("[[") && !response.contains("]]"));
    assertTrue("Response ("+response+") should contain '"+itemId+"' but it doesn't", 
      response.contains(itemId));
    assertTrue("Response ("+response+") should contain '"+isBookable+"' but it doesn't", 
      response.contains(isBookable));
    assertTrue("Response ("+response+") should contain '"+url+"' but it doesn't", 
      response.contains(url));
    assertTrue("Response ("+response+") should contain '"+successTitle+"' but it doesn't", 
      response.contains(successTitle));
    assertTrue("Response ("+response+") should contain '"+successMsg+"' but it doesn't", 
      response.contains(successMsg));
    assertTrue("Response ("+response+") should contain '"+errorTitle+"' but it doesn't", 
      response.contains(errorTitle));
    assertTrue("Response ("+response+") should contain '"+errorMsg+"' but it doesn't", 
      response.contains(errorMsg));
  }
}

The test should pass correctly for all cases.

This article shows the way of making the views in Spring. In the first part we discovered to main components used to it: ViewResolver and View. The first is used to "translate" controller's method result to appropriate View instance. View instance is used further in the process to generate output to the final user. The second part presented how to implement customized view and view resolvers. We made that for dynamically created JavaScript files.

The comments are moderated. I publish them when I answer, so don't worry if you don't see yours immediately :)

📚 Newsletter Get new posts, recommended reading and other exclusive information every week. SPAM free - no 3rd party ads, only the information about waitingforcode!