springboot使用j2cache框架和aspectj自定义缓存
文章目錄
- 依賴和工具介紹
- 項目代碼
- spring上下文工具類:
- 自定義緩存注解
- 緩存生成鍵工具類
- 自定義緩存攔截器
- 緩存處理器
- 緩存結果和緩存信息實體封裝
- 開啟聲明式注解
- controller層使用緩存
- 總結
依賴和工具介紹
<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客戶端使用lettuce,也可以使用Jedisl2-cache-open: true #開啟二級緩存broadcast: net.oschina.j2cache.cache.support.redis.SpringRedisPubSubPolicy# broadcast: jgroupsL1: #指定一級緩存提供者為caffeineprovider_class: caffeineL2: #指定二級緩存提供者為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 # 這個配置文件需要放在項目中 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目前正在使用的兩級緩存框架。
j2cache的兩級緩存結構:
L1: 進程內緩存 caffeine/ehcache
L2: 集中式緩存 Redis/Memcached
j2cache其實并不是在重復造輪子,而是作資源整合,即將Ehcache、Caffeine、redis、Spring Cache等進行整合。
由于大量的緩存讀取會導致L2的網絡成為整個系統的瓶頸,因此L1的目標是降低對L2的讀取次數。該緩存框架主要用于集群環境中。單機也可使用,用于避免應用重啟導致的ehcache緩存數據丟失。
j2cache從1.3.0版本開始支持JGroups和Redis Pub/Sub兩種方式進行緩存事件的通知。
數據讀取順序 -> L1 -> L2 -> DB
關于j2cache的region概念:
J2Cache 的 Region 來源于 Ehcache 的 Region 概念。
一般我們在使用像 Redis、Caffeine、Guava Cache 時都沒有 Region 這樣的概念,特別是 Redis 是一個大哈希表,更沒有這個概念。
在實際的緩存場景中,不同的數據會有不同的 TTL 策略,例如有些緩存數據可以永不失效,而有些緩存我們希望是 30 分鐘的有效期,有些是 60 分鐘等不同的失效時間策略。在 Redis 我們可以針對不同的 key 設置不同的 TTL 時間。但是一般的 Java 內存緩存框架(如 Ehcache、Caffeine、Guava Cache 等),它沒法為每一個 key 設置不同 TTL,因為這樣管理起來會非常復雜,而且會檢查緩存數據是否失效時性能極差。所以一般內存緩存框架會把一組相同 TTL 策略的緩存數據放在一起進行管理。
像 Caffeine 和 Guava Cache 在存放緩存數據時需要先構建一個 Cache 實例,設定好緩存的時間策略,如下代碼所示:
Caffeine<Object, Object> caffeine = Caffeine.newBuilder();
caffeine = caffeine.maximumSize(size).expireAfterWrite(expire, TimeUnit.SECONDS);
Cache<String, Object> theCache = caffeine.build()
這時候你才可以往 theCache 寫入緩存數據,而不能再單獨針對某一個 key 設定不同的 TTL 時間。
而 Redis 可以讓你非常隨意的給不同的 key 設置不同的 TTL。
J2Cache 是內存緩存和 Redis 這類集中式緩存的一個橋梁,因此它只能是兼容兩者的特性。
J2Cache 默認使用 Caffeine 作為一級緩存,其配置文件位于 caffeine.properties 中。一個基本使用場景如下:
#########################################
# Caffeine configuration # [name] = size, xxxx[s|m|h|d]#########################################
default = 1000, 30m
users = 2000, 10m
blogs = 5000, 1h
上面的配置定義了三個緩存 Region ,分別是:
默認緩存,大小是 1000 個對象,TTL 是 30 分鐘
users 緩存,大小是 2000 個對象,TTL 是 10 分鐘
blogs 緩存,大小是 5000 個對象,TTL 是 1 個小時
例如我們可以用 users 來存放用戶對象的緩存,用 blogs 來存放博客對象緩存,兩種的 TTL 是不同的。
項目代碼
現在上代碼,Springbootzhenghej2cache進行緩存:
spring上下文工具類:
/*** Spring上下文工具類*/ @Primary @Component public class SpringApplicationContextUtils {private static ApplicationContext springContext;@Autowiredprivate ApplicationContext applicationContext;@PostConstructprivate void init() {springContext = applicationContext;}/*** 獲取當前ApplicationContext** @return ApplicationContext*/public static ApplicationContext getApplicationContext() {return springContext;}}自定義緩存注解
如果項目中很多模塊都需要使用緩存功能,這些模塊都需要調用j2cache的API來進行緩存操作,這種j2cache提供的原生API使用起來就比較繁瑣了,并且操作緩存的代碼和我們的業務代碼混合到一起,即j2cache的API對我們的業務代碼具有侵入性。那么我們如何更加簡潔、優雅的使用j2cache提供的緩存功能呢?
答案就是使用聲明式緩存。所謂聲明式緩存,就是定義緩存注解,在需要使用緩存功能的方法上加入緩存注解即可自動進行緩存操作。
這種使用方式類似于我們以前使用的聲明式事務,即在類的方法上加入事務注解就可以實現事務控制。
注意:j2cache原生API和我們實現的聲明式緩存可以兼容,即在項目中可以同時使用,互為補充。例如在Controller的方法中需要將多類業務數據載入緩存,此時通過聲明式緩存就無法做到(因為聲明式緩存只能將方法的返回值載入緩存),這種場景下就需要調用j2cache的原生API來完成。
/*** 緩存注解*/ @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層需要注解的方法上加對應注解即可
緩存生成鍵工具類
/*** 緩存鍵生成工具*/ public class CacheKeyBuilder {/*** 生成key** @param key 鍵* @param params 參數* @param args 參數值* @return* @throws IllegalAccessException 當訪問異常時拋出*/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只能支持到方法級別的切入。換句話說,切入點只能是某個方法。
在上面的攔截器中使用了 CachesAnnotationProcessor processor = AbstractCacheAnnotationProcessor.getProcessor(proceedingJoinPoint, cache);來對緩存進行處理,需要自定義緩存處理器:
緩存處理器
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;/*** 初始化公共屬性,供子類使用*///注意這里使用應用上下文來獲得對應的CacheChannel這個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;}/*** 動態解析注解信息,生成key* @param proceedingJoinPoint* @param cache* @return*/protected String generateKey(ProceedingJoinPoint proceedingJoinPoint,Cache cache) throws IllegalAccessException{String key = cache.key();//abif(!StringUtils.hasText(key)){//如果當前key為空串,重新設置當前可以為:目標Controller類名:方法名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;}/*** 抽象方法,處理緩存操作,具體應該由子類具體實現* @param proceedingJoinPoint* @return* @throws Throwable*/public abstract Object process(ProceedingJoinPoint proceedingJoinPoint) throws Throwable;/*** 獲得緩存注解處理器對象* @param proceedingJoinPoint* @param cache* @return*/public static CachesAnnotationProcessor getProcessor(ProceedingJoinPoint proceedingJoinPoint, Cache cache){return new CachesAnnotationProcessor(proceedingJoinPoint,cache);}/*** 獲得清理緩存注解處理器對象* @param proceedingJoinPoint* @param cacheEvictor* @return*/public static CacheEvictorAnnotationProcessor getProcessor(ProceedingJoinPoint proceedingJoinPoint, CacheEvictor cacheEvictor){return new CacheEvictorAnnotationProcessor(proceedingJoinPoint,cacheEvictor);} }清理緩存注解的處理器:
/*** 清理緩存數據處理器*/ public class CacheEvictorAnnotationProcessor extends AbstractCacheAnnotationProcessor{/*** 封裝注解信息集合*/private List<AnnotationInfo<Cache>> cacheList = new ArrayList<>();/*** 初始化清理緩存注解處理器對象,同時初始化一些緩存操作的對象* @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();//清理緩存數據cacheChannel.evict(region,key);}//調用目標方法(就是Controller中的方法)return proceedingJoinPoint.proceed(proceedingJoinPoint.getArgs());} }緩存注解處理器:
/*** 緩存注解處理器*/ public class CachesAnnotationProcessor extends AbstractCacheAnnotationProcessor {private static final Logger logger = LoggerFactory.getLogger(CachesAnnotationProcessor.class);private AnnotationInfo<Cache> annotationInfo;/*** 初始化處理器,同時將相關的對象進行初始化* @param proceedingJoinPoint* @param cache*/public CachesAnnotationProcessor(ProceedingJoinPoint proceedingJoinPoint, Cache cache) {super();//創建注解信息對象annotationInfo = getAnnotationInfo(proceedingJoinPoint,cache);}/*** 具體緩存處理邏輯* @param proceedingJoinPoint* @return* @throws Throwable*/public Object process(ProceedingJoinPoint proceedingJoinPoint) throws Throwable{Object result = null;boolean existsCache = false;//1、獲取緩存數據CacheHolder cacheHolder = getCache(annotationInfo);if(cacheHolder.isExistsCache()){//2、如果緩存數據存在則直接返回(相當于controller的目標方法沒有執行)result = cacheHolder.getValue();//緩存結果數據existsCache = true;}if(!existsCache){//3、如何緩存數據不存在,放行調用Controller的目標方法result = invoke(proceedingJoinPoint);//4、將目標方法的返回值載入緩存setCache(result);}//5、將結果返回return result;}/*** 獲取緩存數據* @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();//獲得緩存結果數據return CacheHolder.newResult(value,true);}return CacheHolder.newResult(value,false);}/*** 調用目標方法* @param proceedingJoinPoint* @return*/private Object invoke(ProceedingJoinPoint proceedingJoinPoint) throws Throwable{return proceedingJoinPoint.proceed(proceedingJoinPoint.getArgs());}/*** 設置緩存數據* @param result*/private void setCache(Object result){cacheChannel.set(annotationInfo.getRegion(),annotationInfo.getKey(),result);} }在上面的處理器中用 return CacheHolder.newResult(value,true);來獲得緩存結果,需要自定義一個結果封裝類
緩存結果和緩存信息實體封裝
緩存信息封裝
/*** Cache相關信息封裝*/ 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);} }緩存結果封裝
/*** 緩存結果封裝*/ public class CacheHolder {private Object value;//緩存的數據private boolean existsCache;//緩存數據是否存在private Throwable throwable;/*** 初始化緩存占位*/private CacheHolder() {}/*** 獲取值** @return*/public Object getValue() {return value;}/*** 是否存在緩存** @return*/public boolean isExistsCache() {return existsCache;}/*** 是否有錯誤** @return*/public boolean hasError() {return throwable != null;}/*** 生成緩存結果的占位** @param value 結果* @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;} }開啟聲明式注解
/*** 開啟聲明式緩存功能*/@Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE}) @Documented @Import(CacheMethodInterceptor.class) public @interface EnableCache { }注意自定義這個注解以后在主啟動類上加@EnableCache即可表示開啟注解
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();}/*** 查詢地址簿詳情** @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;}/*** 分頁查詢** @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();} }總結
使用aspectj:AOP 技術利用一種稱為"橫切"的技術,剖解開封裝的對象內部,并將那些影響了多個類的公共行為封裝到一個可重用模塊,并將其命名為"Aspect",即切面。所謂"切面",簡單說就是那些與業務無關,卻為業務模塊所共同調用的邏輯或責任封裝起來,便于減少系統的重復代碼,降低模塊之間的耦合度,并有利于未來的可操作性和可維護性。
這里的切面即用戶請求時先查詢緩存這一過程。
注意:這里使用的是aspectj而非Springaop,故使用時用法有不一樣。
使用j2cache框架的整體邏輯:自定義緩存注解,類似springboot自帶的cache,但是這里粒度更細,而且更好控制超時時間
緩存層類似如下圖:
然后需要用到aspectj的aop邏輯,自定義橫切關注點,這里的連接點即是controller層的方法,需要判斷每個方法上是否存在cahce注解,如果不存在則直接放行( proceedingJoinPoint.proceed),如果存在則交給緩存處理器進行處理,這里添加和刪除緩存主要用的是j2cache組件的cachechannel,個人理解它這里類似一個連接到緩存服務器的通道,且有相應的api可以供增刪操作(cacheChannel.set(annotationInfo.getRegion(),annotationInfo.getKey(),result))。在讀取緩存時首先是從一級緩存中取,然后從二級緩存中取,如果沒找到則查詢數據庫。對于緩存結果的獲得通過封裝一個緩存結果類和獲得cache注解的信息類來獲得( AnnotationInfo ,制定了這個類的數據類型是Annotation的子類)。
總結
以上是生活随笔為你收集整理的springboot使用j2cache框架和aspectj自定义缓存的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Quartz详解和使用CommandLi
- 下一篇: 【代码学习】lua+redis分布式锁代