玩转Spring Cache --- @Cacheable使用在MyBatis的Mapper接口上(解决Null key returned for cache operation)【享学Spring】(上)

本文涉及的产品
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: 玩转Spring Cache --- @Cacheable使用在MyBatis的Mapper接口上(解决Null key returned for cache operation)【享学Spring】(上)

前言


据我观察,很多小伙伴学习一门技术一般都是度娘 + ctrl v的模式。比如本文的知识点,从网络的世界里你能找到有人介绍说:@Cacheable不仅仅能标注在实例方法上,也能标注在接口方法上。


so,你回来试了试把它标注在自己的MyBatis的Mapper接口上,希望它能帮助分摊DB的压力。想法非常正派且看似可行,但一经实操却发现发现报错如下:


java.lang.IllegalArgumentException: Null key returned for cache operation (maybe you are using named params on classes without debug info?) Builder ...


顿时丈二的和尚了有木有,难道网上说法有误是个坑:@Cacheable不能使用在接口上吗?

其实都不是,而是因为Spring它只说了其一,并没有说其二。所以请相信,本文会给你意想不到的收获~


缓存注解使用在接口上的场景实用吗?

答:非常实用。


我们知道MyBatis作为一个优秀的、灵活的持久层框架,现在被大量的使用在我们项目中(国内使用Hibernate、JPA还是较少的)。并且我们大都是用Mapper接口 + xml文件/注解的方式去使用它来操作DB,而缓存作为缓解DB压力的一把好手,因此我们亟待需要在某些请求中在DB前面挡一层缓存。


举例最为经典的一个使用场景:DB里会存在某些配置表,它大半年都变不了一次,但读得又非常非常的频繁,这种场景还不在少数,因此这种case特别适合在Mapper接口层加入一层缓存,极大的减轻DB的压力~


本文目标

我们目标是:没有蛀牙–>能让缓存注解在Mapper接口上正常work~~~


Demo示例构造


现在我通过一个示例,模拟小伙伴们在MyBatis的Mapper接口中使用缓存注解的真实场景。


第一步:准备MyBatis环境

@Configuration
@MapperScan(basePackages = {"com.fsx.dao"})
@PropertySource(value = "classpath:jdbc.properties", ignoreResourceNotFound = false, encoding = "UTF-8")
public class MyBatisConfig {
    @Value("${datasource.username}")
    private String userName;
    @Value("${datasource.password}")
    private String password;
    @Value("${datasource.url}")
    private String url;
    // 配置数据源
    @Bean
    public DataSource dataSource() {
        MysqlDataSource dataSource = new MysqlDataSource();
        dataSource.setUser(userName);
        dataSource.setPassword(password);
        dataSource.setURL(url);
        return dataSource;
    }
    @Bean
    public SqlSessionFactoryBean sqlSessionFactory() {
        SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
        factoryBean.setDataSource(dataSource());
        // factoryBean.setMapperLocations(); // 若使用全注解写SQL 此处不用书写Mapper.xml文件所在地
        return factoryBean;
    }
}


准备Mapper接口(为了简便用MyBatis注解方式实现SQL查询)


// @Repository //备注:这个注解是没有必要的    因为已经被@MapperScan扫进去了  
public interface CacheDemoMapper {
    @Select("select * from user where id = #{id}")
    @Cacheable(cacheNames = "demoCache", key = "#id")
    User getUserById(Integer id);
}


单元测试:


@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {RootConfig.class, CacheConfig.class, MyBatisConfig.class})
public class TestSpringBean {
    @Autowired
    private CacheDemoMapper cacheDemoMapper;
    @Autowired
    private CacheManager cacheManager;
    @Test
    public void test1() {
        cacheDemoMapper.getUserById(1);
        cacheDemoMapper.getUserById(1);
        System.out.println("----------验证缓存是否生效----------");
        Cache cache = cacheManager.getCache("demoCache");
        System.out.println(cache);
        System.out.println(cache.get(1, User.class));
    }
}


看似一切操作自如,风平浪静,但运行后报错如下:


java.lang.IllegalArgumentException: Null key returned for cache operation (maybe you are using named params on classes without debug info?) Builder[public final com.fsx.bean.User com.sun.proxy.$Proxy51.getUserById(java.lang.Integer)] caches=[demoCache] | key='#id' | keyGenerator='' | cacheManager='' | cacheResolver='' | condition='' | unless='' | sync='false'
  at org.springframework.cache.interceptor.CacheAspectSupport.generateKey(CacheAspectSupport.java:578)
  ...


错误提示竟然告诉我没有key,不禁爆粗口:接口方法上注解里写的key = "#id"难道程序瞎吗?


