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 是实现某些情况下不缓存的。