This article will explain how to implement caching for custom objects in Liferay 7.0 – using liferay’s caching mechanism, and also by integrating a third party cache server (Redis).

Table of Contents

1.0 Pre-requisites

  • Understanding of Liferay 7, Service Builder, OSGI, etc
  • Understanding of basic concepts and exposure to Redis and Jedis. If you want to learn more about Redis, please refer to following articles.

2.0 Creation of a Liferay 7 Module for caching.

We have two options here

  1. Create an API module project
  2. Create a fake Service builder project

We want to use Spring Dependency Injection in our project. Unfortunately it only works in Service Builder projects and not in API projects. Hence we decided to use #2 as our project template.

However since we do not need any entities in our project, we will create a FAKE service builder project (a one without any actual entity). It will just create the service classes where we will put our code, and can use Spring

This will help us with Spring integration.

2.1 Creating a ‘fake’ service builder project.

Open Liferay Eclipse, and create a new Service builder module project.

liferay-spring-01

liferay-spring02

liferay-spring-03

Once the project is created, you will see two modules created – cache-wrapper-api and cache-wrapper-service.

Go to cache-wrapper -> cache-wrapper-service -> service.xml , and create an fake entity.

<?xml version="1.0"?>
<!DOCTYPE service-builder PUBLIC "-//Liferay//DTD Service Builder 7.0.0//EN" "http://www.liferay.com/dtd/liferay-service-builder_7_0_0.dtd">

<service-builder package-path="com.liferay.cache">
   <namespace>test</namespace>    
   <entity local-service="true" name="Cache" remote-service="false" uuid="false">
      <!--  Remove all fields -->
   </entity>
</service-builder>

2.2 Adding dependencies

Open build.gradle in your cache-wrapper-api project and add following dependencies

// https://mvnrepository.com/artifact/redis.clients/jedis         
compile group: 'redis.clients', name: 'jedis', version: '2.9.0'

// https://mvnrepository.com/artifact/org.apache.commons/commons-pool2
compile group: 'org.apache.commons', name: 'commons-pool2', version: '2.5.0'

Refresh the gradle project, to download the dependencies.

2.3 Building project and running application 

Go to base folder of your project, and run the Build Service command.

./gradlew :modules:cache-wrapper:cache-wrapper-service:buildService

This will build the code and generate all the service and API classes.

Build and deploy the code to existing liferay application

./gradlew deploy

Start and run your existing application.

2.4 Verifying module deployment in Felix Gogo Shell

Once liferay application has started, then open a command terminal and run following command, to connect to Felix Gogo shell

telnet localhost 11311

You will see something like this

liferay-spring-felix08

Run below command to verify all the modules that start with name ‘cache’

lb | grep cache

Output will be something like this

 107|Active | 10|cache-wrapper-api (1.0.0)
 108|Active | 10|cache-wrapper-service (1.0.0)

This confirms that the module has been deployed successfully.

3.0 Resolving dependency issues

3.1 Add dummy code to CacheLocalServiceImpl

Go to cache-wrapper -> cache-wrapper-service, and open the file com.liferay.cache.service.impl.CacheLocalServiceImpl, and then add following code

@ProviderType
public class CacheLocalServiceImpl extends CacheLocalServiceBaseImpl {
   /*
    * NOTE FOR DEVELOPERS:
    *
    * Never reference this class directly. Always use {@link com.liferay.cache.service.CacheLocalServiceUtil} to access the cache local service.
    */
   private JedisPool jedisPool;
   
   private void testPool() {
      if(jedisPool != null) {
         Jedis jedis = jedisPool.getResource();
         jedis.close();
      }
   }
}

This is just the test code to confirm that the redis/jedis dependencies have been added correctly, and module deploys correctly. This code will be removed later.

Build the Service first, then build the module and deploy

3.2 Fixing dependency issues

Now connect to Felix gogo shell and check the module detail

lb | grep cache

