Mybatis:一種 Redis 緩存實現

前言

本文介紹一種 mybatis redis 緩存的實現方法,使用實例以下:redis

@Repository
public interface UserDao {
    @Cache(prefix="user:")
    @Select(...)
    public User findUserById(int userId);
}

經過使用 Cache 註解來標註哪些數據庫訪問(select)須要緩存,prefix 屬性設置 Redis key 前綴,這樣作的好處是將緩存的實現和業務邏輯分開,可擴展性強spring

實現

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 修改爲咱們提供的 MapperFactoryBeanProxymybatis

MapperFactoryBeanProxy

MapperFactoryBeanProxy 是對 mybatis 提供的 MapperFactoryBean 的代理,它有三個屬性(字段)app

public class MapperFactoryBeanProxy implements FactoryBean {

    private Class<?> mapperInterface;

    @Autowired
    private MapperCacheStrategy mapperCacheStrategy;

    private MapperFactoryBean mapperFactoryBean;
}
  • mapperInterfacemapper, mapper 接口類(例如 UserDao)ide

  • mapperCacheStrategy, 具體的緩存策略(模式)post

  • mapperFactoryBean, mybatis 提供的默認的 mapper factory beanthis

構造方法

保存 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 方法須要鏈接哪一個數據源~

相關文章
相關標籤/搜索