Implementing Google Guava Cache using Proxy Pattern

Implementing Google Guava Cache using Proxy Pattern

In this post we will learn two things. We will learn about the proxy design pattern and also how to use this pattern to implement a Google Guava in-memory cache. Let’s start with google guava cache, next we will learn about the proxy design pattern and finally the code example that puts the two together.

Google Guava Cache

Google Guava is a core java library from google which also includes an in-memory cache. If you are using maven, use the following dependency to get google guava jar.

...
<dependency>
	<groupId>com.google.guava</groupId>
	<artifactId>guava</artifactId>
	<version>13.0.1</version>
</dependency>
...

Proxy Design Pattern

Copying the definition from wikipedia: A proxy, in its most general form, is a class functioning as an interface to something else. The proxy could interface to anything: a network connection, a large object in memory, a file, or some other resource that is expensive or impossible to duplicate.

In this example, our proxy will interface to the data access layer because retrieving values from an in-memory cache performs far better than performing a query to fetch data from a database.

The Class diagram of a proxy design pattern is simple. Both the proxy, and the object that is being proxied(target) share a common interface. The proxy contains a reference to the target object so that the proxy can delegate processing to the target in case it cannot fulfill the request itself.

In our example, the proxy encapsulates an instance of the google guava cache. When the application first starts up, the cache will be empty. A call for a list of customers will go to the proxy but since our cache is empty, it will result in a cache miss. The proxy class will delegate responsibility to the actual db layer which will retrieve the list of customers from the database. The proxy class will update the cache with the returned result and finally will return the result to the caller. Any subsequent call for list of customers (as long as the cache entry hasn’t expired) will be served by the proxy and will be much faster as a query to the db will not be required.

Implementation

First, the common interface which both the db class(the dao) and the cache proxy class will implement.


package co.syntx.examples.guava;

import java.util.List;

public interface ICustomerDao<CustomerBO> {

	public CustomerBO getObjectById(int id) throws Exception;
	public List<CustomerBO> list() throws Exception;

}

The next class is the cache proxy. This class implements the interface that we described above. The proxy encapsulates the actual dao class (the ‘target’ variable). Both, getObjectById and list functions try looking up values from the cache. If values are not found, the same method call is made using the ‘target’ object. This will result in a query being issued to the DB. If values are found in the cache, they are returned from the proxy class without the dao method being called.


package co.syntx.examples.guava.cache;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.TimeUnit;

import co.syntx.examples.guava.CustomerBO;
import co.syntx.examples.guava.ICustomerDao;
import co.syntx.examples.guava.db.CustomerDao;

import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;

/**
 * A proxy for the CustomerDAO. If values are found in the cache, they are returned 
 * without calls going to the DB. Other wise, values are picked from the db, the cache is 
 * updated and the value is returned
 * @author syntx
 *
 */
public class CustomerCachedProxy implements ICustomerDao<CustomerBO> {

	private ICustomerDao<CustomerBO> target;
	private Cache<String, List<CustomerBO>> cache;

	public CustomerCachedProxy(int maxCacheSize) throws Exception {

		target = new CustomerDao();
		this.cache = CacheBuilder.newBuilder()
			    .maximumSize(maxCacheSize)
			    .expireAfterWrite(1, TimeUnit.MINUTES)
			    .build(); 
	}

	@Override
	public CustomerBO getObjectById(final int id) throws Exception {
		List<CustomerBO> customers  = cache.get(new Integer(id).toString(), new Callable<List<CustomerBO>>() {
			@Override
			public List<CustomerBO> call() throws Exception {
				List<CustomerBO> list = new ArrayList<CustomerBO> ();
				list.add(target.getObjectById(id));
				return list;
			}
		});
		return customers.get(0);
	}

	@Override
	public List<CustomerBO> list() throws Exception {
		List<CustomerBO> customers = cache.get("list", new Callable<List<CustomerBO>>() {
			@Override
			public List<CustomerBO> call() throws Exception {
				return target.list();
			}
		});		
		return customers;
	}

}

