Spring Cache抽象-使用SpEL表达式解析
作者:小小工匠 发布时间:2023-08-23 11:46:44
Spring Cache抽象-使用SpEL表达式
概述
在Spring Cache注解属性中(比如key,condition和unless),Spring的缓存抽象使用了SpEl表达式,从而提供了属性值的动态生成及足够的灵活性。
下面的代码根据用户的userCode进行缓存,对于key属性,使用了表达式自定义键的生成。
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表达式
SpEL表达式可基于上下文并通过使用缓存抽象,提供与root独享相关联的缓存特定的内置参数。
名称 | 位置 | 描述 | 示例 |
---|---|---|---|
methodName | root对象 | 当前被调用的方法名 | #root.methodname |
method | root对象 | 当前被调用的方法 | #root.method.name |
target | root对象 | 当前被调用的目标对象实例 | #root.target |
targetClass | root对象 | 当前被调用的目标对象的类 | #root.targetClass |
args | root对象 | 当前被调用的方法的参数列表 | #root.args[0] |
caches | root对象 | 当前方法调用使用的缓存列表 | #root.caches[0].name |
Argument Name | 执行上下文 | 当前被调用的方法的参数,如findArtisan(Artisan artisan),可以通过#artsian.id获得参数 | #artsian.id |
result | 执行上下文 | 方法执行后的返回值(仅当方法执行后的判断有效,如 unless cacheEvict的beforeInvocation=false) | #result |
如何让自定义注解支持SpEL表达式
SpEL
:即Spring Expression Language,是一种强大的表达式语言。在Spring产品组合中,它是表达式计算的基础。它支持在运行时查询和操作对象图,它可以与基于XML和基于注解的Spring配置还有bean定义一起使用。由于它能够在运行时动态分配值,因此可以为我们节省大量Java代码。可以用于解析特殊字符串(比如Bean的属性可以直接在字符串中的点出来)。SpEL的应用
:常见的应用,如注解上的使用,Spring缓存中使用的注解
@cachable(key="#user.uId")
public User createUser(User user) {
return user;
}
SpEL还可以用在xml等等上面的解析,大家可以去查阅相关资料。本文主要介绍如果将SpEl与自定义注解相结合,从而解析出自定义注解value的实际值。
Spring缓存操作起来非常方便,只需要加上注解便可实现,Spring也提供的CacheManager,使用者可以配置Redis使用Redis缓存。Spring注解也支持自定义的Key命名,功能已经挺齐全了。
但是,如果想要更多的自定义缓存数据存储格式,比如说缓存的数据之间是有层次关系的(比如视频稿件包含视频,视频下面又包含了视频弹幕),此时想要在更新最顶层的视频弹幕时,不删除整个缓存,而只是更新某个视频稿件下的某个视频的这一条视频弹幕,Spring提供的缓存注解似乎有点不够用。
此时有的开发者可能会想到使用自定义注解+AOP+Jedis来更加细分缓存的存储结构,但是又想用到强大的SpEL表达式来为自定义注解的值赋值(不使用SpEL的话,需要在AOP中获取入参或者返回值,但是每个方法的数据类型又不相同,想要拿到特定的值,便需要类型判断-转换),此时便可以使用SpEL提供的SpelExpressionParser工具来进行解析注解的值,使用十分方便,只需按照SpEL的规则(#)来书写即可。
使用方法
generateKeyBySpEL(String spELString, ProceedingJoinPoint joinPoint)方法封装了SpelExpressionParser解析SpEL的方法,使用时只需要传入spELString:注解的值以及AOP的 joinPoint即可,SpelExpressionParser便会自动的为我们解析出注解的实际值
/**
* 用于SpEL表达式解析.
*/
private SpelExpressionParser parser = new SpelExpressionParser();
/**
* 用于获取方法参数定义名字.
*/
private DefaultParameterNameDiscoverer nameDiscoverer = new DefaultParameterNameDiscoverer();
public String generateKeyBySpEL(String spELString, ProceedingJoinPoint joinPoint) {
// 通过joinPoint获取被注解方法
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Method method = methodSignature.getMethod();
// 使用spring的DefaultParameterNameDiscoverer获取方法形参名数组
String[] paramNames = nameDiscoverer.getParameterNames(method);
// 解析过后的Spring表达式对象
Expression expression = parser.parseExpression(spELString);
// spring的表达式上下文对象
EvaluationContext context = new StandardEvaluationContext();
// 通过joinPoint获取被注解方法的形参
Object[] args = joinPoint.getArgs();
// 给上下文赋值
for(int i = 0 ; i < args.length ; i++) {
context.setVariable(paramNames[i], args[i]);
}
// 表达式从上下文中计算出实际参数值
/*如:
@annotation(key="#student.name")
method(Student student)
那么就可以解析出方法形参的某属性值,return “xiaoming”;
*/
return expression.getValue(context).toString();
}
使用案例
1.准备
①.SpringAop相关jar包,
②.Spring-expression
2.自定义注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SelectRedisCache {
String key(); //Redis缓存的HK
String fieldKey() ; //Redis缓存的K
//默认十个小时清空
int expireTime() default 36000;
}
3.定义AOP拦截注解对方法增强进行读写缓存
@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 {
//首先获取注解的实际值,如果是SpEl表达式则进行解析
String key = "";
String fieldKey = "";
Object redisObj = null;
try {
if (!cacheable.key().contains("#")) {
//注解的值非SPEL表达式,直接解析就好
key = cacheable.key();
} else {
//使用注解中的key, 支持SpEL表达式
String spEL = cacheable.key();
//调用SpelExpressionParser方法解析出注解的实际值
key = generateKeyBySpEL(spEL, joinPoint);
System.out.println("key=" +key);
}
//获取fieldKey,同上面的key一样
if (cacheable.fieldKey().equals("")) {
//等于空,则查询整个大Key
fieldKey = "SelectString";
} else {
//使用注解中的key, 支持SpEL表达式
String spEL = cacheable.fieldKey();
fieldKey = generateKeyBySpEL(spEL, joinPoint);
}
//如果注解的fieldKey值为"",则查询大Key
if (fieldKey.equals("SelectString")) {
//直接查询的是大Key
Set keys = redisMap.get(key).keySet();
//使用集合来接收查询出来的对象
List<Object> redisList = new ArrayList<>();
//遍历缓存fieldKey,查出缓存中每一个对象,放入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 {
//否则,查询的是单个对象
redisObj =redisMap.get(key).get(fieldKey);
}
if (redisObj !=null) {
return redisObj;
}
}catch (Exception e) {
Exception e2 = new Exception("查询不到缓存异常");
e.printStackTrace();
e2.printStackTrace();
}
//以上,是使用AOP拦截查询方法,如果缓存中存在,则直接返回缓存结果,
//减少数据库查询压力。
//没有缓存则读取MySQL
System.out.println("查询不到缓存");
//执行方法
Object resultOld = joinPoint.proceed();
//查询结果不为空,则存入缓存,便于下次直接从缓存中查询数据
if (resultOld != null) {
try {
// 然后将读取的结果保存至Redis缓存
boolean resultRow = false;
if (fieldKey.equals("SelectString")) {
//保存的是集合,需要遍历存储
//先类型强转
List<Object> objectList = (List<Object>) resultOld;
//遍历返回值集合,进行缓存
for (Object o : objectList) {
//由于不同对象存储缓存时,使用的key、fieldKey都不相同,
//本次模拟都是以数据表的主键值作为fieldKey来存储,然后用不同的key作为区分。
//因此需要进行类型转换来获取每个不同对象的不同主键调用方法。
//当然,如果你所有的对象获取主键的方法名都一样的话,
//完全可以使用反射中的【使用方法名获取方法】来调用对象返回主键值。
if (o instanceof Barrage) { //缓存弹幕对象
Barrage barrageO = (Barrage) o;
fieldKey = barrageO.getBaId() + "";
//增加单个
redisMap.put(key, new HashMap<>().put(fieldKey,barrageO ))
} else if (o instanceof Video){//缓存视频对象
Video videoO = (Video) o;
fieldKey = barrageO.getvId() + "";
//增加单个
redisMap.put(key, new HashMap<>().put(fieldKey,videoO ))
} else {
//TODO 继续增
}
}
} else {
//增加单个
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.测试
缓存视频稿件:存储数据格式为:
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;
}
来源:https://artisan.blog.csdn.net/article/details/78157834


猜你喜欢
- 前言兄弟们,刚刚又给seata社区修了一个BUG,有用户提了issue反应TransactionHook在某些情况下不会被调用:相关issu
- 什么是数组数组(Array)是有序的元素序列。 若将有限个类型相同的变量的集合命名,那么这个名称为数组名。组成数组的各个变量称为数组的分量,
- 最近碰到个项目要使用到滚动选择器,原生的NumberPicker可定制性太差,不大符合UI要求。网上开源的WheelView是用Scroll
- 应用启动的时候有短暂的白屏,如图:可以通过设置theme的方式来解决 <style name="AppTheme"
- 先看看效果图:源码下载:Android Navigation TabBar控件实现多彩标签栏代码:MainActivity.javapack
- 效果图在开发APP中,经常要实现圆形头像,那么该如何实现呢?要裁剪吗,要重写draw函数吗?不用,只用一行代码就可以实现Glide实现圆形图
- 无意中在一个国外的站点下到了一个利用WCF实现聊天的程序,作者是:Nikola Paljetak。研究了一下,自己做了测试和部分修改,感觉还
- Springboot对配置文件的敏感信息加密前言最近公司对软件的安全问题比较在意,要求对配置文件中的敏感信息如数据库密码等进行加密。但是Sp
- 小编为面试Java程序员的朋友们整理了2017非常热门的面试中的笔试试题,如果你是一个正在准备面试Java程序员的读者,赶快学习一下吧。1,
- 前置说明:这里的代码演示都是在UserController类里面使用UserService类,然后通过启动类调用UserController
- 我贴c#的代码: namespace IWebs.Webs{ using System; using System.Web.Services
- MyBatis Example And与Or混合使用(条件1 and 条件2) or ( 条件3 and 条件4)  
- CLR允许将一个对象转换为它的实际类型,或者它的基类型。 在C#中,可将一个对象隐式转换为它的基类型,将对象转换成派生类型需要显示转换。例:
- 一、系统介绍本系统实现了用户登录,实现了对学生成绩的增删改查,实现了用户修改密码功能,采用MD5加密算法,数据库使用Mysql8.0.13,
- 说到关注功能,可能很多小伙伴要说了。谁不会写但是没有合理的架构,大家写出来的代码很可能是一大堆的复制粘贴。比如十几个页面,都有这个关注按钮。
- 结构型设计模式创建型设计模式主要是为了解决创建对象的问题,而结构型设计模式则是为了解决已有对象的使用问题。适配器模式适配器模式比较好理解,因
- @NonNull导致无法序列化的问题以上这个代码在接参的时候报了一个缺少无参构造函数无法序列化的错误将.class反编译可以看到编译后的源码
- Java 读取外部资源的方法详解在Java代码中经常有读取外部资源的要求:如配置文件等等,通常会把配置文件放在classpath下或者在we
- 一、文件上传原理 1、文件上传的前提:a、form表单的method必须是postb、form表单的enctype必须是multi
- Kotlin this详解及实例为了表示当前函数的接收者(receiver), 们使用this表达式:在类的成员函数中,this指向这个类的