One of my blog follower sends an email asking me to show an example of “RealWorld Usage of Spring AOP”. He mentioned that in most of the examples the usage of Spring AOP is demonstrated for logging method entry/exit or Transaction management or Security checks. He wanted to know how Spring AOP is being used in “Real Project for Real Problems”.

So I would like to show how I have used Spring AOP for one of my project to handle a real problem.

We won’t face some kind of problems in development phases and only come to know during Load Testing or in production environments only.
For example:

  • Remote WebService invocation failures due to network latency issues
  • Database query failures because of Lock exceptions
    etc

In most of the cases just retrying the same operation is sufficient to solve these kind of failures.
Let us see how we can use Spring AOP to automatically retry the method execution if any exception occurs.

We can use Spring AOP @Around advice to create a proxy for those objects whose methods needs to be retried and implement the retry logic in Aspect.

Before jumping on to implementing these Spring Advice and Aspect, first let us write a simple utility to execute a “Task” which automatically retry for N times ignoring the given set of Exceptions.

public interface Task<T> {
	T execute();
}
import java.util.HashSet;
import java.util.Set;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class TaskExecutionUtil 
{
	
	private static Logger logger = LoggerFactory.getLogger(TaskExecutionUtil.class);

	@SafeVarargs
	public static <T> T execute(Task<T> task, 
								int noOfRetryAttempts, 
								long sleepInterval, 
								Class<? extends Throwable>... ignoreExceptions) 
	{
		
		if (noOfRetryAttempts < 1) {
			noOfRetryAttempts = 1;
		}
		Set<Class<? extends Throwable>> ignoreExceptionsSet = new HashSet<Class<? extends Throwable>>();
		if (ignoreExceptions != null && ignoreExceptions.length > 0) {
			for (Class<? extends Throwable> ignoreException : ignoreExceptions) {
				ignoreExceptionsSet.add(ignoreException);
			}
		}
		
		logger.debug("noOfRetryAttempts = "+noOfRetryAttempts);
		logger.debug("ignoreExceptionsSet = "+ignoreExceptionsSet);
		
		T result = null;
		for (int retryCount = 1; retryCount <= noOfRetryAttempts; retryCount++) {
			logger.debug("Executing the task. Attemp#"+retryCount);
			try {
				result = task.execute();
				break;
			} catch (RuntimeException t) {
				Throwable e = t.getCause();
				logger.error(" Caught Exception class"+e.getClass());
				for (Class<? extends Throwable> ignoreExceptionClazz : ignoreExceptionsSet) {
					logger.error(" Comparing with Ignorable Exception : "+ignoreExceptionClazz.getName());
					
					if (!ignoreExceptionClazz.isAssignableFrom(e.getClass())) {
						logger.error("Encountered exception which is not ignorable: "+e.getClass());
						logger.error("Throwing exception to the caller");
						
						throw t;
					}
				}
				logger.error("Failed at Retry attempt :" + retryCount + " of : " + noOfRetryAttempts);
				if (retryCount >= noOfRetryAttempts) {
					logger.error("Maximum retrial attempts exceeded.");
					logger.error("Throwing exception to the caller");
					throw t;
				}
				try {
					Thread.sleep(sleepInterval);
				} catch (InterruptedException e1) {
					//Intentionally left blank
				}
			}
		}
		return result;
	}

}

I hope this method is self explanatory. It is taking a Task and retries noOfRetryAttempts times in case method task.execute() throws any Exception and ignoreExceptions indicates what type of exceptions to be ignored while retrying.

Now let us create a Retry annotation as follows:

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public  @interface Retry {
	
	public int retryAttempts() default 3;
	
	public long sleepInterval() default 1000L; //milliseconds
	
	Class<? extends Throwable>[] ignoreExceptions() default { RuntimeException.class };
	
}

We will use this @Retry annotation to demarcate which methods needs to be retried.

