shiro源码分析篇4:自定义缓存

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/u010399009/article/details/78308322

这篇讲解shiro如何管理session,如何与ehcache结合。我们自己如何写个简单的缓存替换ehcache。

首先来看看配置

  <!-- 缓存管理器 使用Ehcache实现 -->
    <bean id="cacheManagerShiro" class="org.apache.shiro.cache.ehcache.EhCacheManager">
        <property name="cacheManagerConfigFile" value="classpath:ehcache.xml"/>
    </bean>
    <!-- 会话DAO -->
    <bean id="sessionDAO" class="org.apache.shiro.session.mgt.eis.EnterpriseCacheSessionDAO">
        <property name="activeSessionsCacheName" value="shiro-activeSessionCache"/>
        <property name="sessionIdGenerator" ref="sessionIdGenerator"/>
    </bean>
   <!-- 会话验证调度器 -->
    <bean id="sessionValidationScheduler" class="org.apache.shiro.session.mgt.quartz.QuartzSessionValidationScheduler">
        <property name="sessionValidationInterval" value="1800000"/>
        <property name="sessionManager" ref="sessionManager"/>
    </bean>
  <!-- 会话管理器 -->
    <bean id="sessionManager" class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager">
        <property name="globalSessionTimeout" value="1800000"/>
        <property name="deleteInvalidSessions" value="true"/>
        <property name="sessionValidationSchedulerEnabled" value="true"/>
        <property name="sessionValidationScheduler" ref="sessionValidationScheduler"/>
        <property name="sessionDAO" ref="sessionDAO"/>
        <property name="sessionIdCookieEnabled" value="true"/>
        <property name="sessionIdCookie" ref="sessionIdCookie"/>
    </bean>

    <!-- 安全管理器 -->
    <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
        <property name="realm" ref="userRealm"/>
        <property name="sessionManager" ref="sessionManager"/>
        <property name="cacheManager" ref="cacheManagerShiro"/>
    </bean>

先看EhCacheManager

public class EhCacheManager implements CacheManager, Initializable, Destroyable 

public interface CacheManager {
    public <K, V> Cache<K, V> getCache(String name) throws CacheException;
}

CacheManager : 缓存控制器,来管理如用户、角色、权限等的缓存的;因为这些数据基本 上很少去改变,放到缓存中后可以提高访问的性能。
CacheManager从接口方法来看就是用来获取Cache的。
再来看看cache定义了什么

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();

从上面来看,我们要自定义缓存要做两件事情。第一件实现Cache接口。第二件实现CacheManager接口。然后修改配置文件即可。
我们来看看EhCacheManager中getCache具体如何实现

  public final <K, V> Cache<K, V> getCache(String name) throws CacheException {

        if (log.isTraceEnabled()) {
            log.trace("Acquiring EhCache instance named [" + name + "]");
        }

        try {
            net.sf.ehcache.Ehcache cache = ensureCacheManager().getEhcache(name);
            if (cache == null) {
                if (log.isInfoEnabled()) {
                    log.info("Cache with name '{}' does not yet exist.  Creating now.", name);
                }
                this.manager.addCache(name);
                cache = manager.getCache(name);
                if (log.isInfoEnabled()) {
                    log.info("Added EhCache named [" + name + "]");
                }
            } else {
                if (log.isInfoEnabled()) {
                    log.info("Using existing EHCache named [" + cache.getName() + "]");
                }
            }
            return new EhCache<K, V>(cache);
        } catch (net.sf.ehcache.CacheException e) {
            throw new CacheException(e);
        }
    }

return new EhCache

public class EnterpriseCacheSessionDAO extends CachingSessionDAO
public abstract class CachingSessionDAO extends AbstractSessionDAO implements CacheManagerAware 
public abstract class AbstractSessionDAO implements SessionDAO 

先看看CacheManagerAware

public interface CacheManagerAware {
  void setCacheManager(CacheManager cacheManager);
}

