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.