Blog Layout

Caching in XPages - not as straightforward as you would believe

John Dalsgaard • sep. 09, 2014

Well, you may know that I am a fan of MVC (Model-View-Controller) pattern (see more here).


One tempting feature of using an MVC pattern is that it is pretty straightforward to implement a caching mechanism. This can easily be done in the facade layer of the model. And just imagine what happens if all of the data is ready in memory - and you only need to go to the disk/database when some of the data is updated. I can assure you that this means SPEED....


But as with so many things - more wants more. Wouldn't it be nice to preload all data on start of the HTTP server? Then you would avoid the first user hitting the delay of loading data from the database - it will already be ready in memory.


This article is about my experiences with trying to obtain this. And I must warn you - I have not yet found the "silver bullet". But I have discovered a number of mechanisms that may help others understand the underlying features of XPages.


My custom cache - take 1


Ok, how does one create a cache? Basically, you need a way to reference the same memory - no matter from where you do it. I started out using a managed bean in the application scope. As long as the application scope "lives" the data is cached in memory.


I do this by having a "DataBean" - set up as a managed bean in faces-config.xml:


 <managed-bean>

    <managed-bean-name>Data</managed-bean-name>

    <managed-bean-class>dk.dalsgaarddata.demo.bean.DataBean</managed-bean-class>

    <managed-bean-scope>application</managed-bean-scope>

  </managed-bean>


And here is a snippet of the code. Since it is a managed bean it implements Serializable and you can see that I use lazy initialization for setting the facade bean:


public class DataBean implements Serializable {

  private static final long serialVersionUID = 1L;

  private PersonCRUDFacade personFacade = null;

  public PersonCRUDFacade getPersonFacade() {

    if (null == personFacade) {

      personFacade = new PersonCRUDFacade();

    }

    return personFacade;

  }}


In the facade bean I have implemented the cache as a HashMap - with the key of the data as key of the entry in the HashMap. Here is a snippet from the facade bean:


public class PersonCRUDFacade implements Serializable {

   private static final long serialVersionUID = 1L;

   private Map<String, Person> persons = null;

   private PersonDAO getDao() {

     return new DominoPersonDAO();  // Not serialized - handles access to the Domino database

  }

   private Map<String, Person> getCachedPersons() {

     if (null == persons) {

       System.out.println("read persons into cache...");

       persons = getDao().getAllPersons();   // With key also as key to the hash map

     }

     return persons;

   }

 

   public List<Person> getAllPersons() {

     return sortByName(getCachedPersons());

   }

    public Person findPerson(String key) {

     return getCachedPersons().get(key);

   }

   :

   :

 }


The above are just small snippets. I have left out some of the details (like reading in the cache as Anonymous - but using sessionAsSigner). But hopefully you get the idea.


So the next thing I did was to create an XPage (preload.xsp) that basically reads all the data that I want to cache (by referencing the various facade beans through Expression Language - and simply just returning the size of the map). And then I added the following to notes.ini (through a configuration document, of course):


XPagesPreload=1

XPagesPreloadDB=demo/mvc.nsf/preload.xsp


Restart the server, and looking at the console we can see that data is loaded into the cache. Great! But when we open the application - we can see that the data is loaded into cache again.... What is happening???


My custom cache - take 2


My first thought was that for some reason the application scope was not the right place to put the cache - since it somehow was cleared after preload and before I opened the application. My next thought therefore was to create a true singleton object (using an enum which seems to be best practice) and cache the facade beans in there. And a small snippet to give you the idea:


public enum CacheContainer {

  INSTANCE;

  private PersonCRUDFacade personFacade = new PersonCRUDFacade();

  private CacheContainer() {

    System.out.println("Instantiating enum CacheContainer...");

  }

  public PersonCRUDFacade getPersonFacade() {

    if (null == personFacade) {

      System.out.println("Create personFacade...");

      personFacade = new PersonCRUDFacade();

    }

    return personFacade;

  }

  :

  :

}


In our DataBean we can use the singleton object this way:


public PersonCRUDFacade getPersonFacade() {

 return CacheContainer.INSTANCE.getPersonFacade();

}


Back to testing with preload - ... and the same result...! The cache is re-read after preload when the application is accessed afterwards.


My custom cache - take 3


Ok, so how can we then create a "true" caching mechanism? Well, the ServerBean (part of the OpenNTF Domino API) comes to our rescue. This is the true server side caching mechanism. It implements ConcurrentHashMap and as such is ideal for the purpose. So off I went implementing the ServerBean approach. This is a snippet from the DataBean class:


public PersonCRUDFacade getPersonFacade() {

  PersonCRUDFacade personFacade = (PersonCRUDFacade) ServerBean.getCurrentInstance().get(PersonCRUDFacade.class.getName());

  if (null == personFacade) {

    personFacade = new PersonCRUDFacade();

    ServerBean.getCurrentInstance().put(PersonCRUDFacade.class.getName(), personFacade);

  }

  return personFacade;

}


So back to testing the preload etc. And now we get an error - which seems a little strange when you look at it:


javax.faces.el.EvaluationException: Error getting property 'persons' from bean of type dk.dalsgaarddata.demo.bean.DataBean: java.lang.ClassCastException: dk.dalsgaarddata.demo.dao.facade.PersonCRUDFacade incompatible with dk.dalsgaarddata.demo.dao.facade.PersonCRUDFacade


But what does that mean??? That seems to say that the same class is not compatible with itself...???? What is going on????


The explanation


Following the above error I tried to put a String with a timestamp into the cache - and retrieve that at the same time as I tried to retrieve the PersonCRUDFacade instance. And guess what - the String object was cached quite all right. This led to the explanation of what was going on.


First, the singleton approach would probably have worked quite as good as the ServerBean approach. The problem lies in classloaders....


This has been pointed out by Nathan Freeman and Philip Riand - both very competent and way above my level of knowledge on XPages.


So when you preload an XPage it is done in one classloader. When you open an XPage in application it is done in its own classloader.... i.e. a DIFFERENT classloader - and they cannot share the same objects.... And what happens if the application scope times out? Well, the classloader unloads all classes and start from scratch when a new user hits the application - and in our case that means they have to re-read data into the cache.... (well with the ServerBean implementation we would get the ClassX is incompatible with ClassX error)


Sigh....


What can you do??


I am currently working with this approach:


  • Do not try to preload a specific XPage - it won't help you
  • Call the application via a URL that will preload the data, e.g. as mention via a special preload XPage. I do this from a DDM probe - which checks that the application (and server) is alive - I even check every 15 minutes which is shorter than my application timeout, so my data is cached while the server is running....


This is not ideal - but it works. The only other solution would be to have the class (facade bean) outside of the NSF. You could put it in a JAR and wrap it up as an OSGi plugin (see the steps here) - but that makes deployment a totally different game - and you may not want to do that.


I hope to have saved you some of the hassle that I have been through by sharing the above information.


Happy coding!

Blog

Af John Dalsgaard 17 Mar, 2023
Dalsgaard Data A/S celebrates 25 years!
25 års jubilæum
Af John Dalsgaard 17 Mar, 2023
Dalsgaard Data A/S fylder 25 år!
Flere indlæg
Share by: