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

一、前言

关于Cacheable、@CacheEvict的用法,网上不少讲解, 如下是引用:javascript

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

一般来讲,Cacheable、@CacheEvict应用在public的类方法上,可是mybatis的mapper是接口的形式,并且我想直接应用在mapper的接口方法上,这样缓存就是以表的形式缓存,可是这样可不能够呢?咱们试一下。spring

二、目标与分析

咱们的目标就是在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);
}
复制代码

启动,运行,而后发现以下错误:express

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

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

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的时候有一段代码:mybatis

CacheEvaluationContext evaluationContext = new CacheEvaluationContext(
				rootObject, targetMethod, args, getParameterNameDiscoverer());
复制代码

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

三、解决办法

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

/** * 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);
复制代码

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

相关文章
相关标签/搜索