Use of SpringBoot cache annotations

I've been busy recently and haven't had time to update. In the last article, I talked about how to use Redis for caching. At the end of the article, I mentioned SpringBoot's support for caching. This article will talk about how to use SpringBoot.

1. SpringBoot's support for caching

For SpringBoot's support for caching, we need to introduce packages:

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-cache</artifactId>
        </dependency>

<!-- Integrate if required redis,need to rejoin redis Bag -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
            <version>2.4.2</version>
        </dependency>

The support of caching depends on the implementation of the interface: org.springframework.cache.annotation.CachingConfigurer. So we only need to implement the CachingConfigurer interface and give a reasonable implementation to use SpringBoot to customize the cache. So we usually use caches such as Ecache, Redis and other better cache frameworks have already been implemented. By default, SpringBoot also uses a local cache, and the cache configuration items can be configured through the configuration file. For details on how to configure, please refer to my previous article: How to use Redis for caching
SpringBoot does a lot of things for us, we only need to understand 3 annotations to use the cache: @Cacheable, @CachePut, @CacheEvict. The role of Cacheable is to read the cache, CachePut is to place the cache, and the role of CacheEvict is to clear the cache.

2. Cacheable annotation

We will be relatively familiar with this annotation, in In the previous article We have already mentioned the use of this annotation, copy and paste here again:

/**
 * Just occupy the source code and say
 */
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Cacheable {
	/**
     * The setting uses the replaced name, these two values ​​are the same, we use this value to distinguish different cache configurations
     * For example, we can set different cacheName s to set the cache time, set different key generation strategies, and so on.
     */
	@AliasFor("cacheNames")
	String[] value() default {};
	@AliasFor("value")
	String[] cacheNames() default {};
	/**
     * Set the key generation strategy, support spel expressions, and there is a root method, you can use the root object through #root.method,#root.target, etc.
     * You can also fix the cache key as a fixed string.
     * If not set, SpringBoot provides a default key generation strategy.
     */
	String key() default "";
	/**
     * Specify the key generator in the annotation
     */
	String keyGenerator() default "";
	/**
     * Specify the cache manager used by this annotation in the annotation
     */    
	String cacheManager() default "";
	/**
     * Specify the cache parser of this annotation in the annotation, and the cache manager is mutually exclusive
     */
	String cacheResolver() default "";
	/**
     * Set the cache condition and use Spel expression to parse. If the expression returns false, the execution does not go through the cache logic
     */
	String condition() default "";
	/**
     * Set no cache condition, Spel expression. If the expression returns true, the cached result is not cached.
     * The difference between this and condition() is that the execution timing is different. The condition method is called before the method is executed, and the unless method is called after the target method is executed.
     */
	String unless() default "";
	/**
     * Use synchronous method, default false
     */
	boolean sync() default false;
}

This annotation mainly describes the usage strategy of the cache

Take a chestnut:

    public static final String D1 = "cache_1d";

    @Cacheable(value = CacheTimes.D1, key = "#root.methodName", unless = "#result == null || #result.size() < 1", condition = "#skip != null")
    public List<String> getList(String skip) {
        return Arrays.stream(UUID.randomUUID().toString().split("-")).collect(Collectors.toList());
    }

The above example uses the @Cacheable annotation, let's analyze it:

  1. value="cache_1d" is the cache value I defined, and this configuration sets the cache for 1 day.
  2. Customize the cache key, the key is the method name. You can also use keyGenerator to set the key generation strategy, but I think using spel expressions is more flexible. If you use keyGenerator, the implementation class of keyGenerator needs to implement the org.springframework.cache.interceptor.KeyGenerator interface. The specific implementation method is implemented in the generate method.
  3. unless means that if the returned result is a null array or the size of the array is 0, the result will not be cached.
  4. The condition determines whether to switch to logic. If the skip is null, that is, the condition is false, the target method is directly executed instead of reading the cache.
  5. If necessary, you can specify cacheManager or cacheResolver. For example, RedisCacheManager is used by default. However, a certain cache does not need to use Redis, and Ecache can be used alone. You can specify the cachemanager of Ecache in the annotation.

3. CachePut annotation

Above we said that the CachePut annotation is used to place the cache. It's a bit awkward, which means that the CachePut annotation can set some caches that meet the conditions. Although we usually use it to update the cache (if you use Redis as a cache, you can use it to set the value of redis. Although it can be achieved, it is not recommended to use this to set the value of Redis, because it violates the annotation itself. It means that others read it and think it is a cache), but I don’t think it can be said to update the cache. Update means that it must be there and then change its value.
The source code of CachePut is basically the same as that of Cacheable

public @interface CachePut {
	@AliasFor("cacheNames")
	String[] value() default {};
	@AliasFor("value")
	String[] cacheNames() default {};

	String key() default "";
	String condition() default "";
	String unless() default "";
    