有没有觉得很熟悉,Aware当然这里是shiro自定义的,不过同样的道理,给我们的CachingSessionDAO赋予了CacheManager的能力。看看如何实现的

 public void setCacheManager(CacheManager cacheManager) {
        this.cacheManager = cacheManager;
    }

现在CachingSessionDAO已经具备缓存管理的能力了。

sessionDAO就提供了一些对session操作的基本功能了CRUD。

拿一个进行分析:
Serializable create(Session session);

实现这个接口方法的地方.CachingSessionDao

扫描二维码关注公众号,回复: 4561076 查看本文章
  public Serializable create(Session session) {
        Serializable sessionId = super.create(session);
        cache(session, sessionId);
        return sessionId;
    }

调用父类的create(session)。也就是AbstractSessionDAO

  public Serializable create(Session session) {
        Serializable sessionId = doCreate(session);
        verifySessionId(sessionId);
        return sessionId;
    }
    protected abstract Serializable doCreate(Session session);

doCreate是一个抽象的方法。可以理解为模板模式。将该方法延迟到子类实现。可以有不同的实现。这个实现类就是EnterpriseCacheSessionDAO

    protected Serializable doCreate(Session session) {
        Serializable sessionId = generateSessionId(session);
        assignSessionId(session, sessionId);
        return sessionId;
    }

generateSessionId用我们的生成sessionId的类来执行,也就是我们spring-shiro.xml中定义的

<!-- 会话ID生成器 -->
    <bean id="sessionIdGenerator" class="org.apache.shiro.session.mgt.eis.JavaUuidSessionIdGenerator"/>

是不是现在又有种感觉,我可以自定义这个会话id生成器,只有实现这个生成id的接口即可。

好的软件设计是开发关闭原则,对外是可扩展的。

来看看生成Session之后如何缓存的。

  protected void cache(Session session, Serializable sessionId) {
        if (session == null || sessionId == null) {
            return;
        }
        Cache<Serializable, Session> cache = getActiveSessionsCacheLazy();
        if (cache == null) {
            return;
        }
        cache(session, sessionId, cache);
    }

先看看getActiveSessionsCacheLazy();

 private Cache<Serializable, Session> getActiveSessionsCacheLazy() {
        if (this.activeSessions == null) {
            this.activeSessions = createActiveSessionsCache();
        }
        return activeSessions;
    }

    protected Cache<Serializable, Session> createActiveSessionsCache() {
        Cache<Serializable, Session> cache = null;
        CacheManager mgr = getCacheManager();
        if (mgr != null) {
            String name = getActiveSessionsCacheName();
            cache = mgr.getCache(name);
        }
        return cache;
    }

先获取CacheManager,也就是我们定义的EhCacheManager,
cache = mgr.getCache(name);调用我们实现CacheManager的getCache方法返回Ehcache。

 protected void cache(Session session, Serializable sessionId, Cache<Serializable, Session> cache) {
        cache.put(sessionId, session);
    }

缓存session就是调用了Ehcache.put。

关于其他的sessionDao读者可以自行分析。

来看看sessionManager
sessionManager:如果写过 Servlet 就应该知道 Session 的概念,Session 呢需要有人去管理 它的生命周期,这个组件就是 SessionManager;而 Shiro 并不仅仅可以用在 Web 环境,也 可以用在如普通的 JavaSE 环境、EJB 等环境;所有呢,Shiro 就抽象了一个自己的 Session 来管理主体与应用之间交互的数据;这样的话,比如我们在 Web 环境用,刚开始是一台 Web 服务器;接着又上了台 EJB 服务器;这时想把两台服务器的会话数据放到一个地方, 这个时候就可以实现自己的分布式会话(如把数据放到 Memcached 服务器);

public class DefaultWebSessionManager extends DefaultSessionManager implements WebSessionManager
public class DefaultSessionManager extends AbstractValidatingSessionManager implements CacheManagerAware
public abstract class AbstractValidatingSessionManager extends AbstractNativeSessionManager
        implements ValidatingSessionManager, Destroyable
