| Home: www.vipan.com | Vipan Singla | e-mail: vipan@vipan.com |
|
<%@ page import="java.util.*, com.vipan.util.*" %>
<%!
// timeToLive, accessTimeout, maximumCachedQuantity, timerInterval (millisecs)
ExpiringCache cache =
new ExpiringCache(24*60*60*1000, 30*60*1000, 100*1000, 2*60*60*1000);
%>
<html>
<body>
<h1>Expiring Cache Usage</h1>
<%
// If the cache does not contain your object, get the data to cache from some
// other place. Put it in cache for use next time. If "oldObject" turns out
// to be "not null", you have overwritten an object already in cache.
String mykey = "mykey";
Object myCachedObject = cache.recover(mykey);
if (myCachedObject == null) {
myCachedObject = new Object(); // Placeholder dummy object for demo only
Object oldObject = cache.admit(mykey, myCachedObject);
}
%>
myCachedObject is <%= myCachedObject %><br>
</body>
</html>
To run this JSP, you will need "log4j.jar", "expiringcache.jar" and "common-collections.jar" (ver 3 and later) in the "WEB-INF/lib" directory (or a similar place such that these files are recognized by the Java server as being in the JSPs classpath).
package com.vipan.util; import java.util.*; import org.apache.log4j.*; import org.apache.commons.collections.map.LRUMap; import org.apache.commons.collections.MapIterator; public class ExpiringCache { static Category logger = Category.getInstance(ExpiringCache.class);An object will expire after its time-to-live is exceeded from the time it was created so that stale data is not used. It will also expire if it's been idle for some time (it has not been accessed for some time). These two expiration criteria still allow for the possibility that a cache will grow too large for system resources too handle. So the cache will start removing "least recently used" (LRU) objects once the cache size (here, in terms of number of objects in cache, not the collective size, in bytes, of objects) gets to a certain number.
public static final long DEFAULT_TIME_TO_LIVE = 10 * 60 * 1000; public static final long DEFAULT_ACCESS_TIMEOUT = 5 * 60 * 1000; public static final long DEFAULT_TIMER_INTERVAL = 2 * 60 * 1000; long ttl = DEFAULT_TIME_TO_LIVE; long ato = DEFAULT_ACCESS_TIMEOUT; long tiv = DEFAULT_TIMER_INTERVAL;Apache commons collections has a
Map implementation called LRUMap which maintains its specified size by throwing out least recently used objects. A server may have multiple threads accessing the cache. However, synchronization is not used in this implementation. I thought it is unnecessary. At worst, a user will get no object back, in which case, this code suggests to get the object from its actual place and put it in the cache. You will have to guard against other code overwriting your cached object by using unique keys (just as you would do when putting objects in a servlet's or JSP's session or servletContext).
LRUMap cacheMap;
A cache needs to be cleaned from time to time. A Java Timer can run a task periodically.
Timer cacheManager;
I am not sure if this will work but for insurance, cancel() will suggest to the timer thread that it is no longer needed. This may prevent the JVM from hanging.protected void finalize() throws Throwable { if (cacheManager != null) cacheManager.cancel(); } public ExpiringCache() { cacheMap = new LRUMap(); initialize(); } // All times in millisecs public ExpiringCache(long timeToLive, long accessTimeout, int maximumCachedQuantity, long timerInterval ) { ttl = timeToLive; ato = accessTimeout; cacheMap = new LRUMap(maximumCachedQuantity); tiv = timerInterval; initialize(); } public void setTimeToLive(long milliSecs) { ttl = milliSecs; initialize(); } public void setAccessTimeout(long milliSecs) { ato = milliSecs; initialize(); } public void setCleaningInterval(long milliSecs) { tiv = milliSecs; initialize(); } public void initialize() { if (logger.isDebugEnabled()) logger.debug("initialize() started");First cancel any timer thread that may be running. Then, instantiate a new timer whose thread will be a daemon so that it won't stop the app from exiting.
if (cacheManager != null) cacheManager.cancel();
cacheManager = new Timer(true);
cacheManager.schedule(
This is where all the action happens. A new TimerTask is created as an anonymous class whose run method contains that code to remove stale or idle objects from the LRUMap. (Apache's LRUMap already has the code to remove least recently used objects if the map gets too big.)
new TimerTask() {
public void run() {
NDC.push("TimerTask");
long now = System.currentTimeMillis();
try {
MapIterator itr = cacheMap.mapIterator();
while (itr.hasNext()) {
Object key = itr.next();
CachedObject cobj = (CachedObject) itr.getValue();
if (cobj == null || cobj.hasExpired(now)) {
if (logger.isDebugEnabled()) logger.debug(
"Removing " + key + ": Idle time=" +
(now - cobj.timeAccessedLast) + "; Stale time:" +
(now - cobj.timeCached));
itr.remove();
Thread.yield();
}
}
}
catch (ConcurrentModificationException cme) {
/*
Ignorable. This is just a timer cleaning up.
It will catchup on cleaning next time it runs.
*/
if (logger.isDebugEnabled()) logger.debug(
"Ignorable ConcurrentModificationException");
}
NDC.remove();
}
},
0,
tiv
);
}
public int howManyObjects() {
return cacheMap.size();
}
public void clear() {
cacheMap.clear();
}
Although any object can be used as a key, you are better off using just plain old Strings as keys because they have the proper implementation of equals() and hashcode() for use in a Map (more here on this topic). We can't simply put the object to be cached in the map. It needs to be tagged with the time it was put in cache and the last time it was accessed, among other things. So we wrap the object in a CachedObject and then put it in the cache.
/**
If the given key already maps to an existing object and the new object
is not equal to the existing object, existing object is overwritten
and the existing object is returned; otherwise null is returned.
You may want to check the return value for null-ness to make sure you
are not overwriting a previously cached object. May be you can use a
different key for your object if you do not intend to overwrite.
*/
public Object admit(Object key, Object dataToCache) {
//cacheMap.put(key, new CachedObject(dataToCache));
//return null;
CachedObject cobj = (CachedObject) cacheMap.get(key);
if (cobj == null) {
cacheMap.put(key, new CachedObject(dataToCache));
return null;
}
else {
Object obj = cobj.getCachedData(key);
if (obj == null) {
if (dataToCache == null) {
// Avoids creating unnecessary new cachedObject
// Number of accesses is not reset because object is the same
cobj.timeCached = cobj.timeAccessedLast = System.currentTimeMillis();
return null;
}
else {
cacheMap.put(key, new CachedObject(dataToCache));
return null;
}
}
else if (obj.equals(dataToCache)) {
// Avoids creating unnecessary new cachedObject
// Number of accesses is not reset because object is the same
cobj.timeCached = cobj.timeAccessedLast = System.currentTimeMillis();
return null;
}
else {
cacheMap.put(key, new CachedObject(dataToCache));
return obj;
}
}
}
public Object admit(Object key, Object dataToCache, long objectTimeToLive, long objectIdleTimeout) {
//cacheMap.put(key, new CachedObject(dataToCache));
//return null;
CachedObject cobj = (CachedObject) cacheMap.get(key);
if (cobj == null) {
cacheMap.put(key, new CachedObject(dataToCache, objectTimeToLive, objectIdleTimeout));
return null;
}
else {
Object obj = cobj.getCachedData(key);
if (obj == null) {
if (dataToCache == null) {
// Avoids creating unnecessary new cachedObject
// Number of accesses is not reset because object is the same
cobj.timeCached = cobj.timeAccessedLast = System.currentTimeMillis();
cobj.objectTTL = objectTimeToLive;
cobj.objectIdleTimeout = objectIdleTimeout;
cobj.userTimeouts = true;
return null;
}
else {
cacheMap.put(key, new CachedObject(dataToCache, objectTimeToLive, objectIdleTimeout));
return null;
}
}
else if (obj.equals(dataToCache)) {
// Avoids creating unnecessary new cachedObject
// Number of accesses is not reset because object is the same
cobj.timeCached = cobj.timeAccessedLast = System.currentTimeMillis();
cobj.objectTTL = objectTimeToLive;
cobj.objectIdleTimeout = objectIdleTimeout;
cobj.userTimeouts = true;
return null;
}
else {
cacheMap.put(key, new CachedObject(dataToCache, objectTimeToLive, objectIdleTimeout));
return obj;
}
}
}
public Object recover(Object key) {
CachedObject cobj = (CachedObject) cacheMap.get(key);
if (cobj == null) return null;
else return cobj.getCachedData(key);
}
public void discard(Object key) {
cacheMap.remove(key);
}
public long whenCached(Object key) {
CachedObject cobj = (CachedObject) cacheMap.get(key);
if (cobj == null) return 0;
return cobj.timeCached;
}
public long whenLastAccessed(Object key) {
CachedObject cobj = (CachedObject) cacheMap.get(key);
if (cobj == null) return 0;
return cobj.timeAccessedLast;
}
public int howManyTimesAccessed(Object key) {
CachedObject cobj = (CachedObject) cacheMap.get(key);
if (cobj == null) return 0;
return cobj.numberOfAccesses;
}
An inner class to store some tags for an object to be cached. Also to update these tags when an object is accessed.
/**
A cached object, needed to store attributes such as the last time
it was accessed.
*/
protected class CachedObject {
Object cachedData;
long timeCached;
long timeAccessedLast;
int numberOfAccesses;
long objectTTL;
long objectIdleTimeout;
boolean userTimeouts;
CachedObject(Object cachedData) {
long now = System.currentTimeMillis();
this.cachedData = cachedData;
timeCached = now;
timeAccessedLast = now;
++numberOfAccesses;
}
CachedObject(Object cachedData, long timeToLive, long idleTimeout) {
long now = System.currentTimeMillis();
this.cachedData = cachedData;
objectTTL = timeToLive;
objectIdleTimeout = idleTimeout;
userTimeouts = true;
timeCached = now;
timeAccessedLast = now;
++numberOfAccesses;
}
Object getCachedData(Object key) {
long now = System.currentTimeMillis();
if (hasExpired(now)) {
cachedData = null;
cacheMap.remove(key);
return null;
}
timeAccessedLast = now;
++numberOfAccesses;
return cachedData;
}
boolean hasExpired(long now) {
long usedTTL = userTimeouts?objectTTL:ttl;
long usedATO = userTimeouts?objectIdleTimeout:ato;
if (now > timeAccessedLast + usedATO ||
now > timeCached + usedTTL
) {
return true;
}
else return false;
}
}
} // END OF CLASS
ActiveTestSuite starts each test in its own thread which worked for me. However, ActiveTestSuite does not have a constructor which automatically adds all testXXX methods in a class to the test suite. I tried addTestSuite method with class name as the argument, but it added all tests in the class to run sequentially in the same thread. So, I had to manually add each test name to the ActiveTestSuite.
<property name="log4j_home" value="C:\Programs\jakarta-log4j-1.2.8" /> <property name="junit_home" value="C:\Programs\junit3.7" /> <property name="apache_collections_home" value="C:\Programs\commons-collections-3.0" />
id.
<path id="project_class_path">
<pathelement location="${log4j_home}/dist/lib/log4j-1.2.8.jar" />
<pathelement location="${junit_home}/junit.jar" />
<pathelement location="${apache_collections_home}\commons-collections-3.0.jar" />
</path>
clean target in case you made a mistake in specifying only .class files to clean!
manifest attribute updates the MANIFEST.MF file of a JAR file with what you want.
Main-Class: line identifies the class containing the main method. The Class-Path: line specifies relative paths to external JAR files (separated by spaces) on which the code in your JAR file depends.
Relative path means path relative to the location of your JAR file. Here, four files - log4j-1.2.8.jar, junit.jar, expiringcache.jar and commons-collections-3.0.jar - must be located in the same directory as the testcache.jar file (which contains JUnit test code).
Main-Class: entry as described earlier. Also, there is no point in setting up the classpath because Java will ignore it! Instead, specify the directories and JAR/ZIP files required in the MANIFEST.MF's Class-Path: entry. Make sure to physically add those files/directories to the directory relative to your JAR file where you said to look for in the Class-Path: entry.
bundle directory has been created, you can run this whole application by going to the bundle directory and, from there, typing the command line:java -jar testcache.jar