Spring-ehcache RMI形式的分佈式緩存配置

簡介

這是本人因爲工作需要研究的關於ehcache的分佈式RMI模式的使用心得已經自己的一些心得。

git 源碼

git 樣例代碼

詳細介紹

話不多說,下面結合demo分步做詳細的介紹

jar依賴

  • ehcache所需jar:ehchache-core
  • spring註解所需 spring-context
<dependency>
   <groupId>net.sf.ehcache</groupId>
   <artifactId>ehcache-core</artifactId>
   <version>2.6.6</version>
</dependency>
<dependency>
   <groupId>org.springframework</groupId>
   <artifactId>spring-context-support</artifactId>
   <version>4.2.7.RELEASE</version>
</dependency>

spring.xml文件中引用spring-ehcache.xml

註解使用cacheManager

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:cache="http://www.springframework.org/schema/cache"
       xmlns:p="http://www.springframework.org/schema/p"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
			http://www.springframework.org/schema/beans/spring-beans.xsd
			http://www.springframework.org/schema/cache
       	http://www.springframework.org/schema/cache/spring-cache.xsd">
    
    <!-- ehcache config -->
    <cache:annotation-driven  cache-manager="ehCacheCacheManager"/>
    <bean id="ehCacheCacheManager" class="org.springframework.cache.ehcache.EhCacheCacheManager" p:cache-manager-ref="cacheManagerFactoryBean"/>
    <!-- EhCache library setup -->
    <bean id="cacheManagerFactoryBean" class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean" p:config-location="classpath:spring-ehcache.xml" p:shared="true"/>

</beans>

spring-ehcache.xml

<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:noNamespaceSchemaLocation="ehcache.xsd" updateCheck="false"
    monitoring="autodetect" dynamicConfig="true">

ehcache標籤的屬性

  • name 名稱-可選,唯一。名字用於註解或區別Terracotta集羣緩存。對於
    Terracotta集羣緩存,緩存管理器的名字和緩存的名字的組合能夠在Terracotta集羣緩存中標誌一個特定的緩存存儲。
  • updateCheck 更新檢查-一個可選的布爾值標誌符,用於標誌緩存管理器是否應該通過網絡檢查Ehcache的新版本。默認爲true。
  • dynamicConfig 動態配置 - 可選。用於關閉與緩存管理器相關的緩存的動態配置。默認爲true,即動態配置爲開啓狀態。動態配置的緩存可以根據緩存對象在運行狀態改變自己的TTI,TTL和最大磁盤空間和內在容量
  • monitoring 監控 - 可選。決定緩存管理器是否應該自動註冊SampledCacheMBean到系統MBean服務器上。

cacheManagerPeerProviderFactory

它分佈式緩存管理器提供者,指定一個CacheManagerPeerProviderFactory,它將用於創建一個CacheManagerPeerProvider, CacheManagerPeerProvider偵測集羣中的其它事務管理器,實現和分佈式環境下的緩存同步。

相關屬性介紹:

  • propertySeparator 拆分上面properties屬性的分隔符
  • peerDiscovery=manual 手動的緩存同步
  • peerDiscovery=automatic 廣播式的緩存同步
  • rmiUrls 指的是手動偵測緩存地址,每一次當請求相應的緩存信息時,程序會先從配置的rmiUrls裏去分別讀取緩存,如果無該緩存信息,則生成緩存,存儲在cacheManagerPeerListenerFactory所配置的地址和端口的對應的緩存裏
    地址與地址之間用|(豎線)來分割。
    • url填寫規則:
      //(雙斜槓)+cacheManagerPeerListenerFactory屬性中配置的hostName+:(冒號)+端口+/(斜槓)+緩存屬性名稱

RMI 手動配置

<!-- 手動配置rmi同步的地址信息(這種模式本人親測多服務器試驗還有問題)-->
<cacheManagerPeerProviderFactory
        class="net.sf.ehcache.distribution.RMICacheManagerPeerProviderFactory"
              properties="peerDiscovery=manual, 
              rmiUrls=//127.0.0.1:40002/testCache|//127.0.0.1:40002/testCache2 "
        propertySeparator="," />

RMI 自動組播