	String keyGenerator() default "";
	String cacheManager() default "";
	String cacheResolver() default "";
}

Similarly for Cacheable, the CachePut annotation takes effect only when condition()true and unless()false are satisfied.

How to lift chestnuts

    @CachePut(value = CacheTimes.D1, key = "#root.methodName", unless = "#result == null || #result.size() < 1", condition = "#condition" )
    public List<String> setListCache(List<String> list, boolean condition){
        if(list != null && list.size() > 0){
            return list;
        }
        return Arrays.stream(UUID.randomUUID().toString().split("-")).collect(Collectors.toList());
    }

In the above example, unless and condition are used in the same way as Cacheable. The difference is that when the CachePut annotation method is called, it will not read the data stored in the cache, but only judge whether to write the data into the cache at the end of the call. That is, if the parameter condition=true is satisfied and the return value is not NullList, the execution result will be stored in the cache with key=setListCache.
If the key setting is the same as the key of the Cacheable cache, the cache will be updated at the end of the method call.

  • Strange and strange: Use CachePut to assign values ​​to Redis (in fact, it is to make up the number of words. Since it is a cache annotation, it is better for us to only use it for caching, and it is not necessarily Redis that is used for caching, hahahahahahahaha)
    @CachePut(value = CacheTimes.D1, key = "#key")
    public Object setKV(String key, Object value){
        return value;
    }

If you use it like this, you should pay attention to the expiration time of your value cache and the use of the CacheEvict annotation. . . .

4. CacheEvict annotation

Compared with the previous two annotations, the CacheEvict annotation has two more attributes: allEntries and beforeInvocation

public @interface CacheEvict {
    /** Whether to delete all caches */
	boolean allEntries() default false;
 	/** Delete cache operation before/after method execution */
	boolean beforeInvocation() default false;
    
	@AliasFor("cacheNames")
	String[] value() default {}; 
	@AliasFor("value")
	String[] cacheNames() default {};
	String key() default "";
	String condition() default "";
    
	String keyGenerator() default "";
	String cacheManager() default "";
	String cacheResolver() default "";
}

Parameters: allEntries

The function of the allEntries parameter is to delete all cached data, the default is false, let us specify the key to delete the matching cache. See the following two examples in detail:

  • Delete all cached data
    @CacheEvict(value = CacheTimes.D1, condition = "#condition", allEntries = true)
    public void clearCache(boolean condition){
        ......
    }

The above method means that when the parameter condition=true, the CacheEvict annotation takes effect, because allEntries=true, so all cached data under value=CacheTimes.D1 will be deleted.
For example, before setting the cache under CacheTime.D1: "a"="b","c"="c","d"="d", after calling the clearCache method, the above cache will be all deleted.

  • Specify key to delete
    @CacheEvict(value = CacheTimes.D1, allEntries = false, key = "#key")
    public void clearCache(String key){

    }

If allEntries=false, CacheEvict will delete the key value of the specified key. It should be true that allEntries should be false when the key is specified; otherwise, the specified key will be invalid.

So when allEntries=true, how does springboot judge which data should be deleted?
We need to figure out the key generation strategy of the cache first: by default, the cache will use the value in the annotation as the prefix + "::" + your custom key generation strategy as the real key of the cache. When we call the value of allEntries in the CacheEvict annotation to be true, springboot will match the data in the cache system according to the above key generation strategy, that is, delete the key prefixed with the value value in the annotation.
Then there will be such a problem: If we rewrite the above key generation strategy so that the cache does not have a prefix when generating keys, what will happen when deleting?
The answer is as you imagined: all data in the cache system will be deleted. If the cache uses redis, all data in redis will be cleared.

Parameters: beforeInvocation

