Hi,
In a previous article Java/Spring: Measure performance and time processing with AOP on server side, I have presented a solution in order to measure performance and time processing on server side via an AOP layer (AOPALLIANCE library in Spring framework) to measure the execution time of the methods in the persistence layer HIBERNATE.
So in this article, I propose you to use an other AOP layer (AOPALLIANCE library in Spring framework) in order to caching data on server-side by intercept method calls and put their returns in a cache. Then, I will expose several solutions to invalidate the data cached: AOP layer, MBEAN/JCONSOLE, servlet/web service. This solution is more useful for data which don’t be modified frequently.
I. Technologies
To avoid the systematic calls to the persistence layer for data which change rarely, we will develop a system data cache on server-side with AOP layer to intercept and put in cache the calls to methods defined in the given beans.
To illustrate our previous explanations, we will create a Web project with:
- a Spring context defined in a XML file spring-cacheinterceptor-config.xml,
- a servlet context listener named SpringContextLoader to load this Spring context,
- a singleton named ApplicationContextContainer which will give an access to the Spring context (see my post on this topic Access to Spring context in all places or layers of an application),
- a mock service from persistence layer with the classes ServiceToCache,
- an AOP layer with a probe named CacheInterceptor,
- a simple test class named CacheInterceptorTest,
- the JARs cglib-2.0.2.jar, commons-lang-2.0.jar, commons-logging-1.0.4.jar, commons-logging-1.1.1.jar, commons-pool-1.1.jar, commons-validator.jar (version 1.0.2), jnp-client.jar (version 4.2.3.GA), jstl.jar (version 1.1.2), mail.jar (version 1.3), spring-2.5.6.jar, spring-2.5.6.SR03.jar, spring-mock-1.2.7.jar, spring-webmvc-2.5.6.jar, spring-webmvc-2.5.6.SR03.jar and standard.jar (version 1.1.2).
II. Spring context
The Spring context is defined in a XML file spring-cacheinterceptor-config.xml:
01 | <? xml version = "1.0" encoding = "UTF-8" ?> |
04 | "-//SPRING//DTD BEAN//EN" |
09 | default-lazy-init = "false" |
10 | default-dependency-check = "none" > |
15 | < bean name = "serviceToCache" class = "com.ho.test.aop.cache.service.ServiceToCache" /> |
20 | < bean name = "cacheInterceptor" class = "com.ho.test.aop.cache.service.common.CacheInterceptor" singleton = "true" > |
21 | < property name = "cacheTimeInSeconds" value = "86400" /> |
22 | < property name = "methodsToCache" > |
24 | < value >method1ToCache</ value > |
25 | < value >method3</ value > |
30 | < bean name = "module.logger.Proxy" class = "org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator" > |
31 | < property name = "beanNames" > |
33 | < value >serviceToCache</ value > |
36 | < property name = "interceptorNames" > |
38 | < value >cacheInterceptor</ value > |
… more, we declare a servlet context listener named SpringContextLoader to load this Spring context in the web deployment descriptor web.xml:
01 | <? xml version = "1.0" encoding = "UTF-8" ?> |
05 | < listener-class >com.ho.test.aop.cache.spring.utils.SpringContextLoader</ listener-class > |
08 | < param-name >CONFIG_SPRING_FILE</ param-name > |
09 | < param-value >/WEB-INF/spring-cacheinterceptor-config.xml</ param-value > |
The code of servlet context listener SpringContextLoader:
01 | public class SpringContextLoader implements ServletContextListener { |
03 | * Method contextDestroyed |
05 | * @see javax.servlet.ServletContextListener#contextDestroyed(javax.servlet.ServletContextEvent) |
07 | public void contextDestroyed(ServletContextEvent arg0) { } |
10 | * Method contextInitialized |
12 | * @see javax.servlet.ServletContextListener#contextInitialized(javax.servlet.ServletContextEvent) |
14 | public void contextInitialized(ServletContextEvent ctx) { |
15 | String springFileName = ctx.getServletContext().getInitParameter( "CONFIG_SPRING_FILE" ); |
16 | ApplicationContextContainer.getInstance(springFileName, ctx.getServletContext()); |
… and singleton ApplicationContextContainer:
01 | public class ApplicationContextContainer { |
03 | private static Log log = LogFactory.getLog(ApplicationContextContainer. class ); |
05 | private final static String SPRING_BUSINESS_CONFIG_XML = "/WEB-INF/spring-cacheinterceptor-config.xml" ; |
10 | private static ApplicationContextContainer instance = null ; |
13 | * Contains the spring configuration. |
15 | private XmlWebApplicationContext ctx = null ; |
17 | private ApplicationContextContainer() {} |
22 | public static synchronized ApplicationContextContainer getInstance() { |
23 | return getInstance(SPRING_BUSINESS_CONFIG_XML, null ); |
25 | public static synchronized ApplicationContextContainer getInstance(String springContextFile, ServletContext servletContext) { |
26 | if ( null == instance) { |
27 | instance = new ApplicationContextContainer(); |
28 | instance.ctx = new XmlWebApplicationContext(); |
29 | instance.ctx.setConfigLocation(springContextFile); |
30 | instance.ctx.setServletContext(servletContext); |
31 | instance.ctx.refresh(); |
37 | * Retrieve the spring bean corresponding to the given key. |
41 | public static Object getBean(String key) { |
42 | return getInstance().ctx.getBean(key); |
45 | public XmlWebApplicationContext getContext() { |
III. Beans to “cache”
ServiceToCache: The calls to the methods method1ToCache and method3 of this class will be monitored by AOP in order to put in cache their returns. But its methods method2, save, delete, update and saveOrUpdate.
01 | public class ServiceToCache implements IServiceToCache { |
03 | private static int numero = 0 ; |
07 | * This method should wait 2 seconds and return the result. |
09 | public String method1ToCache() { |
11 | String output = "result..." + ( new UID()).toString(); |
15 | public String method1ToCache(String valeur) { |
16 | System.out.println( "exécution de method1ToCache, valeur = " + valeur); |
17 | return valeur + numero++ + ".txt" ; |
20 | public String method2() { |
21 | return "test..." + ( new UID()).toString(); |
24 | public String method3() { |
25 | String output = "result3...." + ( new UID()).toString(); |
30 | public String save() { |
31 | String output = "save...." + ( new UID()).toString(); |
35 | public String delete() { |
36 | String output = "delete...." + ( new UID()).toString(); |
40 | public String update() { |
41 | String output = "update...." + ( new UID()).toString(); |
45 | public String saveOrUpdate() { |
46 | String output = "saveOrUpdate...." + ( new UID()).toString(); |
52 | private void pWait( long time) { |
53 | long begin = Calendar.getInstance().getTimeInMillis(); |
54 | while ((Calendar.getInstance().getTimeInMillis() - begin) < time) |
IV. AOP Alliance
The class CacheInterceptor is a probe used to intercept the calls to the certain methods of the bean ServiceToCache defined in the Spring Context via the bean module.logger.Proxy which is type of org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator. As you could see in the below code, the probe:
- generates the MethodKey of intercepted method with getMethodKey(…),
- tries to retrieve the cache output from the cache:
If the output is already present in the cache
-> return it,
Else
-> executes the intercepted method/business and compute the output, store it in the cache and return it.
So, the class named Cache will contain the cached items and several methods to put/retrieve/remove/update an item in the cache and clean the cache.
01 | public class Cache <KeyType, ElementType> implements ICache<KeyType, ElementType> { |
04 | * Contains the cached items. |
06 | private Hashtable<KeyType, Pair<ElementType, Long>> cache = new Hashtable<KeyType, Pair<ElementType, Long>>(); |
09 | * Put an item in the cache. |
11 | public void put(KeyType key, ElementType value) { |
12 | Pair<ElementType, Long> p = new Pair(value, Calendar.getInstance().getTimeInMillis()/ 1000 ); |
17 | * Retrieve an item from the Cache. |
20 | public ElementType get(KeyType key, long cacheTimeInSeconds) { |
21 | Pair<ElementType, Long> p = cache.get(key); |
27 | if (isRefreshNeeded(p, cacheTimeInSeconds)) { |
36 | * Update an item in the cache. |
38 | public void update(KeyType key, ElementType value) { |
39 | Pair<ElementType, Long> p = new Pair(value, Calendar.getInstance().getTimeInMillis()/ 1000 ); |
46 | public void clear() { cache.clear(); } |
49 | * @return true if a cache refresh is needed for the given element. |
51 | public boolean isRefreshNeeded(Pair<ElementType, Long> p, long cacheTimeInSeconds) { |
54 | boolean needRefresh = false ; |
57 | long ageInSeconds = (Calendar.getInstance().getTimeInMillis()/ 1000 ) - p.getSecond(); |
58 | if (ageInSeconds>cacheTimeInSeconds) { |
An other central class will be CacheInterceptor. This component, implementing the MethodInterceptor interface of AOP, looks for the requested method output in the cache and returns it if found. If not found, it execute the requested method, cache the output and returns it.
01 | public class CacheInterceptor implements ICacheInterceptor { |
03 | * Contains all the methods name to cache. |
05 | private List<String> methodsToCache = new ArrayList<String>(); |
08 | * Contains all the cached outputs. |
09 | * <br/>The items stored as Object are stored with a key in the cache corresponding to : |
10 | * <br/>method name + parameters separated with "_"; |
12 | private Cache<String, Object> cache = new Cache<String, Object>(); |
15 | * Time in seconds that returned object will stay active in the cache. |
17 | private long cacheTimeInSeconds = 0 ; |
24 | * @see org.aopalliance.intercept.MethodInterceptor#invoke(org.aopalliance.intercept.MethodInvocation) |
26 | public Object invoke(MethodInvocation method) throws Throwable { |
28 | if (- 1 != methodsToCache.indexOf(method.getMethod().getName())) { |
31 | String cacheKey = getCacheKey(method); |
36 | output = cache.get(cacheKey, cacheTimeInSeconds); |
48 | output = method.proceed(); |
50 | cache.put(cacheKey, output); |
55 | output = method.proceed(); |
62 | * Erase all the items stored in the cache. |
70 | * @return the key used to store the items in the cache. |
72 | private String getCacheKey(MethodInvocation method) { |
73 | StringBuffer cacheNameBuffer = new StringBuffer(); |
74 | cacheNameBuffer.append(method.getMethod().getDeclaringClass().getName() + "." + method.getMethod().getName() + "(" ); |
77 | if (method.getArguments() != null && method.getArguments().length > 0 ) { |
78 | for ( int a = 0 ; a < method.getArguments().length; a++) { |
79 | cacheNameBuffer.append( "-" ); |
80 | if (method.getArguments()[a] == null ) { |
81 | cacheNameBuffer.append( "null" ); |
83 | cacheNameBuffer.append(method.getArguments()[a].toString() |
89 | return cacheNameBuffer.toString(); |
V. Tests
The project in attachement contains a test class named CacheInterceptorTest:
001 | public class CacheInterceptorTest extends TestCase { |
003 | * Contains a reference to the spring context. |
005 | private static ApplicationContext ctx = null ; |
007 | public void testCache() { |
009 | IServiceToCache service = (IServiceToCache) ctx.getBean( "serviceToCache" ); |
010 | String output1, output2; |
016 | Calendar begin = Calendar.getInstance(); |
017 | output1 = service.method1ToCache(); |
018 | System.out.println(output1); |
019 | Calendar end = Calendar.getInstance(); |
020 | long time = end.getTimeInMillis()-begin.getTimeInMillis(); |
022 | fail( "the cache was active..." ); |
031 | Calendar begin = Calendar.getInstance(); |
032 | output2 = service.method1ToCache(); |
033 | System.out.println(output2); |
034 | Calendar end = Calendar.getInstance(); |
035 | long time = end.getTimeInMillis()-begin.getTimeInMillis(); |
037 | fail( "the cache was NOT active..." ); |
041 | assertTrue(output1.equals(output2)); |
045 | public void testNoCache() { |
047 | IServiceToCache service = (IServiceToCache) ctx.getBean( "serviceToCache" ); |
048 | String output1, output2; |
054 | output1 = service.method2(); |
055 | System.out.println(output1); |
062 | output2 = service.method2(); |
063 | System.out.println(output2); |
066 | assertFalse(output1.equals(output2)); |
070 | public void testCacheClear() { |
072 | IServiceToCache service = (IServiceToCache) ctx.getBean( "serviceToCache" ); |
073 | String output1, output2, output3, output4, output5, output6; |
079 | output1 = service.method1ToCache( "zut" ); |
080 | System.out.println( "output1 = " + output1); |
081 | output2 = service.method1ToCache( "zut" ); |
082 | System.out.println( "output2 = " + output2); |
083 | output3 = service.method1ToCache( "zut" ); |
084 | System.out.println( "output3 = " + output3); |
086 | assertTrue(output1.equals(output2)); |
087 | assertTrue(output1.equals(output3)); |
092 | CacheInterceptor ix = (CacheInterceptor ) ctx.getBean( "cacheInterceptor" ); |
100 | output4 = service.method1ToCache( "tac" ); |
101 | System.out.println( "output4 = " + output4); |
102 | output5 = service.method1ToCache( "zut" ); |
103 | System.out.println( "output5 = " + output5); |
104 | output6 = service.method1ToCache( "zut" ); |
105 | System.out.println( "output6 = " + output6); |
107 | assertFalse(output4.equals(output5)); |
108 | assertTrue(output5.equals(output6)); |
115 | * @see junit.framework.TestCase#setUp() |
118 | protected void setUp() throws Exception { |
121 | if ( this .ctx == null ){ |
122 | this .ctx = new FileSystemXmlApplicationContext( "C:\\MyFiles\\Development\\Java\\dev\\test_AOP_cache\\WebContent\\WEB-INF\\spring-cacheinterceptor-config.xml" ); |
… so, the outputs in console could be like below. For more information, please, analyze the JUNIT CacheInterceptorTest and the bean to cache ServiceToCache .
01 | result...356e43a2:138bb6a6e49:-8000 |
02 | result...356e43a2:138bb6a6e49:-8000 |
03 | test...356e43a2:138bb6a6e49:-7fff |
04 | test...356e43a2:138bb6a6e49:-7ffe |
05 | exécution de method1ToCache, valeur = zut |
09 | exécution de method1ToCache, valeur = tac |
11 | exécution de method1ToCache, valeur = zut |
VI. Invalidate the cache
Here, I will expose several solutions to invalidate the data cached: AOP layer, MBEAN/JCONSOLE, servlet/web service.
By default, the validity of a data in cache will be 86400 seconds or 24 hours. But, the invalidation of the cache could be automatical or manual:
- Automatically: the date of validity for cached data is expired, so the CacheInterceptor will be remove the expired data from cache,
- Automatically: we could implement an other interceptor CacheCleanerInterceptor which will invalidate the data cached when certain methods are called, for example the methods for deleting, modifying or saving. In the project in attachement, there is the interceptor CacheCleanerInterceptor:
01 | public class CacheCleanerInterceptor implements ICacheCleanerInterceptor { |
04 | * Contains all the methods name to cache. |
06 | private List<String> methodsCleaner = new ArrayList<String>(); |
09 | * Contains the cache interceptor |
11 | private ICacheInterceptor cache = null ; |
18 | * @see org.aopalliance.intercept.MethodInterceptor#invoke(org.aopalliance.intercept.MethodInvocation) |
20 | public Object invoke(MethodInvocation method) throws Throwable { |
22 | if (- 1 != methodsCleaner.indexOf(method.getMethod().getName())) { |
27 | Object output = method.proceed(); |
31 | public List<String> getMethodsCleaner() { |
32 | return methodsCleaner; |
35 | public void setMethodsCleaner(List<String> methodsCleaner) { |
36 | this .methodsCleaner = methodsCleaner; |
39 | public ICacheInterceptor getCache() { |
43 | public void setCache(ICacheInterceptor cache) { |
… This interceptor is defined in the Spring Context file spring-cacheinterceptor-config.xml. In our example, the interceptor CacheCleanerInterceptor will invalidate the cache when the methods save, delete, update and saveOrUpdate of the bean serviceCleaner (com.ho.test.aop.cache.service.ServiceToCache) are called:
05 | < bean name = "serviceCleaner" class = "com.ho.test.aop.cache.service.ServiceToCache" /> |
10 | < bean name = "cacheCleanerInterceptor" class = "com.ho.test.aop.cache.service.common.CacheCleanerInterceptor" singleton = "true" > |
11 | < property name = "cache" ref = "cacheInterceptor" /> |
12 | < property name = "methodsCleaner" > |
17 | < value >saveOrUpdate</ value > |
22 | < bean name = "module.logger.ProxyCleaner" class = "org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator" > |
23 | < property name = "beanNames" > |
25 | < value >serviceCleaner</ value > |
28 | < property name = "interceptorNames" > |
30 | < value >cacheCleanerInterceptor</ value > |
- Manually: it will be possible to clear the cache manually via a call to a specific servlet like “AjaxToSpringBeanServlet” which exposes the Spring beans as HTTP service / AJAX. A post Spring: Expose all Spring beans as HTTP/AJAX service has been written about this servlet, so an example to call this servlet in order to clean the cache could be:
http://localhost:8080/myproject/ajaxToSpringBeanService.do?beanName=cacheInterceptor&beanMethod=clear
- Manually: an other last solution to clear the cache manually via the use of MBEAN JMX. In the project in attachement, I will expose the CacheInterceptor Spring bean like MBEAN through the component CommonMbeanExporter (org.springframework.jmx.export.MBeanExporter), IRemoteMbeanService and MBeanVo.
… This interceptor is defined in the Spring Context file spring-cacheinterceptor-config.xml:
05 | < bean name = "aop.cache.MbeanExporter" class = "com.ho.test.mbean.common.CommonMbeanExporter" |
07 | < property name = "beans" > |
09 | < entry key = "cacheInterceptor:application=HUOCACHE,component=HUOCACHECacheInterceptor" |
10 | value-ref = "cacheInterceptor" /> |
For more information, I advise you the post Quickly Exposing Spring Beans as JMX MBeans by Cris Holdorph about MBEAN exposing.
If you encountered error during the start of tomcat, add the last startup parameters of TOMCAT configuration (“VM Arguments” tab):
-Dcatalina.base=”C:\MyFiles\Development\Java\tools\TomCat” -Dcatalina.home=”C:\MyFiles\Development\Java\tools\TomCat”
-Dwtp.deploy=”C:\MyFiles\Development\Java\tools\TomCat\webapps”
-Djava.endorsed.dirs=”C:\MyFiles\Development\Java\tools\TomCat\common\endorsed”
-Djava.security.auth.login.config=”C:\MyFiles\Development\Java\dev\SpringHibernateIntegration2\README_CONF\jaas.config”
-Dcom.sun.management.jmxremote.port=”18080″
-Dcom.sun.management.jmxremote.authenticate=”false”
…for more information, read http://java.sun.com/j2se/1.5.0/docs/guide/management/agent.html
…after the deployment of Web application on an AS:
1 | INFO: Loading XML bean definitions from file [C:\MyFiles\Development\Java\dev\test_AOP_cache\WebContent\WEB-INF\spring-cacheinterceptor-config.xml] |
2 | INFO: Pre-instantiating singletons in org.springframework.beans.factory. |
3 | support.DefaultListableBeanFactory@4cb9e45a: defining beans [serviceToCache,cacheInterceptor,module.logger.Proxy,serviceCleaner, |
4 | cacheCleanerInterceptor,module.logger.ProxyCleaner,aop.cache.MbeanExporter]; root of factory hierarchy |
6 | 25 juil. 2012 01:51:49 org.springframework.jmx.export.MBeanExporter afterPropertiesSet |
7 | INFO: Registering beans for JMX exposure on startup |
8 | 25 juil. 2012 01:51:49 org.springframework.jmx.export.MBeanExporter registerBeanInstance |
9 | INFO: Located managed bean 'cacheInterceptor:application=HUOCACHE,component=HUOCACHECacheInterceptor': registering with JMX server as MBean [cacheInterceptor:application=HUOCACHE,component=HUOCACHECacheInterceptor] |
…we can access to defined MBEAN via JConsole . JConsole is a tool for following which is compliance with specification JMX (Java Management Extensions). JConsole uses the instrumentation of the JVM to provide information on performance and resource consumption of applications running on the Java platform. The JConsole executable is in JDK_HOME/bin with JDK_HOME equals to the installation path of JDK (C:\Program Files (x86)\Java\jdk1.6.0_32\bin). If the directory is in the Path environment variable, you can run JConsole by simply typing “JConsole” in the command line. Otherwise, you must type the full path of the executable.
Finally, there is a class named MbeanTools which is not used in the attachement’s project, however thi class allows the access to MBEAN on a server SERVER1: [SERVER1 (management interface)] –RMI–> [SERVER2 (service of access to MBEANs from a tierce server)] –MbeanTools–> Access to MBEANs on MBEANServer (invoke, get, set, update ..MBEAN).
So, in JConsole, our mbean is accessible like:

… click on the button getMethodsToCache:

…and we can clear the cache by clicking on the button clear:

I don’t have been exhaustive in this article, but, I hope that it will be useful for your needs about data caching on server side. This “home-made” solution is more useful for data which don’t be modified frequently, but, for more complex needs,
there is also other solution of data caching like ECache.
For more informations, you could contact me.
Download: test_AOP_cache.zip
Best regards,
Huseyin OZVEREN
Related