Springboot应用cache,将@Cacheable、@CacheEvict注解应用在mybatis mapper的接口方法上

3,092 阅读5分钟

1、前言

关于Cacheable、@CacheEvict的用法,网上很多讲解, 以下是引用:

@Cacheable可以标记在一个方法上,也可以标记在一个类上。当标记在一个方法上时表示该方法是支持缓存的,当标记在一个类上时则表示该类所有的方法都是支持缓存的。对于一个支持缓存的方法,Spring会在其被调用后将其返回值缓存起来,以保证下次利用同样的参数来执行该方法时可以直接从缓存中获取结果,而不需要再次执行该方法。Spring在缓存方法的返回值时是以键值对进行缓存的,值就是方法的返回结果,至于键的话,Spring又支持两种策略,默认策略和自定义策略,这个稍后会进行说明。需要注意的是当一个支持缓存的方法在对象内部被调用时是不会触发缓存功能的。@Cacheable可以指定三个属性,value、key和condition。

通常来说,Cacheable、@CacheEvict应用在public的类方法上,但是mybatis的mapper是接口的形式,而且我想直接应用在mapper的接口方法上,这样缓存就是以表的形式缓存,但是这样可不可以呢?我们试一下。

2、目标与分析

我们的目标就是在mapper上可以应用缓存注解。

mapper代码如下:

@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);
}

启动,运行,然后发现如下错误:

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

看源码分析,在CacheAspectSupport类中找到这一段:

	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;
	}

看来罪魁祸首就是它了,是key为null引起的,那key为什么没得到呢,我们在分析context.generateKey(result);

		@Nullable
		protected Object generateKey(@Nullable Object result) {
		    //如果注解上的key不为空,则走这个逻辑
			if (StringUtils.hasText(this.metadata.operation.getKey())) {
				EvaluationContext evaluationContext = createEvaluationContext(result);
				return evaluator.key(this.metadata.operation.getKey(), this.metadata.methodKey, evaluationContext);
			}
			 //如果注解上的key为空,则走这个逻辑
			return this.metadata.keyGenerator.generate(this.target, this.metadata.method, this.args);
		}

上面createEvaluationContext(result)方法其中创建createEvaluationContext的时候有一段代码:

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

其中getParameterNameDiscoverer()是获取的DefaultParameterNameDiscoverer对象,我们知道,DefaultParameterNameDiscoverer是拿不到接口参数名的,所以key的值解析不出来,结果就是将@Cacheable应用在mapper上失败。

3、解决办法

第二步中,generateKey方法判断key是否为空,然后走不同的逻辑。@Cacheable注解中有一个keyGenerator属性:

	/**
	 * The bean name of the custom {@link org.springframework.cache.interceptor.KeyGenerator}
	 * to use.
	 * <p>Mutually exclusive with the {@link #key} attribute.
	 * @see CacheConfig#keyGenerator
	 */
	String keyGenerator() default "";

我们可以自定义一个keyGenerator,自定义生成key。
ps:问:keyGenerator和key可以同时使用吗?答:不可以,原因如下: CacheAdviceParser的内部类Props有判断:

	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.");
	}

所以二者不可得兼

我们按理说,要写一个自定义的KeyGenerator,如下:

@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
                xxxx
                return key值;
            }
        };
    }
}

在mapper使用的时候,这样用:

@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);
}

但是问题来了。

  • 如果参数有多个,我只想用其中的一个或者几个当key怎么办?
  • 如果参数是个对象,我只想用对象的其中一个属性或几个属性当key怎么办?

目前的情况,满足不了我们的需求,所以我们新写两个注解@MyCacheable、@MyCacheEvict,他们只比@Cacheable、@CacheEvict多一个属性newKey(这里的变量名只是举例,具体可以自己指定有意义的变量名),newKey属性来指定以哪个字段为key。

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

    String newKey() default "";
    
    xxxxx 内容如@Cacheable
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
@CacheEvict
public @interface MyCacheEvict {

    String newKey() default "";
    
    xxxxx 内容如@CacheEvict

然后我们在写自定义的KeyGenerator,如下:

@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) {
                //获得注解
                MyCacheable myCacheable = AnnotationUtils.findAnnotation(method, MyCacheable.class);
                MyCacheEvict myCacheEvict = AnnotationUtils.findAnnotation(method, MyCacheEvict.class);
                //至少存在一个注解
                if(null != myCacheable || null != myCacheEvict){
                    //获得注解的newKey值
                    String newKey = myCacheable != null? myCacheable.newKey() : myCacheEvict.newKey();
                    //获取方法的参数集合
                    Parameter[] parameters = method.getParameters();
                    StandardEvaluationContext context = new StandardEvaluationContext();

                    //遍历参数,以参数名和参数对应的值为组合,放入StandardEvaluationContext中
                    for (int i = 0; i< parameters.length; i++) {
                        context.setVariable(parameters[i].getName(), params[i]);
                    }

                    //根据newKey来解析获得对应值
                    Expression expression = parser.parseExpression(newKey);
                    return expression.getValue(context, String.class);
                }
                return params[0].toString();
            }
        };
    }
}

然后我们在mapper上使用它。 你可以这样:

    //以第一个参数为key
    @Select("SELECT name AS name FROM t_test where id = #{id}")
    @MyCacheable(value = "selectById@", keyGenerator = "myParamKeyGenerator", newKey = "#arg0")
    PersonInfo selectById(Long id);

这样:

    //以第二个参数为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);

甚至这样:

    //以unUse_id对应的值为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);

如果是对象的话:

    @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);

arg0对应第一个参数,arg1对应第二个参数,以此类推。 如果你不喜欢用arg,如果是java8以上的话,可以在maven中添加parameters:

   <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>
   		<compilerArgs>
   		    <arg>-parameters</arg>
   		</compilerArgs>
   	</configuration>
   </plugin>

在idea中添加parameters:

然后就可以用#变量名的形式:

    @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);
   @Select("SELECT name AS name FROM t_test where id = #{id}")
   @MyCacheable(value = "selectById@", keyGenerator = "myParamKeyGenerator", newKey = "#id")
   PersonInfo selectById(Long id);

启动,访问,发现数据已经按照我们想象中的缓存到缓存中。

如果数据进行修改和删除,我们对缓存进行删除操作:

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

这样就确保,缓存中的数据和数据库保持一致了。 以上。