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);
複製代碼

這樣就確保,緩存中的數據和數據庫保持一致了。 以上。

相關文章
相關標籤/搜索