You will get output like

 616|Active | 10|cache-wrapper-api (1.0.0)
 617|Installed | 10|cache-wrapper-service (1.0.0)

You can see that the module 617 – cache-wrapper-service is in Installed status and not Active. This means it has failed due to some reason.

Lets try to start it manually. Run below command on Felix gogo shell

start 617

You will get following error

org.osgi.framework.BundleException: Could not resolve module: com.liferay.cache.service [617]
 Unresolved requirement: Import-Package: redis.clients.jedis; version="[2.9.0,3.0.0)"

Now lets modify the build.gradle file and modify the dependencies from ‘compile‘ to ‘compileInclude

// https://mvnrepository.com/artifact/redis.clients/jedis         
compileInclude group: 'redis.clients', name: 'jedis', version: '2.9.0'

// https://mvnrepository.com/artifact/org.apache.commons/commons-pool2
compileInclude group: 'org.apache.commons', name: 'commons-pool2', version: '2.5.0'

Refresh gradle dependencies, and then build and deploy again.

You will notice that the module is still in ‘Installed‘ status in Felix gogo shell. And when you try to start it manually, you will get a new error now.

org.osgi.framework.BundleException: Could not resolve module: com.liferay.cache.service [617]
 Unresolved requirement: Import-Package: net.sf.cglib.proxy

So now let’s add this dependency

compileInclude group: 'cglib', name: 'cglib', version: '2.2.2'

Refresh gradle dependencies, and then build and deploy again.

Now you can see that the module is deploying correctly

 616|Active | 10|cache-wrapper-api (1.0.0)
 617|Active | 10|cache-wrapper-service (1.0.0)

4.0 Adding Caching Support

We will build a caching implementation that can work either with

  • Liferay’s default cache implementation
  • Or Integrate with Redis.

We will provide a configuration property that controls which type of cache will be used by the system.

Also we will provide a fallback mechanism -> If user chooses Redis cache but if application cannot connect to Redis due to some reason, then the system will fallback to Liferay default caching mechanism.

4.1 Adding new properties to portal-ext.properties

Add following properties to portal-ext.properties file

#cache timeout
cache.default.ttl.seconds=3600

#configuration property to decide whether to use redis cache or liferay cache.
#If true, then redis is used else liferay's default cache is used.
cache.use.redis=true

# Redis Connection Properties
# redis hostname
cache.redis.hostname=localhost
# redis port
cache.redis.port=6379
# redis password. 
cache.redis.password=
# whether redis connection will use ssl
cache.redis.usessl=false
# max connections in redis connection pool
cache.redis.connections.max=50
# max idle connection in redis connection pool
cache.redis.connections.maxIdle=20
# min idle connection in redis connection pool
cache.redis.connections.minIdle=5
# connection timeout in milliseconds
cache.redis.connections.timeout.millis=30000

4.2 Creating a Cache Manager interface.

Add a cache manager interface for providing caching implementation.
This interface will provide the common method definitions for adding to cache, removing from cache, etc.

There will be two implementation classes of this interface

  • One will implement using Liferay’s default caching mechanism
  • Other will implement Redis cache implementation

Add this interface to module cache-wrapper -> cache-wrapper-api -> src/main/java -> com.liferay.cache.service

public interface CacheManager {
   
   //Add to cache
   public void addToCache(String cacheName, Serializable key, Serializable value, int ttl) throws PortalException ;

   //Get from cache
   public Serializable getFromCache(String cacheName, Serializable key) throws PortalException;

   //Remove from cache
   public void removeFromCache(String cacheName, Serializable key) throws PortalException;

   //Clear the entire cache
   public void clearCache(String cacheName) throws PortalException;
   
   //Close the cache connection pool, if implemented.
   public void closeCachePool();
   
   //Ping the connection to find if it is working
   public void ping() throws PortalException;

}

4.3 Creating Liferay implementation of the cache.

Now we will create the first implementation of the CacheManager, using liferay’s com.liferay.portal.kernel.cache.MultiVMPoolUtil.