Now let us implement the Aspect which applies to the method with @Retry annotation.

import java.lang.reflect.Method;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

@Component
@Aspect
public class MethodRetryHandlerAspect {
	
	private static Logger logger = LoggerFactory.getLogger(MethodRetryHandlerAspect.class);
	
	@Around("@annotation(com.sivalabs.springretrydemo.Retry)")
	public Object audit(ProceedingJoinPoint pjp) 
	{
		Object result = null;
		result = retryableExecute(pjp);
	    return result;
	}
	
	protected Object retryableExecute(final ProceedingJoinPoint pjp)
	{
		MethodSignature signature = (MethodSignature) pjp.getSignature();
		Method method = signature.getMethod();
		logger.debug("-----Retry Aspect---------");
		logger.debug("Method: "+signature.toString());

		Retry retry = method.getDeclaredAnnotation(Retry.class);
		int retryAttempts = retry.retryAttempts();
		long sleepInterval = retry.sleepInterval();
		Class<? extends Throwable>[] ignoreExceptions = retry.ignoreExceptions();
		
		Task<Object> task = new Task<Object>() {
			@Override
			public Object execute() {
				try {
					return pjp.proceed();
				} catch (Throwable e) {
					throw new RuntimeException(e);
				}
			}
		};
		return TaskExecutionUtil.execute(task, retryAttempts, sleepInterval, ignoreExceptions);
	}
}

That’s it. We just need some test cases to actually test it.

First create AppConfig.java configuration class as follows:

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

@Configuration
@ComponentScan
@EnableAspectJAutoProxy
public class AppConfig {

}

And couple of dummy Service beans.

import org.springframework.stereotype.Service;

@Service
public class ServiceA {
	
	private int counter = 1;
	
	public void method1() {
		System.err.println("----method1----");
	}
	
	@Retry(retryAttempts=5, ignoreExceptions={NullPointerException.class})
	public void method2() {
		System.err.println("----method2 begin----");
		if(counter != 3){
			counter++;
			throw new NullPointerException();
		}
		System.err.println("----method2 end----");		
	}
}
import java.io.IOException;
import org.springframework.stereotype.Service;

@Service
public class ServiceB {
	
	@Retry(retryAttempts = 2, ignoreExceptions={IOException.class})
	public void method3() {
		System.err.println("----method3----");
		if(1 == 1){
			throw new ArrayIndexOutOfBoundsException();
		}
	}
	
	@Retry
	public void method4() {
		System.err.println("----method4----");
	}
}

Finally write a simple Junit test to invoke these methods.

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;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes=AppConfig.class)
public class RetryTest 
{
	@Autowired ServiceA svcA;
	@Autowired ServiceB svcB;
	
	@Test
	public void testA()
	{
		svcA.method1();
	}
	
	@Test
	public void testB()
	{
		svcA.method2();
	}
	
	@Test(expected=RuntimeException.class)
	public void testC()
	{
		svcB.method3();
	}
	
	@Test
	public void testD()
	{
		svcB.method4();
	}
}

Yeah, I know I could have written these test methods a bit better, but I hope you got the idea.

Run the JUnit tests and observe the log statement to verify whether the method retry is happening in case of Exception or not.

Case#1: When invoking ServiceA.method1() is invoked MethodRetryHandlerAspect won’t be applied at all.

Case#2: When invoking ServiceA.method2() is invoked, we are maintaining a counter and throwing NullPointerException for 2 times. But we have marked that method to ignore NullPointerExceptions. So it will continue to retry for 5 times. But 3rd time method will be executed normally and exits the method normally.

Case#3: When invoking ServiceB.method3() is invoked, we are throwing ArrayIndexOutOfBoundsException but that method is marked to ignore only IOException only.
So this method execution won’t be retried and throws the Exception immediately.

Case#4: When invoking ServiceB.method4() is invoked, everything is fine so it should exit in the first attempt itself normally.

