- How to Cache a File in Java
- Caching Files in Java
- Algorithm for Caching Files
- Class Diagram
- Data Structure for Cached File
- Algorithm Implementation
- Cache API
- Java Code for Caching Files
- Adding Java Cache Cacheonix to Your Application
- Next
- Кеширование данных — Java Spring
- Кеш в Spring
- Кеш в Oracle PL-SQL функции
How to Cache a File in Java
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:
- An algorithm for caching the file
- A data structure for holding the cached content
- 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:
- Get a value from the cache using a fully qualified file path as a key.
- If a key is not found, read the file content and put it to the cache.
- If the key is found, check if a timestamp of the cached content matches the file timestamp.
- If the timestamps are equal, return the cached content.
- 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:
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:
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 . * * @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'; > >
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:
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(); Cachecache = 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; > >
Adding Java Cache Cacheonix to Your Application
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:
Next
Кеширование данных — Java Spring
Многократно вычитывая одни и те же данные, встает вопрос оптимизации, данные не меняются или редко меняются, это различные справочники и др. информация, т.е. функция получения данных по ключу — детерминирована. Тут наверно все понимают — нужен Кеш! Зачем всякий раз повторно выполнять поиск данных или вычисление?
Так вот здесь я покажу как делать кеш в Java Spring и поскольку это тесно связанно скорее всего с Базой данных, то и как сделать это в СУБД на примере одной конкретной.
Кеш в Spring
Далее все поступают примерно одинаково, в Java используют различные HasMap, ConcurrentMap и др. В Spring тоже для это есть решение, простое, удобное, эффективное. Я думаю что в большинстве случаев это поможет в решении задачи. И так, все что нужно, это включить кеш и аннотировать функцию.
@SpringBootApplication @EnableCaching public class DemoCacheAbleApplication < public static void main(String[] args) < SpringApplication.run(DemoCacheAbleApplication.class, args); >>
Кешируем данные поиска функции
@Cacheable(cacheNames="person") public Person findCacheByName(String name) < //. >
В аннотации указывается название кеша и есть еще другие параметры. Работает как и ожидается так, первый раз код выполняется, результат поиска помещается в кеш по ключу (в данном случае name) и последующие вызовы код уже не выполняется, а данные извлекаются из кеша.
Пример реализации репозитория «Person» с использованием кеша
@Component public class PersonRepository < private static final Logger logger = LoggerFactory.getLogger(PersonRepository.class); private Listpersons = new ArrayList<>(); public void initPersons(List persons) < this.persons.addAll(persons); >private Person findByName(String name) < Person person = persons.stream() .filter(p ->p.getName().equals(name)) .findFirst() .orElse(null); return person; > @Cacheable(cacheNames="person") public Person findCacheByName(String name) < logger.info("find person . " + name); final Person person = findByName(name); return person; >>
@RunWith(SpringRunner.class) @SpringBootTest public class DemoCacheAbleApplicationTests < private static final Logger logger = LoggerFactory.getLogger(DemoCacheAbleApplicationTests.class); @Autowired private PersonRepository personRepository; @Before public void before() < personRepository.initPersons(Arrays.asList(new Person("Иван", 22), new Person("Сергей", 34), new Person("Игорь", 41))); >private Person findCacheByName(String name) < logger.info("begin find " + name); final Person person = personRepository.findCacheByName(name); logger.info("find result = " + person.toString()); return person; >@Test public void findByName() < findCacheByName("Иван"); findCacheByName("Иван"); >>
@Test public void findByName()
, первый раз происходит вызов, поиск, в второй раз результат берется уже из кеша. Это видно в консоли
Удобно, можно точечно оптимизировать существующий функционал. Если в функции более одного аргумента, то можно указать имя параметра, какой использовать в качестве ключа.
@Cacheable(cacheNames="person", key="#name") public Person findByKeyField(String name, Integer age)
Есть и более сложные схемы получения ключа, это в документации.
Но конечно встанет вопрос, как обновить данные в кеше? Для этой цели есть две аннотации.
Функция с этой аннотацией будет всегда вызывать код, а результат помещать в кеш, таким образом она сможет обновить кеш.
Добавлю в репозиторий два метода: удаления и добавления Person
public boolean delete(String name) < final Person person = findByName(name); return persons.remove(person); >public boolean add(Person person)
Выполню поиск Person, удалю, добавлю, опять поиск, но по прежнему буду получать одно и тоже лицо из кеша, пока не вызову «findByNameAndPut»
@CachePut(cacheNames="person") public Person findByNameAndPut(String name)
@Test public void findCacheByNameAndPut()
Другая аннотация это @CacheEvict
Позволяет не просто посещать хранилище кеша, но и выселять. Этот процесс полезен для удаления устаревших или неиспользуемых данных из кеша.
По умолчанию Spring для кеша использует — ConcurrentMapCache, если есть свой отличный класс для организации кеша, то это возможно указать в CacheManager
@SpringBootApplication @EnableCaching public class DemoCacheAbleApplication < public static void main(String[] args) < SpringApplication.run(DemoCacheAbleApplication.class, args); >@Bean public CacheManager cacheManager() < SimpleCacheManager cacheManager = new SimpleCacheManager(); cacheManager.setCaches(Arrays.asList( new ConcurrentMapCache("person"), new ConcurrentMapCache("addresses"))); return cacheManager; >>
Там же указываются имена кешей, их может быть несколько. В xml конфигурации это указывается так:
public class Person < private String name; private Integer age; public Person(String name, Integer age) < this.name = name; this.age = age; >public String getName() < return name; >public Integer getAge() < return age; >@Override public String toString()
4.0.0 com.example demoCacheAble 0.0.1-SNAPSHOT jar DemoCacheAble Demo project for Spring Boot org.springframework.boot spring-boot-starter-parent 2.0.6.RELEASE UTF-8 UTF-8 1.8 org.springframework.boot spring-boot-starter-cache org.springframework.boot spring-boot-starter-test test org.springframework.boot spring-boot-maven-plugin
Кеш в Oracle PL-SQL функции
Ну и в конце, тем кто не пренебрегает мощностью СУБД, а использует ее, могут использовать кеширование на уровне БД, в дополнение или как альтернативу. Так например в Oracle не менее элегантно можно превратить обычную функцию, в функцию с кешированием результата, добавив к ней
CREATE OR REPLACE FUNCTION GET_COUNTRY_NAME(P_CODE IN VARCHAR2) RETURN VARCHAR2 RESULT_CACHE IS CODE_RESULT VARCHAR2(50); BEGIN SELECT COUNTRY_NAME INTO CODE_RESULT FROM COUNTRIES WHERE COUNTRY_ID = P_CODE; -- имитация долгой работы dbms_lock.sleep (1); RETURN(CODE_RESULT); END;