Spring?Cache抽象-使用SpEL表達(dá)式解析
Spring Cache抽象-使用SpEL表達(dá)式
概述
在Spring Cache注解屬性中(比如key,condition和unless),Spring的緩存抽象使用了SpEl表達(dá)式,從而提供了屬性值的動(dòng)態(tài)生成及足夠的靈活性。
下面的代碼根據(jù)用戶的userCode進(jìn)行緩存,對(duì)于key屬性,使用了表達(dá)式自定義鍵的生成。
public class UserService {
private Map<Integer, User> users = new HashMap<Integer, User>();
{
users.put(1, new User("1", "w1",37));
users.put(2, new User("2", "w2", 34));
}
@Cacheable(value = "users", key = "#user.userCode" condition = "#user.age < 35")
public User getUser(User user) {
System.out.println("User with id " + user.getUserId() + " requested.");
return users.get(Integer.valueOf(user.getUserId()));
}
SpEl表達(dá)式
SpEL表達(dá)式可基于上下文并通過(guò)使用緩存抽象,提供與root獨(dú)享相關(guān)聯(lián)的緩存特定的內(nèi)置參數(shù)。
| 名稱 | 位置 | 描述 | 示例 |
|---|---|---|---|
| methodName | root對(duì)象 | 當(dāng)前被調(diào)用的方法名 | #root.methodname |
| method | root對(duì)象 | 當(dāng)前被調(diào)用的方法 | #root.method.name |
| target | root對(duì)象 | 當(dāng)前被調(diào)用的目標(biāo)對(duì)象實(shí)例 | #root.target |
| targetClass | root對(duì)象 | 當(dāng)前被調(diào)用的目標(biāo)對(duì)象的類 | #root.targetClass |
| args | root對(duì)象 | 當(dāng)前被調(diào)用的方法的參數(shù)列表 | #root.args[0] |
| caches | root對(duì)象 | 當(dāng)前方法調(diào)用使用的緩存列表 | #root.caches[0].name |
| Argument Name | 執(zhí)行上下文 | 當(dāng)前被調(diào)用的方法的參數(shù),如findArtisan(Artisan artisan),可以通過(guò)#artsian.id獲得參數(shù) | #artsian.id |
| result | 執(zhí)行上下文 | 方法執(zhí)行后的返回值(僅當(dāng)方法執(zhí)行后的判斷有效,如 unless cacheEvict的beforeInvocation=false) | #result |
如何讓自定義注解支持SpEL表達(dá)式
SpEL:即Spring Expression Language,是一種強(qiáng)大的表達(dá)式語(yǔ)言。在Spring產(chǎn)品組合中,它是表達(dá)式計(jì)算的基礎(chǔ)。它支持在運(yùn)行時(shí)查詢和操作對(duì)象圖,它可以與基于XML和基于注解的Spring配置還有bean定義一起使用。由于它能夠在運(yùn)行時(shí)動(dòng)態(tài)分配值,因此可以為我們節(jié)省大量Java代碼。可以用于解析特殊字符串(比如Bean的屬性可以直接在字符串中的點(diǎn)出來(lái))。SpEL的應(yīng)用:常見(jiàn)的應(yīng)用,如注解上的使用,Spring緩存中使用的注解
@cachable(key="#user.uId")
public User createUser(User user) {
return user;
}
SpEL還可以用在xml等等上面的解析,大家可以去查閱相關(guān)資料。本文主要介紹如果將SpEl與自定義注解相結(jié)合,從而解析出自定義注解value的實(shí)際值。
- Spring緩存操作起來(lái)非常方便,只需要加上注解便可實(shí)現(xiàn),Spring也提供的CacheManager,使用者可以配置Redis使用Redis緩存。Spring注解也支持自定義的Key命名,功能已經(jīng)挺齊全了。
- 但是,如果想要更多的自定義緩存數(shù)據(jù)存儲(chǔ)格式,比如說(shuō)緩存的數(shù)據(jù)之間是有層次關(guān)系的(比如視頻稿件包含視頻,視頻下面又包含了視頻彈幕),此時(shí)想要在更新最頂層的視頻彈幕時(shí),不刪除整個(gè)緩存,而只是更新某個(gè)視頻稿件下的某個(gè)視頻的這一條視頻彈幕,Spring提供的緩存注解似乎有點(diǎn)不夠用。
- 此時(shí)有的開(kāi)發(fā)者可能會(huì)想到使用自定義注解+AOP+Jedis來(lái)更加細(xì)分緩存的存儲(chǔ)結(jié)構(gòu),但是又想用到強(qiáng)大的SpEL表達(dá)式來(lái)為自定義注解的值賦值(不使用SpEL的話,需要在AOP中獲取入?yún)⒒蛘叻祷刂?,但是每個(gè)方法的數(shù)據(jù)類型又不相同,想要拿到特定的值,便需要類型判斷-轉(zhuǎn)換),此時(shí)便可以使用SpEL提供的SpelExpressionParser工具來(lái)進(jìn)行解析注解的值,使用十分方便,只需按照SpEL的規(guī)則(#)來(lái)書(shū)寫(xiě)即可。
使用方法
generateKeyBySpEL(String spELString, ProceedingJoinPoint joinPoint)方法封裝了SpelExpressionParser解析SpEL的方法,使用時(shí)只需要傳入spELString:注解的值以及AOP的 joinPoint即可,SpelExpressionParser便會(huì)自動(dòng)的為我們解析出注解的實(shí)際值
/**
* 用于SpEL表達(dá)式解析.
*/
private SpelExpressionParser parser = new SpelExpressionParser();
/**
* 用于獲取方法參數(shù)定義名字.
*/
private DefaultParameterNameDiscoverer nameDiscoverer = new DefaultParameterNameDiscoverer();
public String generateKeyBySpEL(String spELString, ProceedingJoinPoint joinPoint) {
// 通過(guò)joinPoint獲取被注解方法
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Method method = methodSignature.getMethod();
// 使用spring的DefaultParameterNameDiscoverer獲取方法形參名數(shù)組
String[] paramNames = nameDiscoverer.getParameterNames(method);
// 解析過(guò)后的Spring表達(dá)式對(duì)象
Expression expression = parser.parseExpression(spELString);
// spring的表達(dá)式上下文對(duì)象
EvaluationContext context = new StandardEvaluationContext();
// 通過(guò)joinPoint獲取被注解方法的形參
Object[] args = joinPoint.getArgs();
// 給上下文賦值
for(int i = 0 ; i < args.length ; i++) {
context.setVariable(paramNames[i], args[i]);
}
// 表達(dá)式從上下文中計(jì)算出實(shí)際參數(shù)值
/*如:
@annotation(key="#student.name")
method(Student student)
那么就可以解析出方法形參的某屬性值,return “xiaoming”;
*/
return expression.getValue(context).toString();
}
使用案例
1.準(zhǔn)備
①.SpringAop相關(guān)jar包,
②.Spring-expression
2.自定義注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SelectRedisCache {
String key(); //Redis緩存的HK
String fieldKey() ; //Redis緩存的K
//默認(rèn)十個(gè)小時(shí)清空
int expireTime() default 36000;
}
3.定義AOP攔截注解對(duì)方法增強(qiáng)進(jìn)行讀寫(xiě)緩存
@Aspect
public class SelectRedisCacheAop extends SPELUtil {
private Map<String,Map<String,Object>> redisMap = new HashMap();
@Around("execution(@com.XliXli.annotation.consuRedisCache.SelectRedisCache * *.*(..)) && @annotation(cacheable)")
public Object aroundCacheable(ProceedingJoinPoint joinPoint, SelectRedisCache cacheable) throws Throwable {
//首先獲取注解的實(shí)際值,如果是SpEl表達(dá)式則進(jìn)行解析
String key = "";
String fieldKey = "";
Object redisObj = null;
try {
if (!cacheable.key().contains("#")) {
//注解的值非SPEL表達(dá)式,直接解析就好
key = cacheable.key();
} else {
//使用注解中的key, 支持SpEL表達(dá)式
String spEL = cacheable.key();
//調(diào)用SpelExpressionParser方法解析出注解的實(shí)際值
key = generateKeyBySpEL(spEL, joinPoint);
System.out.println("key=" +key);
}
//獲取fieldKey,同上面的key一樣
if (cacheable.fieldKey().equals("")) {
//等于空,則查詢整個(gè)大Key
fieldKey = "SelectString";
} else {
//使用注解中的key, 支持SpEL表達(dá)式
String spEL = cacheable.fieldKey();
fieldKey = generateKeyBySpEL(spEL, joinPoint);
}
//如果注解的fieldKey值為"",則查詢大Key
if (fieldKey.equals("SelectString")) {
//直接查詢的是大Key
Set keys = redisMap.get(key).keySet();
//使用集合來(lái)接收查詢出來(lái)的對(duì)象
List<Object> redisList = new ArrayList<>();
//遍歷緩存fieldKey,查出緩存中每一個(gè)對(duì)象,放入redisList中
for (Object fieldKey2 : keys) {
Object innerObj = redisMap.get(key).get(fieldKey2);
redisList.add(innerObj);
}
redisObj = redisList;
if (redisList == null || redisList.size() <1) {
redisObj = null;
}
} else {
//否則,查詢的是單個(gè)對(duì)象
redisObj =redisMap.get(key).get(fieldKey);
}
if (redisObj !=null) {
return redisObj;
}
}catch (Exception e) {
Exception e2 = new Exception("查詢不到緩存異常");
e.printStackTrace();
e2.printStackTrace();
}
//以上,是使用AOP攔截查詢方法,如果緩存中存在,則直接返回緩存結(jié)果,
//減少數(shù)據(jù)庫(kù)查詢壓力。
//沒(méi)有緩存則讀取MySQL
System.out.println("查詢不到緩存");
//執(zhí)行方法
Object resultOld = joinPoint.proceed();
//查詢結(jié)果不為空,則存入緩存,便于下次直接從緩存中查詢數(shù)據(jù)
if (resultOld != null) {
try {
// 然后將讀取的結(jié)果保存至Redis緩存
boolean resultRow = false;
if (fieldKey.equals("SelectString")) {
//保存的是集合,需要遍歷存儲(chǔ)
//先類型強(qiáng)轉(zhuǎn)
List<Object> objectList = (List<Object>) resultOld;
//遍歷返回值集合,進(jìn)行緩存
for (Object o : objectList) {
//由于不同對(duì)象存儲(chǔ)緩存時(shí),使用的key、fieldKey都不相同,
//本次模擬都是以數(shù)據(jù)表的主鍵值作為fieldKey來(lái)存儲(chǔ),然后用不同的key作為區(qū)分。
//因此需要進(jìn)行類型轉(zhuǎn)換來(lái)獲取每個(gè)不同對(duì)象的不同主鍵調(diào)用方法。
//當(dāng)然,如果你所有的對(duì)象獲取主鍵的方法名都一樣的話,
//完全可以使用反射中的【使用方法名獲取方法】來(lái)調(diào)用對(duì)象返回主鍵值。
if (o instanceof Barrage) { //緩存彈幕對(duì)象
Barrage barrageO = (Barrage) o;
fieldKey = barrageO.getBaId() + "";
//增加單個(gè)
redisMap.put(key, new HashMap<>().put(fieldKey,barrageO ))
} else if (o instanceof Video){//緩存視頻對(duì)象
Video videoO = (Video) o;
fieldKey = barrageO.getvId() + "";
//增加單個(gè)
redisMap.put(key, new HashMap<>().put(fieldKey,videoO ))
} else {
//TODO 繼續(xù)增
}
}
} else {
//增加單個(gè)
redisTemplate.opsForHash().put(key, fieldKey, resultOld);
}
} catch (Exception e) {
Exception e2 = new Exception("查詢后添加緩存異常");
e.printStackTrace();
e2.printStackTrace();
}
}
return resultOld;
}
public String generateKeyBySpEL(String spELString, ProceedingJoinPoint joinPoint) {
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
String[] paramNames = nameDiscoverer.getParameterNames(methodSignature.getMethod());
Expression expression = parser.parseExpression(spELString);
EvaluationContext context = new StandardEvaluationContext();
Object[] args = joinPoint.getArgs();
for(int i = 0 ; i < args.length ; i++) {
context.setVariable(paramNames[i], args[i]);
}
return expression.getValue(context).toString();
}
}
4.測(cè)試
緩存視頻稿件:存儲(chǔ)數(shù)據(jù)格式為:
- VideoByVSIdXXX - VideoId-Video
- VideoByVSId+視頻稿件Id—視頻Id—視頻
@SelectRedisCache(key = "'VideoByVSId' + #V_OriginId", fieldKey = "")
public List<Video> findByOrigin(Long V_OriginType, Long V_OriginId) {
List<Video> videoList = videoMapper.selectList(new EntityWrapper<Video>().eq("V_OriginType",V_OriginType).eq("V_OriginId",V_OriginId));
for (Video video:videoList) {
video.setBarrages(barrageService.findByVId(video.getvId()));
}
return videoList;
}
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
- Spring中@Value使用詳解及SPEL表達(dá)式
- 詳解Spring中Spel表達(dá)式和el表達(dá)式的區(qū)別
- SpringDataElasticsearch與SpEL表達(dá)式實(shí)現(xiàn)ES動(dòng)態(tài)索引
- Spring AOP如何在注解上使用SPEL表達(dá)式注入對(duì)象
- spring之SpEL表達(dá)式詳解
- 使用Springboot自定義注解,支持SPEL表達(dá)式
- 基于spring?@Cacheable?注解的spel表達(dá)式解析執(zhí)行邏輯
- Spring實(shí)戰(zhàn)之Bean定義中的SpEL表達(dá)式語(yǔ)言支持操作示例
- Spring組件開(kāi)發(fā)模式支持SPEL表達(dá)式
- Spring spel表達(dá)式使用方法示例
- Spring中SpEL表達(dá)式的使用全解
相關(guān)文章
Spring Boot配置線程池拒絕策略的場(chǎng)景分析(妥善處理好溢出的任務(wù))
本文通過(guò)實(shí)例代碼給大家介紹下如何為線程池配置拒絕策略、如何自定義拒絕策略。對(duì)Spring Boot配置線程池拒絕策略的相關(guān)知識(shí)感興趣的朋友一起看看吧2021-09-09
Java項(xiàng)目中大批量數(shù)據(jù)查詢導(dǎo)致OOM的解決
本文主要介紹了Java項(xiàng)目中大批量數(shù)據(jù)查詢導(dǎo)致OOM的解決,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2023-06-06
Mybatis批量插入Oracle數(shù)據(jù)的方法實(shí)例
在開(kāi)發(fā)中或多或少都會(huì)遇到數(shù)據(jù)批量插入的功能,最近我在做項(xiàng)目的過(guò)程中就遇到了這樣一個(gè)問(wèn)題,下面這篇文章主要給大家介紹了關(guān)于Mybatis批量插入Oracle數(shù)據(jù)的相關(guān)資料,需要的朋友可以參考下2022-01-01
SpringBoot如何通過(guò)配置禁用swagger
這篇文章主要給大家介紹了關(guān)于SpringBoot如何通過(guò)配置禁用swagger的相關(guān)資料,Swagger用來(lái)在開(kāi)發(fā)階段方便前后端分離的項(xiàng)目實(shí)戰(zhàn)中,提高前后端人員的工作效率,降低交流成本,但是版本上線之后要是把Swagger帶上去會(huì)存在很大的風(fēng)險(xiǎn),需要的朋友可以參考下2023-08-08
使用jackson實(shí)現(xiàn)對(duì)象json之間的相互轉(zhuǎn)換(spring boot)
這篇文章主要介紹了使用jackson實(shí)現(xiàn)對(duì)象json之間的相互轉(zhuǎn)換(spring boot),具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-09-09