這樣當緩存改變時,ehcache會向230.0.0.1端口4446發RMI UDP組播包
** 坑巨**,組播模式下請勿自己設置 hostName=localhost 因爲這樣解析出來的地址127.0.0.1 當在不同設備上部署的時候根本識別不出來
如果需要區分環境,開發和準生產,可以設置不同的組播地址,避免不同環境相互干擾

  • mulicastGroupAddress 組播組地址
    • 組播地址:稱爲組播組的一組主機所共享的地址。組播地址的範圍在224.0.0.0 —— 239.255.255.255之間(都爲D類地址 1110開頭)
  • mulicastGroupPort 廣播組端口
  • timeToLive
    0是限制在同一個服務器
    1是限制在同一個子網 (ip 和 子網掩碼轉換成2進制數進行與操作,得到值想用即爲統一子網)
    32是限制在同一個網站
    64是限制在同一個region
    128是限制在同一個大洲
    255是不限制
<!-- 自動廣播式rmi形式(親測可用) --> 
<cacheManagerPeerProviderFactory 
  class="net.sf.ehcache.distribution.RMICacheManagerPeerProviderFactory"
	    properties="peerDiscovery=automatic, 
	               multicastGroupAddress=230.0.0.1,
	               multicastGroupPort=4446, 
	               timeToLive=32"/>
    
<cacheManagerPeerListenerFactory
    class="net.sf.ehcache.distribution.RMICacheManagerPeerListenerFactory"
            properties="port=40002,socketTimeoutMillis=2000"/>
    

cacheManagerPeerListenerFactory

每個CacheManagerPeerListener監聽從成員們發向當前CacheManager的消息。配置 CacheManagerPeerListener需要指定一個CacheManagerPeerListenerFactory,它以插件的機制實現, 用來創建CacheManagerPeerListener。
  Ehcache有一個內置的基於RMI的分佈系統。它的監聽器是RMICacheManagerPeerListener,這個監聽器可以用RMICacheManagerPeerListenerFactory來配置

<!-- 本機緩存的信息對應的地址和端口配置監聽器的工廠類 -->
      <cacheManagerPeerListenerFactory 
         class="net.sf.ehcache.distribution.RMICacheManagerPeerListenerFactory"
          properties="hostName=127.0.0.1, port=40002,socketTimeoutMillis=2000" /> 
  • hostname (可選) – 運行監聽器的服務器名稱。標明瞭做爲集羣羣組的成員的地址,同時也是你想要控制的從集羣中接收消息的接口。
       在CacheManager初始化的時候會檢查hostname是否可用。
       如果hostName不可用,CacheManager將拒絕啓動並拋出一個連接被拒絕的異常。
      如果指定,hostname將用InetAddress.getLocalHost().getHostAddress()來得到。
  • port – 監聽器監聽的端口。
  • socketTimeoutMillis (可選) – Socket超時的時間。默認是2000ms。當你socket同步緩存請求地址比較遠,不是本地局域網。你可能需要把這個時間配置大些,不然很可能延時導致同步緩存失敗。

具體cache 對象配置

<!-- 緩存最長存在10分鐘後失效,如果5分鐘未訪問,緩存也會失效 -->
<cache name="testCache"
           maxEntriesLocalHeap="10000" 
           eternal="false"
           timeToIdleSeconds="300" 
           timeToLiveSeconds="600"> 
        <cacheEventListenerFactory
            class="net.sf.ehcache.distribution.RMICacheReplicatorFactory"
            properties="replicateAsynchronously=false, replicatePuts=true,
                            replicatePutsViaCopy=true, replicateUpdates=true,
                            replicateUpdatesViaCopy=true, replicateRemovals=true" />
    </cache>
    
     <!-- 緩存最長存在99天后失效,如果99天未訪問,緩存也會失效 -->
    <cache name="testCache2" maxEntriesLocalHeap="10000" eternal="false"
        timeToIdleSeconds="8640000" timeToLiveSeconds="8640000">
        <cacheEventListenerFactory
            class="net.sf.ehcache.distribution.RMICacheReplicatorFactory"
            properties="replicateAsynchronously=false, replicatePuts=true,
                            replicatePutsViaCopy=true, replicateUpdates=true,
                            replicateUpdatesViaCopy=true, replicateRemovals=true" />
    </cache>
</ehcache>