The following class is the DAO class or the data access object. It implements the interface described above by issuing queries to the Database and fetching lists of customers in the list function or a single customer by id in the getObjectById method.


package co.syntx.examples.guava.db;

import java.io.IOException;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;

import org.apache.log4j.Logger;

import co.syntx.examples.guava.CustomerBO;
import co.syntx.examples.guava.ICustomerDao;

/**
 * Dao class that is responsible for interacting with the 
 * 'customer' table.
 * @author syntx
 *
 */
public class CustomerDao implements ICustomerDao<CustomerBO>{

	private Logger log = Logger.getLogger(CustomerDao.class);
	private Datasource datasource = Datasource.getInstance();

	public CustomerDao() throws IOException, SQLException {
		super();
	}

	public List<CustomerBO> list() throws SQLException{
		return executeQuery("select * from customer where isActive='Y'");
	}

	@Override
	public CustomerBO getObjectById(int id) throws SQLException {
		return executeQuery("select * from customer where isActive='Y' and id = "+ id).get(0);
	}

	private List<CustomerBO> executeQuery(String query) throws SQLException {
		log.info("Executing Query: "+ query);

		List<CustomerBO> customers = new ArrayList<CustomerBO>();
		Connection connection = datasource.getConnection(); 
		Statement stmt = null;
		ResultSet rs = null;
		stmt = connection.createStatement();

		try {
			 stmt = datasource.getConnection().createStatement();
			 rs = stmt.executeQuery(query);

			 while(rs.next()){
				 CustomerBO customer = new CustomerBO();
				 customer.setId(rs.getInt("id"));
				 customer.setName(rs.getString("name"));
				 customer.setIsActive(rs.getString("isActive").charAt(0));
				 customers.add(customer);
			 }

		} catch (SQLException e) {
			e.printStackTrace();
		}finally{
			try {
				rs.close();
				stmt.close();
				connection.close();

			} catch (SQLException e) {
				e.printStackTrace();
			}
		}

		return customers;
	}
}

The following class encapsulates a connection pooled data source for the mysql database.



package co.syntx.examples.guava.db;

import java.io.IOException;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Properties;

import com.mchange.v2.c3p0.ComboPooledDataSource;

/**
 * A singleton that represents a pooled datasource. It is composed of a C3PO pooled datasource. 
 * Can be changed to any connect pool provider
 * @author syntx
 *
 */
public class Datasource {

	private Properties props;
	private ComboPooledDataSource cpds;
	private static Datasource datasource;

	private Datasource() throws IOException, SQLException{
		// load datasource properties
		props = Utils.readProperties("datasource.properties");
		cpds = new ComboPooledDataSource();
		cpds.setJdbcUrl(props.getProperty("jdbcUrl"));
	    cpds.setUser(props.getProperty("username"));
		cpds.setPassword(props.getProperty("password"));

		cpds.setInitialPoolSize(new Integer((String)props.getProperty("initialPoolSize")));
		cpds.setAcquireIncrement(new Integer((String)props.getProperty("acquireIncrement")));
		cpds.setMaxPoolSize(new Integer((String)props.getProperty("maxPoolSize")));
		cpds.setMinPoolSize(new Integer((String)props.getProperty("minPoolSize")));
		cpds.setMaxStatements(new Integer((String)props.getProperty("maxStatements")));

		Connection testConnection = null;
		Statement testStatement = null;

		//test connectivity and initialize pool
		try {
			testConnection = cpds.getConnection();
			testStatement = testConnection.createStatement();
			testStatement.executeQuery("select 1+1 from DUAL");
		} catch (SQLException e) {
			throw e;
		}finally{
			testStatement.close();
			testConnection.close();
		}

	}

	public static Datasource getInstance() throws IOException, SQLException{
		if(datasource == null){
			datasource = new Datasource();
			return datasource;
		}else{
			return datasource;
		}
	}

