(1) Author: Eric Liu Tags: [] Categories:

  • hexo

Caching is a very common way to improve performance in the real world, and we will use caching in many scenarios.

This article begins with a simple example that shows the power of Spring Cache by comparing our original custom cache with Spring’s annotation-based cache configuration approach, and then introduces its basic principles, extension points, and usage scenario limitations. By reading this article, you should be able to quickly grasp the powerful caching technology that Spring brings, providing caching capabilities for existing code with minimal configuration.

An overview of the

Spring 3.1 introduces exciting annotation-based caching technology, which is nota concrete caching implementation (such as EHCache or OSCache) per se, but rather an abstraction of the use of caching, By adding a few of the various annotations it defines to the existing code, you can achieve the effect of caching the method’s return object.

Spring’s caching technology is also quite flexible. It can not only use SpEL (Spring Expression Language) to define the cache key and various conditions, but also provide a temporary storage solution for the cache out of the box. Integration with major professional caches such as EHCache is also supported.

Its characteristics are summarized as follows:

  • Existing code can be cache-enabled by configuring a few annotation annotations
  • Out-of-the-box support, which means you can use The cache without installing and deploying additional third-party components
  • Support for Spring Express Language allows you to define cached keys and conditions using any property or method of the object
  • Support for AspectJ and caching support for any method through it
  • Support custom key and custom cache manager, with considerable flexibility and scalability

In this article, we will give a detailed introduction to Spring Cache, starting with a simple example and principles. We will then take a look at a practical caching example, and finally introduce the limitations and considerations of Using Spring Cache. All right, let’s get started

How did we implement caching ourselves before

Here’s a completely custom cache implementation that doesn’t require any third party components to implement an in-memory cache of an object.

The scenario is as follows:

The account name is key and the account object is value. When an account is queried using the same account name, the result is directly returned from the cache. Otherwise, the cache is updated. The account query service also supports reload caching.

Start by defining an entity class: an account class with basic ID and name attributes and getter and setter methods

    public class Account {  
        private int id;  
        private String name;  
        public Account(String name) {  
            this.name = name;  
        }  
        public int getId() {  
            return id;  
        }  
        public void setId(int id) {  
            this.id = id;  
        }  
        public String getName() {  
            return name;  
        }  
        public void setName(String name) { this.name = name; }}Copy the code