报错原因分析


要相信:所有人都可能骗人,但程序不会骗人。

其实报错能给我们释放至少两个信号:


  1. 缓存注解确实开启而且生效了(若注解完全不生效,就不会报错)
  2. 缓存注解使用时,key为null而报错


从异常信息一眼就能看出,key为null了。但是我们确实写了key = "#id"为何还会为null呢?根据异常栈去到源码处:


因为前面有详细分析过缓存注解的处理过程、原理,因此此处只关心关键代码即可


public abstract class CacheAspectSupport extends AbstractCacheInvoker implements BeanFactoryAware, InitializingBean, SmartInitializingSingleton {
  ...
  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);
    }
    return key;
  }
  ...
}


就是调用这个方法生成key的时候,context.generateKey(result);这一句返回了null才抛出了异常,罪魁祸首就是它了。继续看本类generateKey()这个方法:


    @Nullable
    protected Object generateKey(@Nullable Object result) {
      if (StringUtils.hasText(this.metadata.operation.getKey())) {
        EvaluationContext evaluationContext = createEvaluationContext(result);
        return evaluator.key(this.metadata.operation.getKey(), this.metadata.methodKey, evaluationContext);
      }
      return this.metadata.keyGenerator.generate(this.target, this.metadata.method, this.args);
    }


从代码中可知,这是因为解析#id这个SpEL表达式的时候返回了null,到这就定位到问题的根本所在了。


若要继续分析下去,那就是和SpEL关系很大了。所以我觉得有必要先了解Spring的SpEL的解析过程和简单原理,若你还不了解,可以参照:【小家Spring】SpEL你感兴趣的实现原理浅析spring-expression~(SpelExpressionParser、EvaluationContext、rootObject)


其实导致SpEL返回null的最初原因,在于MethodBasedEvaluationContext这个类对方法参数的解析上。它解析方法参数时用到了ParameterNameDiscoverer去解析方法入参的名字,而关键在于:实现类DefaultParameterNameDiscoverer是拿不到接口参数名的。


从代码中可知,这是因为解析#id这个SpEL表达式的时候返回了null,到这就定位到问题的根本所在了。


若要继续分析下去,那就是和SpEL关系很大了。所以我觉得有必要先了解Spring的SpEL的解析过程和简单原理,若你还不了解,可以参照:【小家Spring】SpEL你感兴趣的实现原理浅析spring-expression~(SpelExpressionParser、EvaluationContext、rootObject)


其实导致SpEL返回null的最初原因,在于MethodBasedEvaluationContext这个类对方法参数的解析上。它解析方法参数时用到了ParameterNameDiscoverer去解析方法入参的名字,而关键在于:实现类DefaultParameterNameDiscoverer是拿不到接口参数名的。


DefaultParameterNameDiscoverer获取方法参数名示例


下面给出一个示例,方便更直观的看到DefaultParameterNameDiscoverer的效果:

    public static void main(String[] args) throws NoSuchMethodException {
        DefaultParameterNameDiscoverer discoverer = new DefaultParameterNameDiscoverer();
        // 实现类CacheDemoServiceImpl能正常获取到方法入参的变量名
        Method method = CacheDemoServiceImpl.class.getMethod("getFromDB", Integer.class);
        String[] parameterNames = discoverer.getParameterNames(method);
        System.out.println(ArrayUtils.toString(parameterNames)); //{id}
        // 直接从method里拿  永远都是arg0/arg1...哦~~~需要注意
        Arrays.stream(method.getParameters()).forEach(p -> System.out.println(p.getName())); //arg0
        // 接口CacheDemoService不能获取到方法的入参
        method = CacheDemoService.class.getMethod("getFromDB", Integer.class);
        parameterNames = discoverer.getParameterNames(method);
        System.out.println(ArrayUtils.toString(parameterNames)); //{}
        method = CacheDemoMapper.class.getMethod("getUserById", Integer.class);
        parameterNames = discoverer.getParameterNames(method);
        System.out.println(ArrayUtils.toString(parameterNames)); //{}
        //虽然DefaultParameterNameDiscoverer拿不到,但是method自己是可以拿到的,只是参数名是arg0/arg1... 这样排序的
        Arrays.stream(method.getParameters()).forEach(p -> System.out.println(p.getName())); //arg0
    }


通过此例就不用我再多说了,知道上面的generateKey()方法返回null是肿么回事了吧~