The interface is added to the cache-wrapper-api module, but the implementation classes are added to the cache-wrapper-service module.

Add this implementation class to module cache-wrapper -> cache-wrapper-service -> src/main/java -> com.liferay.cache.service.impl

public class LiferayCacheManagerImpl implements CacheManager {

   private static final Log logger = LogFactoryUtil.getLog(CacheLocalService.class);
   
   @Override
   public void addToCache(String cacheName, Serializable key, Serializable value, int ttl) {
      logger.debug("Liferay Cache: Adding to cache. CacheName = " + cacheName + ", Key = " + key + ", TTL : " + ttl);
      PortalCache<Serializable, Serializable> cache = MultiVMPoolUtil.getPortalCache(cacheName);
      cache.put(key, value, ttl);
   }

   @Override
   public Serializable getFromCache(String cacheName, Serializable key) {
      logger.debug("Liferay Cache: Fetching from cache. CacheName = " + cacheName + ", Key = " + key);
    
      PortalCache<Serializable, Serializable> cache = MultiVMPoolUtil.getPortalCache(cacheName);
      return cache.get(key);
   }

   public void removeFromCache(String cacheName, Serializable key) throws PortalException {
      logger.debug("Liferay Cache: Removing from cache. CacheName = " + cacheName + ", Key = " + key);
      PortalCache<Serializable, Serializable> cache = MultiVMPoolUtil.getPortalCache(cacheName);
      cache.remove(key);

   }

   public void clearCache(String cacheName) throws PortalException {
      logger.debug("Liferay Cache: Clearing cache. CacheName = " + cacheName);
      PortalCache<Serializable, Serializable> cache = MultiVMPoolUtil.getPortalCache(cacheName);
      cache.removeAll();
   }

   @Override
   public void closeCachePool() {
      MultiVMPoolUtil.clear();
   }

   @Override
   public void ping() throws PortalException {
      // DO NOTHING.
      
   }

}

4.4 Creating Redis implementation of the cache.

Now we will create the second implementation of the CacheManager, using Jedis java implementation for Redis. We will create a Jedis Connection pool and use it to work with Redis cache.

The interface is added to the cache-wrapper-api module, but the implementation classes are added to the cache-wrapper-service module.

Add this implementation class to module cache-wrapper -> cache-wrapper-service -> src/main/java -> com.liferay.cache.service.impl

We will make use of Redis HASH to store the objects.
The cache name will be used as key, and object Identifier will the the hash field.
e.g consider following example of what the Redis hash key and field would be for different type of objects.

 Object Type  Object Id Redis Hash key   Redis Hash Field
 user 1000 users 1000
 user 1001 users 1001
 document 1000  docs  1000

and so on..

The reason we have used Redis hash is to group similar type of objects in a common hash, so that if required we can clear one particular hash easily without impacting the full cache.

e.g. after large scale data import of users, we can directly clear out the users hash, to clear the cache of all users.

If we had stored each object as individual key, then to clear all users objects would had been a very difficult job.

public class RedisCacheManagerImpl implements CacheManager {
   
   private static final Log logger = LogFactoryUtil.getLog(CacheLocalService.class);

   //Injecting the Jedis Connection Pool Spring bean.
   //Autowire does not work here but we will use Liferay's @BeanReference to inject the bean here.
   //The bean definition will be defined later in Spring bean xml.
   @BeanReference(name = "jedisPool")
   private JedisPool jedisPool;
   
   @Override
   public void addToCache(String cacheName, Serializable key, Serializable value, int ttl) throws PortalException {
      logger.debug("Redis Cache: Adding to cache. CacheName = " + cacheName + ", Key = " + key + ", TTL : " + ttl);
      Jedis jedis = getResource();
      try {
         jedis.hset(serialize(cacheName), serialize(key), serialize(value));
      } catch (Exception ex) {
         logger.error("Redis Cache: Error adding object to cache. CacheName = " + cacheName + ", Key = " + key);
      }finally {
         jedis.close();
      }
   }