I hope this example demonstrate a good enough real world usage of Spring AOP :-)

In this Iteration#8 we will implement showing the Customer Account and Order History functionality in our ShoppingCart application.

  • Customer MyAccount Page
    • Profile
    • Order History

Once the customer is logged in our system he can click on MyAccount link at the top of the header and view his profile details and order history.

First let us write the Controller handler method in our CustomerController to show myAccount details.

@Controller
public class CustomerController extends JCartSiteBaseController
{	
	@Autowired private CustomerService customerService;
	...
	...	
	
	@RequestMapping(value="/myAccount", method=RequestMethod.GET)
	protected String myAccount(Model model)
	{
		String email = getCurrentUser().getCustomer().getEmail();
		Customer customer = customerService.getCustomerByEmail(email);
		model.addAttribute("customer", customer);
		List<Order> orders = customerService.getCustomerOrders(email);
		model.addAttribute("orders", orders);
		return "myAccount";
	}
}

Now create the myAccount.html view to render customer details and customer order history.

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
	xmlns:th="http://www.thymeleaf.org"
	xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3"
	layout:decorator="layout/mainLayout">
<head>
<title>My Account</title>
</head>
<body>
	<div layout:fragment="content">
		<div class="single-product-area">
			<div class="zigzag-bottom"></div>
			<div class="container">
				<div role="tabpanel">
					<ul class="customer-tab" role="tablist">
						<li role="presentation" class="active"><a href="#profile"
							aria-controls="profile" role="tab" data-toggle="tab">Customer
								Info</a></li>
						<li role="presentation"><a href="#orders"
							aria-controls="orders" role="tab" data-toggle="tab">Orders</a></li>
					</ul>
					<div class="tab-content">
						<div role="tabpanel" class="tab-pane fade in active" id="profile">
							<h2>Customer Info</h2>
							<form role="form" action="#" th:object="${customer}"
								method="post">								
								<div class="form-group">
									<label>FirstName</label> <input type="text"
										class="form-control" th:field="*{firstName}"
										readonly="readonly" />
								</div>
								<div class="form-group">
									<label>LastName</label> <input type="text" class="form-control"
										th:field="*{lastName}" readonly="readonly" />
								</div>
								<div class="form-group">
									<label>Email</label> <input type="email" class="form-control"
										th:field="*{email}" readonly="readonly" />
								</div>
								<div class="form-group">
									<label>Phone</label> <input type="text" class="form-control"
										th:field="*{phone}" readonly="readonly" />
								</div>
							</form>
						</div>
						<div role="tabpanel" class="tab-pane fade" id="orders">
							<h2>Orders</h2>
							<table cellspacing="0" class="shop_table cart">
								<thead>
									<tr>
										<th>#</th>
										<th>Order Number</th>
										<th>Status</th>
									</tr>
								</thead>
								<tbody>
									<tr th:each="order,iterStat : ${orders}">
										<td><span th:text="${iterStat.count}">1</span></td>
										<td><a href="#" th:text="${order.orderNumber}"
											th:href="@{/orders/{orderNumber}(orderNumber=${order.orderNumber})}">OrderNumber</a>
										</td>
										<td><span th:text="${order.status}">Status</span></td>

									</tr>
								</tbody>
							</table>
						</div>
					</div>
				</div>
			</div>
		</div>
	</div>
</body>
</html>

Now you can login as customer and click on MyAccount and see the profile. When you click on Orders tab you can see the list of orders that customer is placed. Also you can click on Order Number to see more details of the Order.

For Managing Customers we need a provision to see all the list of customers and view any Customers details.

Let us start with implementing the back-end Customer service.

public interface CustomerRepository extends JpaRepository<Customer, Integer>
{

	Customer findByEmail(String email);

	@Query("select o from Order o where o.customer.email=?1")
	List<Order> getCustomerOrders(String email);

}
@Service
@Transactional
public class CustomerService {
	@Autowired CustomerRepository customerRepository;
	