	public Connection getConnection() throws SQLException{
		return this.cpds.getConnection();
	}
}

The following class puts everything together. It simply instantiates a CustomerCachedProxy object and assigns it to a ICustomerDao reference. The list method is called to print the list of customer names.


import java.io.IOException;
import java.sql.SQLException;
import java.util.List;

import org.apache.log4j.Logger;

import co.syntx.examples.guava.CustomerBO;
import co.syntx.examples.guava.ICustomerDao;
import co.syntx.examples.guava.cache.CustomerCachedProxy;

public class Test {
	private static Logger log = Logger.getLogger(Test.class);

	private ICustomerDao dao;

	public Test() throws Exception{
		dao = new CustomerCachedProxy(1000);	
	}

	public void printCustomers() throws Exception{
		List<CustomerBO> customers = dao.list();

		for(CustomerBO customer: customers){
			log.info(customer.getName());
		}
	}

	/**
	 * @param args
	 * @throws Exception 
	 */
	public static void main(String[] args) throws Exception {
		Test test = new Test();

		log.info("First call");
		test.printCustomers();
		log.info("Second call");
		test.printCustomers();
	}

}

Finally, running the code we have described above, we get a log trace similar to this. Note that after the line First Call, we see our dao executing a query. This obviously happened because initially the cache was empty. After the first call, the cache is populated with the customer list. You will note that after the second call, there is no call to the database because in this case the list of customers are being returned from the cache.

main INFO  2013-02-25 13:06:37,004: MLog clients using log4j logging.
main INFO  2013-02-25 13:06:37,186: Initializing c3p0-0.9.1.2 [built 21-May-2007 15:04:56; debug? true; trace: 10]
main INFO  2013-02-25 13:06:37,319: Initializing c3p0 pool... com.mchange.v2.c3p0.ComboPooledDataSource [ acquireIncrement -> 10, acquireRetryAttempts -> 30, acquireRetryDelay -> 1000, autoCommitOnClose -> false, automaticTestTable -> null, breakAfterAcquireFailure -> false, checkoutTimeout -> 0, connectionCustomizerClassName -> null, connectionTesterClassName -> com.mchange.v2.c3p0.impl.DefaultConnectionTester, dataSourceName -> 1hge1cl8t4jpfs91mnsnqb|7bb2f811, debugUnreturnedConnectionStackTraces -> false, description -> null, driverClass -> null, factoryClassLocation -> null, forceIgnoreUnresolvedTransactions -> false, identityToken -> 1hge1cl8t4jpfs91mnsnqb|7bb2f811, idleConnectionTestPeriod -> 0, initialPoolSize -> 10, jdbcUrl -> jdbc:mysql://localhost:3306/db, maxAdministrativeTaskTime -> 0, maxConnectionAge -> 0, maxIdleTime -> 0, maxIdleTimeExcessConnections -> 0, maxPoolSize -> 200, maxStatements -> 200, maxStatementsPerConnection -> 0, minPoolSize -> 10, numHelperThreads -> 3, numThreadsAwaitingCheckoutDefaultUser -> 0, preferredTestQuery -> null, properties -> {user=******, password=******}, propertyCycle -> 0, testConnectionOnCheckin -> false, testConnectionOnCheckout -> false, unreturnedConnectionTimeout -> 0, usesTraditionalReflectiveProxies -> false ]
main INFO  2013-02-25 13:06:37,765: First call
main INFO  2013-02-25 13:06:37,780: Executing Query: select * from customer where isActive='Y'
main INFO  2013-02-25 13:06:37,784: Dev-XYZ
main INFO  2013-02-25 13:06:37,784: Dev-ABC
main INFO  2013-02-25 13:06:37,784: Second call
main INFO  2013-02-25 13:06:37,784: Dev-XYZ
main INFO  2013-02-25 13:06:37,784: Dev-ABC