Spring缓存抽象,Mybatis的Mapper接口使用@Cacheable注解的异常问题

在处理一些读多写少的操作,通过缓存很大程度减轻了后端的压力,测试Spring的缓存抽象,通过@EnableCaching注解方式来使用缓存,持久层用了Mybatis框架,但在@Cacheable注解select方法的时候,一直报一个错误,都怀疑是否注解能放在Mybatis的Mapper interface接口方法上,差点都换Hibernate来操作了

首先表信息如下

mysql root@localhost:user_center> select * from user_info;
+----+-------+-------------+-----+
| id | name  | phone       | age |
+----+-------+-------------+-----+
| 1  | lihui | 17777777777 | 17  |
| 2  | lilei | 18888888888 | 19  |
| 3  | litao | 19999999999 | 20  |
+----+-------+-------------+-----+

对应的model实体,重写了toString方法,方便打印

package com.lihuia.cache.model;

import lombok.Builder;

/**
* Copyright (C), 2018-2019
* FileName: User
* Author: lihui
* Date: 2019/11/24
*/

@Builder
public class User {

private int id;
private String name;
private String phone;
private int age;

@Override
public String toString() {
return "id=" + id + ", name=" + name + ", phone=" +phone + ", age=" + age;
}
}

下面就是Mybatis的Mapper接口定义

1、添加了@Mapper注解,说明是一个Mapper接口,供@MapperScan来扫描

2、分别定义了插入,删除,更新,查询单个和所有5个方法,@Results里,通过数据库表里的字段映射成类成员变量的驼峰规则

3、这里最关键的一点,在getUserByName()方法上,添加了一个@Cacheable注解,意味着在调用了该方法后,会将返回的结果添加到缓存里面去

package com.lihuia.cache.mapper;

import java.util.List;

import com.lihuia.cache.model.User;
import org.apache.ibatis.annotations.Delete;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Result;
import org.apache.ibatis.annotations.Results;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;

import org.springframework.cache.annotation.Cacheable;

/**
* Copyright (C), 2018-2019
* FileName: UserMapper
* Author: lihui
* Date: 2019/11/24
*/

@Mapper
public interface UserMapper {

@Insert("insert into user_info (name, phone, age) " +
"values (#{name}, #{phone}, #{age})")
int insertUser(User user);

@Update("update user_info set age = #{age} where name = #{name}")
int updateUser(User user);

@Delete("delete from user_info where name = #{name}")
int deleteUser(String name);

@Select("select * from user_info where name = #{name}")
@Results({
@Result(id = true, column = "id", property = "id"),
@Result(column = "name", property = "name"),
@Result(column = "phone", property = "phone"),
@Result(column = "age", property = "age"),
})
@Cacheable(cacheNames = "cacheLucy", key = "#name")
User getUserByName(@Param("name") String name);

@Select("select id, name, phone, age from user_info")
@Results({
@Result(id = true, column = "id", property = "id"),
@Result(column = "name", property = "name"),
@Result(column = "phone", property = "phone"),
@Result(column = "age", property = "age"),
})
List<User> getAllUsers();
}

properties文件里配置dataSource的相关信息

jdbc.driver=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://127.0.0.1:3306/user_center
jdbc.username=root
jdbc.password=123456

下面是配置类

1、@PropertySource注解指明了dataSource的配置文件

2、@EnableCaching注解开启缓存抽象

3、@MapperScan注解指定了扫描Mapper接口的路径,完成接口的注入,这里是依赖于SqlSessionFactory这个bean的

4、缓存抽象依赖CacheManager这个bean

package com.lihuia.cache.config;

import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.annotation.MapperScan;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.concurrent.ConcurrentMapCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.jdbc.datasource.DriverManagerDataSource;

/**
* Copyright (C), 2018-2019
* FileName: CacheConfig
* Author: lihui
* Date: 2019/11/24
*/


@PropertySource(value = {"classpath:config/db.properties"})
@Configuration
@EnableCaching
@MapperScan("com.lihuia.cache.mapper")
public class CacheConfig {

@Value("${jdbc.driver}")
private String driver;
@Value("${jdbc.url}")
private String url;
@Value("${jdbc.username}")
private String username;
@Value("${jdbc.password}")
private String password;

@Bean
public DriverManagerDataSource dataSource() {
DriverManagerDataSource dataSource = new DriverManagerDataSource();
dataSource.setDriverClassName(driver);
dataSource.setUrl(url);
dataSource.setUsername(username);
dataSource.setPassword(password);
return dataSource;
}

@Bean
public SqlSessionFactory sqlSessionFactory() throws Exception {
SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
factoryBean.setDataSource(dataSource());
return factoryBean.getObject();
}

@Bean
public CacheManager cacheManager() {
return new ConcurrentMapCacheManager();
}
}

测试比较简单,某一条记录查询后放到缓存里,然后数据查询都从缓存里取