	public Customer getCustomerByEmail(String email) {
		return customerRepository.findByEmail(email);
	}

	public Customer createCustomer(Customer customer) {
		return customerRepository.save(customer);
	}

	public List<Customer> getAllCustomers() {
		return customerRepository.findAll();
	}

	public Customer getCustomerById(Integer id) {
		return customerRepository.findOne(id);
	}

	public List<Order> getCustomerOrders(String email) {
		return customerRepository.getCustomerOrders(email);
	}
}

Now let us implement CustomerController to handle the requests to display list of customers and the selected customer details.

@Controller
@Secured(SecurityUtil.MANAGE_CUSTOMERS)
public class CustomerController extends JCartAdminBaseController
{
	private static final String viewPrefix = "customers/";
	
	@Autowired 
	private CustomerService customerService;
	
	@Override
	protected String getHeaderTitle()
	{
		return "Manage Customers";
	}
		
	@RequestMapping(value="/customers", method=RequestMethod.GET)
	public String listCustomers(Model model) {
		List<Customer> list = customerService.getAllCustomers();
		model.addAttribute("customers",list);
		return viewPrefix+"customers";
	}
	
	@RequestMapping(value="/customers/{id}", method=RequestMethod.GET)
	public String viewCustomerForm(@PathVariable Integer id, Model model) {
		Customer customer = customerService.getCustomerById(id);
		model.addAttribute("customer",customer);
		return viewPrefix+"view_customer";
	}		
}

Create the thymeleaf view template for showing list of customers customers.html as follows:

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" 
	 xmlns:th="http://www.thymeleaf.org"
	  xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3"
      layout:decorator="layout/mainLayout">      
<head>
	<title>Customers</title>
</head>
<body>    	        
	<div layout:fragment="content">
		<div class="row">
		<div class="col-md-12">
		  <div class="box">
			<div class="box-header">
			  <h3 class="box-title">List of Customers</h3>
			</div>
			<div class="box-body table-responsive no-padding">
			  <table class="table table-hover">
				<tr>
				  <th style="width: 10px">#</th>
				  <th>Customer Name</th>
				  <th>Email</th>
				  <th>View</th>                      
				</tr>
				<tr th:each="customer,iterStat : ${customers}">
				  <td><span th:text="${iterStat.count}">1</span></td>
				  <td th:text="${customer.firstName}">Customer Name</td>
				  <td th:text="${customer.email}">Customer Email</td>
				  <td><a th:href="@{/customers/{id}(id=${customer.id})}" 
				  class="btn btn-sm btn-default"><i class="fa fa-search"></i> View</a></td>
				</tr>                    
			  </table>
			</div>                
		  </div>
		</div>
		</div>		  
	</div>    	
</body>    
</html>

Create the thymeleaf view template for showing customer details view_customer.html as follows:

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
	xmlns:th="http://www.thymeleaf.org"
	xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3"
	layout:decorator="layout/mainLayout">

<head>
<title>Customer - View</title>
</head>
<body>
	<div layout:fragment="content">
		<div class="box box-warning">
			<div class="box-header with-border">
				<h3 class="box-title">View Customer</h3>
			</div>
			<div class="box-body">
				<form role="form" action="#" th:object="${customer}" method="post">
					<div class="form-group">
						<label>FirstName</label> <input type="text" class="form-control"
							th:field="*{firstName}" readonly="readonly" />
					</div>

					<div class="form-group">
						<label>LastName</label> <input type="text" class="form-control"
							th:field="*{lastName}" readonly="readonly" />
					</div>

					<div class="form-group">
						<label>Email</label> <input type="email" class="form-control"
							th:field="*{email}" readonly="readonly" />
					</div>

					<div class="form-group">
						<label>Phone</label> <input type="text" class="form-control"
							th:field="*{phone}" readonly="readonly" />
					</div>
				</form>
			</div>
		</div>
	</div>

