Pass method argument in Aspect of custom annotation

前端 未结 5 2000
说谎
说谎 2021-02-13 03:14

I\'m trying to use something similar to org.springframework.cache.annotation.Cacheable :

Custom annotation:

@Target(ElementType.METHOD)
             


        
相关标签:
5条回答
  • 2021-02-13 03:57

    I think you probably misunderstand what the framework is supposed to do for you vs. what you have to do.

    SpEL support has no way to be triggered automagically so that you can access the actual (resolved) value instead of the expression itself. Why? Because there is a context and as a developer you have to provide this context.

    The support in Intellij is the same thing. Currently Jetbrains devs track the places where SpEL is used and mark them for SpEL support. We don't have any way to conduct the fact that the value is an actual SpEL expression (this is a raw java.lang.String on the annotation type after all).

    As of 4.2, we have extracted some of the utilities that the cache abstraction uses internally. You may want to benefit from that stuff (typically CachedExpressionEvaluator and MethodBasedEvaluationContext).

    The new @EventListener is using that stuff so you have more code you can look at as examples for the thing you're trying to do: EventExpressionEvaluator.

    In summary, your custom interceptor needs to do something based on the #id value. This code snippet is an example of such processing and it does not depend on the cache abstraction at all.

    0 讨论(0)
  • 2021-02-13 03:58

    Spring uses internally an ExpressionEvaluator to evaluate the Spring Expression Language in the key parameter (see CacheAspectSupport)

    If you want to emulate the same behaviour, have a look at how CacheAspectSupport is doing it. Here is an snippet of the code:

    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 must deal with the @Cacheable annotation in a different way than with the others in order to highlight the params.

    0 讨论(0)
  • 2021-02-13 03:59

    Your annotation can be used with methods with more than 1 parameter, but that doesn't mean you can't use the arguments array. Here's a sollution:

    First we have to find the index of the "id" parameter. This you can do like so:

     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" param

    Next you can get the actual id value from the arguments like so:

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

    Of course this assumes that you always name that parameter "id". One fix there could be to allow to specify the parameter name on 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 讨论(0)
  • 2021-02-13 04:01

    Adding another simpler way of doing it using Spring Expression. Refer below:

    Your Annotation:

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

    Your Service:

    @Service
    @Transactional
    public class EntityServiceImpl implements EntityService {
    
        @CheckEntity(keyPath = "[0]")
        public Entity getEntity(Long id) {
            return new Entity(id);
        }
    
        @CheckEntity(keyPath = "[1].otherId")
        public Entity methodWithMoreThanOneArguments(String message, CustomClassForExample object) {
            return new Entity(object.otherId);
        }
    }  
    
    class CustomClassForExample {
       Long otherId;
    }
    

    Your Aspect:

    @Component
    @Aspect
    public class CheckEntityAspect {
    
        @Before("execution(* *.*(..)) && @annotation(checkEntity)")
        public void checkEntity(JoinPoint joinPoint, CheckEntitty checkEntity) {
            Object[] args = joinPoint.getArgs();
            ExpressionParser elParser = new SpelExpressionParser();
            Expression expression = elParser.parseExpression(checkEntity.keyPath());
            Long id = (Long) expression.getValue(args);
    
            // Do whatever you want to do with this id 
    
            // This works for both the service methods provided above and can be re-used for any number of similar methods  
    
        }
    }
    

    PS: I am adding this solution because I feel this is a simpler/clearner approach as compared to other answers and this might be helpful for someone.

    0 讨论(0)
  • 2021-02-13 04:08

    Thanks to @StéphaneNicoll I managed to create a first version of a working solution:

    The 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);
      }
    }
    

    The 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;
      }
    }
    

    The 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;
      }
    }
    
    0 讨论(0)
提交回复
热议问题