【问题标题】:Pass method argument in Aspect of custom annotation在自定义注释的方面传递方法参数
【发布时间】:2015-10-08 20:03:58
【问题描述】:

我正在尝试使用类似于org.springframework.cache.annotation.Cacheable 的东西:

自定义注解:

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

方面:

@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
@Transactional
public class EntityServiceImpl implements EntityService {

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

我的 IDE (IntelliJ) 没有看到 key = "#id" 用法有什么特别之处,而 Cacheable 的类似用法则以不同于纯文本的颜色显示。我提到 IDE 部分只是作为提示,以防万一它有帮助,看起来 IDE 提前知道这些注释,或者它只是实现了一些在我的示例中不存在的连接。

checkEntity.key 中的值是“#id”而不是预期的数字。 我尝试使用ExpressionParser,但可能不正确。

在 checkEntity 注释中获取参数值的唯一方法是访问参数数组,这不是我想要的,因为此注释也可以在具有多个参数的方法中使用。

有什么想法吗?

【问题讨论】:

  • 没有 IDE 能够为您提供它对 @Cacheable 的上下文感知支持,因为您的方面是量身定制的。我能问一下您试图为您的 Aspect 提供什么类型的功能吗?您是否尝试检查实体是否已存在?
  • 这是为了检查这个id(比如departmentId)是否存在于loggedIn用户可以访问的部门中,否则抛出一个accessdenied异常
  • Spring Security 或 Apache Shiro 不会提供这样的功能而不必滚动您自己的实现吗?
  • 我不这么认为,这是基于用户数据的额外安全检查。您可以为呼叫定义角色级别,但我认为您不能根据被调用方法的关系(例如,使用 param deparmentId)和 loginInUser 的其他详细信息(例如,部门 ID 列表可以有)另外定义访问权限访问)

标签: spring annotations aspectj spring-el


【解决方案1】:

使用 Spring Expression 添加另一种更简单的方法。参考如下:

您的注释:

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

您的服务:

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

你的方面:

@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:我添加此解决方案是因为与其他答案相比,我觉得这是一种更简单/更清晰的方法,这可能对某人有所帮助。

【讨论】:

  • 很好的答案。非常感谢:)
  • 这是我想要实现的最接近的答案。但我不想提供参数索引,而是直接使用参数名称(例如:“#id”或“#object.otherId”)。你对此有什么建议吗?
  • 我从这个链接中找到了它。可能对某人有用:programmersought.com/article/7536253399
【解决方案2】:

感谢@StéphaneNicoll 我设法创建了一个工作解决方案的第一个版本:

方面

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

表达式评估器

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

根对象

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

【讨论】:

  • 你知道Spring 3怎么做吗? Spring 3 不支持 AnnotatedElementKey。
  • @AmitSadafule 如果您使用 java8,您可以通过将 -parameters 传递给编译器来做到这一点。找不到旧 Java 版本的方法。见docs.oracle.com/javase/tutorial/reflect/member/…
  • @Chirs:谢谢。我在 Spring 3 中找到了一种方法。为此,必须在项目中复制 ExpressionEvaluator 和 LazyParamAwareEvaluationContext,因为这些类不是公共的(它们是在包级别范围内定义的)。然后需要按照 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); }
  • @AmitSadafule 如果您对这种方法感到满意,那没关系。我个人尝试通过提取和重构它以使其更简单和更通用,但我不喜欢我必须维护它的事实,因为它应该是框架的一部分,而不是应用程序的一部分,所以我认为 java8 的 params 参数将是一种更简洁的方法。
  • 除了这个有用的答案之外,为了在 Spring 代理的目标对象上使用它,您需要从代理中提取实际的目标对象:stackoverflow.com/questions/8121551/…
【解决方案3】:

我认为您可能误解了框架应该为您做什么以及您必须做什么。

SpEL 支持无法自动触发,因此您可以访问实际(已解析)值而不是表达式本身。为什么?因为有一个上下文,作为开发人员,你必须提供这个上下文。

Intellij 中的支持是一样的。目前 Jetbrains 开发人员跟踪使用 SpEL 的地方,并将其标记为支持 SpEL。我们无法确定该值是实际的 SpEL 表达式这一事实(毕竟这是注释类型上的原始 java.lang.String)。

从 4.2 开始,我们提取了一些缓存抽象内部使用的实用程序。您可能希望从这些东西中受益(通常是 CachedExpressionEvaluatorMethodBasedEvaluationContext)。

新的@EventListener 正在使用这些东西,因此您可以查看更多代码作为您尝试做的事情的示例:EventExpressionEvaluator

总而言之,您的自定义拦截器需要根据#id 值做一些事情。这个code snippet就是这样处理的一个例子,它根本不依赖于缓存抽象。

【讨论】:

  • 嗨斯蒂芬,这看起来像是一个正确方向的答案。我会试试的,我会告诉你的。谢谢!
【解决方案4】:

Spring 在内部使用 ExpressionEvaluator 来评估 key 参数中的 Spring 表达式语言(请参阅 CacheAspectSupport

如果您想模拟相同的行为,请查看CacheAspectSupport 是如何做到的。这是代码的sn-p:

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

我不知道您使用的是哪个 IDE,但它必须以与其他方式不同的方式处理 @Cacheable 注释以突出显示参数。

【讨论】:

  • 这似乎是缓存相关的实现。我提到缓存只是作为一个例子来说明我需要如何使用它。据说spel为你做了大部分的魔法。关于 IDE,它是 IntelliJ,我只是提到它作为提示,以防万一。
【解决方案5】:

您的注解可以与超过 1 个参数的方法一起使用,但这并不意味着您不能使用 arguments 数组。这是一个解决方案:

首先我们必须找到“id”参数的索引。你可以这样做:

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

其中 "paramName" = 你的 "id" 参数

接下来,您可以像这样从参数中获取实际的 id 值:

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

当然,这假设您始终将该参数命名为“id”。一种解决方法可能是允许在注释上指定参数名称,例如

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

【讨论】:

  • ProceedingJointPoint 只能用于@Around 建议,不能用于@Before。参数名称为空。有可能可以修复它(stackoverflow.com/questions/25226441/…),但我不希望在要使用此注释的地方删除接口或添加参数注释。
猜你喜欢
  • 2021-12-06
  • 1970-01-01
  • 2021-12-28
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多