</body>
</html>

Now you can run the application and click on Customers menu item in left navigation. You can see list of customers and click on View button to view customer details.

For Managing Orders we need a provision to see all the list of orders and view an order details and updating the order status.

Let us start with implementing the back-end order service.

@Service
@Transactional
public class OrderService
{
	@Autowired EmailService emailService;
	@Autowired OrderRepository orderRepository;
	
	...
	...
	
	public Order getOrder(String orderNumber)
	{
		return orderRepository.findByOrderNumber(orderNumber);
	}

	public List<Order> getAllOrders()
	{
		Sort sort = new Sort(Direction.DESC, "createdOn");
		return orderRepository.findAll(sort);
	}

	public Order updateOrder(Order order) {
		Order o = getOrder(order.getOrderNumber());
		o.setStatus(order.getStatus());
		Order savedOrder = orderRepository.save(o);
		return savedOrder;
	}
}

Now let us implement OrderController to handle the requests to display list of orders, the selected order details and updating the Order status.

@Controller
@Secured(SecurityUtil.MANAGE_ORDERS)
public class OrderController extends JCartAdminBaseController
{
	private static final String viewPrefix = "orders/";

	@Autowired protected EmailService emailService;
	@Autowired protected OrderService orderService;
	@Autowired private TemplateEngine templateEngine;
	
	@Override
	protected String getHeaderTitle()
	{
		return "Manage Orders";
	}
	
	
	@RequestMapping(value="/orders", method=RequestMethod.GET)
	public String listOrders(Model model) {
		List<Order> list = orderService.getAllOrders();
		model.addAttribute("orders",list);
		return viewPrefix+"orders";
	}
	
	@RequestMapping(value="/orders/{orderNumber}", method=RequestMethod.GET)
	public String editOrderForm(@PathVariable String orderNumber, Model model) {
		Order order = orderService.getOrder(orderNumber);
		model.addAttribute("order",order);
		return viewPrefix+"edit_order";
	}
	
	@RequestMapping(value="/orders/{orderNumber}", method=RequestMethod.POST)
	public String updateOrder(@ModelAttribute("order") Order order, BindingResult result, 
			Model model, RedirectAttributes redirectAttributes) {		
		Order persistedOrder = orderService.updateOrder(order);
		this.sendOrderStatusUpdateEmail(persistedOrder);
		logger.debug("Updated order with orderNumber : {}", persistedOrder.getOrderNumber());
		redirectAttributes.addFlashAttribute("info", "Order updated successfully");
		return "redirect:/orders";
	}
	
	protected void sendOrderStatusUpdateEmail(Order order)
	{
		try {
			final Context ctx = new Context();
	        ctx.setVariable("order", order);
			final String htmlContent = this.templateEngine.process("order-status-update-email", ctx);
	        
			emailService.sendEmail(order.getCustomer().getEmail(), 
								   "QuilCartCart - Order Status Update", 
								   htmlContent);
		} catch (JCartException e) {
			logger.error(e);
		}
	}
}

We are using thymeleaf email template jcart-core/src/main/resources/email-templates/order-status-update-email.html for sending the Order Status Update email.

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
  <head>
    <title th:remove="all">Template for HTML email</title>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
  </head>
  <body>
    <p th:text="${'Hello '+order.customer.firstName}">
      Hello Customer
    </p>
    <p>
       Order Number : <span th:text="${order.orderNumber}">Number</span><br/>
       Status: <span th:text="${order.status}">Status</span>
    </p>
    <p>
      Regards, <br />
      <em>The QuilCart Team</em>
    </p>
  </body>
</html>

Create the thymeleaf view template for showing list of orders orders.html as follows:

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" 
	  xmlns:th="http://www.thymeleaf.org"
	  xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3"
      layout:decorator="layout/mainLayout">      
 <head>
	<title>Orders</title>
