Spring 3 maven 2 web application step by step

Before writting this I admin that there are lots of tutorials like this but what I found is that they are not step by step guides, with missing vital parts of process of making it all happen from scratch. This makes you revert to the documentation and therefore defies the point of going through the tutorial. With this one I hope to make it a bit clearer, though I do not attempt to make it IDE specific, therefore there will be no guidance on how to do it in specific IDE. The layout will be to work with standard Maven project structure.

What is in this tutorial?

  • Maven Spring project configurations
  • Persistence using Hibernate and HSQLDB
  • Cache support using Ehcahe
  • Spring MVC configurations
  • Example integration tests for DB and Caching (Please not there are no Unit tests as I assume that the reader is familiar with those already)

Link to the full set of sources for this tutorial:

denis-pavlov-spring3mvc-mvn2-example-updated.zip

First things first - the POM:


    4.0.0
    dp.examples
    shoppingcart
    0.0.1-SNAPSHOT
    war
    ShoppingCart

    
        
            
                maven-compiler-plugin
                
                    1.5
                    1.5
                
            
        
    

    
    
        3.1.2.RELEASE

        1.4
        3.7.2
        2.2.9

        4.1.9.Final
        4.2.0.Final

        2.7.0

        3.1

        2.6.6

    

    

        
            log4j
            log4j
            1.2.15
            
                
                    javax.mail
                    mail
                
                
                    javax.jms
                    jms
                
                
                    com.sun.jdmk
                    jmxtools
                
                
                    com.sun.jmx
                    jmxri
                
            
            runtime
        

        
        
            taglibs
            standard
            1.1.2
        
        
            javax.servlet
            jstl
            1.2
        


        
        
            org.springframework
            spring-core
            ${org.springframework.version}
        

        
        
            org.springframework
            spring-expression
            ${org.springframework.version}
        

        
        
            org.springframework
            spring-beans
            ${org.springframework.version}
        

        
        
            org.springframework
            spring-aop
            ${org.springframework.version}
        

        
        
            org.springframework
            spring-context
            ${org.springframework.version}
        

        
        
            org.springframework
            spring-context-support
            ${org.springframework.version}
        

        
        
            org.springframework
            spring-tx
            ${org.springframework.version}
        

        
        
            org.springframework
            spring-jdbc
            ${org.springframework.version}
        

        
        
            org.springframework
            spring-orm
            ${org.springframework.version}
        

        
        
            org.springframework
            spring-oxm
            ${org.springframework.version}
        

        
        
            org.springframework
            spring-web
            ${org.springframework.version}
        

        
        
            org.springframework
            spring-webmvc
            ${org.springframework.version}
        

        
        
            org.springframework
            spring-webmvc-portlet
            ${org.springframework.version}
        

        
        
            org.springframework
            spring-test
            ${org.springframework.version}
            test
        

        
        
            org.hibernate
            hibernate-core
            ${hibernate-core.version}
            
                
                    xml-apis
                    xml-apis
                
            
        
        
            xalan
            xalan
            ${xalan.version}
        
        

        
        
            commons-dbcp
            commons-dbcp
            ${commons-dbcp.version}
            runtime
        

        
        
            org.hsqldb
            hsqldb
            ${hsqldb-jdbc.version}
            runtime
        

        
        
            net.sf.ehcache
            ehcache-core
            ${ehcache.version}
        

        
        
            junit
            junit
            4.9
            test
        


    

At this point we have done all we need maven wise and can safely proceed to next step.

BTW if you are using eclipse and started of with a maven2 project and now trying to convert it to web projects I think you need to read this

