JavaBlog.fr / Java.lu DEVELOPMENT,Java,Spring Java/Spring: Data caching – Intercept method calls and put their returns in server-side cache with AOP/MBEAN/JCONSOLE

Java/Spring: Data caching – Intercept method calls and put their returns in server-side cache with AOP/MBEAN/JCONSOLE

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:

<?xml version="1.0" encoding="UTF-8"?>

<!DOCTYPE beans PUBLIC
    "-//SPRING//DTD BEAN//EN"
    "http://www.springframework.org/dtd/spring-beans.dtd">
    
<beans
	default-autowire="no"
	default-lazy-init="false"
	default-dependency-check="none">

<!--
 | BEAN METIER 
 -->
	<bean name="serviceToCache" class="com.ho.test.aop.cache.service.ServiceToCache" />

<!--
 | MISE EN CACHE: mettre en cache le résultat des méthodes "methode1ToCache" et "method3" du bean ServiceToCache.
 -->
	<bean name="cacheInterceptor" class="com.ho.test.aop.cache.service.common.CacheInterceptor" singleton="true" >
		<property name="cacheTimeInSeconds" value="86400"/>
		<property name="methodsToCache">
			<list>
		 		<value>method1ToCache</value>
		 		<value>method3</value>
			</list>
		</property>
	</bean>
	
	 <bean name="module.logger.Proxy" class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator">
	 	<property name="beanNames">
			<list>
				<value>serviceToCache</value>
	 		</list>
	 	</property>
	 	<property name="interceptorNames">
	 		<list>
	 			<value>cacheInterceptor</value>
	 		</list>
	 	</property>
	 </bean>
 
<!--  ... -->

</beans>

… more, we declare a servlet context listener named SpringContextLoader to load this Spring context in the web deployment descriptor web.xml:

<?xml version="1.0" encoding="UTF-8"?>
<web-app ...>
...
<listener>
	<listener-class>com.ho.test.aop.cache.spring.utils.SpringContextLoader</listener-class>
</listener>
<context-param>
	<param-name>CONFIG_SPRING_FILE</param-name>
	<param-value>/WEB-INF/spring-cacheinterceptor-config.xml</param-value>
</context-param>
</web-app>

The code of servlet context listener SpringContextLoader:

public class SpringContextLoader implements ServletContextListener {
	/**
	 * Method contextDestroyed
	 * @param arg0
	 * @see javax.servlet.ServletContextListener#contextDestroyed(javax.servlet.ServletContextEvent)
	 */
	public void contextDestroyed(ServletContextEvent arg0) { }

	/**
	 * Method contextInitialized
	 * @param arg0
	 * @see javax.servlet.ServletContextListener#contextInitialized(javax.servlet.ServletContextEvent)
	 */
	public void contextInitialized(ServletContextEvent ctx) {
		String springFileName = ctx.getServletContext().getInitParameter("CONFIG_SPRING_FILE");
		ApplicationContextContainer.getInstance(springFileName, ctx.getServletContext());
	}
}

… and singleton ApplicationContextContainer:

public class ApplicationContextContainer {

    private static Log log = LogFactory.getLog(ApplicationContextContainer.class);

    private final static String SPRING_BUSINESS_CONFIG_XML = "/WEB-INF/spring-cacheinterceptor-config.xml";
    
    /**
     * Instance
     */
    private static ApplicationContextContainer instance = null;
    
    /**
     * Contains the spring configuration.
     */
    private XmlWebApplicationContext ctx = null;

    private ApplicationContextContainer() {}
    
    /**
     * Getinstance method.
     */
    public static synchronized ApplicationContextContainer getInstance() {
        return getInstance(SPRING_BUSINESS_CONFIG_XML, null);
    }
    public static synchronized ApplicationContextContainer getInstance(String springContextFile, ServletContext servletContext) {
    	if (null == instance) {
    		instance = new ApplicationContextContainer();
    		instance.ctx = new XmlWebApplicationContext();  
    		instance.ctx.setConfigLocation(springContextFile);
    		instance.ctx.setServletContext(servletContext);
    		instance.ctx.refresh();
        } // end-if
        return instance;
    }
    
    /**
     * Retrieve the spring bean corresponding to the given key.
     * @param key
     * @return
     */
    public static Object getBean(String key) {
        return getInstance().ctx.getBean(key);
    }
    
    public XmlWebApplicationContext getContext() {
        return ctx;
    }
}

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.

public class ServiceToCache implements IServiceToCache {
	// -------------------------------------------------------------- CONSTANTS
	private static int numero = 0;
	// --------------------------------------------------------- PUBLIC METHODS