   @Override
   public Serializable getFromCache(String cacheName, Serializable key) throws PortalException {
      logger.debug("Redis Cache: Fetching from cache. CacheName = " + cacheName + ", Key = " + key);
      Jedis jedis = getResource();
      try {
         byte[] result = jedis.hget(serialize(cacheName), serialize(key));
         return Optional.ofNullable(result).map(res -> deserialize(res)).orElse(null);
      } finally {
         jedis.close();
      }
   }

   public void removeFromCache(String cacheName, Serializable key) throws PortalException {
      logger.debug("Redis Cache: Removing from cache. CacheName = " + cacheName + ", Key = " + key);
      Jedis jedis = getResource();
      try {
         jedis.hdel(serialize(cacheName), serialize(key));
      } finally {
         jedis.close();
      }
   }

   public void clearCache(String cacheName) throws PortalException {
      logger.debug("Redis Cache: Clearing cache. CacheName = " + cacheName);
      Jedis jedis = getResource();
      try {
         jedis.del(serialize(cacheName));
      } finally {
         jedis.close();
      }
   }
   
   
   private Jedis getResource() throws PortalException {
      if(Validator.isNull(jedisPool)) {
         logger.error("Redis connection pool is not yet configured.");
         throw new PortalException("Redis connection pool is not yet configured.");
      }
//    logger.debug("Redis Cache: Active: " + jedisPool.getNumActive() + ", Idle: " +
//          jedisPool.getNumIdle() + ", Waiting: " + jedisPool.getNumWaiters());      
      return jedisPool.getResource();
      
   }
   
   //Serializes an Object to byte array
   private byte[] serialize(Serializable obj) {
       return SerializationUtils.serialize(obj);
   }

   //De-serialize a byte array back to Object
   private Serializable deserialize(byte[] bytes) {
       return (Serializable)SerializationUtils.deserialize(bytes);
   }
   
   // Bean Destruction method that will be called on container shutdown.
   // Configuration for this will be done in spring xml file
   public void destroy() {
      if(Validator.isNotNull(jedisPool)) {
         logger.debug("Destroying Jedis Connection Pool");
         jedisPool.destroy();
      }
   }

   @Override
   public void closeCachePool() {
      destroy();    
   }

   @Override
   public void ping() throws PortalException {
      try {
         Jedis jedis = jedisPool.getResource();
         jedis.ping();
         jedis.close();
      }catch(Exception ex) {
         logger.error("Redis Cache : Error pinging redis server. Error = " + ex.getMessage());
         throw new PortalException(ex);
      }
      
   }

}

 

4.5 CacheManager bean creation factory-method

Now we need to a factory method to either associate the Liferay cache implementation or the Redis cache implementation, based on configuration.

The create method is the below example is a static method that returns a CacheManager, and will be used in spring xml file to create the CacheManager at runtime.

Add this class to module cache-wrapper -> cache-wrapper-service -> src/main/java -> com.liferay.cache.service.impl

We also have added a fallback, that in case Redis cache manager’s connection ping fails, system will fallback to Liferay cache manager implementation.

public class CacheManagerFactoryBean{
   
   private static final Log logger = LogFactoryUtil.getLog(CacheLocalService.class);

   public static CacheManager create(boolean useRedis, 
         CacheManager liferayCacheManager, CacheManager redisCacheManager) throws Exception {
      if(useRedis) {
         if(Validator.isNotNull(redisCacheManager)) {
            //Check if redis connection is available
            try {
               redisCacheManager.ping();
            
               logger.debug("Using Redis cache manager");
               return redisCacheManager;
            }catch(PortalException ex) {
               logger.error("RedisCacheManager not initialized... Falling back on liferay cache manager");
            }
         }
      }
      logger.debug("Using Liferay cache manager");
      return liferayCacheManager;
   }

}

 