cache屬性介紹

  • 必須屬性:
    name:設置緩存的名稱,用於標誌緩存,惟一
    maxElementsInMemory:在內存中最大的對象數量
    maxElementsOnDisk:在DiskStore中的最大對象數量,如爲0,則沒有限制
    eternal:設置元素是否永久的,如果爲永久,則timeout忽略
    overflowToOffHeap:
    overflowToDisk:是否當memory中的數量達到限制後,保存到Disk
  • 可選的屬性:
    timeToIdleSeconds:設置元素過期前的空閒時間,緩存自創建日期起至失效時的間隔時間。值爲零,意味空閒時間爲無窮,默認爲零。
    timeToLiveSeconds:設置元素過期前的活動時間,緩存創建以後,最後一次訪問緩存的日期至失效之時的時間間隔。值爲零,意味存活時間爲無窮,默認爲零。
    diskPersistent:是否disk store在虛擬機啓動時持久化。默認爲false
    diskExpiryThreadIntervalSeconds:運行disk終結線程的時間,默認爲120秒
    clearOnFlush:內存數量最大時是否清除。
    memoryStoreEvictionPolicy:策略關於Eviction

cacheEventListenerFactory

註冊相應的的緩存監聽類,用於處理緩存事件,如put,remove,update,和expire bootstrap CacheLoaderFactory:指定相應的BootstrapCacheLoader,用於在初始化緩存,以及自動設置。

  • replicatePuts=true|false - 默認爲true。新加入的緩存中的元素是否要複製到其它節點中去。
  • replicatePutsViaCopy=true|false - 默認爲true。新加入的緩存中的元素是否要複製到其它 緩存中,或者一條刪除消息是否發送。
  • replicateUpdates=true|false - 默認爲true。當新加入的元素與已存在的元素鍵值出現衝突時,是否要覆蓋已存在元素。
  • replicateRemovals=true - 默認爲true。被移去的元素是否要複製。
  • replicateAsynchronously=true | false - 默認爲true。true表示複製是異步的,false表示複製是同步的。
  • replicateUpdatesViaCopy=true | false - 默認爲true。
  • asynchronousReplicationIntervalMillis= - 默認值爲1000,最小值爲10。只有在replicateAsynchronously=true,該屬性才適用。

bootstrapCacheLoaderFactory

指定相應的BootstrapCacheLoader,用於在初始化緩存,以及自動設置

代碼中緩存標記的使用

注意:想針對統一資源緩存做緩存的增、改、刪,一定要注意,key 必須要設置成一樣的

讀取/生成緩存@Cacheable

能夠根據方法的請求參數對其結果進行緩存。即當重複使用相同參數調用方法的時候,方法本身不會被調用執行,即方法本身被略過了,取而代之的是方法的結果直接從緩存中找到並返回了。

  • value:緩存位置名稱,不能爲空,如果使用EHCache,就是ehcache.xml中聲明的cache的name
  • key:緩存的key(保證唯一的參數),默認爲空,既表示使用方法的參數類型及參數值作爲key,支持SpEL
  • 緩存key還可以用如下規則組成,當我們要使用root作爲key時,可以不用寫root直接@Cache(key=“caches[1].name”)。因爲他默認是使用#root的
    1.methodName 當前方法名 #root.methodName
    2.method 當前方法 #root.method.name
    3.target 當前被動用對象 #root.target
    4.targetClass 當前被調用對象 Class#root.targetClass
    5.args 當前方法參數組成的數組 #root.args[0]
    6.caches 當前被調用方法所使用的Cache #root.caches[0],name
    7.方法參數 假設包含String型參數str #str
    #p0代表方法的第一個參數
    假設包含HttpServletRequest型參數request #request.getAttribute(‘usId32’) 調用入參對象的相關包含參數的方法
    假設包含User型參數user #user.usId 調用入參對象的無參方法可以直接用此形式
    8.字符串 ‘字符串內容’
  • condition:觸發條件,只有滿足條件的情況纔會加入緩存,默認爲空,既表示全部都加入緩存,支持SpEL
  • unless: 觸發條件,只有不滿足條件的情況纔會加入緩存,默認爲空,既表示全部都加入緩存,支持SpEL
  • #result 可以獲得返回結果對象
    /**
     * 生成緩存,同時下一次在調用此方法優先從緩存中獲取信息
     * 讀取/生成緩存@Cacheable
     * 能夠根據方法的請求參數對其結果進行緩存。即當重複使用相同參數調用方法的時候,方法本身不會被調用執行,即方法本身被略過了,取而代之的是方法的結果直接從緩存中找到並返回了。
     * @param hosId
     * @param request
     * @return
     */
    @Cacheable(value="testCache",key="#hosId+'_'+'createTestCacheSuccess'", condition="#hosId!=null",unless="#result.result!=true or #result.data==null")
    @RequestMapping(value = "/{hosId}/createTestCacheSuccess", method = RequestMethod.GET, produces = "application/json;charset=UTF-8")
    @ResponseBody
    public ResultVo createTestCacheSuccess(@PathVariable Long hosId, HttpServletRequest request) {

        ResultVo resultVo = new ResultVo();
        resultVo.setKind(SUCCESS_CODE);
        resultVo.setResult(true);
        resultVo.setData((Object)("createTestCacheSuccess成功生成緩存"+System.currentTimeMillis()));
        log.debug("進入實際生成緩存方法體,本次請求未使用緩存,本方法可以生成有效緩存,緩存未失效之前調用該方法將不會進入到方法體");
        return resultVo;
    }