We are making a webapp, so the second thing is web.xml (That should be in the src/main/webapp/WEB-INF/):




  
  
    log4jConfigLocation
    classpath:META-INF/properties/log4j.properties
  
  
    org.springframework.web.util.Log4jConfigListener
  
  
  
  
    contextConfigLocation
    classpath*:WEB-INF/spring/*.xml
  
  
    org.springframework.web.context.ContextLoaderListener
  
  
  
  
    spring
    org.springframework.web.servlet.DispatcherServlet
    
      contextConfigLocation
      /WEB-INF/spring/*.xml
    
    1
  
  
  
    spring
    /
  

  
    
      index.jsp
    
  


Then the Spring context configuration (for webapp that is located in WEB-INF/spring/ directory as specified in web.xml parameters to the servlet):




    
        
            
                
                classpath:spring-context.properties
            
        
    


    
    

	
	
        
            
                
            
        
	
	
	
	

	
	

	
	

		
	
		
		
	

	
	

	
	

	
	
		
		
	

	
	
		
		
	


The main context file contains two imports: spring/cache.xml that contains ehcache configuration and spring/persistence.xml that contains hibernate configuration



    

    

    

Note the p:shared="true" attribute. This ensures that we are using singleton manager for the Ehcache, which is important if you would like to use the same cache manager for your webapp and your hibernate 2nd level cache. "p:configLocation="classpath:ehcache.xml" tells Spring that we what to use the src/main/resources/ehcache.xml for our cache configurations



    

    

    


Persistence condifuration is quite simple. We setup dataSource first. Note that we use Spring variable substitution, which is defined by propertyConfigurer bean in context.xml that takes values from spring-context.properties. Then we define the hibernate factory and txProxyTemplate, which is used by transactional beans. It is good practice to manage transaction boundaries on the service layer. I.e. each method of service beans represents a unit of work and hence is a good candidate for a single transaction. For that purpose our Data access object is specifically not marked as transactional to prevent using it directly and hence have transaction per single db operation, which is not optimal. Instead we will make our service (WebCartServiceCached) transactional which will be using DAO and hence the transaction will be propagated to DAO.




    
        
        
        
        
    

    

    

    
        
        
        
            
                hibernate/mapping.hbm.xml
            
        
    

    
        
    

    
        
            NOTE: see org.springframework.transaction.TransactionDefinition

            DAO: all beans must be wrapped into this proxy to allow seamless
            transaction management integration via AOP.
        
        
        
            
                
                
                PROPAGATION_REQUIRED,-Throwable
            
        
    

    
        
    

DAO object is very simple and uses Hibernates current session which is provided by txProxyTemplate from WebCartService to perform DB operations. Note that if we to use DAO bean directly we would have a Hibernate exception and no session is available standalone.

package dp.example.shoppingcart.dao.impl;

import dp.example.shoppingcart.dao.CartDao;
import dp.example.shoppingcart.domain.impl.CartEntity;
import dp.example.shoppingcart.dto.Cart;
import dp.example.shoppingcart.dto.Item;
import dp.example.shoppingcart.service.CartService;
import org.hibernate.SessionFactory;

import java.util.List;

/**
 * Web implementation of cart service
 * 
 * @author DPavlov
 */
public class CartDaoImpl implements CartDao
{

    private final SessionFactory sessionFactory;

	public CartDaoImpl(final SessionFactory sessionFactory) {
		this.sessionFactory = sessionFactory;
	}

    public Cart createCart() {
        final Cart cart = new CartEntity();
        save(cart);
        return cart;
    }

    public Cart findCart(final long pk) {
        return (Cart) sessionFactory.getCurrentSession().get(CartEntity.class, pk);
    }

    public void save(final Cart cart) {
        sessionFactory.getCurrentSession().save(cart);
    }
}

Our service uses DAO to provide business functions represented by WebCartServiceCached methods. For example #addToCart(final long pk, String item) will either find or create cart with specific primary key, add and item and persist the cart. All these db operations will happen in a single transaction. And if we have an exception all these changes are rolled back. So we have a good "all or nothing" strategy that will keep our db in a consistent state.

Caching is very simple. Facilitated by two annotations: @Cacheable(value = "cart") and @CacheEvict(value = "cart", allEntries = false, key = "#pk"). There are lots of configurations for this but this is the basic setup. @Cacheable tells ehcache to use cache named "cart" (this is why we have this entry in ehcache.xml) to keep results of this method. @CacheEvict allows to remove cache entries. In this case we remove a single entry by key 'pk', but it is possible to evict all items. As I already said it is very configurable.

package dp.example.shoppingcart.service.impl;

import dp.example.shoppingcart.dao.CartDao;
import dp.example.shoppingcart.dto.Cart;
import dp.example.shoppingcart.dto.Item;
import dp.example.shoppingcart.service.CartService;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;

import java.util.List;

/**
 * Web implementation of cart service
 * 
 * @author DPavlov
 */
public class WebCartServiceCached implements CartService
{

    private final CartDao cartDao;

	public WebCartServiceCached(final CartDao cartDao) {
		this.cartDao = cartDao;
	}

    @CacheEvict(value = "cart", allEntries = false, key = "#pk")
    public void addToCart(final long pk, String item) {
        final Cart cart = findOrCreateCart(pk);
		cart.addItem(item);
        cartDao.save(cart);
	}