4.6 Adding code to CacheLocalServiceImpl

First of all let’s create following functional interface.
Add this interface in cache-wrapper -> cache-wrapper-api -> src/main/java -> com.liferay.cache.service

@FunctionalInterface
public interface PortalFunction<K extends Serializable, V extends Serializable> {

   /**
    * Executes the function on the given argument
    * @param key, argument to the function
    * @return the function result V
    * @throws PortalException
    */
    V execute(K key) throws PortalException;
}

 

Now we will add implementation for CacheLocalServiceImpl.

  • CacheLocalService is the actual class that is exposed to clients for caching APIs.
  • The CacheManager APIs are not exposed to the clients but are internally used by the CacheLocalService
  • The clients need to pass a PortalFunction, to the CacheLocalService Get API.
    • These APIs are wrapper API on top of CacheManager that we have already implemented.
    • It will try to get data from the cache first.
    • But if no data found from cache, it will execute the PortalFunction to fetch the data (most probably PortalFunction will fetch from database).
    • The data fetched from PortalFunction execution will be then put back in the cache.

Remove all the dummy code we had added earlier, and add following code to it.

@ProviderType
public class CacheLocalServiceImpl extends CacheLocalServiceBaseImpl {
   /*
    * NOTE FOR DEVELOPERS:
    *
    * Never reference this class directly. Always use {@link com.adp.gckb.cache.service.CacheLocalServiceUtil} to access the cache local service.
    */
   
   private static final Log logger = LogFactoryUtil.getLog(CacheLocalService.class);
   
   //This property will be set by Spring dependency injection
   private int timeToLive;

   //This property will be set by Spring dependency injection, using the factory method we created earlier
   private CacheManager cacheManager;
   
   public Serializable getFromCache(String cacheName, Serializable key, PortalFunction<Serializable,Serializable> databaseFetchFunction) throws PortalException {
      if(Validator.isNull(cacheManager)) {
         logger.debug("CacheName = " + cacheName + ", key = " + key + 
               " : Cache Manager is not initialized. So fetching from database");
         return databaseFetchFunction.execute(key);
      }
      
      Serializable cachedObject = null;
      try {
         cachedObject = cacheManager.getFromCache(cacheName, key);
      }catch (Exception ex) {
         logger.debug("CacheName = " + cacheName + ", key = " + key + 
               " : Error getting object from cache. Error = " + ex.getMessage());
      }
      if(Validator.isNull(cachedObject)) {
         logger.debug("CacheName = " + cacheName + ", key = " + key + 
               " : Object cannot be fetched from cache. So retrieving from DB and putting it back in cache.");
         cachedObject = databaseFetchFunction.execute(key);
         addToCache(cacheName, key, cachedObject);
         
      }
      return cachedObject;
   }
   
   
   public void addToCache(String cacheName, Serializable key, Serializable value) {
      try {
         cacheManager.addToCache(cacheName, key, value, timeToLive);
      }catch (Exception ex) {
         logger.debug("CacheName = " + cacheName + ", key = " + key + 
               " : Error adding object to cache. Error = " + ex.getMessage());
      }
   }
   
   
   public void removeFromCache(String cacheName, Serializable key) {
      if(Validator.isNotNull(cacheManager)) {
         try{
            cacheManager.removeFromCache(cacheName, key);
         } catch (Exception ex) {
            logger.debug("CacheName = " + cacheName + ", key = " + key + 
                  " : Error removing key from cache. Error = " + ex.getMessage());
         }
      }     
   }

   public void clearCache(String cacheName) {
      if(Validator.isNotNull(cacheManager)) {
         try{
            cacheManager.clearCache(cacheName);
         } catch (Exception ex) {
            logger.debug("CacheName = " + cacheName +   
                  " : Error clearing the cache. Error = " + ex.getMessage());
         }
      }     
   }


   public void setCacheManager(CacheManager cacheManager) {
      this.cacheManager = cacheManager;
   }