public abstract class AbstractNativeSessionManager extends AbstractSessionManager implements NativeSessionManager
public abstract class AbstractSessionManager implements SessionManager  

说白了sessionDao相当于对session进行操作。而sessionManager就是我们具体的业务处理了。

来看看SecurityManager
SecurityManager:相当于 SpringMVC 中的 DispatcherServlet 或者 Struts2 中的 FilterDispatcher;是 Shiro 的心脏;所有具体的交互都通过 SecurityManager 进行控制;它管 理着所有 Subject、且负责进行认证和授权、及会话、缓存的管理。

public class DefaultWebSecurityManager extends DefaultSecurityManager implements WebSecurityManager
public class DefaultSecurityManager extends SessionsSecurityManager
public abstract class SessionsSecurityManager extends AuthorizingSecurityManager 
public abstract class AuthorizingSecurityManager extends AuthenticatingSecurityManager 
....

SecurityManager就是进行资源协调,资源控制。

我们来看看一个Session获取的过程,看看使用了哪些类,哪些接口
还是从

    public Subject createSubject(SubjectContext subjectContext) {
        //create a copy so we don't modify the argument's backing map:
        SubjectContext context = copy(subjectContext);

        //ensure that the context has a SecurityManager instance, and if not, add one:
        context = ensureSecurityManager(context);

        //Resolve an associated Session (usually based on a referenced session ID), and place it in the context before
        //sending to the SubjectFactory.  The SubjectFactory should not need to know how to acquire sessions as the
        //process is often environment specific - better to shield the SF from these details:
        context = resolveSession(context);

        //Similarly, the SubjectFactory should not require any concept of RememberMe - translate that here first
        //if possible before handing off to the SubjectFactory:
        context = resolvePrincipals(context);

        Subject subject = doCreateSubject(context);

        //save this subject for future reference if necessary:
        //(this is needed here in case rememberMe principals were resolved and they need to be stored in the
        //session, so we don't constantly rehydrate the rememberMe PrincipalCollection on every operation).
        //Added in 1.2:
        save(subject);

        return subject;
    }

这个类是DefaultSecurityManager.createSubject,管理subject的创建,进入context = resolveSession(context);

 protected SubjectContext resolveSession(SubjectContext context) {
        if (context.resolveSession() != null) {
            log.debug("Context already contains a session.  Returning.");
            return context;
        }
        try {
            //Context couldn't resolve it directly, let's see if we can since we have direct access to 
            //the session manager:
            Session session = resolveContextSession(context);
            if (session != null) {
                context.setSession(session);
            }
        } catch (InvalidSessionException e) {
            log.debug("Resolved SubjectContext context session is invalid.  Ignoring and creating an anonymous " +
                    "(session-less) Subject instance.", e);
        }
        return context;
    }

context.resolveSession()从context取出session。
第一次肯定是没有的。那么就要生成
调用Session session = resolveContextSession(context);

SessionsSecurityManager类
public Session getSession(SessionKey key) throws SessionException {
        return this.sessionManager.getSession(key);
    }

sessionManager.getSession(key).根据key获取session,sessionManager业务操作,下面肯定要使用sessionDao了

AbstractNativeSessionManager类
 public Session getSession(SessionKey key) throws SessionException {
        Session session = lookupSession(key);
        return session != null ? createExposedSession(session, key) : null;
    }

AbstractValidatingSessionManager类

    @Override
    protected final Session doGetSession(final SessionKey key) throws InvalidSessionException {
        enableSessionValidationIfNecessary();

        log.trace("Attempting to retrieve session with key {}", key);

        Session s = retrieveSession(key);
        if (s != null) {
            validate(s, key);
        }
        return s;
    }

DefaultSessionManager类

 protected Session retrieveSession(SessionKey sessionKey) throws UnknownSessionException {
        Serializable sessionId = getSessionId(sessionKey);
        if (sessionId == null) {
            log.debug("Unable to resolve session ID from SessionKey [{}].  Returning null to indicate a " +
                    "session could not be found.", sessionKey);
            return null;
        }
        Session s = retrieveSessionFromDataSource(sessionId);
        if (s == null) {
            //session ID was provided, meaning one is expected to be found, but we couldn't find one:
            String msg = "Could not find session with ID [" + sessionId + "]";
            throw new UnknownSessionException(msg);
        }
        return s;
    }
  protected Session retrieveSessionFromDataSource(Serializable sessionId) throws UnknownSessionException {
        return sessionDAO.readSession(sessionId);
    }

业务层调用数据库层。现在到了return sessionDAO.readSession(sessionId);

CachingSessionDAO类

public Session readSession(Serializable sessionId) throws UnknownSessionException {
        Session s = getCachedSession(sessionId);
        if (s == null) {
            s = super.readSession(sessionId);
        }
        return s;
    }
 protected Session getCachedSession(Serializable sessionId) {
        Session cached = null;
        if (sessionId != null) {
            Cache<Serializable, Session> cache = getActiveSessionsCacheLazy();
            if (cache != null) {
                cached = getCachedSession(sessionId, cache);
            }
        }
        return cached;
    }

更加sessionId从缓存中取出Session。

到此,又把shiro如何从缓存中取出Session讲了一遍。

接下来本篇的自定义缓存,上面提到要自定义缓存必须实现两个接口
Cache,CacheManager

CustomCache.java

package com.share1024.cache;

import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheException;

import java.util.Collection;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

/**
 * @author : yesheng
 * @Description :
 * @Date : 2017/10/22
 */
public class CustomCache<K,V> implements Cache<K,V>{

    private ConcurrentHashMap<K,V> cache = new ConcurrentHashMap<K, V>();

    public V get(K key) throws CacheException {
        return cache.get(key);
    }

    public V put(K key, V value) throws CacheException {
        return cache.put(key,value);
    }

    public V remove(K key) throws CacheException {
        return cache.remove(key);
    }

    public void clear() throws CacheException {
        cache.clear();
    }

    public int size() {
        return cache.size();
    }

    public Set<K> keys() {
        return cache.keySet();
    }

    public Collection<V> values() {
        return cache.values();
    }
}

CustomCacheManager.java

package com.share1024.cache;

import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheException;
import org.apache.shiro.cache.CacheManager;

import java.util.concurrent.ConcurrentHashMap;

/**
 * @author : yesheng
 * @Description :
 * @Date : 2017/10/22
 */
public class CustomCacheManager implements CacheManager{

    private final ConcurrentHashMap<String,Cache> caches = new ConcurrentHashMap<String, Cache>();

    public <K, V> Cache<K, V> getCache(String name) throws CacheException {

        Cache cache = caches.get(name);

        if(cache == null){
            cache = new CustomCache<K,V>();
            caches.put(name,cache);
        }
        return cache;
    }
}

然后修改spring-shiro.xml即可。

上面的例子只是用一个map来做缓存,只是为了说明如何自定义,没有做过多的处理,比如缓存失效,线程安全,命中率等等问题。

本人曾经尝试过用redis来处理shiro框架中session跨域问题,但是一直百度,google,想找到现成的例子,加工就能用。但是一直找了几次没有找到合适的,就放弃了。这次彻底研究源码,下篇就讲解如何实现。

PS:源码这个东西建议大家多读读,我新去一家公司,由于项目比较老,ORM用的是公司自己的框架,没有用hibernate,jpa,mybatis。遇到问题网上没有资料,如何解决就是进去取源码,找到报错的地方,既然一个框架能运行这么多年没有问题,那么基本是就是自己没有玩转它,有地方用错了,通过跟踪源码一下子就发现问题了。


菜鸟不易,望有问题指出,共同进步。

猜你喜欢

转载自blog.csdn.net/u010399009/article/details/78308322