    @Cacheable(value = "cart")
    public List getItemsInCart(final long pk) {
        final Cart cart = findOrCreateCart(pk);
        return cart.getItems();
	}

    @CacheEvict(value = "cart", allEntries = false, key = "#pk")
	public void removeFromCart(final long pk, String item) {
        final Cart cart = findOrCreateCart(pk);
        cart.removeItem(item);
        cartDao.save(cart);
    }


    private Cart findOrCreateCart(final long pk) {
        Cart cart = cartDao.findCart(pk);
        if (cart == null) {
            cart = cartDao.createCart();
        }
        return cart;
    }


}

Basic controller uses @RequestMapping and @PathVariable to give a nice looking and SEO friendly url pattern.

package dp.example.shoppingcart.web;

import dp.example.shoppingcart.dto.Item;
import dp.example.shoppingcart.service.CartService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.servlet.ModelAndView;

import java.util.List;

/**
 * Basic controller
 * 
 * @author DPavlov
 */
@Controller
public class CartController {
	
	private CartService cartService;
	
	private int counter = 1;

	@Autowired
	public CartController(CartService cartService) {
		this.cartService = cartService;
	}
	
	@RequestMapping(value = "/cart/{pk}", method = RequestMethod.GET)
	public ModelAndView getCart(@PathVariable final long pk) {
		final List list = cartService.getItemsInCart(pk);
		ModelAndView mav = new ModelAndView("cart");
		mav.addObject("pk", pk);
		mav.addObject("items", list);
		mav.addObject("add", "Item" + counter++);
		return mav;
		
	}
	
	@RequestMapping(value = "/cart/{pk}/addtocart/{item}", method = RequestMethod.GET)
	public String addToCart(@PathVariable final long pk, @PathVariable final String item) {
		
		cartService.addToCart(pk, item);
		return "redirect:/cart/" + pk;  // Do redirect to prevent double post
		
	}
	
	@RequestMapping(value = "/cart/{pk}/removefromcart/{item}", method = RequestMethod.GET)
	public String removeFromCart(@PathVariable final long pk, @PathVariable final String item) {
		
		cartService.removeFromCart(pk, item);
        return "redirect:/cart/" + pk;  // Do redirect to prevent double post
		
	}

}

Basic view (if you look at the spring context.xml file you will see that we are using spring resolver that look for views in WEB-INF/views/ and uses view name as file name with extension .jsp):

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page isELIgnored="false" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>

Items

  

Items:

${item.articleNo} @ ${item.price}, qty: ${item.quantity} [-] remove [+] add
add "${add}"

${pageContext.request.contextPath} in jsp allows to reference the root of your webapp. This is very usefull if your webapp context name changes from environment to environment as this kind of url building approach will work everywhere consistently. Now if you navigate to localhost:8080/cart/1 you will see contents for cart with PK 1.

Here is screenshot of the outcome:

This sums up the basic Spring webapp project. Now we can have a look at how we can do integration tests for what we have written.

Firstly we need an alternative Spring context where we can remove all the MVC part so we can use it for core testing (i.e. services, dao). Note: that I am jumping to integration tests straight away since I assume that you know (and already done) unit tests. Integration tests is the final frontier! Before doing such tests appropriate unit test needs to be done! Please do not substitute these kind of tests for unit tests as they are considerably slower to run and much harder to test thoroughly.

package dp.example.shoppingcart.service.impl;

import dp.example.shoppingcart.dto.Item;
import dp.example.shoppingcart.service.CartService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.transaction.annotation.Transactional;

import java.math.BigDecimal;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import static org.junit.Assert.*;

/**
 * User: denispavlov
 * Date: 29/11/2013
 * Time: 13:17
 */
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = { "/spring/testContext.xml" })
@Transactional
public class WebCartServiceCachedTest {

    @Autowired
    private CartService cartService;

