• Unreal is funny !!!

项目中使用 spring spel 组件

Java 站长 4年前 (2021-09-16) 384次浏览 已收录 0个评论
文章目录[隐藏]

spel 介绍

Spring spel expression 是使用一小段 查询语法去找到一个值,你可以把它理解为一个很小的编程语言,给定一堆上下文变量(比如方法参数,方法信息),通过一个小的字符串,解析并给出一个值,在spring boot 中 经常用在一些注解上获取一些值或者 条件,优雅整洁。
比如:

// 这里加缓存,使用参数里的 user 的id 作为 redis 的key
@Cacheable(value="users", key="#user.id")   
   public User find(User user) {
      returnnull;
   }

目的

我们也有一个类似的 spring cache 的 redis 注解的功能,但是 spring cache 是不支持在注解上指定过期时间的,虽然可以在其他地方配置,但是作为追求优雅的开发者,我要自己实现一套类似 spring cache 但是仅仅针对redis 的缓存,而且支持 spel ,做到某些情况下不缓存,比如 如果 参数 noCache=true,那就不缓存

实现

核心 是只有一个类 MyExpressionEvaluater :

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.aop.support.AopUtils;
import org.springframework.context.expression.AnnotatedElementKey;
import org.springframework.context.expression.CachedExpressionEvaluator;
import org.springframework.context.expression.MethodBasedEvaluationContext;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;

import java.lang.reflect.Method;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

public class MyExpressionEvaluator extends CachedExpressionEvaluator {

    // shared param discoverer since it caches data internally
    private final ParameterNameDiscoverer paramNameDiscoverer = new DefaultParameterNameDiscoverer();

    private final Map<ExpressionKey, Expression> conditionCache = new ConcurrentHashMap<>(64);

    private final Map<AnnotatedElementKey, Method> targetMethodCache = new ConcurrentHashMap<>(64);

    public <T> T getValue(JoinPoint joinPoint, String key, Class<T> retClazz) {
        return getValue(null, joinPoint.getArgs(),
                joinPoint.getTarget().getClass(),
                ((MethodSignature) joinPoint.getSignature()).getMethod(), key, retClazz);
    }

    private <T> T getValue(Object rootObject, Object[] args, Class clazz, Method method, String condition, Class<T> retClazz) {
        if (args == null) {
            return null;
        }
        EvaluationContext evaluationContext = createEvaluationContext(rootObject, clazz, method, args);
        AnnotatedElementKey methodKey = new AnnotatedElementKey(method, clazz);
        return condition(condition, methodKey, evaluationContext, retClazz);
    }

    /**
     * Create the suitable {@link EvaluationContext} for the specified event handling
     * on the specified method.
     */
    public EvaluationContext createEvaluationContext(Object rootObject, Class<?> targetClass, Method method, Object[] args) {

        Method targetMethod = getTargetMethod(targetClass, method);
        return new MethodBasedEvaluationContext(rootObject, targetMethod, args, this.paramNameDiscoverer);
    }

    /**
     * Specify if the condition defined by the specified expression matches.
     */
    public <T> T condition(String conditionExpression, AnnotatedElementKey elementKey, EvaluationContext evalContext, Class<T> clazz) {
        return getExpression(this.conditionCache, elementKey, conditionExpression).getValue(evalContext, clazz);
    }

    private Method getTargetMethod(Class<?> targetClass, Method method) {
        AnnotatedElementKey methodKey = new AnnotatedElementKey(method, targetClass);
        Method targetMethod = this.targetMethodCache.get(methodKey);
        if (targetMethod == null) {
            targetMethod = AopUtils.getMostSpecificMethod(method, targetClass);
            if (targetMethod == null) {
                targetMethod = method;
            }
            this.targetMethodCache.put(methodKey, targetMethod);
        }
        return targetMethod;
    }
}

api 是 getValue() , 我们 createEvaluationContext 创建一个 MethodBasedEvaluationContext ,顾名思义,上下文就是方法信息,我们用在 注解的切面上,可以在切面中获取到 JoinPoint 方法参数信息,这里有几个局部变量:

ParameterNameDiscoverer 获取方法参数名的 discover, 缓存作用,
conditionCache 解析表达式的缓存,
targetMethodCache 方法的反射缓存
详细可粘贴进项目 进行分析。

使用

在注解切面中使用



/**
 * 注解切面的实现
 */
@Component
@Aspect
@Slf4j
public class CacheAspect {
    
    private MyExpressionEvaluator evaluator = new MyExpressionEvaluator();

    /**
     * @CacheAdd logic implementation
     * if cache exists, return it, or get and put into cache
     * if redis operation errors,get data from db
     */
    @Around("@annotation(com.example.CacheAdd)")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        CacheAdd cacheAdd = (((MethodSignature) joinPoint.getSignature()).getMethod()).getAnnotation(CacheAdd.class);
        if (isConditionNotFulfill(joinPoint, cacheAdd)) {
            //如果condition 条件不满足
            return joinPoint.proceed();
        }
        .......
        return result;
    }
    
    /**
     * CacheAdd 注解的 condition 条件是否不满足?
     */
    private Boolean isConditionNotFulfill(JoinPoint joinPoint, CacheAdd cacheAdd) {
        try {
            return StringUtils.isNotBlank(cacheAdd.condition()) && !evaluator.getValue(joinPoint, cacheAdd.condition(), Boolean.class);
        } catch (Exception e) {
            log.error("isConditionNotFulfill error,cacheAdd:{},args:{}", cacheAdd, joinPoint.getArgs());
            return true;
        }

    }

}

// @CacheAdd

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface CacheAdd {

    String prefix() default "";

    String key() default "";

    String condition() default "";

    int expireSeconds() default -1;

}

这里的注解使用 和 spring 的 @Cacheable 的使用基本一样,condition 是实现某些情况下不缓存的。


本网站采用BY-NC-SA协议进行授权
转载请注明原文链接:项目中使用 spring spel 组件
喜欢 (3)
[]
分享 (0)
发表我的评论
取消评论
表情 贴图 加粗 删除线 居中 斜体 签到

Hi,您需要填写昵称和邮箱!

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址