mybatis实现自定义二级缓存,最简单的做法,实现一个org.apache.ibatis.cache.Cache接口就可以了
然后就是在sql xml文件里使用,<cache eviction="LRU" type="com.mark.demo.shiro.mybatis.cache.MyBatisRedisCache" ></cache>
这种简单实现有更新操作时会过期同一个配置文件里的查询缓存,但是跨配置文件就不好处理了。
为了处理跨配置文件更新过期,需要自己实现RedisCachingExecutor implements Interceptor
下面贴代码:
package com.mark.demo.shiro.mybatis.cache;
import java.io.Serializable;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import org.apache.ibatis.cache.Cache;
import org.apache.ibatis.cache.CacheKey;
import org.apache.ibatis.cache.CacheKey;
import com.mark.demo.shiro.utils.JedisUtils;
import com.mark.demo.shiro.utils.ObjectUtils;
/*
*hxp(hxpwangyi@126.com)
*2017年9月8日
*
*/
public class MyBatisRedisCache implements Cache,Serializable{
public static final String mybatis_cache_prefix="mybatis_cache";
private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
private String id;
public MyBatisRedisCache(final String id) {
if (id == null) {
throw new IllegalArgumentException("缓存没有初始化id");
}
this.id = id;
}
@Override
public String getId() {
return this.id;
}
@Override
public int getSize() {
return JedisUtils.getMapLen(mybatis_cache_prefix);
}
@Override
public void putObject(Object key, Object value) {
CacheKey cacheKey=(CacheKey)key;
String [] keyAry=cacheKey.toString().split(":");
String myKey=keyAry[2];
JedisUtils.setMapField(mybatis_cache_prefix, ObjectUtils.serialize(myKey), value);
}
@Override
public Object getObject(Object key) {
CacheKey cacheKey=(CacheKey)key;
String [] keyAry=cacheKey.toString().split(":");
String myKey=keyAry[2];
return JedisUtils.getMapFiled(mybatis_cache_prefix, ObjectUtils.serialize(myKey));
}
@Override
public Object removeObject(Object key) {
Object ret=JedisUtils.getMapFiled(mybatis_cache_prefix, ObjectUtils.serialize(key));
JedisUtils.removeMapField(mybatis_cache_prefix, ObjectUtils.serialize(key));
return ret;
}
@Override
public void clear() {
JedisUtils.del(mybatis_cache_prefix);
}
@Override
public ReadWriteLock getReadWriteLock() {
return readWriteLock;
}
}
package com.mark.demo.shiro.mybatis.cache;
import java.util.HashSet;
import java.util.Properties;
import java.util.Set;
import org.apache.ibatis.cache.CacheKey;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Plugin;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
/*
*hxp(hxpwangyi@126.com)
*2017年9月8日
*
*/
@Intercepts(value = {
@Signature(args = {MappedStatement.class, Object.class, RowBounds.class,ResultHandler.class}, method = "query", type = Executor.class),
@Signature(args = {MappedStatement.class, Object.class}, method = "update", type = Executor.class),
@Signature(args = {boolean.class}, method = "commit", type = Executor.class),
@Signature(args = {boolean.class}, method = "rollback", type = Executor.class),
@Signature(args = {boolean.class}, method = "close", type = Executor.class)
})
public class RedisCachingExecutor implements Interceptor {
private Set<String> updateStatementOnCommit = new HashSet<String>();
RedisCachingManager cachingManager = RedisCachingManagerImpl.getInstance();
public Object intercept(Invocation invocation) throws Throwable {
String name = invocation.getMethod().getName();
Object result =null;
if("query".equals(name))
{
result = this.processQuery(invocation);
}
else if("update".equals(name))
{
result = this.processUpdate(invocation);
}
else if("commit".equals(name))
{
result = this.processCommit(invocation);
}
else if("rollback".equals(name))
{
result = this.processRollback(invocation);
}
else if("close".equals(name))
{
result = this.processClose(invocation);
}
return result;
}
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
/**
* when executing a query operation
* 1. record this statement's id and it's corresponding Cache Object into Global Caching Manager;
* 2. record this statement's id and
* @param invocation
* @return
* @throws Throwable
*/
protected Object processQuery(Invocation invocation) throws Throwable {
Object result = invocation.proceed();
return result;
}
protected Object processUpdate(Invocation invocation) throws Throwable {
Object result = invocation.proceed();
MappedStatement mappedStatement = (MappedStatement)invocation.getArgs()[0];
updateStatementOnCommit.add(mappedStatement.getId());
return result;
}
protected Object processCommit(Invocation invocation) throws Throwable {
Object result = invocation.proceed();
refreshCache();
return result;
}
protected Object processRollback(Invocation invocation) throws Throwable {
Object result = invocation.proceed();
clearSessionData();
return result;
}
protected Object processClose(Invocation invocation) throws Throwable {
Object result = invocation.proceed();
boolean forceRollback = (Boolean) invocation.getArgs()[0];
if(forceRollback)
{
clearSessionData();
}
else
{
refreshCache();
}
return result;
}
/**
* when the sqlSession has been committed,rollbacked,or closed,
* session buffer query CacheKeys and update Statement collections should be cleared.
*
* 当SqlSession 执行了commit()、rollback()、close()方法,
* Session级别的查询语句产生的CacheKey集合以及 执行的更新语句集合应该被清空
*/
private synchronized void clearSessionData()
{
updateStatementOnCommit.clear();
}
/**
* refresh the session cache,there are two things have to do:
* 1. add this session scope query logs to global cache Manager
* 2. clear the related caches according to the update statements as configured in "dependency" file
* 3. clear the session data
*/
private synchronized void refreshCache()
{
cachingManager.clearRelatedCaches(updateStatementOnCommit);
clearSessionData();
}
/**
*
*
* Executor插件配置信息加载点
* properties中有 "dependency" 属性来指示 配置的缓存依赖配置信息,读取文件,初始化EnhancedCacheManager
*/
public void setProperties(Properties properties) {
if(!cachingManager.isInitialized())
{
cachingManager.initialize(properties);
}
}
}
package com.mark.demo.shiro.mybatis.cache;
import java.util.Properties;
import java.util.Set;
import org.apache.ibatis.cache.Cache;
public interface RedisCachingManager {
public boolean isInitialized();
/**
* MyBatis是否开启了二级缓存,即 <setting name="cacheEnabled" value="true"/>
* @return
*/
public boolean isCacheEnabled();
/**
*
* 初始化 缓存管理器,应该只被调用一次;
*
* @param properties
* properties 中至少包含两个属性:
* dependency : 该值表示着缓存依赖配置文件的位置
* cacheEnbled : "true" or "false",该配置必须要与<setting name="cacheEnabled">的值保持一致
*/
public void initialize(Properties properties);
/**
* 整个插件的核心,根据指定的statementId更新与之关联的二级缓存
* 传入的StatementId集合是由会话级别的update语句对应的StatementId,
* EnhancedCachingManager将通过查询相应的StatementId去查看是否配置了依赖关系,如果有,则将依赖关系中的statementId的查询缓存全部清空
* @param set
*/
public void clearRelatedCaches(Set<String> set);
}
package com.mark.demo.shiro.mybatis.cache;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import org.apache.ibatis.cache.Cache;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.parsing.XNode;
import org.apache.ibatis.parsing.XPathParser;
import com.mark.demo.shiro.utils.JedisUtils;
import com.mark.demo.shiro.utils.PropertiesLoader;
public class RedisCachingManagerImpl implements RedisCachingManager{
//每一个statementId 更新依赖的statementId集合
private Map<String,Set<String>> observers=new ConcurrentHashMap<String,Set<String>>();
private boolean initialized = false;
private boolean cacheEnabled = false;
private volatile static RedisCachingManagerImpl enhancedCacheManager;
private RedisCachingManagerImpl(){}
public static RedisCachingManagerImpl getInstance()
{
if(enhancedCacheManager==null){
synchronized (RedisCachingManagerImpl.class) {
if(enhancedCacheManager==null){
enhancedCacheManager=new RedisCachingManagerImpl();
}
}
}
return enhancedCacheManager;
}
public void clearRelatedCaches(final Set<String> set) {
for(String observable:set)
{
Set<String> relatedStatements = observers.get(observable);
for(String statementId:relatedStatements)
{
JedisUtils.removeMapField(MyBatisRedisCache.mybatis_cache_prefix, statementId);
}
}
}
public boolean isInitialized() {
return initialized;
}
public void initialize(Properties properties)
{
PropertiesLoader loader=new PropertiesLoader("mybatis.properties");
String dependency = loader.getProperty("dependencys");
if(!("".equals(dependency) || dependency==null))
{
InputStream inputStream;
try
{
inputStream = Resources.getResourceAsStream(dependency);
XPathParser parser = new XPathParser(inputStream);
List<XNode> statements = parser.evalNodes("/dependencies/statements/statement");
for(XNode node :statements)
{
Set<String> temp = new HashSet<String>();
List<XNode> obs = node.evalNodes("observer");
for(XNode observer:obs)
{
temp.add(observer.getStringAttribute("id"));
}
this.observers.put(node.getStringAttribute("id"), temp);
}
initialized = true;
} catch (IOException e) {
e.printStackTrace();
}
}
//cacheEnabled
String cacheEnabled = properties.getProperty("cacheEnabled", "true");
if("true".equals(cacheEnabled))
{
this.cacheEnabled = true;
}
}
public boolean isCacheEnabled() {
return cacheEnabled;
}
}
<?xml version="1.0" encoding="UTF-8"?>
<dependencies>
<statements>
<!-- 角色授权后刷新角色缓存 -->
<statement id="com.mark.demo.shiro.mapper.MenuMapper.updateMenu">
<observer id="com.mark.demo.shiro.mapper.UserMapper.getMenuTopLever"/>
</statement>
</statements>
</dependencies>
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<!-- 全局参数 -->
<settings>
<!-- 使全局的映射器启用或禁用缓存。 -->
<setting name="cacheEnabled" value="true"/>
<!-- 全局启用或禁用延迟加载。当禁用时,所有关联对象都会即时加载。 -->
<setting name="lazyLoadingEnabled" value="false"/>
<!-- 当启用时,有延迟加载属性的对象在被调用时将会完全加载任意属性。否则,每种属性将会按需要加载。 -->
<setting name="aggressiveLazyLoading" value="true"/>
<!-- 是否允许单条sql 返回多个数据集 (取决于驱动的兼容性) default:true -->
<setting name="multipleResultSetsEnabled" value="true"/>
<!-- 是否可以使用列的别名 (取决于驱动的兼容性) default:true -->
<setting name="useColumnLabel" value="true"/>
<!-- 允许JDBC 生成主键。需要驱动器支持。如果设为了true,这个设置将强制使用被生成的主键,有一些驱动器不兼容不过仍然可以执行。 default:false -->
<setting name="useGeneratedKeys" value="false"/>
<!-- 指定 MyBatis 如何自动映射 数据基表的列 NONE:不隐射 PARTIAL:部分 FULL:全部 -->
<setting name="autoMappingBehavior" value="PARTIAL"/>
<!-- 这是默认的执行类型 (SIMPLE: 简单; REUSE: 执行器可能重复使用prepared statements语句;BATCH: 执行器可以重复执行语句和批量更新) -->
<setting name="defaultExecutorType" value="REUSE"/>
<!-- 使用驼峰命名法转换字段。 -->
<setting name="mapUnderscoreToCamelCase" value="true"/>
<!-- 设置本地缓存范围 session:就会有数据的共享 statement:语句范围 (这样就不会有数据的共享 ) defalut:session -->
<setting name="localCacheScope" value="STATEMENT"/>
<!-- 设置但JDBC类型为空时,某些驱动程序 要指定值,default:OTHER,插入空值时不需要指定类型 -->
<setting name="jdbcTypeForNull" value="NULL"/>
</settings>
<typeAliases>
<package name="com.mark.demo.shiro.entity" />
</typeAliases>
<!-- 插件配置 -->
<plugins>
<plugin interceptor="com.mark.demo.shiro.mybatis.PaginationInterceptor"/>
<plugin interceptor="com.mark.demo.shiro.mybatis.cache.RedisCachingExecutor">
<property name="dependency" value="dependencys.xml"/>
<property name="cacheEnabled" value="true"/>
</plugin>
</plugins>
</configuration>
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.mark.demo.shiro.mapper.UserMapper">
<cache eviction="LRU" type="com.mark.demo.shiro.mybatis.cache.MyBatisRedisCache" ></cache>
<resultMap type="User" id="userMap">
<result column="userId" property="id" />
<result column="userName" property="userName" />
<result column="password" property="password" />
<result column="age" property="age" />
<result column="sex" property="sex" />
<result column="phone" property="phone" />
</resultMap>
<select id="getUserByUserName" parameterType="String" resultMap="userMap">
select * from user where userName=#{userName}
</select>
<resultMap type="Menu" id="menuMap">
<result column="menuId" property="id"/>
<result column="menuName" property="menuName"/>
<result column="menuDesc" property="menuDesc"/>
<result column="link" property="link"/>
<result column="icon" property="icon"/>
</resultMap>
<select id="getMenuTopLever" resultMap="menuMap" useCache="true">
select * from menu where pid=-1
</select>
</mapper>
demo地址:https://github.com/13567436138/shiro-demo.git