Since it is a cache, we should operate the cache more comprehensively. Then suppose we have cached a User table. When adding data, we use Cacheable to set the cache; when reading, we also retrieve data according to the Cacheable strategy; when modifying, we use CachePut to update the cache; when deleting, We should use CacheEvict to delete the cache. At this time, there will be a problem: when calling the method to delete a user record with id=123, an exception occurs due to business reasons (the status of whether the deletion is successful or not is unknown), then at this time, should we clear the cache.
In this case beforeInvocation gives us a choice. When beforeInvocation=true, SpringBoot performs the cache operation first (delete the cache first) before executing the method logic. When beforeInvocation=false, the method business logic is executed first, and then the cache is deleted. (Of course, it doesn't matter when the business is running normally)
When the business is abnormal, we either choose to delete the cache or not.

  • Cache operation first (delete the cache first, then execute the logic, no matter whether it is abnormal or not, the cache has been cleared)
@CacheEvict(value = CacheTimes.D1, condition = "#condition", beforeInvocation = true, allEntries = true)
public void clearCache(boolean condition){
    throw new RuntimeException("exception");
}
  • After the cache operation (by default, the cache will not be deleted if an exception occurs)
@CacheEvict(value = CacheTimes.D1, condition = "#condition",beforeInvocation = false, allEntries = true)
public void clearCache(boolean condition){
    throw new RuntimeException("exception");
}

5. Caching annotations implement complex caching logic

Caching is reading, writing and updating, so the above three annotations are enough. What does Caching do?

public @interface Caching {
	Cacheable[] cacheable() default {};
	CachePut[] put() default {};
	CacheEvict[] evict() default {};
}

The Caching annotation includes three annotations: Cacheable, CachePut, and CacheEvict. So we should be able to imagine that multiple cache annotations can be used in Caching, mainly to implement some complex cache logic, and it does not need to be implemented in multiple methods.
There is nothing to say about this, it is concise and clear.

6. Common (I encountered) business implementation and use. . .

1. Cache a table in mysql

This kind of logic usually does not use a stand-alone cache, but often uses a separate cache system: such as Redis, elasticsearch as a cache, because the consistency of the data must be maintained.

	class user{......}
	//Store in the cache while storing in the database
    @CachePut(value = "user", condition = "#user != null", key = "#user.id")
    public User insert(User user){
        jdbc.insert(user)
        return user;
    }
	//Read, if the cache expires, check the data again and rewrite the cache
	@Cacheable(value = "user", condition = "#userId != null", key = "#userId", unless = "#result != null")
    public User select(Integer userId){
        User user = jdbc.select(userId);
        return user;
    }
	//Also clear the cache when deleting data
    @CacheEvict(value = "user", condition = "#userId != null", key = "#userId", allEntries = false, beforeInvocation = true)
    public void delete(Integer userId){
		jdbc.delete(userId);
    }

2. Cache complex query logic

Usually, a restful interface of ours has very complex logic, which causes the interface request time to be too long. At this time, we will consider caching the data in the interface instead of going through the business again every time it enters. The business is changeable, and we may not only read the cache in the setting of the entire interface, but we may need to update and clear the cache at the same time according to different parameters. The demo is as follows:

    @Caching(
            cacheable = {
                    @Cacheable(value = CacheTimes.D1, key = "#root.methodName + #userId", condition = "#update == null", unless = "#result != null || #result.size() < 1"),
                    @Cacheable(value = CacheTimes.D7, key = "#root.methodName + #userId", condition = "#update == null", unless = "#result != null", cacheManager = "cacheManager")
            },
            put = {
                    @CachePut(value = CacheTimes.D1, key = "#root.methodName + #userId", condition = "#update != null && #update.size() > 0"),
                    @CachePut(value = CacheTimes.D7, key = "#root.methodName + #userId", condition = "#update != null", cacheManager = "cacheManager")
            }
    )
    public List<Object> recommend(String userId, List<Object> update){
        if(update != null && update.size > 0){
            return update;
        }
        List<Object> l1 = selectTable1(userId);
        List<Object> l2 = selectTable2(userId);
        List<Object> l3 = selectTable3(userId);
        List<Object> l4 = selectTable4(userId);
        List<Object> l5 = selectTable5(userId);
        List<Object> result = new ArrayList();
        result.addAll(l1);
        result.addAll(l2);
        result.addAll(l3);
        result.addAll(l4);
        result.addAll(l5);
        return converVo(result);
    }

1. As in the above demo, when the parameter update is not null, we directly return the value of update. If update is not null, the CachePut annotation is triggered. We will update two caches, one is the default cache update manager, and the other specifies the cache manager .
2. So what happens when the Cachebale annotation is met? We defined two Cacheable annotations above. When both of them meet the annotation conditions and the values ​​in the two caches are different, will an exception be reported? Of course not, when reading the cache when there are two, SpringBoot will return the value of your first annotation.
3. What if the parameters conform to CachePut and Cacheable at the same time? Through testing, I found that if the conditions of these two annotations are slowed down at the same time, the priority of CachePut will be higher, so 1. The cache will be updated; 2. The returned result is the updated cache.
4. If I add another CacheEvict, will the cache be cleared?
of course.

3. More are still in your practice. If there are mistakes, welcome to guide, and if there are expansions, please comment.

Well, this is the end of this, and everyone is welcome to give their opinions. Post your own suggestions, if you need to add, comments can also be seen at the same time. Bye-Bye.
I think I planned to write an article a week at the beginning, but it’s not very easy to do. I’m really brainwashed. Facing the computer, I can’t figure out how to say this sentence. I wrote and deleted it, and then wrote it again. Written in such a way, I really envy those students with good writing skills, who can express themselves accurately and make others understand better.
Give yourself a like: Welcome to like, follow and comment, and I hope you will gain something after reading this article.

Tags: Java Spring Boot Cache

Posted by Sh0t on Sun, 04 Dec 2022 15:57:50 +0300