刪除緩存@CacheEvict

根據value 和key值來唯一找到緩存記錄,並且清理緩存信息

  • value:緩存的位置,不能爲空。
  • key:緩存的key,默認爲空。
  • condition:觸發的條件,只有滿足條件的情況纔會清楚緩存,默認爲空,支持SpEL。
  • allEntries:true表示清除value中的全部緩存(可以理解爲清空表),默認爲false(刪除單條數據)。
  • beforeInvocation:當我們設置爲true時,Spring會在調用該方法之前進行緩存的清除。清除操作默認是在方法成功執行之後觸發的。
   /**
     * 刪除緩存
     * 刪除緩存@CacheEvict
     * 根據value 和key值來唯一找到緩存記錄,並且清理緩存信息
     * @param hosId
     * @param request
     * @return
     */
    @CacheEvict(value="testCache",key="#hosId+'_'+'createTestCacheSuccess'", condition="#hosId!=null")
    @RequestMapping(value = "/{hosId}/deleteCreateTestCacheSuccess", method = RequestMethod.GET, produces = "application/json;charset=UTF-8")
    @ResponseBody
    public ResultVo deleteCreateTestCacheSuccess(@PathVariable Long hosId, HttpServletRequest request) {
        log.debug("刪除createTestCacheSuccess生成的緩存");
        ResultVo resultVo = new ResultVo();
        resultVo.setKind(SUCCESS_CODE);
        resultVo.setResult(true);
        resultVo.setData((Object)("刪除緩存成功"+System.currentTimeMillis()));
        return resultVo;
    }

更新緩存@CachePut

它雖然也可以聲明一個方法支持緩存,但它執行方法前是不會去檢查緩存中是否存在之前執行過的結果,而是每次都執行該方法,並將執行結果放入指定緩存中。

  • value:緩存的位置,不能爲空。
  • key:緩存的key,默認爲空。
  • condition:觸發的條件,只有滿足條件的情況纔會清楚緩存,默認爲空,支持SpEL。
 /**
     * 生成緩存,同時下一次在調用此方法還是會執行該方法並且同時更新緩存內容
     *
     * 更新緩存@CachePut
     * 它雖然也可以聲明一個方法支持緩存,但它執行方法前是不會去檢查緩存中是否存在之前執行過的結果,而是每次都執行該方法,並將執行結果放入指定緩存中。
     * @param hosId
     * @param request
     * @return
     */
    @CachePut(value="testCache",key="#hosId+'_'+'createTestCacheSuccess'", condition="#hosId!=null",unless="#result.result!=true or #result.data==null")
    @RequestMapping(value = "/{hosId}/updateTestCacheSuccess", method = RequestMethod.GET, produces = "application/json;charset=UTF-8")
    @ResponseBody
    public ResultVo updateTestCacheSuccess(@PathVariable Long hosId, HttpServletRequest request) {
        log.debug("進入實際生成緩存方法體,本方法可以生成有效緩存,下一次調用該方法依然會進入到方法體");
        ResultVo resultVo = new ResultVo();
        resultVo.setKind(SUCCESS_CODE);
        resultVo.setResult(true);
        resultVo.setData((Object)("updateTestCacheSuccess成功生成緩存"+System.currentTimeMillis()));
        return resultVo;
    }

