Pass argument parameter in custom annotation aspect

I am trying to use something similar to org.springframework.cache.annotation.Cacheable :

User annotation:

 @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface CheckEntity { String message() default "Check entity msg"; String key() default ""; } 

Format:

 @Component @Aspect public class CheckEntityAspect { @Before("execution(* *.*(..)) && @annotation(checkEntity)") public void checkEntity(JoinPoint joinPoint, CheckEntitty checkEntity) { System.out.println("running entity check: " + joinPoint.getSignature().getName()); } } 

Services:

 @Service @Transactional public class EntityServiceImpl implements EntityService { @CheckEntity(key = "#id") public Entity getEntity(Long id) { return new Entity(id); } } 

My IDE (IntelliJ) does not see anything special in using key = "#id" , unlike similar applications for Cacheable , where it is displayed with a different color than plain text. I mention the IDE part as a hint, if it helps, it looks like the IDE knows in advance about these annotations or just implements some kind of connection that does not exist in my example.

The value in checkEntity.key means "#id" instead of the expected number. I tried using ExpressionParser , but maybe not in the right direction.

The only way to get the parameter value inside the checkEntity annotation is to access an array of arguments, which is not what I want, because this annotation can also be used in methods with more than one argument.

Any idea?

+6
source share
4 answers

Thanks @ StéphaneNicoll I was able to create the first version of a working solution:

Aspect

 @Component @Aspect public class CheckEntityAspect { protected final Log logger = LogFactory.getLog(getClass()); private ExpressionEvaluator<Long> evaluator = new ExpressionEvaluator<>(); @Before("execution(* *.*(..)) && @annotation(checkEntity)") public void checkEntity(JoinPoint joinPoint, CheckEntity checkEntity) { Long result = getValue(joinPoint, checkEntity.key()); logger.info("result: " + result); System.out.println("running entity check: " + joinPoint.getSignature().getName()); } private Long getValue(JoinPoint joinPoint, String condition) { return getValue(joinPoint.getTarget(), joinPoint.getArgs(), joinPoint.getTarget().getClass(), ((MethodSignature) joinPoint.getSignature()).getMethod(), condition); } private Long getValue(Object object, Object[] args, Class clazz, Method method, String condition) { if (args == null) { return null; } EvaluationContext evaluationContext = evaluator.createEvaluationContext(object, clazz, method, args); AnnotatedElementKey methodKey = new AnnotatedElementKey(method, clazz); return evaluator.condition(condition, methodKey, evaluationContext, Long.class); } } 

Expression Evaluator

 public class ExpressionEvaluator<T> 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); /** * Create the suitable {@link EvaluationContext} for the specified event handling * on the specified method. */ public EvaluationContext createEvaluationContext(Object object, Class<?> targetClass, Method method, Object[] args) { Method targetMethod = getTargetMethod(targetClass, method); ExpressionRootObject root = new ExpressionRootObject(object, args); return new MethodBasedEvaluationContext(root, targetMethod, args, this.paramNameDiscoverer); } /** * Specify if the condition defined by the specified expression matches. */ public 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; } } 

Root object

 public class ExpressionRootObject { private final Object object; private final Object[] args; public ExpressionRootObject(Object object, Object[] args) { this.object = object; this.args = args; } public Object getObject() { return object; } public Object[] getArgs() { return args; } } 
+1
source

I think you probably misunderstand what the system should do for you and what you need to do.

SpEL support cannot be started automatically so that you can access the actual (permitted) value instead of the expression itself. What for? Since there is a context, and as a developer you must provide this context.

Intellij support is the same thing. Jetbrains developers are currently tracking locations where SpEL is used and marking them to support SpEL. We have no way to ensure that the value is the actual SpEL expression (after all, this is the java.lang.String type for the annotation type).

Starting with 4.2, we have extracted some of the utilities that internal cache abstraction is used. You can benefit from this stuff (usually CachedExpressionEvaluator and MethodBasedEvaluationContext ).

The new @EventListener uses this stuff, so you have more code that you can see as examples for what you are trying to do: EventExpressionEvaluator .

So your custom interceptor should do something based on the #id value. This piece of code is an example of such processing and generally does not depend on cache abstraction.

+3
source

Spring uses the ExpressionEvaluator internally to evaluate the Spring expression language in the key parameter (see CacheAspectSupport )

If you want to emulate the same behavior, see how CacheAspectSupport does it . Here is the code snippet:

 private final ExpressionEvaluator evaluator = new ExpressionEvaluator(); /** * Compute the key for the given caching operation. * @return the generated key, or {@code null} if none can be generated */ protected Object generateKey(Object result) { if (StringUtils.hasText(this.metadata.operation.getKey())) { EvaluationContext evaluationContext = createEvaluationContext(result); return evaluator.key(this.metadata.operation.getKey(), this.methodCacheKey, evaluationContext); } return this.metadata.keyGenerator.generate(this.target, this.metadata.method, this.args); } private EvaluationContext createEvaluationContext(Object result) { return evaluator.createEvaluationContext( this.caches, this.metadata.method, this.args, this.target, this.metadata.targetClass, result); } 

I don't know which IDE you are using, but it has to deal with the @Cacheable annotation differently than the others in order to highlight the parameters.

+1
source

Your annotation can be used with methods with more than one parameter, but this does not mean that you cannot use an array of arguments. Here it is allowed:

First we need to find the index of the id parameter. This can be done like this:

  private Integer getParameterIdx(ProceedingJoinPoint joinPoint, String paramName) { MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature(); String[] parameterNames = methodSignature.getParameterNames(); for (int i = 0; i < parameterNames.length; i++) { String parameterName = parameterNames[i]; if (paramName.equals(parameterName)) { return i; } } return -1; } 

where "paramName" = your id parameter

Then you can get the actual id value from these arguments:

  Integer parameterIdx = getParameterIdx(joinPoint, "id"); Long id = joinPoint.getArgs()[parameterIdx]; 

Of course, this assumes that you always call this parameter "id". One fix could be to specify the parameter name in the annotation, something like

 @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface CheckEntity { String message() default "Check entity msg"; String key() default ""; String paramName() default "id"; } 
0
source

All Articles