Play already comes with an easy to scale cache solution included, namely memcached. However, you might not want to use memcached at all. It might not fit your needs, your admin may not want to install another application on a system, or anything else. Instead of not using a cache at all, this recipe shows you how to write your own implementation of a cache. This example will use Hazelcast, a pure Java distributed system as implementation.
You can find the source code of this example in the examples/chapter6/caching-implementation
directory.
It requires a little bit of work to get to the point to start programming. We need to get Hazelcast instance up and running and setup an application for development. Hazelcast is basically a distributed data platform for the JVM. Hazelcast has tons of more features that are not needed in this little example—basically only a distributed map is featured here. Hazelcast scales very well, there are live examples of even 100 parallel running instances.
So, first download Hazelcast. Go to http://www.hazelcast.com, click on the Downloads menu and get the latest version, it usually comes in a ZIP file. Unzip it, and run the following commands:
cd hazelcast-${VERSION} cd bin sh run.sh
That's it. You should now have a running Hazelcast instance on localhost.
Furthermore, you need to copy two jars into the lib/
directory of your Play application. These are:
hazelcast-${VERSION}.jar hazelcast-client-${VERSION}.jar
The files are in the lib/
directory of your Hazelcast installation.
As there is already a caching test included in the samples-and-tests directory of every Play framework distribution, we can reuse this test to make sure the hazelcast-based cache implementation works. You should copy the following files to the corresponding directories in your created application:
samples-and-tests/just-test-case/app/models/User.java
samples-and-tests/just-test-case/app/models/MyAddress.java
samples-and-tests/just-test-case/test/CacheTest.java
samples-and-tests/just-test-case/test/users.yml
After copying these files and running the test in your web browser, you should get a working test because no code to change the cache has been built yet. If you are using a Play version other than 1.2, please check the Java files, to see whether you might need to copy other dependant classes as well in order to be able to compile your application.
You should create a new module for your application, for example, via play new-module hazelcast
. You should put the following into the conf/dependencies.yml
file of your module and call play deps
afterwards:
self: play -> hazelcast 0.1 require: - play - com.hazelcast -> hazelcast-client 1.9.3.1: transitive: false - com.hazelcast -> hazelcast 1.9.3.1: transitive: false
Now create a play.plugins
file like this, which references the plugin, which is going to be created:
1000:play.modules.hazelcast.HazelcastPlugin
The next step is to create the plugin itself, which checks whether the Hazelcast cache should be enabled and replaces the cache appropriately:
public class HazelcastPlugin extends PlayPlugin { public void onApplicationStart() { Boolean isEnabled = new Boolean(Play.configuration.getProperty("hazelcast.enabled")); if (isEnabled) { Logger.info("Setting cache to hazelcast implementation"); Cache.forcedCacheImpl = HazelCastCacheImpl.getInstance(); Cache.init(); } } }
Now the only missing part is the cache implementation itself. All this code belongs to one class, but to help you understand the code better, some comments follow between some methods. The first step is to create a private constructor and a getInstance()
method in order to implement the singleton pattern for this class. The constructor also loads some default parameters from the configuration file. Put this code into your module at src/play/modules/hazelcast/HazelCastCacheImpl.java
:
public class HazelCastCacheImpl implements CacheImpl { private HazelcastClient client; private static HazelCastCacheImpl instance; public static HazelCastCacheImpl getInstance() { if (instance == null) { instance = new HazelCastCacheImpl(); } return instance; } private HazelCastCacheImpl() { String groupName = Play.configuration.getProperty("hazelcast.groupname", "dev"); String groupPass = Play.configuration.getProperty("hazelcast.grouppass", "dev-pass"); String[] addresses = Play.configuration.getProperty("hazelcast.addresses", "127.0.0.1:5701").replaceAll(" ", "").split(","); client = HazelcastClient.newHazelcastClient(groupName, groupPass, addresses); } private IMap getMap() { return client.getMap("default"); }
The add()
and safeAdd()
methods only put objects in the cache, if they do not exist in the cache already. On the other hand the set()
and safeSet()
methods overwrite existing objects. Note that any method with a safe
prefix in its name has the contract of executing the action (add, set, replace, or delete) synchronous and must make sure the cache has finished the operation before returning to the caller:
@Override public void add(String key, Object value, int expiration) { if (!getMap().containsKey(key)) { getMap().put(key, value, expiration, TimeUnit.SECONDS); } } @Override public boolean safeAdd(String key, Object value, int expiration) { getMap().putIfAbsent(key, value, expiration, TimeUnit.SECONDS); return getMap().get(key).equals(value); } @Override public void set(String key, Object value, int expiration) { getMap().put(key, value, expiration, TimeUnit.SECONDS); } @Override public boolean safeSet(String key, Object value, int expiration) { try { set(key, value, expiration); return true; } catch (Exception e) {} return false; }
The replace()
and safeReplace()
methods only place objects in the cache if other objects already exist under the referenced key. The get()
method returns a not casted object from the cache, which also might be null. The get()
method taking the keys
parameter as varargs allows the developer to get several values at once. In case you are in need of easily incrementing or decrementing numbers, you also should implement the incr()
and decr()
methods:
@Override public void replace(String key, Object value, int expiration) { if (getMap().containsKey(key)) { getMap().replace(key, value); } } @Override public boolean safeReplace(String key, Object value, int expiration) { if (getMap().containsKey(key)) { getMap().replace(key, value); return true; } return false; } @Override public Object get(String key) { return getMap().get(key); } @Override public Map<String, Object> get(String[] keys) { Map<String, Object> map = new HashMap(keys.length); for (String key : keys) { map.put(key, getMap().get(key)); } return map; } @Override public long incr(String key, int by) { if (getMap().containsKey(key)) { getMap().lock(key); Object obj = getMap().get(key); if (obj instanceof Long) { Long number = (Long) obj; number += by; getMap().put(key, number); } getMap().unlock(key); return (Long) getMap().get(key); } return 0; } @Override public long decr(String key, int by) { return incr(key, -by); }
The last methods are also quickly implemented. The clear()
method should implement a complete cache clear. The delete()
and deleteSafe()
methods remove a single element from the cache. An important method to implement is the stop()
method, which allows the implementation to correctly shutdown before it stops working. This is executed if your Play application is stopped, for example:
@Override public void clear() { getMap().clear(); } @Override public void delete(String key) { getMap().remove(key); } @Override public boolean safeDelete(String key) { if (getMap().containsKey(key)) { getMap().remove(key); return true; } return false; } @Override public void stop() { client.shutdown(); } }
You can now build the module via play build-module
and then switch to the application, which includes the CacheTest
class.
There is only one thing left to do to get the new cache implementation working in your application. Enable your newly built module in your application configuration. You need to edit conf/application.conf
and add Hazelcast-specific configuration parameters:
hazelcast.enabled=true hazelcast.groupname=dev hazelcast.grouppass=dev-pass hazelcast.addresses=127.0.0.1:5701
Then start the application and you should be able to run the preceding CacheTest
that is referenced.
The configuration is pretty self explanatory. Every Hazelcast client needs a username, a password, and an IP to connect to. The HazelcastPlugin
sets the field forcedCacheImpl
of the Cache class to the hazelcast implementation and reruns the init()
method. From now on the Hazelcast cache is used.
I will not explain every method of the cache, as most of the methods are pretty straightforward. The getMap()
method fetches the default map of the Hazelcast cluster which basically works like the map interface, but has a few more features such as the expiration.
Primarily, the class implements the singleton pattern by not providing a public constructor, but a getInstance()
method in order to make sure that always the same instance is used.
There is also a client variable of type HazelcastClient
, which does the communication with the cluster. The private constructor parses the Hazelcast specific setup variables and tries to connect to the cluster then.
The getMap()
method returns a Hazelcast-specific IMAP, which basically is a map, but also has support for expiry. In case you are wondering where the client.getMap("default")
is coming from, it is the default name of the distributed map, configured in Hazelcast. There is a hazelcast.xml
file in the same directory, where the run.sh
file resides.
The IMap
structure gives us basically everything needed to implement the add, set, replace, and delete and their safe companion methods used in the cache. As opposed to other cache implementations, often there is no difference between the regular and the safe implementation due to the behavior of Hazelcast.
A little trick had to be done for the incr()
method. First, the CacheTest
enforces that incrementing in the cache only works if the key has already been created before. Second, it is the only code where a lock is put on the key of the map (not on the whole map), though no increments are actually missing out. If another process is incrementing, the method blocks until the key is unlocked again. Third, we only add increment counters if the values are of type Long and thus can be incremented.
It is also useful to implement the stop()
method of the cache interface, which allows a graceful shutdown of the client.
When you start the Hazelcast server, you get into a console where you can issue some commands. For example, you can issue m.size()
there to get the current size of the distributed map. This could help you to debug some issues you might face.
You should read more about Hazelcast as it is a really interesting technology. Go to http://www.hazelcast.com/ or http://code.google.com/p/hazelcast/ for more information.
3.14.129.194