NewImage

测试类

package com.lihuia.cache;

import com.lihuia.cache.config.CacheConfig;
import com.lihuia.cache.mapper.UserMapper;
import com.lihuia.cache.model.User;
import lombok.extern.slf4j.Slf4j;
import org.testng.annotations.Test;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.testng.AbstractTestNGSpringContextTests;

/**
* Copyright (C), 2018-2019
* FileName: UserTest
* Author: lihui
* Date: 2019/11/24
*/

@ContextConfiguration(classes = {CacheConfig.class})
@Slf4j
public class UserTest extends AbstractTestNGSpringContextTests {

@Autowired
private UserMapper userMapper;

@Test(description = "测试Spring缓存抽象")
public void userTest() {
log.info("1.查询user_info表");
userMapper.getAllUsers().forEach(e -> log.info(e.toString()));
log.info("2.新增Lucy的信息");
userMapper.insertUser(User.builder().id(4).name("Lucy").age(16).phone("16666666666").build());
log.info("3.查询Lucy的信息");
log.info(userMapper.getUserByName("Lucy").toString());
log.info("4.更新Lucy的年龄");
userMapper.updateUser(User.builder().age(26).name("Lucy").build());
log.info("5.查询user_info表");
userMapper.getAllUsers().forEach(e -> log.info(e.toString()));
log.info("6.查询Lucy的年龄");
log.info(userMapper.getUserByName("Lucy").toString());
log.info("7.删除Lucy的信息");
userMapper.deleteUser("Lucy");
log.info("8.查询user_info表");
userMapper.getAllUsers().forEach(e -> log.info(e.toString()));
}
}

可是执行结果却报了一个错

NewImage

从打印的消息可以看出来,是调用getUserByName()方法的执行报的错,加上都是cache相关的报错,可以肯定的是@Cacheable注解的问题

在org.springframework.cache.interceptor包下面的类

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);
}
if (logger.isTraceEnabled()) {
logger.trace("Computed cache key '" + key + "' for operation " + context.metadata.operation);
}
return key;
}

