前言
本文介绍一种 mybatis redis 缓存的实现方法,使用实例如下:
@Repository
public interface UserDao {
@Cache(prefix="user:")
@Select(...)
public User findUserById(int userId);
}
通过使用 Cache 注解来标注哪些数据库访问(select)需要缓存,prefix 属性设置 Redis key 前缀,这样做的好处是将缓存的实现和业务逻辑分开,可扩展性强
实现
Cache
如上所述,Cache 注解用于注释需要缓存的 mapper 接口方法
public @interface Cache {
long expire() default DEFAULT_EXPIRE_TIME;
String prefix();
}
MapperScannerConfigurer
在 spring 容器中 mybatis 通常都会配置一个 MapperScannerConfigurer 用来指定 mapper(ORM)的位置(包名),通过阅读相关源代码可以知道 MapperScannerConfigurer 会为每个 mapper 接口生成一个动态代理,我们要做的就是扩展 MapperScannerConfigurer,给 mybatis 提供的动态代理提供一个 wrapper 包装
public class MapperScannerConfigurerProxy extends MapperScannerConfigurer {
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
super.postProcessBeanDefinitionRegistry(registry);
String[] beanDefinitionNames = registry.getBeanDefinitionNames();
for (String beanDefinitionName : beanDefinitionNames) {
BeanDefinition beanDefinition = registry.getBeanDefinition(beanDefinitionName);
if (!(beanDefinition instanceof GenericBeanDefinition)) {
continue;
}
GenericBeanDefinition genericBeanDefinition = (GenericBeanDefinition) beanDefinition;
if (!genericBeanDefinition.hasBeanClass()) {
continue;
}
if (genericBeanDefinition.getBeanClass() != MapperFactoryBean.class) {
continue;
}
genericBeanDefinition.setBeanClass(MapperFactoryBeanProxy.class);
genericBeanDefinition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
}
}
}
postProcessBeanDefinitionRegistry 是 spring bean 生命周期回调方法,这里先调用父类方法,然后遍历 bean registry,找到 MapperFactoryBean(mapper 动态代理工厂 bean),将它的 bean class 修改成我们提供的 MapperFactoryBeanProxy
MapperFactoryBeanProxy
MapperFactoryBeanProxy 是对 mybatis 提供的 MapperFactoryBean 的代理,它有三个属性(字段)
public class MapperFactoryBeanProxy implements FactoryBean {
private Class<?> mapperInterface;
@Autowired
private MapperCacheStrategy mapperCacheStrategy;
private MapperFactoryBean mapperFactoryBean;
}
mapperInterfacemapper, mapper 接口类(例如 UserDao)
mapperCacheStrategy, 具体的缓存策略(模式)
mapperFactoryBean, mybatis 提供的默认的 mapper factory bean
构造方法
保存 mapper interface 的引用以及创建 mybatis MapperFactoryBean 对象
public MapperFactoryBeanFactory(Class<?> mapperInterface) {
this.mapperInterface = mapperInterface;
mapperFactoryBean = new MapperFactoryBean<>(mapperInterface);
}
getObject
spring 通过调用 FactoryBean 的 getObject 方法创建 bean 对象,这里先调用 mybatis MapperFactoryBean 的 getObject 方法获取 mybatis 创建的动态代理,然后调用 Proxy.newProxyInstance 方法基于 mapper interface 再创建一个动态代理,handler 为 MapperProxy
@Override
public Object getObject() throws Exception {
mapperFactoryBean.afterPropertiesSet();
Object object = mapperFactoryBean.getObject();
return Proxy.newProxyInstance(getClass().getClassLoader(),
new Class<?>[]{mapperFactoryBean.getMapperInterface()},
new MapperProxy(object, mapperCacheStrategy));
}
MapperProxy
MapperProxy 动态代理 handler 用于实现具体的缓存策略
public class MapperProxy implements InvocationHandler {
private Object target;
private MapperCacheStrategy mapperCacheStrategy;
public MapperProxy(Object target, MapperCacheStrategy mapperCacheStrategy) {
this.target = target;
this.mapperCacheStrategy = mapperCacheStrategy;
}
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
...
}
}
invoke 方法基本流程:
如果是 Object 中定义的方法直接返回
判断方法时候有 Cache 注解,如果没有,表明不需要缓存,直接返回
调用 mapper cache strategy 类的 get 方法获取缓存,如果命中直接返回
调用 原始方法(查库)并更新缓存
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (Object.class.equals(method.getDeclaringClass())) {
try {
return method.invoke(this, args);
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}
Cache annotation = method.getAnnotation(Cache.class);
if (annotation == null) {
return method.invoke(target, args);
}
long expire = annotation.expire();
String prefix = annotation.prefix();
String key = getKey(prefix, args);
Object object = null;
try {
object = mapperCacheStrategy.get(key, method.getGenericReturnType());
} catch (Exception e) {
logger.error("mapperCacheStrategy.get " + key, e);
}
if (object != null) {
return object;
}
object = method.invoke(target, args);
if (!isBlank(object)) {
mapperCacheStrategy.set(key, object, expire);
}
return object;
}
这里没有考虑诸如 缓存穿透 之类的问题,读者可以自行扩展
总结
本文介绍了一种通过 扩展 mybatis,增加自定义注解来实现数据库缓存的方法,该方法不仅可以用于缓存处理,稍微修改一下就可以实现数据的读写分离,例如定义一个 DataSource 注解注释 mapper 方法需要连接哪个数据源~
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。