在SpringBoot中,我们经常会使用自定义注解+AOP的方式来封装一些重复的操作,例如方法的参数校验,获取分布式锁等。
如果我们需要在注解中动态的传入参数,例如在加锁的操作中,需要根据方法的入参动态的传入userId作为lock的key,这个动态参数在aspect类中应该如何解析出来呢?答案是利用SpEL表达式实现,下面以一个简单的例子说明具体用法。
SpEL表达式实现注解动态参数
首先,设计一个自定义的注解:
@Target(value = {ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface DynamicParam {
String value() default "";
// 用于传入动态参数
String key() default "";
}
然后,在方法上使用这个注解:
@DynamicParam(value = "演示注解用法", key = "#user.id")
public void testMethod(User user){
// do something...
}
最后,设计一个aspect来从注解中解析出user.id:
@Aspect
@Component
@Slf4j
public class SpelAspect {
private static final ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();
@Around(value = "@annotation(dynamicParam)")
public Object lock(ProceedingJoinPoint joinPoint, DynamicParam dynamicParam) throws Throwable {
// 解析出注解里的动态参数
String param = getDynamicParam(joinPoint, dynamicParam);
try{
// do something...
return joinPoint.proceed();
}catch (Throwable e) {
log.error(e.getMessage(),e);
return null;
}
}
/**
* 解析出注解里的动态参数
* @param joinPoint
* @param dynamicParam
* @return
*/
private String getDynamicParam(ProceedingJoinPoint joinPoint, DynamicParam dynamicParam) {
StringBuilder sb = new StringBuilder();
// spel解析器
ExpressionParser parser = new SpelExpressionParser();
// spel表达式对象
Expression expression = parser.parseExpression(dynamicParam.key());
// 上下文对象
EvaluationContext context = new MethodBasedEvaluationContext(TypedValue.NULL, resolveMethod(joinPoint),
joinPoint.getArgs(), parameterNameDiscoverer);
// 解析出的动态参数
Object value = expression.getValue(context);
sb.append(ObjectUtils.nullSafeToString(value));
return sb.toString();
}
/**
* 获取注解所在的method
* @param joinPoint
* @return
*/
private Method resolveMethod(ProceedingJoinPoint joinPoint) {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Class<?> targetClass = joinPoint.getTarget().getClass();
return getDeclaredMethodFor(targetClass, signature.getName(),
signature.getMethod().getParameterTypes());
}
private Method getDeclaredMethodFor(Class<?> clazz, String name, Class<?>... parameterTypes) {
try {
return clazz.getDeclaredMethod(name, parameterTypes);
} catch (NoSuchMethodException e) {
Class<?> superClass = clazz.getSuperclass();
if (superClass != null) {
return getDeclaredMethodFor(superClass, name, parameterTypes);
}
}
throw new IllegalStateException("Cannot resolve target method: " + name);
}
}
这样就可以顺利的从自定义的注解中解析出动态参数user.id了。
解析SpEL的过程分析
上面案例中,最核心的是getDynamicParam方法:
private String getDynamicParam(ProceedingJoinPoint joinPoint, DynamicParam dynamicParam) {
StringBuilder sb = new StringBuilder();
// spel解析器
ExpressionParser parser = new SpelExpressionParser();
// spel表达式对象
Expression expression = parser.parseExpression(dynamicParam.key());
// 上下文对象
EvaluationContext context = new MethodBasedEvaluationContext(TypedValue.NULL, resolveMethod(joinPoint),
joinPoint.getArgs(), parameterNameDiscoverer);
// 解析出的动态参数
Object value = expression.getValue(context);
sb.append(ObjectUtils.nullSafeToString(value));
return sb.toString();
}
这个方法比较清晰的体现了解析SpEL表达式的过程:
1.构建一个SpEL表达式的解析器
2.利用解析器把注解中传入的SpEL表达式解析成一个表达式对象
3.构建一个上下文对象,它是表达式对象的执行环境
4.从上下文对象中,解析出动态参数
为了方便理解,可以把上述过程做进一步说明:
我们在注解中传入的SpEL表达式相当于一个指令,解析器就是要执行这个指令的人,上下文对象就是这个人的工作环境,他只有在这个工作环境中执行这个命令,才能拿到我们想要的结果。
代码中一些类的说明
ExpressionParser接口:
它表示解析器,这个接口有3个实现类,其中TemplateAwareExpressionParser implements ExpressionParser
另外两个则是继承了TemplateAwareExpressionParser
SpelExpressionParser是这个接口的默认实现类
EvaluationContext接口:
它表示的是上下文环境,它的默认实现类是StandardEvaluationContext,我们在代码中使用的MethodBasedEvaluationContext继承自StandardEvaluationContext。
当我们用MethodBasedEvaluationContext构建出一个上下文环境context后,它的内部就会包含这个method的信息以及method入参的具体值,这也是为什么解析器必须在这个环境中才能解析出我们想要的结果。
Expression接口:
它表示SpEL表达式对象,默认实现类是SpelExpression。
表达式对象的getValue用于获取表达式的值,setValue方法则用于设置根对象或上下文环境中的值。
ParameterNameDiscoverer接口:
在获取Method对象的过程中,我们还用到了ParameterNameDiscoverer接口,Spring用它来获取方法中的参数的名称,这里只是简单使用,没有做深究。
希望上面的内容能给你带来帮助!