	/**
	 * This method should wait 2 seconds and return the result.
	 */
	public String method1ToCache() {
		pWait(2000);
		String output = "result..." + (new UID()).toString();
		return output;
	}

	public String method1ToCache(String valeur) {
		System.out.println("exécution de method1ToCache, valeur = " + valeur);
		return valeur + numero++ + ".txt";
	}

	public String method2() {
		return "test..." + (new UID()).toString();
	}

	public String method3() {
		String output = "result3...." + (new UID()).toString();
		return output;
	}

	// ---------- METHODS CACHE cleaner 
	public String save() {
		String output = "save...." + (new UID()).toString();
		return output;
	}
	
	public String delete() {
		String output = "delete...." + (new UID()).toString();
		return output;
	}
	
	public String update() {
		String output = "update...." + (new UID()).toString();
		return output;
	}
	
	public String saveOrUpdate() {
		String output = "saveOrUpdate...." + (new UID()).toString();
		return output;
	}
	
	
	// -------------------------------------------------------- PRIVATE METHODS
	private void pWait(long time) {
		long begin = Calendar.getInstance().getTimeInMillis();
		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.

public class Cache <KeyType, ElementType> implements ICache<KeyType, ElementType> {
	
	/**
	 * Contains the cached items.
	 */
	private Hashtable<KeyType, Pair<ElementType, Long>> cache = new Hashtable<KeyType, Pair<ElementType, Long>>();

	/**
	 * Put an item in the cache.
	 */
	public void put(KeyType key, ElementType value) {
		Pair<ElementType, Long> p = new Pair(value, Calendar.getInstance().getTimeInMillis()/1000);
		cache.put(key, p);
	}
	
	/**
	 * Retrieve an item from the Cache.
	 * @param key
	 */
	public ElementType get(KeyType key, long cacheTimeInSeconds) {
		Pair<ElementType, Long> p = cache.get(key);

		if (null == p) {
			return null;
		} // end-if
		
		if (isRefreshNeeded(p, cacheTimeInSeconds)) {
			remove(key);
			return null;
		} // end-if
		
		return p.getFirst();
	}
	
	/**
	 * Update an item in the cache.
	 */
	public void update(KeyType key, ElementType value) {
		Pair<ElementType, Long> p = new Pair(value, Calendar.getInstance().getTimeInMillis()/1000);
		cache.put(key, p);
	}
	
	/**
	 * Clear the cache.
	 */
	public void clear() { cache.clear(); }

	/**
	 * @return true if a cache refresh is needed for the given element.
	 */
	public boolean isRefreshNeeded(Pair<ElementType, Long> p, long cacheTimeInSeconds) {
		
		// Verify if the item time is not greater than the given cacheTimeInSeconds
		boolean needRefresh = false;
		{
			// Compute the current item age
			long ageInSeconds = (Calendar.getInstance().getTimeInMillis()/1000) - p.getSecond();
			if (ageInSeconds>cacheTimeInSeconds) {
				 needRefresh = true;
			} // end-if
		} // end-block
		
		return needRefresh;

	}

	//...
}

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.

public class CacheInterceptor implements ICacheInterceptor {
	/**
	 * Contains all the methods name to cache.
	 */
	private List<String> methodsToCache = new ArrayList<String>();

	/**
	 * Contains all the cached outputs.
	 * <br/>The items stored as Object are stored with a key in the cache corresponding to :
	 * <br/>method name + parameters separated with "_";
	 */
	private Cache<String, Object> cache = new Cache<String, Object>();

	/**
	 * Time in seconds that returned object will stay active in the cache.
	 */
	private long cacheTimeInSeconds = 0;

	/**
	 * Method invoke
	 * @param arg0
	 * @return
	 * @throws Throwable
	 * @see org.aopalliance.intercept.MethodInterceptor#invoke(org.aopalliance.intercept.MethodInvocation)
	 */
	public Object invoke(MethodInvocation method) throws Throwable {

		if (-1 != methodsToCache.indexOf(method.getMethod().getName())) {

			// Computes the cache key
			String cacheKey = getCacheKey(method);

			// Try to retrieve the cache output from the cache
			Object output = null;
			{
				output = cache.get(cacheKey, cacheTimeInSeconds);
			} // end-block

			// If the output is already present in the cache
			// return it
			if (null != output) {
				return output;
			} // end-if

			// Else, compute the output,
			// store it in the cache
			// and return it.
			output = method.proceed();

			cache.put(cacheKey, output);

			return output;
		} else {
			Object output = null;
			output = method.proceed();
			return output;

		} // end-if
	}

	/**
	 * Erase all the items stored in the cache.
	 */
	public void clear() {
		cache.clear();
	}

	/**
	 * Get the cache key
	 * @return the key used to store the items in the cache.
	 */
	private String getCacheKey(MethodInvocation method) {
	    StringBuffer cacheNameBuffer = new StringBuffer();
	    cacheNameBuffer.append(method.getMethod().getDeclaringClass().getName() + "." + method.getMethod().getName() + "(");
		//cacheNameBuffer.append(method.getMethod().getName());

		if (method.getArguments() != null && method.getArguments().length > 0) {
			for (int a = 0; a < method.getArguments().length; a++) {
				cacheNameBuffer.append("-");
				if (method.getArguments()[a] == null) {
					cacheNameBuffer.append("null");
				} else {
					cacheNameBuffer.append(method.getArguments()[a].toString()
							.toLowerCase());
				} // end-if
			} // end-for
		} // end-if

		return cacheNameBuffer.toString();
	}
	
	
	//...
}

V. Tests
The project in attachement contains a test class named CacheInterceptorTest:

public class CacheInterceptorTest extends TestCase {
	/**
	 * Contains a reference to the spring context.
	 */
	private static ApplicationContext ctx = null;

	public void testCache() {
		
		IServiceToCache service = (IServiceToCache) ctx.getBean("serviceToCache");
		String output1, output2;
		
		// Call the method1ToCache, this should take 2 seconds...
		// verify if the call to the service takes 2 seconds,
		// if not, fail
		{
			Calendar begin = Calendar.getInstance();
			output1 = service.method1ToCache();
			System.out.println(output1);
			Calendar end = Calendar.getInstance();
			long time = end.getTimeInMillis()-begin.getTimeInMillis();
			if (time < 2000) {
				fail("the cache was active...");
			} // end-if
			
		} // end-if
		
		// Call the method1ToCache again
		// verify if the call to the service does NOT take 2 seconds,
		// if not, fail
		{
			Calendar begin = Calendar.getInstance();
			output2 = service.method1ToCache();
			System.out.println(output2);
			Calendar end = Calendar.getInstance();
			long time = end.getTimeInMillis()-begin.getTimeInMillis();
			if (time >= 2000) {
				fail("the cache was NOT active...");
			} // end-if
		} // end-if
		
		assertTrue(output1.equals(output2));
	}

	
	public void testNoCache() {
		
		IServiceToCache service = (IServiceToCache) ctx.getBean("serviceToCache");
		String output1, output2;
		
		// Call the method2, this method id not in cache...
		// verify if the call to the service takes a different return,
		// if not, fail
		{
			output1 = service.method2();
			System.out.println(output1);
		} // end-if
		
		// Call the method2 again
		// verify if the call to the service takes a different return,
		// if not, fail
		{
			output2 = service.method2();
			System.out.println(output2);
		} // end-if
		
		assertFalse(output1.equals(output2));
	}
	

	public void testCacheClear() {
		// 
		IServiceToCache service = (IServiceToCache) ctx.getBean("serviceToCache");
		String output1, output2, output3, output4, output5, output6;
		
		// Call the method1ToCache(..), this method id in cache...
		// verify if the call to the service takes a different return,
		// if not, fail
		{
			output1 = service.method1ToCache("zut");
			System.out.println("output1 = " + output1);
			output2 = service.method1ToCache("zut");
			System.out.println("output2 = " + output2);
			output3 = service.method1ToCache("zut");
			System.out.println("output3 = " + output3);
			//
			assertTrue(output1.equals(output2));
			assertTrue(output1.equals(output3));
		} // end-if
		
		// Clear the cache
		{
			CacheInterceptor ix = (CacheInterceptor ) ctx.getBean("cacheInterceptor");
			ix.clear();
		}
		
		// Call the method1ToCache(..), this method id in cache...
		// verify if the call to the service takes a different return,
		// if not, fail
		{
			output4 = service.method1ToCache("tac");
			System.out.println("output4 = " + output4);
			output5 =  service.method1ToCache("zut");
			System.out.println("output5 = " + output5);
			output6 =  service.method1ToCache("zut");
			System.out.println("output6 = " + output6);
			//
			assertFalse(output4.equals(output5));
			assertTrue(output5.equals(output6));
		}
	}

	/**
	 * Method setUp
	 * @throws Exception
	 * @see junit.framework.TestCase#setUp()
	 */
	@Override
	protected void setUp() throws Exception {
		super.setUp();

		if(this.ctx == null){
			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 .

result...356e43a2:138bb6a6e49:-8000
result...356e43a2:138bb6a6e49:-8000
test...356e43a2:138bb6a6e49:-7fff
test...356e43a2:138bb6a6e49:-7ffe
exécution de method1ToCache, valeur = zut
output1 = zut0.txt
output2 = zut0.txt
output3 = zut0.txt
exécution de method1ToCache, valeur = tac
output4 = tac1.txt
exécution de method1ToCache, valeur = zut
output5 = zut2.txt
output6 = zut2.txt

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:
    public class CacheCleanerInterceptor implements ICacheCleanerInterceptor {
    
    	/**
    	 * Contains all the methods name to cache.
    	 */
    	private List<String> methodsCleaner = new ArrayList<String>();
    
    	/**
    	 * Contains the cache interceptor
    	 */
    	private ICacheInterceptor cache = null;
    
    	/**
    	 * Method invoke
    	 * @param arg0
    	 * @return
    	 * @throws Throwable
    	 * @see org.aopalliance.intercept.MethodInterceptor#invoke(org.aopalliance.intercept.MethodInvocation)
    	 */
    	public Object invoke(MethodInvocation method) throws Throwable {
    
    		if (-1 != methodsCleaner.indexOf(method.getMethod().getName())) {
    			// Erase all the items stored in the cache.
    			cache.clear();
    		} // end-if
    		
    		Object output = method.proceed();
    		return output;
    	}
    
    	public List<String> getMethodsCleaner() {
    		return methodsCleaner;
    	}
    
    	public void setMethodsCleaner(List<String> methodsCleaner) {
    		this.methodsCleaner = methodsCleaner;
    	}
    
    	public ICacheInterceptor getCache() {
    		return cache;
    	}
    
    	public void setCache(ICacheInterceptor cache) {
    		this.cache = 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:

    ...
    <!--
     | BEAN METIER 
     -->
    	<bean name="serviceCleaner" class="com.ho.test.aop.cache.service.ServiceToCache" />
    
    <!--
     | NETTOYAGE DU CACHE: Invalidation automatique
     -->
    	<bean name="cacheCleanerInterceptor" class="com.ho.test.aop.cache.service.common.CacheCleanerInterceptor" singleton="true">
    		<property name="cache" ref="cacheInterceptor"/>
    		<property name="methodsCleaner">
    			<list>
    				<value>save</value>
    				<value>delete</value>
    				<value>update</value>
    				<value>saveOrUpdate</value>
    			</list>
    		</property>
    	</bean>
    
    	<bean name="module.logger.ProxyCleaner" class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator">
    		<property name="beanNames">
    			<list>
    				<value>serviceCleaner</value>
    			</list>
    		</property>
    		<property name="interceptorNames">
    			<list>
    				<value>cacheCleanerInterceptor</value>
    			</list>
    		</property>
    	</bean>
    ...
    
    

  • 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:

    ...
    <!-- 
     | JMX exposition of the bean cacheInterceptor
     -->
    <bean name="aop.cache.MbeanExporter" class="com.ho.test.mbean.common.CommonMbeanExporter" 
    	lazy-init="false">
    	 	<property name="beans">
    	 		<map>
    	 			<entry 	key="cacheInterceptor:application=HUOCACHE,component=HUOCACHECacheInterceptor" 
    	 					value-ref="cacheInterceptor" />
    	 		</map>
    	 	</property>
    </bean>
    ...
    

    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:

    INFO: Loading XML bean definitions from file [C:\MyFiles\Development\Java\dev\test_AOP_cache\WebContent\WEB-INF\spring-cacheinterceptor-config.xml]
    INFO: Pre-instantiating singletons in org.springframework.beans.factory.
    support.DefaultListableBeanFactory@4cb9e45a: defining beans [serviceToCache,cacheInterceptor,module.logger.Proxy,serviceCleaner,
    cacheCleanerInterceptor,module.logger.ProxyCleaner,aop.cache.MbeanExporter]; root of factory hierarchy
    ...
    25 juil. 2012 01:51:49 org.springframework.jmx.export.MBeanExporter afterPropertiesSet
    INFO: Registering beans for JMX exposure on startup
    25 juil. 2012 01:51:49 org.springframework.jmx.export.MBeanExporter registerBeanInstance
    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

Leave a Reply

Your email address will not be published.

Time limit is exhausted. Please reload CAPTCHA.

Related Post