相关文章
|
7天前
|
Java 数据库连接 数据库
spring复习05,spring整合mybatis,声明式事务
这篇文章详细介绍了如何在Spring框架中整合MyBatis以及如何配置声明式事务。主要内容包括:在Maven项目中添加依赖、创建实体类和Mapper接口、配置MyBatis核心配置文件和映射文件、配置数据源、创建sqlSessionFactory和sqlSessionTemplate、实现Mapper接口、配置声明式事务以及测试使用。此外,还解释了声明式事务的传播行为、隔离级别、只读提示和事务超时期间等概念。
spring复习05,spring整合mybatis,声明式事务
|
10天前
|
Java 数据库连接 数据库
SpringBoot 整合jdbc和mybatis
本文详细介绍了如何在SpringBoot项目中整合JDBC与MyBatis,并提供了具体的配置步骤和示例代码。首先,通过创建用户实体类和数据库表来准备基础环境;接着,配置Maven依赖、数据库连接及属性;最后,分别展示了JDBC与MyBatis的集成方法及其基本操作,包括增删查改等功能的实现。适合初学者快速入门。
SpringBoot 整合jdbc和mybatis
|
20天前
|
缓存 前端开发 Java
【Java面试题汇总】Spring,SpringBoot,SpringMVC,Mybatis,JavaWeb篇(2023版)
Soring Boot的起步依赖、启动流程、自动装配、常用的注解、Spring MVC的执行流程、对MVC的理解、RestFull风格、为什么service层要写接口、MyBatis的缓存机制、$和#有什么区别、resultType和resultMap区别、cookie和session的区别是什么?session的工作原理
【Java面试题汇总】Spring,SpringBoot,SpringMVC,Mybatis,JavaWeb篇(2023版)
|
7天前
|
XML Java 关系型数据库
springboot 集成 mybatis-plus 代码生成器
本文介绍了如何在Spring Boot项目中集成MyBatis-Plus代码生成器,包括导入相关依赖坐标、配置快速代码生成器以及自定义代码生成器模板的步骤和代码示例,旨在提高开发效率,快速生成Entity、Mapper、Mapper XML、Service、Controller等代码。
springboot 集成 mybatis-plus 代码生成器
|
7天前
|
SQL XML Java
springboot整合mybatis-plus及mybatis-plus分页插件的使用
这篇文章介绍了如何在Spring Boot项目中整合MyBatis-Plus及其分页插件,包括依赖引入、配置文件编写、SQL表创建、Mapper层、Service层、Controller层的创建,以及分页插件的使用和数据展示HTML页面的编写。
springboot整合mybatis-plus及mybatis-plus分页插件的使用
|
7天前
|
XML Java 数据库连接
springboot中整合mybatis及简单使用
这篇文章介绍了如何在Spring Boot项目中整合MyBatis,包括依赖引入、配置数据源、创建测试表、编写Mapper接口和XML文件、以及创建Service和Controller层的步骤。
springboot中整合mybatis及简单使用
|
20天前
|
Java 数据库连接 数据格式
【Java笔记+踩坑】Spring基础2——IOC,DI注解开发、整合Mybatis,Junit
IOC/DI配置管理DruidDataSource和properties、核心容器的创建、获取bean的方式、spring注解开发、注解开发管理第三方bean、Spring整合Mybatis和Junit
【Java笔记+踩坑】Spring基础2——IOC,DI注解开发、整合Mybatis,Junit
|
2月前
|
Java 数据库连接 测试技术
SpringBoot 3.3.2 + ShardingSphere 5.5 + Mybatis-plus:轻松搞定数据加解密,支持字段级!
【8月更文挑战第30天】在数据驱动的时代,数据的安全性显得尤为重要。特别是在涉及用户隐私或敏感信息的应用中,如何确保数据在存储和传输过程中的安全性成为了开发者必须面对的问题。今天,我们将围绕SpringBoot 3.3.2、ShardingSphere 5.5以及Mybatis-plus的组合,探讨如何轻松实现数据的字段级加解密,为数据安全保驾护航。
92 1
|
2月前
|
Web App开发 前端开发 关系型数据库
基于SpringBoot+Vue+Redis+Mybatis的商城购物系统 【系统实现+系统源码+答辩PPT】
这篇文章介绍了一个基于SpringBoot+Vue+Redis+Mybatis技术栈开发的商城购物系统,包括系统功能、页面展示、前后端项目结构和核心代码,以及如何获取系统源码和答辩PPT的方法。
|
2月前
|
Java 关系型数据库 MySQL
1、Mybatis-Plus 创建SpringBoot项目
这篇文章是关于如何创建一个SpringBoot项目,包括在`pom.xml`文件中引入依赖、在`application.yml`文件中配置数据库连接,以及加入日志功能的详细步骤和示例代码。
下一篇
无影云桌面