1, the preface

Cacheable @cacheevict There are many explanations on the web about how to use Cacheable and @cacheevict. Here are the quotes:

@cacheable can be marked on a method or on a class. A tag on a method indicates that the method is cache-enabled, and a tag on a class indicates that all methods in that class are cache-enabled. For a method that supports caching, Spring caches its return value after it is called to ensure that the next time the method is executed with the same parameters, the result can be fetched directly from the cache without having to execute the method again. Spring caches the return value of a method as a key-value pair, and the value is the result of the method. For keys, Spring supports two policies, default and custom, which will be explained later. Note that caching is not triggered when a method that supports caching is called inside an object. @cacheable can specify three attributes, value, key, and condition.

Cacheable @cacheevict normally applies to public class methods, but mybatis mapper is an interface, and I’d like to apply it directly to mapper’s interface methods, so that the cache is cached as a table. Let’s try it out.

2. Objectives and analysis

Our goal is to make caching annotations available on Mapper.Copy the code

The mapper code is as follows:

@Mapper
@Repository
public interface MyDao {

    @Select("SELECT name AS name FROM t_test where id = #{id}")
    @Cacheable(value = "selectById@", key = "#id")
    PersonInfo selectById(Long id);
}
Copy the code

Start, run, and find the following error:

Null key returned for cache operation (maybe you are using named params on classes without debug info?) 
Copy the code

Look at the source code and find this in the CacheAspectSupport class:

	private Object generateKey(CacheOperationContext context, @Nullable Object result) {
		Object key = context.generateKey(result);
		if (key == null) {
			throw new IllegalArgumentException("Null key returned for cache operation (maybe you are " +
					"using named params on classes without debug info?) " + context.metadata.operation);
		}
		if (logger.isTraceEnabled()) {
			logger.trace("Computed cache key '" + key + "' for operation " + context.metadata.operation);
		}
		return key;
	}
Copy the code

GenerateKey (result); context.generateKey(result);

		@Nullable
		protected Object generateKey(@Nullable Object result) {
		    // If the key on the annotation is not empty, use this logic
			if (StringUtils.hasText(this.metadata.operation.getKey())) {
				EvaluationContext evaluationContext = createEvaluationContext(result);
				return evaluator.key(this.metadata.operation.getKey(), this.metadata.methodKey, evaluationContext);
			}
			 // If the key on the annotation is empty, this logic is used
			return this.metadata.keyGenerator.generate(this.target, this.metadata.method, this.args);
		}
Copy the code

CreateEvaluationContext (result); createEvaluationContext(result);

		CacheEvaluationContext evaluationContext = new CacheEvaluationContext(
				rootObject, targetMethod, args, getParameterNameDiscoverer());
Copy the code

Which getParameterNameDiscoverer () is to obtain DefaultParameterNameDiscoverer objects, as we know, DefaultParameterNameDiscoverer is get the interface parameter name, As a result, applying @Cacheable to mapper fails.

3. Solutions

In the second step, the generateKey method determines whether the key is empty and then follows a different logic. The @cacheable annotation has a keyGenerator property:

	/** * The bean name of the custom {@link org.springframework.cache.interceptor.KeyGenerator} * to use. * 

Mutually exclusive with the {@link #key} attribute. * @see CacheConfig#keyGenerator */

String keyGenerator() default ""; Copy the code

We can customize a keyGenerator to generate a custom key. Ps: Q: Can keyGenerator and key be used together? A: No, for the following reasons: The internal class of CacheAdviceParser supports:

	if (StringUtils.hasText(builder.getKey()) && StringUtils.hasText(builder.getKeyGenerator())) {
		throw new IllegalStateException("Invalid cache advice configuration on '" +
			element.toString() + "'. Both 'key' and 'keyGenerator' attributes have been set. " +
			"These attributes are mutually exclusive: either set the SpEL expression used to" +
			"compute the key at runtime or set the name of the KeyGenerator bean to use.");
	}
Copy the code

So you can’t have both

We should write a custom KeyGenerator as follows:

@Configuration
public class ParamKeyConfiguration {
    @Bean(name = "myParamKeyGenerator")
    public KeyGenerator myParamKeyGenerator(){
        return new KeyGenerator() {
            @Override
            public Object generate(Object target, Method method, Object. params) { xxxx doSomething xxxxreturnThe key value; }}; }}Copy the code

When using Mapper, use this:

@Mapper
@Repository
public interface MyDao {

    @Select("SELECT name AS name FROM t_test where id = #{id}")
    @Cacheable(value = "selectById@", keyGenerator = "myParamKeyGenerator")
    PersonInfo selectById(Long id);
}
Copy the code

