web-dev-qa-db-ja.com

カスタムアノテーションの側面でメソッド引数を渡す

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)は、プレーンテキストとは異なる色で表示されるCacheableの同様の使用法とは対照的に、key = "#id"の使用法では特別なことは何も見ていません。 IDEの部分は、役立つ場合のヒントとして言及しています。IDEはこれらの注釈について事前に認識しているか、認識しているようです。私の例には存在しない接続。

CheckEntity.keyの値は、予想される数ではなく「#id」です。 ExpressionParserを使用してみましたが、正しい方法ではない可能性があります。

CheckEntityアノテーション内のパラメーター値を取得する唯一の方法は、arguments配列にアクセスすることです。これは、このアノテーションが複数の引数を持つメソッドでも使用できるため、私が望むものではありません。

何か案が?

13
Chris

おかげで @StéphaneNic​​oll 私はなんとか実用的なソリューションの最初のバージョンを作成することができました:

アスペクト

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

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:これは他の回答と比較してより単純で明確なアプローチであり、誰かに役立つ可能性があると感じたため、このソリューションを追加しています。

5
Sahil Chhabra

フレームワークがあなたのために何をすべきか、あなたがしなければならないことをおそらく誤解していると思います。

SpELサポートには、式自体の代わりに実際の(解決された)値にアクセスできるように、自動的にトリガーされる方法がありません。どうして?コンテキストがあり、開発者としてこのコンテキストを提供する必要があるためです。

Intellijでのサポートも同じです。現在、Jetbrainsの開発者は、SpELが使用されている場所を追跡し、SpELサポート用にマークを付けています。値が実際のSpEL式であるという事実を実行する方法はありません(これは、結局のところ、アノテーションタイプの生のJava.lang.Stringです)。

4.2の時点で、キャッシュ抽象化が内部で使用するユーティリティのいくつかを抽出しました。そのようなもの(通常はCachedExpressionEvaluatorMethodBasedEvaluationContext)の恩恵を受けたいと思うかもしれません。

新しい@EventListenerはそのようなものを使用しているので、あなたがやろうとしていることの例として見ることができるより多くのコードがあります:EventExpressionEvaluator

要約すると、カスタムインターセプターは#id値に基づいて何かを行う必要があります。この コードスニペット はそのような処理の例であり、キャッシュの抽象化にはまったく依存しません。

4
Stephane Nicoll

Springは内部的にExpressionEvaluatorを使用して、keyパラメーターのSpring式言語を評価します( CacheAspectSupport を参照)

同じ動作をエミュレートしたい場合は、 CacheAspectSupport がどのように実行しているかを確認してください。コードのスニペットは次のとおりです。

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アノテーションを処理する必要があります。

2
Ruben

アノテーションは複数のパラメーターを持つメソッドで使用できますが、それは引数配列を使用できないという意味ではありません。これが解決策です:

まず、「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」という名前を付けることを前提としています。注釈にパラメータ名を指定できるようにするための1つの修正があります。

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