通過EhCacheCacheManager獲取緩存詳情

EhCacheCacheManager (管理CacheManager的工具類)是在上面spring.xml 中配置的緩存管理對象

   @Resource
   EhCacheCacheManager ehCacheCacheManager;
   
    /**
     * 從cache 中獲取實際緩存信息
     * @param cacheName
     * @param cacheKey
     * @return
     */
    @RequestMapping(value = "getResult", method = RequestMethod.GET, produces = "application/json;charset=UTF-8")
    @ResponseBody
    public ResultVo getResult(String cacheName,String cacheKey){
        ResultVo resultVo = null;
        CacheManager cacheManager=ehCacheCacheManager.getCacheManager();
        if (cacheManager!=null){
            Ehcache ehcache = cacheManager.getEhcache(CACHE_NEMA_TESTCACHE);
            if(ehcache!=null){
                Element element = ehcache.get(cacheKey);
                if(element!=null && element.getObjectValue()!=null
                        && element.getObjectValue() instanceof ResultVo){
                    resultVo = (ResultVo)element.getObjectValue();

                }
            }
        }
        return resultVo;
    }

簡單信息統計

    /**
     * 從cache 中獲取緩存簡單的監控信息(數據量少的時候適合這麼幹,數據量大的時候需要注意性能問題,一下子遍歷所有緩存元素這將是一個災難)
     * @return
     */
    @RequestMapping(value = "getCacheStatistic", method = RequestMethod.GET, produces = "application/json;charset=UTF-8")
    @ResponseBody
    public ResultVo getCacheStatistic(){
        ResultVo resultVo = new ResultVo();
        resultVo.setResult(false);
        CacheManager cacheManager=ehCacheCacheManager.getCacheManager();
        if (cacheManager!=null){
            String []cacheNames=cacheManager.getCacheNames();
            if( null != cacheNames && cacheNames.length>0 ){
                StringBuffer ehcacheBuffer = new StringBuffer();
                ehcacheBuffer.append(StringUtils.rightPad("CacheName", 15));
                ehcacheBuffer.append(" | ");
                ehcacheBuffer.append(StringUtils.rightPad("Key", 40));
                ehcacheBuffer.append(" | ");
                ehcacheBuffer.append(StringUtils.rightPad("HintCount", 10));
                ehcacheBuffer.append(" | ");
                ehcacheBuffer.append(StringUtils.rightPad("CreationTime", 25));
                ehcacheBuffer.append(" | ");
                ehcacheBuffer.append(StringUtils.rightPad("LastAccessTime", 25));
                ehcacheBuffer.append(" | ");
                ehcacheBuffer.append(StringUtils.rightPad("TimeToLive(ms)", 15));
                ehcacheBuffer.append(" | ");
                ehcacheBuffer.append(StringUtils.rightPad("TimeToIdle(ms)", 15));
                //這裏不打印數據值,因爲打印值的話數據量比較大
                ehcacheBuffer.append(" | ");
                ehcacheBuffer.append("\n");
                for (int i = 0; i < cacheNames.length; i++) {
                    Ehcache ehcache = cacheManager.getCache(cacheNames[i]);
                    if(ehcache!=null){
                        List<String> ehcacheKeys = ehcache.getKeys();
                        if( null!=ehcacheKeys && 0< ehcacheKeys.size() ){
                            for (String ehcacheKey:ehcacheKeys) {
                                Element element = ehcache.get(ehcacheKey);
                                if(element!=null ){
                                    ehcacheBuffer.append(StringUtils.rightPad(ehcache.getName(), 15));//cachenName
                                    ehcacheBuffer.append(" | ");
                                    ehcacheBuffer.append(StringUtils.rightPad(ehcacheKey, 40));//key name
                                    ehcacheBuffer.append(" | ");
                                    ehcacheBuffer.append(StringUtils.rightPad(""+element.getHitCount(), 10));//命中次數
                                    ehcacheBuffer.append(" | ");
                                    ehcacheBuffer.append(StringUtils.rightPad(formatDate(element.getCreationTime()), 25));//創建時間
                                    ehcacheBuffer.append(" | ");
                                    ehcacheBuffer.append(StringUtils.rightPad(formatDate(element.getLastAccessTime()), 25));//最後訪問時間
                                    ehcacheBuffer.append(" | ");
                                    ehcacheBuffer.append(StringUtils.rightPad(""+element.getTimeToLive(), 15));   //存活時間
                                    ehcacheBuffer.append(" | ");
                                    ehcacheBuffer.append(StringUtils.rightPad(""+element.getTimeToIdle(), 15));   //空閒時間
                                    ehcacheBuffer.append(" | ");
                                    ehcacheBuffer.append("\n");
                                }
                            }
                        }


                    }
                }
                log.debug("\n"+ehcacheBuffer.toString());
                resultVo.setData(ehcacheBuffer);
                resultVo.setResult(true);
            }
        }
        return resultVo;
    }

日誌效果
在這裏插入圖片描述

手工RMI配置模式下實現rmiUrls更新功能的接口

查看了部分源碼,找到目前rmiUrls的賦值的相關內容,然後用了點反射的小手段來處理這個事。

public static final String URL_DELIMITER = "|";
    /**
     * 修改spring-ehcache.xml 中的相關屬性類(該文件不是spring常規的加載方式,在spring啓動時通過${xxx}獲取到值)
     * @return
     */
    @RequestMapping(value = "changeCacheManagerPeerProviderFactory", method = RequestMethod.GET, produces = "application/json;charset=UTF-8")
    @ResponseBody
    public ResultVo changeCacheManagerPeerProviderFactory(@RequestParam(value = "rmiUrls") String rmiUrls){
        ResultVo resultVo = null;
        //CacheManagerPeerProvider
        CacheManager cacheManager=ehCacheCacheManager.getCacheManager();
        if (cacheManager!=null){
            //此處查看源碼可知返回的是一個ummoifyMap,字面意思不可更改的map(ConcureentHashMap)
            Map<String, CacheManagerPeerProvider> map = cacheManager.getCacheManagerPeerProviders();
            //默認生成的CacheManagerPeerProvider 對應的key是RMI
            CacheManagerPeerProvider cacheManagerPeerProvider = map.get("RMI");
            if( null != cacheManagerPeerProvider && cacheManagerPeerProvider instanceof ManualRMICacheManagerPeerProvider){
                ManualRMICacheManagerPeerProvider manualRMICacheManagerPeerProvider=(ManualRMICacheManagerPeerProvider)cacheManagerPeerProvider;
                StringTokenizer stringTokenizer = new StringTokenizer(rmiUrls, URL_DELIMITER);
                while (stringTokenizer.hasMoreTokens()) {
                    String rmiUrl = stringTokenizer.nextToken();
                    rmiUrl = rmiUrl.trim();
                    manualRMICacheManagerPeerProvider.registerPeer(rmiUrls);
                    log.debug("Registering peer {}", rmiUrl);
                }
                Map<String, CacheManagerPeerProvider> modifiableMap=null;
                Class clazz =null;
                try {
                    clazz = cacheManager.getClass();
                    Field fields[] = clazz.getDeclaredFields();
                    for (Field field:fields) {
                        if( "cacheManagerPeerProviders".equals(field.getName())){
                            field.setAccessible(true);
                            //獲取屬性
                            String name = field.getName();
                            //獲取屬性值
                            Object value = field.get(cacheManager);
                            modifiableMap=(ConcurrentHashMap)value;
                            modifiableMap.put("RMI",manualRMICacheManagerPeerProvider);
                            log.debug("");
                            break;
                        }
                    }
                    log.debug("");
                } catch (Exception e) {
                    log.error("",e);
                }
                ehCacheCacheManager.setCacheManager(cacheManager);
            }
        }
        return resultVo;
    }

參考資料

[1]: EhCache 緩存系統簡介 https://www.ibm.com/developerworks/cn/java/j-lo-ehcache/
[2]: Ehcache配置文件譯文 https://dreamzhong.iteye.com/blog/1161954
[3]: EhCache 系統簡介 https://www.cnblogs.com/duwanjiang/p/6230113.html
[3]: 本人其他平臺早期的文檔 https://blog.51cto.com/tianyang10552/1899550

發佈了22 篇原創文章 · 獲贊 1 · 訪問量 4986
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章