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.
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:
A general algorithm for caching a file must account for file modifications and consists of the following steps:
The activity diagram below provides a visual representation of the algorithm:
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:
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.
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.org/schema/configuration" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.cacheonix.org/schema/configuration http://www.cacheonix.org/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.
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.
Cacheonix is an Open Source Java project that offers a fast local cache and a strictly-consistent distrbuted cache. To add Cacheonix to your Maven project, add the following to the dependencies section of your pom.xml:
<dependency> <groupId>org.cacheonix</groupId> <artifactId>cacheonix-core</artifactId> <version>2.3.1</version> <dependency>