19

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

Custom annotation:

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

Aspect:

@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());
    }
}

Service:

@Service
@Transactional
public class EntityServiceImpl implements EntityService {

    @CheckEntity(key = "#id")
    public Entity getEntity(Long id) {
        return new Entity(id);
    }
}    

My IDE (IntelliJ) doesn't see anything special with the key = "#id" usage in contrast to similar usages for Cacheable where it's shown with different color than plain text. I'm mentioning the IDE part just as a hint in case it helps, it looks like the IDE is aware in advance about these annotations or it just realizes some connection which doesn't exist in my example.

The value in the checkEntity.key is '#id' instead of an expected number. I tried using ExpressionParser but possibly not in the right way.

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

Any idea?

Chris
  • 549
  • 1
  • 5
  • 18
  • No IDE will be able to provide you with context-aware support that it has for `@Cacheable`, because your aspect is tailor-made. Can I ask what type of functionality your are attempting to provide with your Aspect? Are you trying to check and see if an entity already exists? – geoand Oct 12 '15 at 13:24
  • This is to check if this id (say departmentId) exists in loggedIn user's departments they can have access to, throw an accessdenied exception otherwise – Chris Oct 12 '15 at 19:36
  • Wouldn't Spring Security or Apache Shiro provide such features without having to roll your own implementation? – geoand Oct 12 '15 at 19:57
  • I don't think so, this is additional security check based on user's data. You can define the role level for a call, but I don't think you can additionally define the access based on the relation of the called method (e.g. with param deparmentId) with the additional details of the loggedInUser (e.g. department Id list can have access to) – Chris Oct 12 '15 at 21:00

5 Answers5

9

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.

Sahil Chhabra
  • 10,621
  • 4
  • 63
  • 62
  • 2
    Great answer. Thanks a ton :) – Abhinav Sep 26 '19 at 18:36
  • This is the closest answer to what I want to implement. But instead of providing argument indexes I want to use argument name directly (ex.: "#id" or "#object.otherId"). Do you have any suggestions about that? – Joshgun Dec 18 '20 at 07:11
  • 1
    I've found it from this link. May be useful for someone: https://www.programmersought.com/article/7536253399/ – Joshgun Dec 18 '20 at 08:00
8

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;
  }
}
Community
  • 1
  • 1
Chris
  • 549
  • 1
  • 5
  • 18
  • Do you know how to do it in Spring 3? AnnotatedElementKey is not supported in Spring 3. – Amit Sadafule Aug 22 '16 at 10:10
  • @AmitSadafule You can do it by passing the -parameters to the compiler if you use java8. Couldn't find a way for older java version. See https://docs.oracle.com/javase/tutorial/reflect/member/methodparameterreflection.html – Chris Sep 18 '16 at 00:34
  • @Chirs: Thanks. I found a way to do it in Spring 3. For this one has to copy ExpressionEvaluator and LazyParamAwareEvaluationContext in the project as these classes are not public (they are defined with package level scope). Then need to do following private final ExpressionEvaluator evaluator = new ExpressionEvaluator(); private EvaluationContext createEvaluationContext(Method method, Object[] args, Object target, Class> targetClass) { return evaluator.createEvaluationContext(null, method, args, target, targetClass); } – Amit Sadafule Sep 18 '16 at 03:55
  • @AmitSadafule That's OK if you are happy with that approach. Personally I gave it a try by extracting and refactoring it to make it simpler and more generic but I didn't like the fact that I'd had to maintain it given that it should be part of the framework, not the application, so I think the params argument for java8 would be a cleaner approach. – Chris Sep 19 '16 at 09:54
  • Adding to that useful answer, that in order to use it on target objects that are proxied by Spring, you would need to extract the actual target object from the proxy : https://stackoverflow.com/questions/8121551/is-it-possible-to-unproxy-a-spring-bean – Shlomi Uziel Jun 28 '21 at 12:14
4

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.

Stephane Nicoll
  • 31,977
  • 9
  • 97
  • 89
  • Hi Stephane, this looks like an answer in the right direction though. I'll try it and I'll let you know. Thanks! – Chris Oct 23 '15 at 10:12
2

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.

Ruben
  • 3,986
  • 1
  • 21
  • 34
  • 1
    That seems to be too Cache-related implementation. I mentioned the cache just as an example to show how I need to use it. It is supposed that spel does most of the magic for you. Regarding the IDE it's IntelliJ, I just mentioned it as a hint in case it helps. – Chris Oct 09 '15 at 15:21
0

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";
}
David Bulté
  • 2,988
  • 3
  • 31
  • 43
  • `ProceedingJointPoint` can be used only for `@Around` advices, not `@Before`. Parameter names is null. There are chances that it can be fixed (http://stackoverflow.com/questions/25226441/java-aop-joinpoint-does-not-get-parameter-names) but I wouldn't prefer removing the interface nor adding parameter annotations wherever this annotation is going to be used. – Chris Oct 23 '15 at 16:45