提到shiro的缓存要讲两个接口,一个是cacheManager一个是cache
cacheManager接口是获取cache的一个接口,其中只有一个getCache方法。
cache接口是shiro定义对cache数据CRUD操作的一个接口。
public interface CacheManager {
public <K, V> Cache<K, V> getCache(String name) throws CacheException;
}
public interface Cache<K, V> {
public V get(K key) throws CacheException;
public V put(K key, V value) throws CacheException;
public V remove(K key) throws CacheException;
public void clear() throws CacheException;
public int size();
public Set<K> keys();
public Collection<V> values();
}
如果是单机运行状态的话cacheManager直接使用org.apache.shiro.cache.MemoryConstrainedCacheManager这个类基本满足我们的需求,但如果要做第三方的缓存或者集群式就需要去实现CacheManager这个接口。
public class MyCacheManager implements CacheManager {
private RedisTemplate<String,Object> redisTemplate;
@Override
public <K, V> Cache<K, V> getCache(String name) throws CacheException {
System.out.println(name);
return new MyValueCache<K,V>(name,redisTemplate);
}
public RedisTemplate<String, Object> getRedisTemplate() {
return redisTemplate;
}
public void setRedisTemplate(RedisTemplate<String, Object> redisTemplate) {
this.redisTemplate = redisTemplate;
}
}
上面的redisTemplate通过spring的配置文件注入进来。
<bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate">
<property name="connectionFactory" ref="jedisConnFactory" />
<property name="KeySerializer">
<bean class="org.springframework.data.redis.serializer.StringRedisSerializer"></bean>
</property>
</bean>
然后在getCache里面返回一个新的cache对象既可,myValueCache代码如下。
/**
* redis的基础存储方式,此hash不一样,可以单独设置每一条数据的失效时间
* @param <K>
* @param <V>
*/
public class MyValueCache<K,V> implements Cache<K,V>,Serializable {
private final String REDIS_SHIRO_CACHE = "shiro-cache:";
private String cacheKey;
private RedisTemplate<K, V> redisTemplate;
private long globExpire = 1;//*60*60;
public MyValueCache(String name, RedisTemplate client){
this.cacheKey=this.REDIS_SHIRO_CACHE+name+":";
this.redisTemplate = client;
}
@Override
public V get(K key) throws CacheException {
redisTemplate.boundValueOps(getCacheKey(key)).expire(globExpire,TimeUnit.MINUTES);
return redisTemplate.boundValueOps(getCacheKey(key)).get();
}
@Override
public V put(K key, V value) throws CacheException {
V old = get(key);
redisTemplate.boundValueOps(getCacheKey(key)).set(value);
return old;
}
@Override
public V remove(K key) throws CacheException {
V old = get(key);
redisTemplate.delete(getCacheKey(key));
return old;
}
@Override
public void clear() throws CacheException {
redisTemplate.delete(keys());
}
@Override
public int size() {
return keys().size();
}
@Override
public Set keys() {
return redisTemplate.keys(getCacheKey("*"));
}
@Override
public Collection values() {
Set<K> set = keys();
List list = new LinkedList();
for(K s :set){
list.add(get(s));
}
return list;
}
private K getCacheKey(Object k){
return (K) (this.cacheKey+k);
}
}
这里的Cache实现我是直接调用了spring-data-redis包里面的客户端类里面的String方式存储也就是value是一个独立的对象,当然我还尝试了一下使用hash方式去存储:
public class MyHashCache<K,V> implements Cache<K,V>,Serializable {
private final String REDIS_SHIRO_CACHE = "shiro-cache:";
private String cacheKey;
private RedisTemplate<String, V> redisTemplate;
public MyHashCache(String name, RedisTemplate client){
this.cacheKey=this.REDIS_SHIRO_CACHE+name;
this.redisTemplate = client;
}
@Override
public V get(K key) throws CacheException {
return (V) redisTemplate.boundHashOps(this.cacheKey).get(key);
}
@Override
public V put(K key, V value) throws CacheException {
V old = get(key);
redisTemplate.boundHashOps(this.cacheKey).put(key,value);
return old;
}
@Override
public V remove(K key) throws CacheException {
V old = get(key);
redisTemplate.boundHashOps(this.cacheKey).delete(key);
return old;
}
@Override
public void clear() throws CacheException {
redisTemplate.boundHashOps(this.cacheKey).delete();
}
@Override
public int size() {
return redisTemplate.boundHashOps(this.cacheKey).keys().size();
}
@Override
public Set keys() {
return redisTemplate.boundHashOps(this.cacheKey).keys();
}
@Override
public Collection values() {
List<Object> keys = redisTemplate.boundHashOps(this.cacheKey).values();
return keys;
}
}
造成这样的原因是因为一开始使用String去存储的时候keys那个方法始终无法获得该有的set值,后来发现是那个key的参数有问题必须要与put方法传入的key值类型一致可以查询到。
在这里要说一下redis内部的过期时间设置。shiro内部是有实现自动定时器去定时清除过期的session缓存但它不会自动清除权限缓存,如果一个用户在非正常退出情况下系统没有接到退出请求是不会自动去清理缓存的,而此时如果不设置redis值的过期时间就会造成缓存一直存在,如果登陆用户多了就会造成不必要的空间浪费,所以我们要设置一下过期时间,时间的大小最好要超过session的过期时间但不要太多。
使用redis String存储方式可以设置任意一个key值的过期时间,而hash只能设置在统一的key值上,这是因为String的存储方式是(key,value对象),而hash存储方式是(key1,key2,value)想要找到key2必要先找到key1,所以hash设置过期时间只会设计key1下所有key的过期时间,这样就会发生一个新用户刚登陆上去他的缓存就被清除的情况,所以这里最好使用String方式去存储。
最后一点shiro在自己的类中还实现了一个Destroyable接口,但是我在测试的时候发现实现与不实现系统都可以正常运行,而通过代码(如下)可以看出他只是去删除本地的cache缓存类,而我们本地是没有cache缓存的所以就没有实现那个接口。
public void destroy() throws Exception {
while (!caches.isEmpty()) {
for (Cache cache : caches.values()) {
LifecycleUtils.destroy(cache);
}
caches.clear();
}
}
在网上有好多教程都是教大家怎么实现自己的缓存其方法也与我的类似,核心类都是实现cache与CacheManager这两个接口然后再将cacheManager实现类注入到DefaultWebSecurityManager类中,最后再自己实现AbstractSessionDAO这个接口,再将这个实现类注入到DefaultWebSessionManager类中,这种方式相当于权限缓存与session缓存是两个相互没有关系的类,而我所奇怪的地方是经过我的测试,我的这种缓存的实现方式竟然可以同时接管这两种缓存。在经过debug得知某一个方法调用了DefaultWebSessionManager里面的一个setCacheManager方法,这个方法是给cacheManager赋值的而DefaultWebSessionManager又是通过怎样的方式去给SessionDAO赋值这就不得而知了,如果哪位朋友如果知道的话可以在评论区留言贴出代码,不甚感谢。