But here’s the problem.

  • What if there are multiple parameters and I only want to use one or more of them as keys?
  • What if the parameter is an object and I only want to use one or more properties of the object as the key?

@myCacheable, @myCacheevict, @myCacheable, @myCacheevict, @myCacheable, @myCacheevict, @myCacheable, @myCacheevict, @myCacheable The newKey property to specify which field is the key.

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
@Cacheable
public @interface MyCacheable {

    String newKey() default ""; XXXXX content such as @cacheableCopy the code
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
@CacheEvict
public @interface MyCacheEvict {

    String newKey() default ""; XXXXX content such as @cacheevictCopy the code

Then we write a custom KeyGenerator as follows:

@Configuration
public class ParamKeyConfiguration {

    private static ExpressionParser parser = new SpelExpressionParser();

    @Bean(name = "myParamKeyGenerator")
    public KeyGenerator myParamKeyGenerator(){
        return new KeyGenerator() {
            @Override
            public Object generate(Object target, Method method, Object. params) {// Get comments
                MyCacheable myCacheable = AnnotationUtils.findAnnotation(method, MyCacheable.class);
                MyCacheEvict myCacheEvict = AnnotationUtils.findAnnotation(method, MyCacheEvict.class);
                // At least one annotation exists
                if(null! = myCacheable ||null! = myCacheEvict){// Get the newKey value of the annotation
                    StringnewKey = myCacheable ! =null? myCacheable.newKey() : myCacheEvict.newKey();
                    // Get the set of parameters for the method
                    Parameter[] parameters = method.getParameters();
                    StandardEvaluationContext context = new StandardEvaluationContext();

                    / / traverse parameters to parameters name and the corresponding value of the combination, in StandardEvaluationContext
                    for (int i = 0; i< parameters.length; i++) {
                        context.setVariable(parameters[i].getName(), params[i]);
                    }

                    // Get the corresponding value according to newKey
                    Expression expression = parser.parseExpression(newKey);
                    return expression.getValue(context, String.class);
                }
                return params[0].toString(); }}; }}Copy the code

And then we use it on mapper. You can do this:

    // take the first parameter as key
    @Select("SELECT name AS name FROM t_test where id = #{id}")
    @MyCacheable(value = "selectById@", keyGenerator = "myParamKeyGenerator", newKey = "#arg0")
    PersonInfo selectById(Long id);
Copy the code

Like this:

    // Take the second parameter as key
    @Select("SELECT name AS name FROM t_test where id = #{id}")
    @MyCacheable(value = "selectById@", keyGenerator = "myParamKeyGenerator", newKey = "#arg1")
    PersonInfo selectById(String unUse, Long id);
Copy the code

Even this:

    // Use the value of unUse_id as key
    @Select("SELECT name AS name FROM t_test where id = #{id}")
    @MyCacheable(value = "selectById@", keyGenerator = "myParamKeyGenerator", newKey = "#arg0 + '_' + #arg1")
    PersonInfo selectById(String unUse, Long id);
Copy the code

If it is an object:

    @Select("SELECT name AS name FROM t_test where id = #{id}")
    @MyCacheable(value = "selectByBean@", keyGenerator = "myParamKeyGenerator", newKey = "#arg1.id")
    PersonInfo selectByBean(String unUse, PersonInfo personInfo);
Copy the code

Arg0 corresponds to the first argument, arg1 to the second, and so on. If you don’t like using args, you can add parameters in maven for java8 and above:

<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.6.1</version> <configuration> <source>1.8</source> <target>1.8</target> <arg>-parameters</arg>  </compilerArgs> </configuration> </plugin>Copy the code

Add parameters to idea:

We can then use the form # of the variable name:

    @Select("SELECT name AS name FROM t_test where id = #{id}")
    @MyCacheable(value = "selectByBean@", keyGenerator = "myParamKeyGenerator", newKey = "#personInfo.id")
    PersonInfo selectByBean(String unUse, PersonInfo personInfo);
Copy the code
   @Select("SELECT name AS name FROM t_test where id = #{id}")
   @MyCacheable(value = "selectById@", keyGenerator = "myParamKeyGenerator", newKey = "#id")
   PersonInfo selectById(Long id);
Copy the code

Launch, access, and discover that the data has been cached in the cache as we expected.

If the data is modified or deleted, we delete the cache:

   @Update("UPDATE t_test SET name = #{name} WHERE id = #{id}")
   @MyCacheEvict(value = "selectByBean@", keyGenerator = "myParamKeyGenerator", newKey = " #arg0.id")
   void updatePersonInfo(PersonInfo personInfo);
Copy the code

This ensures that the data in the cache is consistent with the database. The above.