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独享相关联的缓存特定的内置参数。

名称位置描述示例
methodNameroot对象当前被调用的方法名#root.methodname
methodroot对象当前被调用的方法#root.method.name
targetroot对象当前被调用的目标对象实例#root.target
targetClassroot对象当前被调用的目标对象的类#root.targetClass
argsroot对象当前被调用的方法的参数列表#root.args[0]
cachesroot对象当前方法调用使用的缓存列表#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

标签:Spring,Cache抽象,SpEL,表达式
0
投稿

猜你喜欢

  • selenium+java破解极验滑动验证码的示例代码

    2022-11-19 21:52:01
  • java判断http地址是否连通(示例代码)

    2023-08-05 03:24:05
  • Spring boot2+jpa+thymeleaf实现增删改查

    2021-06-02 07:21:49
  • Android 自定义闪屏页广告倒计时view效果

    2021-10-13 16:26:39
  • Mybatis中Collection集合标签的使用详解

    2023-07-10 05:09:35
  • 如何基于SpringBoot部署外部Tomcat过程解析

    2021-10-26 07:14:16
  • android中UI主线程与子线程深入分析

    2022-02-06 15:19:25
  • 你所不知道的Spring自动注入详解

    2021-09-04 19:30:08
  • Android开发中多进程共享数据简析

    2023-10-10 05:17:04
  • SpringBoot异常处理器的使用与添加员工功能实现流程介绍

    2021-10-21 19:24:23
  • Android自定义流式布局的实现示例

    2022-10-14 23:30:44
  • C# ComboBox控件“设置 DataSource 属性后无法修改项集合”的完美解决方法

    2023-01-30 04:11:58
  • springboot实现在工具类(util)中调用注入service层方法

    2021-06-17 20:02:51
  • Android开源AndroidSideMenu实现抽屉和侧滑菜单

    2023-10-09 09:24:51
  • C# WinForm程序完全退出的问题解决

    2023-07-02 07:29:34
  • java求余的技巧汇总

    2023-08-06 06:33:16
  • Java在制作jar包时引用第三方jar包的方法

    2023-02-18 23:25:10
  • Android仿微信Viewpager-Fragment惰性加载(lazy-loading)

    2023-12-16 23:50:59
  • Java中if...else语句使用的学习教程

    2023-10-08 03:30:59
  • springboot更新配置Swagger3的一些小技巧

    2023-08-28 06:31:43
  • asp之家 软件编程 m.aspxhome.com