    /**
     * This is NOT unit test but rather an integration suite that proves that
     * our business logic works as expected using actual db and all the wiring.
     */
    @Test
    public void testCartCRUDOperationsIntegrationTest() throws Exception {

        List items;

        // Check empty initially
        items = cartService.getItemsInCart(1L);

        assertNotNull(items);
        assertTrue(items.isEmpty());

        // Add one item and check contents of cart
        cartService.addToCart(1L, "ABC");
        items = cartService.getItemsInCart(1L);

        assertNotNull(items);
        assertFalse(items.isEmpty());
        assertEquals(1, items.size());
        assertEquals("ABC", items.get(0).getArticleNo());
        assertEquals(new BigDecimal(1).compareTo(items.get(0).getQuantity()), 0);

        // Add same item and check contents of cart
        cartService.addToCart(1L, "ABC");
        items = cartService.getItemsInCart(1L);

        assertNotNull(items);
        assertFalse(items.isEmpty());
        assertEquals(1, items.size());
        assertEquals("ABC", items.get(0).getArticleNo());
        assertEquals(new BigDecimal(2).compareTo(items.get(0).getQuantity()), 0);

        // Add another item and check contents of cart
        cartService.addToCart(1L, "DEF");
        items = cartService.getItemsInCart(1L);

        assertNotNull(items);
        assertFalse(items.isEmpty());
        assertEquals(2, items.size());
        // we re-map the items in list to have deterministic assertions
        final Map byArticle = new HashMap();
        for (final Item item : items) {
            byArticle.put(item.getArticleNo(), item);
        }
        // deterministic assertions by article no
        assertTrue(byArticle.containsKey("ABC"));
        assertEquals(new BigDecimal(2).compareTo(byArticle.get("ABC").getQuantity()), 0);
        assertTrue(byArticle.containsKey("DEF"));
        assertEquals(new BigDecimal(1).compareTo(byArticle.get("DEF").getQuantity()), 0);

        // Remove one item
        cartService.removeFromCart(1L, "DEF");
        items = cartService.getItemsInCart(1L);

        assertNotNull(items);
        assertFalse(items.isEmpty());
        assertEquals(1, items.size());
        assertEquals("ABC", items.get(0).getArticleNo());
        assertEquals(new BigDecimal(2).compareTo(items.get(0).getQuantity()), 0);

        // Remove last item
        cartService.removeFromCart(1L, "ABC");
        items = cartService.getItemsInCart(1L);

        assertNotNull(items);
        assertTrue(items.isEmpty());

    }

    /**
     * Since we already established that the CRUD works assertions in this test are specific
     * to making sure that we get the cached list back, so we do not need to check individual elements.
     *
     * @throws Exception
     */
    @Test
    public void testCachingIsWorking() throws Exception {

        // Check empty initially
        List items1st = cartService.getItemsInCart(2L);
        assertEquals(0, items1st.size());

        // add cart item
        cartService.addToCart(2L, "ABC");
        List items2nd = cartService.getItemsInCart(2L);
        assertEquals(1, items2nd.size());

        // now we can ask for the cart contents again and assert that is it same instance of list
        List items3rd = cartService.getItemsInCart(2L);
        assertSame(items3rd, items2nd);

        // we expect that adding items will evict cache
        cartService.addToCart(2L, "ABC");
        List items4th = cartService.getItemsInCart(2L);
        assertEquals(1, items4th.size());
        // but these list should be different now
        assertNotSame(items2nd, items4th);

        // now check the cart contents again which should be cached object from 4th call
        List items5th = cartService.getItemsInCart(2L);
        assertSame(items4th, items5th);

        // make sure that after adding to a different cart we still have cached version of list for 2L
        cartService.addToCart(1L, "ABC");
        List items6th = cartService.getItemsInCart(2L);
        assertSame(items4th, items6th);


    }
}

The important elements are: include JUnit dependency in pom.xml, use @RunWith and @ContextConfiguration to tell JUnit what context configuration file you want to use. If you are testing DAO objects in isolation you can add @Transactional annotation to your Test suite at class level to provide the transaction support.

You also may have noticed that we use alternative context file /spring/testContext.xml. This context imports our original cache.xml and persistence.xml but provides override for spring-context.properties to supply alternative data source configurations and also does not contain the MVC configuration which are not needed for this test.





    
        
            
                
                classpath:spring-context.properties
                classpath:spring-testContext.properties
            
        
    

    
    

	
	
        
            
                
            
        
	

Hope you enjoyed this tutorial and let me know if any information is inaccurate or incomplete. Here is link to the full source for this:

denis-pavlov-spring3mvc-mvn2-example-updated.zip

PLEASE BARE IN MIND THIS IS AN EXAMPLE TO SHOW HOW SPRING MAVEN PROJECT IS SETUP - THIS IS NOT PRODUCTION CODE AND NEVER WAS INTENDED TO IMITATE ONE.

This page was last updated on: 29/11/2013 15:23