这么说,报错的原因是key的问题,@Cacheable(cacheNames = “cacheLucy”, key = “#name”),我看传了一个name,但是从Object key = context.generateKey(result);这行可以debug一下发现key为null

具体的原因和解决方法可以参考这个详细的链接:https://blog.csdn.net/f641385712/article/details/95169002

自定义一个注解,比较麻烦,具体可以参考上面的方法,我这里直接修改@Cacheable注解的key值即可

@Select("select * from user_info where name = #{name}")
@Results({
@Result(id = true, column = "id", property = "id"),
@Result(column = "name", property = "name"),
@Result(column = "phone", property = "phone"),
@Result(column = "age", property = "age"),
})
@Cacheable(cacheNames = "cacheLucy", key = "#a0")
User getUserByName(@Param("name") String name);

这样执行就无误了,结果如下

[main] INFO com.lihuia.cache.UserTest - 1.查询user_info表
[main] INFO com.lihuia.cache.UserTest - id=1, name=lihui, phone=17777777777, age=17
[main] INFO com.lihuia.cache.UserTest - id=2, name=lilei, phone=18888888888, age=19
[main] INFO com.lihuia.cache.UserTest - id=3, name=litao, phone=19999999999, age=20
[main] INFO com.lihuia.cache.UserTest - 2.新增Lucy的信息
[main] INFO com.lihuia.cache.UserTest - 3.查询Lucy的信息
[main] INFO com.lihuia.cache.UserTest - id=72, name=Lucy, phone=16666666666, age=16
[main] INFO com.lihuia.cache.UserTest - 4.更新Lucy的年龄
[main] INFO com.lihuia.cache.UserTest - 5.查询user_info表
[main] INFO com.lihuia.cache.UserTest - id=1, name=lihui, phone=17777777777, age=17
[main] INFO com.lihuia.cache.UserTest - id=2, name=lilei, phone=18888888888, age=19
[main] INFO com.lihuia.cache.UserTest - id=3, name=litao, phone=19999999999, age=20
[main] INFO com.lihuia.cache.UserTest - id=72, name=Lucy, phone=16666666666, age=26
[main] INFO com.lihuia.cache.UserTest - 6.查询Lucy的年龄
[main] INFO com.lihuia.cache.UserTest - id=72, name=Lucy, phone=16666666666, age=16
[main] INFO com.lihuia.cache.UserTest - 7.删除Lucy的信息
[main] INFO com.lihuia.cache.UserTest - 8.查询user_info表
[main] INFO com.lihuia.cache.UserTest - id=1, name=lihui, phone=17777777777, age=17
[main] INFO com.lihuia.cache.UserTest - id=2, name=lilei, phone=18888888888, age=19
[main] INFO com.lihuia.cache.UserTest - id=3, name=litao, phone=19999999999, age=20

仔细分析一下,新增了Lucy信息之后,当调用getUserByName方法查询Lucy信息的时候,会将查询的结果写入cacheLucy缓存里,下次再次调用getUserByName查询Lucy会先从cacheLucy里读取,因此当仅仅更新了Lucy的年龄后,查询整张表所有信息可以看到更新年龄后的Lucy,但是单独getUserByName查询Lucy信息从缓存里读取的还是老的

最后再来确认一下,当查询Lucy的信息之后,会正确写入缓存,调用CacheManager的getCache方法获取缓存数据

package com.lihuia.cache;

import com.lihuia.cache.config.CacheConfig;
import com.lihuia.cache.mapper.UserMapper;
import com.lihuia.cache.model.User;
import lombok.extern.slf4j.Slf4j;
import org.testng.annotations.Test;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.CacheManager;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.testng.AbstractTestNGSpringContextTests;

/**
* Copyright (C), 2018-2019
* FileName: UserTest
* Author: lihui
* Date: 2019/11/24
*/

@ContextConfiguration(classes = {CacheConfig.class})
@Slf4j
public class UserTest extends AbstractTestNGSpringContextTests {

@Autowired
private UserMapper userMapper;

@Autowired
private CacheManager cacheManager;

@Test(description = "测试Spring缓存抽象")
public void userTest() {
log.info("1.查询user_info表");
userMapper.getAllUsers().forEach(e -> log.info(e.toString()));
log.info("2.新增Lucy的信息");
userMapper.insertUser(User.builder().id(4).name("Lucy").age(16).phone("16666666666").build());
log.info("3.查询cacheLucy缓存");
log.info(String.valueOf(cacheManager.getCache("cacheLucy").get("Lucy", User.class)));
log.info("4.查询Lucy的信息");
log.info(userMapper.getUserByName("Lucy").toString());
log.info("5.查询cacheLucy缓存");
log.info(String.valueOf(cacheManager.getCache("cacheLucy").get("Lucy", User.class)));
log.info("6.更新Lucy的年龄");
userMapper.updateUser(User.builder().age(26).name("Lucy").build());
log.info("7.查询user_info表");
userMapper.getAllUsers().forEach(e -> log.info(e.toString()));
log.info("8.查询Lucy的年龄");
log.info(userMapper.getUserByName("Lucy").toString());
log.info("9.删除Lucy的信息");
userMapper.deleteUser("Lucy");
log.info("10.查询user_info表");
userMapper.getAllUsers().forEach(e -> log.info(e.toString()));
}
}

查询的时候,写入缓存

[main] INFO com.lihuia.cache.UserTest - 1.查询user_info表
[main] INFO com.lihuia.cache.UserTest - id=1, name=lihui, phone=17777777777, age=17
[main] INFO com.lihuia.cache.UserTest - id=2, name=lilei, phone=18888888888, age=19
[main] INFO com.lihuia.cache.UserTest - id=3, name=litao, phone=19999999999, age=20
[main] INFO com.lihuia.cache.UserTest - 2.新增Lucy的信息
[main] INFO com.lihuia.cache.UserTest - 3.查询cacheLucy缓存
[main] INFO com.lihuia.cache.UserTest - null
[main] INFO com.lihuia.cache.UserTest - 4.查询Lucy的信息
[main] INFO com.lihuia.cache.UserTest - id=81, name=Lucy, phone=16666666666, age=16
[main] INFO com.lihuia.cache.UserTest - 5.查询cacheLucy缓存
[main] INFO com.lihuia.cache.UserTest - id=81, name=Lucy, phone=16666666666, age=16
[main] INFO com.lihuia.cache.UserTest - 6.更新Lucy的年龄
[main] INFO com.lihuia.cache.UserTest - 7.查询user_info表
[main] INFO com.lihuia.cache.UserTest - id=1, name=lihui, phone=17777777777, age=17
[main] INFO com.lihuia.cache.UserTest - id=2, name=lilei, phone=18888888888, age=19
[main] INFO com.lihuia.cache.UserTest - id=3, name=litao, phone=19999999999, age=20
[main] INFO com.lihuia.cache.UserTest - id=81, name=Lucy, phone=16666666666, age=26
[main] INFO com.lihuia.cache.UserTest - 8.查询Lucy的年龄
[main] INFO com.lihuia.cache.UserTest - id=81, name=Lucy, phone=16666666666, age=16
[main] INFO com.lihuia.cache.UserTest - 9.删除Lucy的信息
[main] INFO com.lihuia.cache.UserTest - 10.查询user_info表
[main] INFO com.lihuia.cache.UserTest - id=1, name=lihui, phone=17777777777, age=17
[main] INFO com.lihuia.cache.UserTest - id=2, name=lilei, phone=18888888888, age=19
[main] INFO com.lihuia.cache.UserTest - id=3, name=litao, phone=19999999999, age=20

OVER

发表回复