Java Cache詳解及簡(jiǎn)單實(shí)現(xiàn)
Java Cache詳解及簡(jiǎn)單實(shí)現(xiàn)
概要:
最近在做spring的項(xiàng)目,想做一個(gè)緩存,訪問(wèn)數(shù)據(jù)庫(kù),定期來(lái)做數(shù)據(jù)更新
要實(shí)現(xiàn)兩個(gè)功能
- 可以通過(guò)http請(qǐng)求來(lái)立刻刷新緩存
- 緩存可以通過(guò)自己配置的時(shí)間間隔來(lái)定期刷新
通過(guò)Controller來(lái)做
因?yàn)樾枰ㄟ^(guò)http來(lái)刷新緩存,所以第一個(gè)想法就是把緩存做成一個(gè)Controller
Controller的實(shí)現(xiàn)
Controller最大的優(yōu)勢(shì),就是可以通過(guò)Spring的配置,注入很多依賴(lài),比如對(duì)Service的依賴(lài),對(duì)數(shù)據(jù)庫(kù)的依賴(lài)等。
大量的訪問(wèn)數(shù)據(jù)庫(kù)跟服務(wù)層的代碼,都可以進(jìn)行復(fù)用
定義一個(gè)Cache接口如下:
public interface Cache { /** * Refresh the cache. If succeed, return true, else return false; * * @return */ boolean refresh(); /** * How much time it will refresh the cache. * * @return */ long interval(); }
但是這里碰到了問(wèn)題,自己寫(xiě)的Controller可以通過(guò)注入的方式輕而易舉的與Http服務(wù)跟Service層,數(shù)據(jù)庫(kù)層連接,但是如果CacheController實(shí)現(xiàn)Cache接口,會(huì)發(fā)現(xiàn)很難調(diào)用interval函數(shù)來(lái)找到間隔的時(shí)間。
因?yàn)镃acheController也是一個(gè)Bean,需要通過(guò)Spring找到這個(gè)bean來(lái)調(diào)用。無(wú)法找到Bean,就不能調(diào)用Interval,也就不能夠順勢(shì)通過(guò)另外的線程來(lái)控制緩存刷新。為了獲取這個(gè)Bean可以將所有的CacheController都Autowired到一個(gè)CacheManagerController之中
@Controller public class CacheManagerController { @Autowired private CacheController cache; private static ScheduledExecutorService executor = Executors .newScheduledThreadPool(1); public CacheManagerController() { executor.scheduleAtFixedRate(() -> cache.refresh(), 0, cache.interval(), TimeUnit.MILLISECONDS); } }
曾經(jīng)考慮這么做,但是發(fā)現(xiàn)一個(gè)問(wèn)題,這樣做,CacheManagerController在初始化的時(shí)候,也就是構(gòu)造Bean的時(shí)候,各種的Cache還沒(méi)有被注入CacheController,而如果不將方法放入構(gòu)造函數(shù),那么CacheManagerController是無(wú)法自動(dòng)的調(diào)用調(diào)度服務(wù)的。需要手動(dòng)調(diào)用才行。但是程序的入口不一定從哪一個(gè)Controller進(jìn)入,如果寫(xiě)攔截器,也是很繁瑣,而且每次調(diào)用都會(huì)執(zhí)行。
這個(gè)時(shí)候,就通過(guò)一個(gè)CacheService來(lái)實(shí)現(xiàn)這個(gè)問(wèn)題
public class CacheService { public static final long ONE_MINUTE = 60 * 1000; private static ScheduledExecutorService executor = Executors .newScheduledThreadPool(1); public static void register(Cache cache) { executor.scheduleAtFixedRate(() -> cache.refresh(), 0, cache.interval(), TimeUnit.MILLISECONDS); } } @Controller public class CacheController implements Cache { // autowire 各種不同的service,或者是repo連接數(shù)據(jù)庫(kù) @Autowired private Service service; public CacheController() { CacheService.register(this); } // cache interface @Override public long interval() { return 1000; } @Override public boolean refresh() { return true; } }
因?yàn)榫唧w的CacheController是通過(guò)反射構(gòu)造成Bean由Spring管理的,所以可以直接通過(guò)無(wú)參構(gòu)造函數(shù)來(lái)注冊(cè)一下,這樣就沒(méi)有問(wèn)題了,當(dāng)Spring在加載CacheController的時(shí)候,就會(huì)直接調(diào)用CacheService的注冊(cè)方法,將緩存注冊(cè)到CacheService中定義的線程池當(dāng)中,然后立刻執(zhí)行刷新方法,同時(shí)還會(huì)根據(jù)時(shí)間間隔來(lái)自動(dòng)的刷新。
至于獲取指定的Cache,更簡(jiǎn)單了,因?yàn)镃ache本身是一個(gè)Controller,所以可以通過(guò)Autowire自動(dòng)注冊(cè)到需要使用的其他Controller之中。
當(dāng)然了,目前這么寫(xiě)是沒(méi)有什么問(wèn)題,但是當(dāng)refresh為立刻調(diào)用的時(shí)候,會(huì)無(wú)法拿到Autowired注入的那些Service。因?yàn)镾pring是統(tǒng)一全部實(shí)例化,然后再進(jìn)行裝載的,所以,如果refresh函數(shù)中調(diào)用了service,那么顯然,程序肯定會(huì)報(bào)空指針異常的。這也是使用Controller來(lái)做Cache的問(wèn)題。如果要獲得全部的Spring裝載的實(shí)例,那么肯定就都要修改構(gòu)造函數(shù)來(lái)將實(shí)例注入到統(tǒng)一的集合當(dāng)中了,那樣就跟前文提到的問(wèn)題一樣了,也就是獲取Bean。如果能夠獲取Bean,那直接就能調(diào)用實(shí)例方法,也就沒(méi)有這么多事情了。
總結(jié)
使用Controller的特點(diǎn)如下:
- 代碼復(fù)用,定義的repo層,service層代碼都可以繼續(xù)使用,不用重寫(xiě)
- 因?yàn)镾pring聲明周期的問(wèn)題,refresh操作立刻執(zhí)行會(huì)拋異常,需要延時(shí)刷新
通過(guò)Listener來(lái)做
Listener有一個(gè)優(yōu)勢(shì),就是可以通過(guò)一個(gè)寫(xiě)一個(gè)PreloadListener 實(shí)現(xiàn)ServletContextListener,這樣就能夠利用Tomcat加載web.xml的時(shí)候,將代碼提前進(jìn)行初始化了。
Listener的實(shí)現(xiàn)
public class PreloadListener implements ServletContextListener { @Override public void contextInitialized(ServletContextEvent servletContextEvent) { CacheFactory.init(); } @Override public void contextDestroyed(ServletContextEvent servletContextEvent) { } }
下面是web.xml的代碼
// web.xml <listener> <listener-class>com.sapphire.listener.PreloadListener</listener-class> </listener>
當(dāng)然了,有優(yōu)勢(shì)肯定會(huì)存在劣勢(shì),因?yàn)槭褂肔istener的方式來(lái)提前加載,也會(huì)因?yàn)閃eb的聲明周期,產(chǎn)生問(wèn)題。
Tomcat在加載Web.xml的時(shí)候,Listener的初始化,會(huì)在Spring容器啟動(dòng)之前,這樣也就碰到一個(gè)問(wèn)題。PreloadListener中可以調(diào)用的代碼,肯定是無(wú)法Autowire到任何的Bean的。這也就是對(duì)比Controller碰到的一個(gè)巨大的劣勢(shì)了,需要自己重寫(xiě)那些Service。
除此以外, 還需要單獨(dú)寫(xiě)一個(gè)Controller來(lái)刷新指定的緩存。
public class CacheFactory { private static ConcurrentHashMap<String, Cache> caches = new ConcurrentHashMap<>(); private static ScheduledExecutorService executorService = Executors.newScheduledThreadPool(1); private static void register(Cache cache) { caches.put(cache.category(), cache); } private static void registerAll() { register(new StockCache()); } public static void init() { registerAll(); for (Cache cache : caches.values()) { executorService.scheduleAtFixedRate(new Runnable() { @Override public void run() { cache.refresh(); } }, 0, cache.interval(), TimeUnit.MILLISECONDS); } } public static Cache getCache(String key) { if (caches.contains(key)) { return caches.get(key); } return null; } } // cache接口除了需要提供interval和refresh以外,還需要提供一個(gè)category來(lái)區(qū)分不同的Cache public interface Cache { /** * Refresh the cache. If succeed, return true, else return false; * * @return */ boolean refresh(); /** * How much time it will refresh the cache. * * @return */ long interval(); /** * Cache's category. Each cache has distinct category. * * @return */ String category(); }
這樣完成的CacheFactory,可以在PreloadListener之中調(diào)用init方法來(lái)初始化所有的Cache,來(lái)完成Cache的啟動(dòng)??梢钥闯觯械腃acheFactory之中的方法都是靜態(tài)方法,可以直接由Controller層隨便調(diào)用。
之后,不同的Cache就需要單獨(dú)來(lái)寫(xiě)init方法,放到各自實(shí)現(xiàn)的refresh方法之中。跟數(shù)據(jù)庫(kù)的鏈接等,都需要建立。不同的Cache都需要重寫(xiě)各自的初始化方法,還需要寫(xiě)一個(gè)讀取文件配置的東西讀取數(shù)據(jù)庫(kù)的一些配置信息。總之,感覺(jué)很麻煩。
總結(jié)
通過(guò)Listener來(lái)實(shí)現(xiàn),更加靈活,可以在容器啟動(dòng)之前就將需要的信息加載到內(nèi)存之中,但是很多業(yè)務(wù)代碼都需要重新來(lái)寫(xiě),數(shù)據(jù)庫(kù)的鏈接,解析Property,靈活刷新的CacheController。
感謝閱讀,希望能幫助到大家,謝謝大家對(duì)本站的支持!
相關(guān)文章
IntelliJ?IDEA?2023.2最新版激活方法及驗(yàn)證ja-netfilter配置是否成功
隨著2023.2版本的發(fā)布,用戶(hù)們渴望了解如何激活這個(gè)最新版的IDE,本文將介紹三種可行的激活方案,包括許可證服務(wù)器、許可證代碼和idea?vmoptions配置,幫助讀者成功激活并充分利用IDEA的功能,感興趣的朋友參考下吧2023-08-08Windows下將JAVA?jar注冊(cè)成windows服務(wù)的方法
這篇文章主要介紹了Windows下將JAVA?jar注冊(cè)成windows服務(wù)的方法,本文通過(guò)圖文并茂的形式給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2023-07-07Java計(jì)算一個(gè)數(shù)加上100是完全平方數(shù),加上168還是完全平方數(shù)
這篇文章主要介紹了Java計(jì)算一個(gè)數(shù)加上100是完全平方數(shù),加上168還是完全平方數(shù),需要的朋友可以參考下2017-02-02Java中關(guān)于Null的9個(gè)解釋(Java Null詳解)
這篇文章主要介紹了Java中關(guān)于Null的9個(gè)解釋(Java Null詳解),本文詳細(xì)講解了Java中Null的9個(gè)相關(guān)知識(shí),需要的朋友可以參考下2015-01-01java數(shù)據(jù)結(jié)構(gòu)與算法之中綴表達(dá)式轉(zhuǎn)為后綴表達(dá)式的方法
這篇文章主要介紹了java數(shù)據(jù)結(jié)構(gòu)與算法之中綴表達(dá)式轉(zhuǎn)為后綴表達(dá)式的方法,簡(jiǎn)單分析了java中綴表達(dá)式轉(zhuǎn)為后綴表達(dá)式的相關(guān)實(shí)現(xiàn)方法與技巧,需要的朋友可以參考下2016-08-08java 全角半角字符轉(zhuǎn)換的方法實(shí)例
這篇文章主要介紹了java 全角半角字符轉(zhuǎn)換的方法,大家參考使用吧2013-11-11JVM核心教程之JVM運(yùn)行與類(lèi)加載全過(guò)程詳解
我們都知道一個(gè)java程序運(yùn)行要經(jīng)過(guò)編譯和執(zhí)行,但是這太概括了,中間還有很多步驟,下面這篇文章主要給大家介紹了關(guān)于JVM核心教程之JVM運(yùn)行與類(lèi)加載全過(guò)程的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),需要的朋友可以參考下。2018-04-04Mybatis動(dòng)態(tài)sql中@Param使用詳解
這篇文章主要介紹了Mybatis動(dòng)態(tài)sql中@Param使用詳解,當(dāng)方法的參數(shù)為非自定義pojo類(lèi)型,且使用了動(dòng)態(tài)sql,那么就需要在參數(shù)前加上@Param注解,需要的朋友可以參考下2023-10-10