springboot使用j2cache框架和aspectj自定义缓存
文章目錄
- 依賴(lài)和工具介紹
- 項(xiàng)目代碼
- spring上下文工具類(lèi):
- 自定義緩存注解
- 緩存生成鍵工具類(lèi)
- 自定義緩存攔截器
- 緩存處理器
- 緩存結(jié)果和緩存信息實(shí)體封裝
- 開(kāi)啟聲明式注解
- controller層使用緩存
- 總結(jié)
依賴(lài)和工具介紹
<dependency><groupId> org.aspectj</groupId ><artifactId> aspectjweaver</artifactId ><version> 1.8.7</version ></dependency><dependency><groupId>net.oschina.j2cache</groupId><artifactId>j2cache-core</artifactId><version>2.8.0-release</version></dependency>配置:
j2cache:cache-clean-mode: passiveallow-null-values: trueredis-client: lettuce #指定redis客戶(hù)端使用lettuce,也可以使用Jedisl2-cache-open: true #開(kāi)啟二級(jí)緩存broadcast: net.oschina.j2cache.cache.support.redis.SpringRedisPubSubPolicy# broadcast: jgroupsL1: #指定一級(jí)緩存提供者為caffeineprovider_class: caffeineL2: #指定二級(jí)緩存提供者為redisprovider_class: net.oschina.j2cache.cache.support.redis.SpringRedisProviderconfig_section: lettucesync_ttl_to_redis: truedefault_cache_null_object: falseserialization: fst #序列化方式:fst、kyro、Java caffeine:properties: /caffeine.properties # 這個(gè)配置文件需要放在項(xiàng)目中 lettuce:mode: singlenamespace:storage: genericchannel: j2cachescheme: redishosts: ${pinda.redis.ip}:${pinda.redis.port}password: ${pinda.redis.password}database: ${pinda.redis.database}sentinelMasterId:maxTotal: 100maxIdle: 10minIdle: 10timeout: 10000caffeine.properties
default=2000, 2h rx=2000, 2h addressBook=2000, 2hj2cache是OSChina目前正在使用的兩級(jí)緩存框架。
j2cache的兩級(jí)緩存結(jié)構(gòu):
L1: 進(jìn)程內(nèi)緩存 caffeine/ehcache
L2: 集中式緩存 Redis/Memcached
j2cache其實(shí)并不是在重復(fù)造輪子,而是作資源整合,即將Ehcache、Caffeine、redis、Spring Cache等進(jìn)行整合。
由于大量的緩存讀取會(huì)導(dǎo)致L2的網(wǎng)絡(luò)成為整個(gè)系統(tǒng)的瓶頸,因此L1的目標(biāo)是降低對(duì)L2的讀取次數(shù)。該緩存框架主要用于集群環(huán)境中。單機(jī)也可使用,用于避免應(yīng)用重啟導(dǎo)致的ehcache緩存數(shù)據(jù)丟失。
j2cache從1.3.0版本開(kāi)始支持JGroups和Redis Pub/Sub兩種方式進(jìn)行緩存事件的通知。
數(shù)據(jù)讀取順序 -> L1 -> L2 -> DB
關(guān)于j2cache的region概念:
J2Cache 的 Region 來(lái)源于 Ehcache 的 Region 概念。
一般我們?cè)谑褂孟?Redis、Caffeine、Guava Cache 時(shí)都沒(méi)有 Region 這樣的概念,特別是 Redis 是一個(gè)大哈希表,更沒(méi)有這個(gè)概念。
在實(shí)際的緩存場(chǎng)景中,不同的數(shù)據(jù)會(huì)有不同的 TTL 策略,例如有些緩存數(shù)據(jù)可以永不失效,而有些緩存我們希望是 30 分鐘的有效期,有些是 60 分鐘等不同的失效時(shí)間策略。在 Redis 我們可以針對(duì)不同的 key 設(shè)置不同的 TTL 時(shí)間。但是一般的 Java 內(nèi)存緩存框架(如 Ehcache、Caffeine、Guava Cache 等),它沒(méi)法為每一個(gè) key 設(shè)置不同 TTL,因?yàn)檫@樣管理起來(lái)會(huì)非常復(fù)雜,而且會(huì)檢查緩存數(shù)據(jù)是否失效時(shí)性能極差。所以一般內(nèi)存緩存框架會(huì)把一組相同 TTL 策略的緩存數(shù)據(jù)放在一起進(jìn)行管理。
像 Caffeine 和 Guava Cache 在存放緩存數(shù)據(jù)時(shí)需要先構(gòu)建一個(gè) Cache 實(shí)例,設(shè)定好緩存的時(shí)間策略,如下代碼所示:
Caffeine<Object, Object> caffeine = Caffeine.newBuilder();
caffeine = caffeine.maximumSize(size).expireAfterWrite(expire, TimeUnit.SECONDS);
Cache<String, Object> theCache = caffeine.build()
這時(shí)候你才可以往 theCache 寫(xiě)入緩存數(shù)據(jù),而不能再單獨(dú)針對(duì)某一個(gè) key 設(shè)定不同的 TTL 時(shí)間。
而 Redis 可以讓你非常隨意的給不同的 key 設(shè)置不同的 TTL。
J2Cache 是內(nèi)存緩存和 Redis 這類(lèi)集中式緩存的一個(gè)橋梁,因此它只能是兼容兩者的特性。
J2Cache 默認(rèn)使用 Caffeine 作為一級(jí)緩存,其配置文件位于 caffeine.properties 中。一個(gè)基本使用場(chǎng)景如下:
#########################################
# Caffeine configuration # [name] = size, xxxx[s|m|h|d]#########################################
default = 1000, 30m
users = 2000, 10m
blogs = 5000, 1h
上面的配置定義了三個(gè)緩存 Region ,分別是:
默認(rèn)緩存,大小是 1000 個(gè)對(duì)象,TTL 是 30 分鐘
users 緩存,大小是 2000 個(gè)對(duì)象,TTL 是 10 分鐘
blogs 緩存,大小是 5000 個(gè)對(duì)象,TTL 是 1 個(gè)小時(shí)
例如我們可以用 users 來(lái)存放用戶(hù)對(duì)象的緩存,用 blogs 來(lái)存放博客對(duì)象緩存,兩種的 TTL 是不同的。
項(xiàng)目代碼
現(xiàn)在上代碼,Springbootzhenghej2cache進(jìn)行緩存:
spring上下文工具類(lèi):
/*** Spring上下文工具類(lèi)*/ @Primary @Component public class SpringApplicationContextUtils {private static ApplicationContext springContext;@Autowiredprivate ApplicationContext applicationContext;@PostConstructprivate void init() {springContext = applicationContext;}/*** 獲取當(dāng)前ApplicationContext** @return ApplicationContext*/public static ApplicationContext getApplicationContext() {return springContext;}}自定義緩存注解
如果項(xiàng)目中很多模塊都需要使用緩存功能,這些模塊都需要調(diào)用j2cache的API來(lái)進(jìn)行緩存操作,這種j2cache提供的原生API使用起來(lái)就比較繁瑣了,并且操作緩存的代碼和我們的業(yè)務(wù)代碼混合到一起,即j2cache的API對(duì)我們的業(yè)務(wù)代碼具有侵入性。那么我們?nèi)绾胃雍?jiǎn)潔、優(yōu)雅的使用j2cache提供的緩存功能呢?
答案就是使用聲明式緩存。所謂聲明式緩存,就是定義緩存注解,在需要使用緩存功能的方法上加入緩存注解即可自動(dòng)進(jìn)行緩存操作。
這種使用方式類(lèi)似于我們以前使用的聲明式事務(wù),即在類(lèi)的方法上加入事務(wù)注解就可以實(shí)現(xiàn)事務(wù)控制。
注意:j2cache原生API和我們實(shí)現(xiàn)的聲明式緩存可以兼容,即在項(xiàng)目中可以同時(shí)使用,互為補(bǔ)充。例如在Controller的方法中需要將多類(lèi)業(yè)務(wù)數(shù)據(jù)載入緩存,此時(shí)通過(guò)聲明式緩存就無(wú)法做到(因?yàn)槁暶魇骄彺嬷荒軐⒎椒ǖ姆祷刂递d入緩存),這種場(chǎng)景下就需要調(diào)用j2cache的原生API來(lái)完成。
/*** 緩存注解*/ @Documented @Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface Cache {String region() default "rx";String key() default "";String params() default ""; } /*** 清理緩存注解*/ @Documented @Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface CacheEvictor {Cache[] value() default {}; }自定義了上面注解以后只需要在controller層需要注解的方法上加對(duì)應(yīng)注解即可
緩存生成鍵工具類(lèi)
/*** 緩存鍵生成工具*/ public class CacheKeyBuilder {/*** 生成key** @param key 鍵* @param params 參數(shù)* @param args 參數(shù)值* @return* @throws IllegalAccessException 當(dāng)訪問(wèn)異常時(shí)拋出*/public static String generate(String key, String params, Object[] args) throws IllegalAccessException {StringBuilder keyBuilder = new StringBuilder("");if (StringUtils.hasText(key)) {keyBuilder.append(key);}if (StringUtils.hasText(params)) {String paramsResult = ObjectAccessUtils.get(args, params, String.class, "_", "null");keyBuilder.append(":");keyBuilder.append(paramsResult);}return keyBuilder.toString();} }自定義緩存攔截器
注意這里的Interceptor是org.aopalliance.intercept包下的
Spring的AOP只能支持到方法級(jí)別的切入。換句話說(shuō),切入點(diǎn)只能是某個(gè)方法。
在上面的攔截器中使用了 CachesAnnotationProcessor processor = AbstractCacheAnnotationProcessor.getProcessor(proceedingJoinPoint, cache);來(lái)對(duì)緩存進(jìn)行處理,需要自定義緩存處理器:
緩存處理器
import com.itheima.j2cache.annotation.Cache; import com.itheima.j2cache.annotation.CacheEvictor; import com.itheima.j2cache.model.AnnotationInfo; import com.itheima.j2cache.utils.CacheKeyBuilder; import com.itheima.j2cache.utils.SpringApplicationContextUtils; import net.oschina.j2cache.CacheChannel; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.Signature; import org.aspectj.lang.reflect.MethodSignature; import org.springframework.context.ApplicationContext; import org.springframework.util.StringUtils;/*** 抽象緩存注解處理器*/ public abstract class AbstractCacheAnnotationProcessor {protected CacheChannel cacheChannel;/*** 初始化公共屬性,供子類(lèi)使用*///注意這里使用應(yīng)用上下文來(lái)獲得對(duì)應(yīng)的CacheChannel這個(gè)beanpublic AbstractCacheAnnotationProcessor(){ApplicationContext applicationContext = SpringApplicationContextUtils.getApplicationContext();cacheChannel = applicationContext.getBean(CacheChannel.class);}/*** 封裝注解信息* @param proceedingJoinPoint* @param cache* @return*/protected AnnotationInfo<Cache> getAnnotationInfo(ProceedingJoinPoint proceedingJoinPoint,Cache cache){AnnotationInfo<Cache> annotationInfo = new AnnotationInfo<>();annotationInfo.setAnnotation(cache);annotationInfo.setRegion(cache.region());try{annotationInfo.setKey(generateKey(proceedingJoinPoint,cache));}catch (Exception e){e.printStackTrace();}return annotationInfo;}/*** 動(dòng)態(tài)解析注解信息,生成key* @param proceedingJoinPoint* @param cache* @return*/protected String generateKey(ProceedingJoinPoint proceedingJoinPoint,Cache cache) throws IllegalAccessException{String key = cache.key();//abif(!StringUtils.hasText(key)){//如果當(dāng)前key為空串,重新設(shè)置當(dāng)前可以為:目標(biāo)Controller類(lèi)名:方法名String className = proceedingJoinPoint.getTarget().getClass().getSimpleName();MethodSignature signature = (MethodSignature) proceedingJoinPoint.getSignature();String methodName = signature.getMethod().getName();key = className + ":" + methodName;}//ab:100key = CacheKeyBuilder.generate(key,cache.params(),proceedingJoinPoint.getArgs());return key;}/*** 抽象方法,處理緩存操作,具體應(yīng)該由子類(lèi)具體實(shí)現(xiàn)* @param proceedingJoinPoint* @return* @throws Throwable*/public abstract Object process(ProceedingJoinPoint proceedingJoinPoint) throws Throwable;/*** 獲得緩存注解處理器對(duì)象* @param proceedingJoinPoint* @param cache* @return*/public static CachesAnnotationProcessor getProcessor(ProceedingJoinPoint proceedingJoinPoint, Cache cache){return new CachesAnnotationProcessor(proceedingJoinPoint,cache);}/*** 獲得清理緩存注解處理器對(duì)象* @param proceedingJoinPoint* @param cacheEvictor* @return*/public static CacheEvictorAnnotationProcessor getProcessor(ProceedingJoinPoint proceedingJoinPoint, CacheEvictor cacheEvictor){return new CacheEvictorAnnotationProcessor(proceedingJoinPoint,cacheEvictor);} }清理緩存注解的處理器:
/*** 清理緩存數(shù)據(jù)處理器*/ public class CacheEvictorAnnotationProcessor extends AbstractCacheAnnotationProcessor{/*** 封裝注解信息集合*/private List<AnnotationInfo<Cache>> cacheList = new ArrayList<>();/*** 初始化清理緩存注解處理器對(duì)象,同時(shí)初始化一些緩存操作的對(duì)象* @param proceedingJoinPoint* @param cacheEvictor*/public CacheEvictorAnnotationProcessor(ProceedingJoinPoint proceedingJoinPoint, CacheEvictor cacheEvictor) {super();Cache[] value = cacheEvictor.value();for(Cache cache : value){AnnotationInfo<Cache> annotationInfo = getAnnotationInfo(proceedingJoinPoint, cache);cacheList.add(annotationInfo);}}/*** 具體清理緩存處理邏輯* @param proceedingJoinPoint* @return* @throws Throwable*/public Object process(ProceedingJoinPoint proceedingJoinPoint) throws Throwable{for (AnnotationInfo<Cache> annotationInfo : cacheList) {String region = annotationInfo.getRegion();String key = annotationInfo.getKey();//清理緩存數(shù)據(jù)cacheChannel.evict(region,key);}//調(diào)用目標(biāo)方法(就是Controller中的方法)return proceedingJoinPoint.proceed(proceedingJoinPoint.getArgs());} }緩存注解處理器:
/*** 緩存注解處理器*/ public class CachesAnnotationProcessor extends AbstractCacheAnnotationProcessor {private static final Logger logger = LoggerFactory.getLogger(CachesAnnotationProcessor.class);private AnnotationInfo<Cache> annotationInfo;/*** 初始化處理器,同時(shí)將相關(guān)的對(duì)象進(jìn)行初始化* @param proceedingJoinPoint* @param cache*/public CachesAnnotationProcessor(ProceedingJoinPoint proceedingJoinPoint, Cache cache) {super();//創(chuàng)建注解信息對(duì)象annotationInfo = getAnnotationInfo(proceedingJoinPoint,cache);}/*** 具體緩存處理邏輯* @param proceedingJoinPoint* @return* @throws Throwable*/public Object process(ProceedingJoinPoint proceedingJoinPoint) throws Throwable{Object result = null;boolean existsCache = false;//1、獲取緩存數(shù)據(jù)CacheHolder cacheHolder = getCache(annotationInfo);if(cacheHolder.isExistsCache()){//2、如果緩存數(shù)據(jù)存在則直接返回(相當(dāng)于controller的目標(biāo)方法沒(méi)有執(zhí)行)result = cacheHolder.getValue();//緩存結(jié)果數(shù)據(jù)existsCache = true;}if(!existsCache){//3、如何緩存數(shù)據(jù)不存在,放行調(diào)用Controller的目標(biāo)方法result = invoke(proceedingJoinPoint);//4、將目標(biāo)方法的返回值載入緩存setCache(result);}//5、將結(jié)果返回return result;}/*** 獲取緩存數(shù)據(jù)* @param annotationInfo* @return*/private CacheHolder getCache(AnnotationInfo<Cache> annotationInfo){Object value = null;String region = annotationInfo.getRegion();String key = annotationInfo.getKey();boolean exists = cacheChannel.exists(region, key);if(exists){CacheObject cacheObject = cacheChannel.get(region, key);value = cacheObject.getValue();//獲得緩存結(jié)果數(shù)據(jù)return CacheHolder.newResult(value,true);}return CacheHolder.newResult(value,false);}/*** 調(diào)用目標(biāo)方法* @param proceedingJoinPoint* @return*/private Object invoke(ProceedingJoinPoint proceedingJoinPoint) throws Throwable{return proceedingJoinPoint.proceed(proceedingJoinPoint.getArgs());}/*** 設(shè)置緩存數(shù)據(jù)* @param result*/private void setCache(Object result){cacheChannel.set(annotationInfo.getRegion(),annotationInfo.getKey(),result);} }在上面的處理器中用 return CacheHolder.newResult(value,true);來(lái)獲得緩存結(jié)果,需要自定義一個(gè)結(jié)果封裝類(lèi)
緩存結(jié)果和緩存信息實(shí)體封裝
緩存信息封裝
/*** Cache相關(guān)信息封裝*/ public class AnnotationInfo<T extends Annotation> {private T annotation;private String region;private String key;//region:key:paramspublic T getAnnotation() {return annotation;}public void setAnnotation(T annotation) {this.annotation = annotation;}public String getRegion() {return region;}public void setRegion(String region) {this.region = region;}public String getKey() {return key;}public void setKey(String key) {this.key = key;}public String toString() {if (annotation == null) {return null;}return JSONObject.toJSONString(this);} }緩存結(jié)果封裝
/*** 緩存結(jié)果封裝*/ public class CacheHolder {private Object value;//緩存的數(shù)據(jù)private boolean existsCache;//緩存數(shù)據(jù)是否存在private Throwable throwable;/*** 初始化緩存占位*/private CacheHolder() {}/*** 獲取值** @return*/public Object getValue() {return value;}/*** 是否存在緩存** @return*/public boolean isExistsCache() {return existsCache;}/*** 是否有錯(cuò)誤** @return*/public boolean hasError() {return throwable != null;}/*** 生成緩存結(jié)果的占位** @param value 結(jié)果* @param existsCache 是否存在緩存* @return 緩存*/public static CacheHolder newResult(Object value, boolean existsCache) {CacheHolder cacheHolder = new CacheHolder();cacheHolder.value = value;cacheHolder.existsCache = existsCache;return cacheHolder;}/*** 生成緩存異常的占位** @param throwable 異常* @return 緩存*/public static CacheHolder newError(Throwable throwable) {CacheHolder cacheHolder = new CacheHolder();cacheHolder.throwable = throwable;return cacheHolder;} }開(kāi)啟聲明式注解
/*** 開(kāi)啟聲明式緩存功能*/@Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE}) @Documented @Import(CacheMethodInterceptor.class) public @interface EnableCache { }注意自定義這個(gè)注解以后在主啟動(dòng)類(lèi)上加@EnableCache即可表示開(kāi)啟注解
controller層使用緩存
/*** 地址簿*/ @Log4j2 @RestController @RequestMapping("addressBook") public class AddressBookController {@Autowiredprivate IAddressBookService addressBookService;@Autowiredprivate CacheChannel cacheChannel;private String region = "addressBook";/*** 新增地址簿** @param entity* @return*/@PostMapping("")public Result save(@RequestBody AddressBook entity) {if (1 == entity.getIsDefault()) {addressBookService.lambdaUpdate().set(AddressBook::getIsDefault, 0).eq(AddressBook::getUserId, entity.getUserId()).update();}boolean result = addressBookService.save(entity);if (result) {//載入緩存cacheChannel.set(region,entity.getId(),entity);return Result.ok();}return Result.error();}/*** 查詢(xún)地址簿詳情** @param id* @return*/@GetMapping("detail/{id}")@Cache(region = "addressBook",key = "ab",params = "id")public AddressBook detail(@PathVariable(name = "id") String id) {AddressBook addressBook = addressBookService.getById(id);return addressBook;}/*** 分頁(yè)查詢(xún)** @param page* @param pageSize* @param userId* @return*/@GetMapping("page")public PageResponse<AddressBook> page(Integer page, Integer pageSize, String userId, String keyword) {Page<AddressBook> iPage = new Page(page, pageSize);Page<AddressBook> pageResult = addressBookService.lambdaQuery().eq(StringUtils.isNotEmpty(userId), AddressBook::getUserId, userId).and(StringUtils.isNotEmpty(keyword), wrapper ->wrapper.like(AddressBook::getName, keyword).or().like(AddressBook::getPhoneNumber, keyword).or().like(AddressBook::getCompanyName, keyword)).page(iPage);return PageResponse.<AddressBook>builder().items(pageResult.getRecords()).page(page).pagesize(pageSize).pages(pageResult.getPages()).counts(pageResult.getTotal()).build();}/*** 修改** @param id* @param entity* @return*/@PutMapping("/{id}")@CacheEvictor(value = {@Cache(region = "addressBook",key = "ab",params = "1.id")})public Result update(@PathVariable(name = "id") String id, @RequestBody AddressBook entity) {entity.setId(id);if (1 == entity.getIsDefault()) {addressBookService.lambdaUpdate().set(AddressBook::getIsDefault, 0).eq(AddressBook::getUserId, entity.getUserId()).update();}boolean result = addressBookService.updateById(entity);if (result) {return Result.ok();}return Result.error();}/*** 刪除** @param id* @return*/@DeleteMapping("/{id}")@CacheEvictor({@Cache(region = "addressBook",key = "ab",params = "id")})public Result del(@PathVariable(name = "id") String id) {boolean result = addressBookService.removeById(id);if (result) {return Result.ok();}return Result.error();} }總結(jié)
使用aspectj:AOP 技術(shù)利用一種稱(chēng)為"橫切"的技術(shù),剖解開(kāi)封裝的對(duì)象內(nèi)部,并將那些影響了多個(gè)類(lèi)的公共行為封裝到一個(gè)可重用模塊,并將其命名為"Aspect",即切面。所謂"切面",簡(jiǎn)單說(shuō)就是那些與業(yè)務(wù)無(wú)關(guān),卻為業(yè)務(wù)模塊所共同調(diào)用的邏輯或責(zé)任封裝起來(lái),便于減少系統(tǒng)的重復(fù)代碼,降低模塊之間的耦合度,并有利于未來(lái)的可操作性和可維護(hù)性。
這里的切面即用戶(hù)請(qǐng)求時(shí)先查詢(xún)緩存這一過(guò)程。
注意:這里使用的是aspectj而非Springaop,故使用時(shí)用法有不一樣。
使用j2cache框架的整體邏輯:自定義緩存注解,類(lèi)似springboot自帶的cache,但是這里粒度更細(xì),而且更好控制超時(shí)時(shí)間
緩存層類(lèi)似如下圖:
然后需要用到aspectj的aop邏輯,自定義橫切關(guān)注點(diǎn),這里的連接點(diǎn)即是controller層的方法,需要判斷每個(gè)方法上是否存在cahce注解,如果不存在則直接放行( proceedingJoinPoint.proceed),如果存在則交給緩存處理器進(jìn)行處理,這里添加和刪除緩存主要用的是j2cache組件的cachechannel,個(gè)人理解它這里類(lèi)似一個(gè)連接到緩存服務(wù)器的通道,且有相應(yīng)的api可以供增刪操作(cacheChannel.set(annotationInfo.getRegion(),annotationInfo.getKey(),result))。在讀取緩存時(shí)首先是從一級(jí)緩存中取,然后從二級(jí)緩存中取,如果沒(méi)找到則查詢(xún)數(shù)據(jù)庫(kù)。對(duì)于緩存結(jié)果的獲得通過(guò)封裝一個(gè)緩存結(jié)果類(lèi)和獲得cache注解的信息類(lèi)來(lái)獲得( AnnotationInfo ,制定了這個(gè)類(lèi)的數(shù)據(jù)類(lèi)型是Annotation的子類(lèi))。
總結(jié)
以上是生活随笔為你收集整理的springboot使用j2cache框架和aspectj自定义缓存的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: Quartz详解和使用CommandLi
- 下一篇: 【代码学习】lua+redis分布式锁代