Then define a cache manager that implements the cache logic, supports object addition, modification, and deletion, and supports value object generics. As follows:

    import com.google.common.collect.Maps;  
    import java.util.Map;  

    public class CacheContext<T> {  
        private Map<String, T> cache = Maps.newConcurrentMap();  
        public T get(String key){  
            returncache.get(key); } public void addOrUpdateCache(String key,T value) { cache.put(key, value); Public void evictCache(String key) {public void evictCache(String key) {if(cache.containsKey(key)) { cache.remove(key); }} // Clear all records in the cache public voidevictCache() { cache.clear(); }}Copy the code

Ok, now that we have the entity class and a cache manager, we also need a service class that provides account query, which uses the cache manager to support account query caching, as follows:

    import com.google.common.base.Optional;  
    import org.slf4j.Logger;  
    import org.slf4j.LoggerFactory;  
    import org.springframework.stereotype.Service;  
    import javax.annotation.Resource;  

    @Service  
    public class AccountService1 {  
        private final Logger logger = LoggerFactory.getLogger(AccountService1.class);  
        @Resource  
        private CacheContext<Account> accountCacheContext;  
        public Account getAccountByName(String accountName) {  
            Account result = accountCacheContext.get(accountName);  
            if(result ! = null) { logger.info("get from cache... {}", accountName);  
                return result;  
            }  
            Optional<Account> accountOptional = getFromDB(accountName);  
            if(! accountOptional.isPresent()) { throw new IllegalStateException(String.format("can not find account by account name : [%s]", accountName));  
            }  
            Account account = accountOptional.get();  
            accountCacheContext.addOrUpdateCache(accountName, account);  
            return account;  
        }  
        public void reload() {  
            accountCacheContext.evictCache();  
        }  
        private Optional<Account> getFromDB(String accountName) {  
            logger.info("real querying db... {}", accountName);  
            //Todo query data from database  
            returnOptional.fromNullable(new Account(accountName)); }}Copy the code

Now let’s start writing a test class that tests whether the cache we just created is valid

    import org.junit.Before;  
    import org.junit.Test;  
    import org.slf4j.Logger;  
    import org.slf4j.LoggerFactory;  
    import org.springframework.context.support.ClassPathXmlApplicationContext;  
    import static org.junit.Assert.*;  
    public class AccountService1Test {  
        private AccountService1 accountService1;  
        private final Logger logger = LoggerFactory.getLogger(AccountService1Test.class);  
        @Before  
        public void setUp() throws Exception {  
            ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("applicationContext1.xml");  
            accountService1 = context.getBean("accountService1", AccountService1.class);  
        }  
        @Test  
        public void testInject(){  
            assertNotNull(accountService1);  
        }  
        @Test  
        public void testGetAccountByName() throws Exception {  
            accountService1.getAccountByName("accountName");  
            accountService1.getAccountByName("accountName");  
            accountService1.reload();  
            logger.info("after reload ....");  
            accountService1.getAccountByName("accountName");  
            accountService1.getAccountByName("accountName"); }}Copy the code

According to the analysis, the result should be: query from the database first, then return the result in the cache directly, after resetting the cache, should query from the database first, then return the result in the cache. The program running log is as follows:

00:53:17. 166. [the main] INFO C.R.S.C ache. Example1. AccountService – real querying the db… accountName

00:53:17. 168. [the main] INFO C.R.S.C ache. Example1. AccountService – get the from the cache… accountName

00:53:17. 168. [the main] INFO C.R.S.C.E xample1. AccountServiceTest – after reload…

00:53:17. 168. [the main] INFO C.R.S.C ache. Example1. AccountService – real querying the db… accountName

00:53:17. 169. [the main] INFO C.R.S.C ache. Example1. AccountService – get the from the cache… accountName

We can see that our caching works, but this custom caching scheme has the following disadvantages:

  • The cache code is too coupled to the business code, as shown in the example above, and the getAccountByName () method in the AccountService has too much cached logic to maintain and change
  • Inflexible. This caching scheme does not support caching under certain conditions, such as only certain types of accounts need to be cached, which can lead to code changes
  • The storage of the cache is dead and can not be flexibly switched to the use of a third-party cache module

If your code is similar to the above, you may want to optimize your code structure, or simplify it, by following this guide. You will find that your code will become much more elegant.

How does Spring Cache work

We modify AccountService1 to create AccountService2:

import com.google.common.base.Optional; import com.rollenholt.spring.cache.example1.Account; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Service; @Service public class AccountService2 { private final Logger logger = LoggerFactory.getLogger(AccountService2.class); // a cache named accountCache@cacheable (value=) is used"accountCache"Public Account getAccountByName(String accountName) {// The internal implementation of the method does not consider the cache logic, directly implement the service logger.info()"real querying account... {}", accountName);  
            Optional<Account> accountOptional = getFromDB(accountName);  
            if(! accountOptional.isPresent()) { throw new IllegalStateException(String.format("can not find account by account name : [%s]", accountName));  
            }  
            return accountOptional.get();  
        }  
        private Optional<Account> getFromDB(String accountName) {  
            logger.info("real querying db... {}", accountName);  
            //Todo query data from database  
            returnOptional.fromNullable(new Account(accountName)); }}Copy the code

We notice a line in the code above:

@Cacheable(value=”accountCache”)

The comment means that when this method is called, it is queried from a cache called accountCache, and if not, the actual method (that is, the database query) is executed and the result is stored in the cache, otherwise the cached object is returned. The key in the cache is the accountName parameter, and the value is the Account object. The “accountCache” cache is the name defined in Spring *.xml. We also need a Spring configuration file to support annotation-based caching

    <beans xmlns="http://www.springframework.org/schema/beans"  
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"  
           xmlns:context="http://www.springframework.org/schema/context"  
           xmlns:cache="http://www.springframework.org/schema/cache"  
           xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/cache http://www.springframework.org/schema/cache/spring-cache.xsd">  
        <context:component-scan base-package="com.rollenholt.spring.cache"/>  
        <context:annotation-config/>  
        <cache:annotation-driven/>  
        <bean id="cacheManager" class="org.springframework.cache.support.SimpleCacheManager">  
            <property name="caches">  
                <set>  
                    <bean class="org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean">  
                        <property name="name" value="default"/>  
                    </bean>  
                    <bean class="org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean">  
                        <property name="name" value="accountCache"/>  
                    </bean>  
                </set>  
            </property>  
        </bean>   
    </beans>  
Copy the code

Note that the Spring configuration file has a key cache-enabled configuration item:

<cache:annotation-driven />

This configuration item is the default name used a cacheManager cache manager, the cache manager has a spring default implementation, namely org. Springframework. Cache. Support. SimpleCacheManager, This cache manager implements the logic of the cache manager we just customized. It needs to be configured with a property caches, the collection of caches managed by this cache manager. In addition to the default cache named Default, we have a custom cache named accountCache. Use the default memory storage solution ConcurrentMapCacheFactoryBean, it is based on Java. Util. Concurrent. A memory cache ConcurrentHashMap implementation scheme.

Then we write the test program:

    import org.junit.Before;  
    import org.junit.Test;  
    import org.slf4j.Logger;  
    import org.slf4j.LoggerFactory;  
    import org.springframework.context.support.ClassPathXmlApplicationContext;  
    import static org.junit.Assert.*;  
    public class AccountService2Test {  
        private AccountService2 accountService2;  
        private final Logger logger = LoggerFactory.getLogger(AccountService2Test.class);  
        @Before  
        public void setUp() throws Exception {  
            ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("applicationContext2.xml");  
            accountService2 = context.getBean("accountService2", AccountService2.class);  
        }  
        @Test  
        public void testInject(){  
            assertNotNull(accountService2);  
        }  
        @Test  
        public void testGetAccountByName() throws Exception {  
            logger.info("first query...");  
            accountService2.getAccountByName("accountName");  
            logger.info("second query...");  
            accountService2.getAccountByName("accountName"); }}Copy the code

The above test code basically does two queries. The first one should query the database, and the second one should return to the cache without looking at the database. Let’s do it and see what happens

01:10:32. 435. [the main] INFO C.R.S.C.E xample2. AccountService2Test – first query…

01:10:32. 456. [the main] INFO C.R.S.C ache. Example2. AccountService2 – real querying the account… accountName

01:10:32. 457. [the main] INFO C.R.S.C ache. Example2. AccountService2 – real querying the db… accountName

01:10:32. 458. [the main] INFO C.R.S.C.E xample2. AccountService2Test – second query…

You can see that the annotation-based caching we set up is working, and in the accountService.java code we don’t see any caching logic, just a line comment: @cacheable (value= “accountCache”) implements the basic caching scheme. Isn’t that powerful?