</head>
<body>			
	<div layout:fragment="content">
		<div class="row">
		<div class="col-md-12">
		  <div class="box">
			<div class="box-header">
			  <h3 class="box-title">List of Orders</h3>
			</div>
			<div class="box-body table-responsive no-padding">
			  <table class="table table-hover">
				<tr>
				  <th style="width: 10px">#</th>
				  <th>Order Number</th>
				  <th>Customer Name</th>
				  <th>Email</th>
				  <th>Edit</th>				  
				</tr>
				<tr th:each="order,iterStat : ${orders}">
				  <td><span th:text="${iterStat.count}">1</span></td>
				  <td th:text="${order.orderNumber}">Order Number</td>
				  <td th:text="${order.customer.firstName}">Customer Name</td>
				  <td th:text="${order.customer.email}">Customer Email</td>
				  <td><a th:href="@{/orders/{orderNumber}(orderNumber=${order.orderNumber})}" 
						 class="btn btn-sm btn-default"><i class="fa fa-edit"></i> Edit</a></td>
				</tr>                    
			  </table>
			</div>			
		  </div>
		 </div>
	</div>		  
	</div>	
</body>    
</html>

Create the thymeleaf view template for showing order details edit_order.html as follows:

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
	xmlns:th="http://www.thymeleaf.org"
	xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3"
	layout:decorator="layout/mainLayout">
<head>
<title>Orders - Edit</title>
</head>
<body>
	<div layout:fragment="content">
		<div class="box box-warning">
			<div class="box-header with-border">
				<h3 class="box-title">Edit Order</h3>
			</div>
			<!-- /.box-header -->
			<div class="box-body">
				<form role="form"
					th:action="@{/orders/{orderNumber}(orderNumber=${order.orderNumber})}"
					th:object="${order}" method="post">
					<p th:if="${#fields.hasErrors('global')}" th:errors="*{global}"
						th:class="text-red">Incorrect data</p>
					<div>
						<div th:unless="${order}">
							<h2>No order found</h2>
						</div>
						<div th:if="${order}">
							<h3>
								Order Number : <span th:text="${order.orderNumber}">Number</span>
							</h3>
							<h3>Order Item Details</h3>
							<table class="table table-hover">
								<thead>
									<tr>
										<th>Name</th>
										<th>Quantity</th>
										<th>Cost</th>
									</tr>
								</thead>
								<tbody>
									<tr th:each="item : ${order.items}">
										<td th:text="${item.product.name}">product.name</td>
										<td th:text="${item.quantity}"></td>
										<td th:text="${item.price * item.quantity}">price</td>
									</tr>
								</tbody>
								<tfoot>

									<tr class="cart-subtotal">
										<th>Order Subtotal</th>
										<td><span class="amount" th:text="${order.totalAmount}">£15.00</span>
										</td>
									</tr>

									<tr class="shipping">
										<th>Shipping and Handling</th>
										<td>Free Shipping</td>
									</tr>

									<tr class="order-total">
										<th>Order Total</th>
										<td><strong><span class="amount"
												th:text="${order.totalAmount}">£15.00</span></strong></td>
									</tr>

								</tfoot>
							</table>
							<div>
								<label>Order Status</label> <select th:field="*{status}">
									<option
										th:each="status: ${T(com.sivalabs.jcart.entities.OrderStatus).values()}"
										th:value="${status}" th:text="${status}">Status</option>
								</select>
							</div>
						</div>
					</div>
					<div class="box-footer">
						<button type="submit" class="btn btn-primary">Submit</button>
					</div>
				</form>
			</div>
		</div>
	</div>
</body>
</html>

Now you can run the application and click on Order menu item in left navigation. You can see list of order and click on Edit button to view order details or edit the order status.

In Iteration#6 we have implemented features in ShoppingCart application to enable Customers place orders. In this Iteration#7 we will implement the features in Administration application to view and manage the Customers and Orders.

As part of Iteration#7 we will implement the following usecases: