1. 程式人生 > >shiro原始碼分析篇4:自定義快取

shiro原始碼分析篇4:自定義快取

這篇講解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

  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。遇到問題網上沒有資料,如何解決就是進去取原始碼,找到報錯的地方,既然一個框架能執行這麼多年沒有問題,那麼基本是就是自己沒有玩轉它,有地方用錯了,通過跟蹤原始碼一下子就發現問題了。


菜鳥不易,望有問題指出,共同進步。