How to Cache a File in Java

Introduction

This article shows how to cache files in Java in order to increase application performance. It discusses an algorithm for caching the file, a data structure for holding the cached content and a cache API for storing cached files.

Caching Files in Java

Reading files from the disk can be slow, especially when an application reads the same file many times. Caching solves this problem by keeping frequently accessed files in memory. This allows the application to read the content of the from the fast local memory instead of the slow hard drive. Design for caching a file in Java includes three elements:

  1. An algorithm for caching the file
  2. A data structure for holding the cached content
  3. A cache API for storing cached files

Algorithm for Caching Files

A general algorithm for caching a file must account for file modifications and consists of the following steps:

  1. Get a value from the cache using a fully qualified file path as a key.
  2. If a key is not found, read the file content and put it to the cache.
  3. If the key is found, check if a timestamp of the cached content matches the file timestamp.
  4. If the timestamps are equal, return the cached content.
  5. If the timestamps are not equal, refresh the cache by reading the file and putting it into the cache.

The activity diagram below provides a visual representation of the algorithm:

Caching Files in Java Activity Diagram
Figure 1. Algorithm for caching files.

Class Diagram

The complete class diagram for the application-level file cache consists of an application, a cache and an object that holds the content of the cached file:

Caching Files in Java Class Diagram
Figure 2. Class diagram for caching files

Data Structure for Cached File

The class that is responsible for holding the content of the cached text file, TextFile, has two fields, the content itself and the timestamp required to support cache invalidation:

/**
 * A value object containing a cached file and file attributes.
 */
public final class TextFile implements Externalizable {

   /**
    * A time when the file was modified last time.
    */
   private long lastModified;

   /**
    * A content of the text file.
    */
   private String content;


   public TextFile() {

   }


   /**
    * Returns file's last modification time stamp.
    *
    * @return file's last modification time stamp.
    */
   public long getLastModified() {

      return lastModified;
   }


   /**
    * Returns file's content.
    *
    * @return file's content.
    */
   public String getContent() {

      return content;
   }


   /**
    * Sets the content of the file.
    *
    * @param content file's content.
    */
   public void setContent(final String content) {

      this.content = content;
   }


   /**
    * Sets file's last modification time stamp.
    *
    * @param lastModified file's last modification time stamp.
    */
   public void setLastModified(final long lastModified) {

      this.lastModified = lastModified;
   }


   /**
    * Saves this object's content by calling the methods of DataOutput.
    *
    * @param out the stream to write the object to
    * @throws IOException Includes any I/O exceptions that may occur
    */
   public void writeExternal(final ObjectOutput out) throws IOException {

      out.writeLong(lastModified);
      out.writeUTF(content);
   }


   /**
    * Restore this object contents by calling the methods of DataInput.  The readExternal method must 
    * read the values in the same sequence and with the same types as were written 
    * by {@link #writeExternal(ObjectOutput)} .
    *
    * @param in the stream to read data from in order to restore the object
    * @throws IOException if I/O errors occur
    */
   public void readExternal(final ObjectInput in) throws IOException {

      lastModified = in.readLong();
      content = in.readUTF();
   }


   public String toString() {

      return "TextFile{" +
              "lastModified=" + lastModified +
              ", content='" + content + '\'' +
              '}';
   }
}
Figure 3. Class for holding cached file content.

Algorithm Implementation

Cache API

A cache is a data structure that holds frequently accessed data in memory. A cache API usually has a java.util.Map interface that allows developers to store and retrieve values by a key.

This article uses open source cache API Cacheonix as a cache to implement the algorithm for caching files. A Cacheonix configuration below defines an LRU cache that can hold up to 2000 files, with maximum total size of cached files being 256 megabytes:

<?xml version ="1.0"?>
<cacheonix xmlns="http://www.cacheonix.com/schema/configuration" 
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xsi:schemaLocation="http://www.cacheonix.com/schema/configuration 
           http://www.cacheonix.com/schema/cacheonix-config-2.0.xsd">
   <local>
      <localCache name="application.file.cache">
         <store>
            <lru maxElements="2000" maxBytes="256mb"/>
         </store>
      </localCache>
   </local>
</cacheonix>
Figure 4. Cache configuration.

Java Code for Caching Files

This article assumes that the file being cached is a text file. A code fragment below implements the caching algorithm for text files:

import java.io.File;
import java.io.FileReader;
import java.io.IOException;

import cacheonix.Cacheonix;
import cacheonix.cache.Cache;
import com.cacheonix.examples.cache.file.cache.data.grid.TextFile;

/**
 * An application demonstrating an application-level file cache.
 */
public class ApplicationFileCacheExample {


   /**
    * Gets a file content from the cache ant prints it in the standard output.
    *
    * @param args arguments
    * @throws IOException if an I/O error occured.
    */
   public static void main(String[] args) throws IOException {

      // Replace the file name with an actual file name
      String pathName = "test.file.txt";

      ApplicationFileCacheExample fileCacheExample = new ApplicationFileCacheExample();
      String fileFromCache = fileCacheExample.getFileFromCache(pathName);
      System.out.print("File content from cache: " + fileFromCache);
   }


   /**
    * Retrieves a file from a cache. Puts it into the cache if it's not cached yet.
    * 
    * This method demonstrates a typical flow an application must follow to cache a file 
    * and to get it from the cache. As you can see, the application is pretty involved 
    * in maintaining the cache. It must read the file, check the the timestamps and update 
    * the cache if its content is stale.
    *
    * @param pathName a file path name.
    * @return a cached file content or null if file not found
    * @throws IOException if an I/O error occurred.
    */
   public String getFileFromCache(String pathName) throws IOException {

      // Get cache
      Cacheonix cacheonix = Cacheonix.getInstance();

      Cache<String, TextFile> cache = cacheonix.getCache("application.file.cache");

      // Check if file exists
      File file = new File(pathName);
      if (!file.exists()) {

         // Invalidate cache
         cache.remove(pathName);

         // Return null (not found)
         return null;
      }

      // Get the file from the cache
      TextFile textFile = cache.get(pathName);

      // Check if the cached file exists
      if (textFile == null) {

         // Not found in the cache, put in the cache

         textFile = readFile(file);

         cache.put(pathName, textFile);
      } else {

         // Found in cache, check the modification time stamp

         if (textFile.getLastModified() != file.lastModified()) {

            // Update cache

            textFile = readFile(file);

            cache.put(pathName, textFile);
         }
      }

      return textFile.getContent();
   }


   /**
    * Reads a file into a new TextFile object.
    *
    * @param file the file to read from.
    * @return a new TextFile object.
    * @throws IOException if an I/O error occurred.
    */
   private static TextFile readFile(File file) throws IOException {

      // Read the file content into a StringBuilder
      char[] buffer = new char[1000];
      FileReader fileReader = new FileReader(file);
      StringBuilder fileContent = new StringBuilder((int) file.length());

      for (int bytesRead = fileReader.read(buffer); bytesRead != -1; ) {
         fileContent.append(buffer, 0, bytesRead);
      }

      // Close the reader
      fileReader.close();

      // Create CachedTextFile object
      TextFile textFile = new TextFile();
      textFile.setContent(fileContent.toString());
      textFile.setLastModified(file.lastModified());

      // Return the result
      return textFile;
   }
}
 
Figure 5. Algorithm implementation.

Next

Share This Article