   public void setTimeToLive(int timeToLive) {
      this.timeToLive = timeToLive;
   }

}

 

Build the Service.

4.7 Creating Spring bean entries

Go to following file cache-wrapper/cache-wrapper-service/src/main/resources/META-INF/spring/module-spring.xml

And add following spring bean definition

<?xml version="1.0"?>

<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" default-destroy-method="destroy" default-init-method="afterPropertiesSet" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

   <!-- Jedis Pool Configuration. Properties read from portal-ext.properties -->
   <bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig">
      <property name="maxTotal" value="#{T(com.liferay.portal.kernel.util.PropsUtil).get('cache.redis.connections.max')}"/>
      <property name="maxIdle" value="#{T(com.liferay.portal.kernel.util.PropsUtil).get('cache.redis.connections.maxIdle')}"/>
      <property name="minIdle" value="#{T(com.liferay.portal.kernel.util.PropsUtil).get('cache.redis.connections.minIdle')}"/>
   </bean>

   <!-- Jedis Pool bean definition. Properties read from portal-ext.properties -->
   <!-- Pass the reference the Jedis Pool configuration bean created above -->
   <bean id="jedisPool" class="redis.clients.jedis.JedisPool">
      <constructor-arg ref="jedisPoolConfig"/>
      <constructor-arg type="java.lang.String" value="#{T(com.liferay.portal.kernel.util.PropsUtil).get('cache.redis.hostname')}" />
      <constructor-arg type="int" value="#{T(com.liferay.portal.kernel.util.PropsUtil).get('cache.redis.port')}"/>
      <constructor-arg type="int" value="#{T(com.liferay.portal.kernel.util.PropsUtil).get('cache.redis.connections.timeout.millis')}"/>
      <constructor-arg type="boolean" value="#{T(com.liferay.portal.kernel.util.PropsUtil).get('cache.redis.usessl')}"/>
   </bean>    
   
   <!-- Create the bean for Redis Cache Manager implemenation -->
   <bean id="redisCacheManager" class="com.liferay.cache.service.impl.RedisCacheManagerImpl"/>

   <!-- Create the bean for Liferay Cache Manager implemenation -->
   <bean id="liferayCacheManager" class="com.liferay.cache.service.impl.LiferayCacheManagerImpl"/>

   
   <!-- Returns the Cache Manager Bean, based on configuration property -->
   <!-- Used the CacheManagerFactoryBean's create factory method to create the bean -->
   <bean id="cacheManager" class="com.liferay.cache.service.impl.CacheManagerFactoryBean" factory-method="create">
      <constructor-arg type="boolean" value="#{T(com.liferay.portal.kernel.util.PropsUtil).get('cache.use.redis')}"/>       
      <constructor-arg ref="liferayCacheManager" />
      <constructor-arg ref="redisCacheManager" />
   </bean> 
    
   
   <!-- Create the CacheLocalService bean and pass the reference to the CacheManager bean -->
   <bean class="com.liferay.cache.service.impl.CacheLocalServiceImpl" 
        id="com.liferay.cache.service.CacheLocalService">
      <property name="cacheManager" ref="cacheManager"/>
      <property name="timeToLive" value="#{T(com.liferay.portal.kernel.util.PropsUtil).get('cache.default.ttl.seconds')}"/>
   </bean>
   
</beans>

Now build all the  modules and deploy in the application.
The cache implementation is ready to be used.

 

4.8 Using Cache Implementation

Modify the following configuration in portal-ext.properties, to decide whether to use Redis cache or liferay cache implementation

cache.use.redis=true

 

In your client modules, add the reference to the CacheLocalService.

@Reference
private CacheLocalService cacheLocalService;

 

Then you can call all the methods of CacheLocalService directly in your client code.

 

 

 


With this we conclude our current article.
In this article we have learned how to create a cache manager implemenation for liferay and use either liferay’s default cache implementation or integrate Redis cache with liferay.