如何直接在方法和另一个注释(然后在方法上使用)上定义注释的切入点?

How to define the Pointcut(s) for annotation directly on method and on another annotation (which is then used on a method)?

我正在尝试弄清楚如何定义切入点以及如何使用 spring AOP 处理多个注释。

我有以下自定义注释:

@RequiresNonBlank

@Retention (RetentionPolicy.RUNTIME)
@Target (value = {ElementType.METHOD, ElementType.ANNOTATION_TYPE})
@Documented
public @interface RequiresNonBlank {
  String value();
  Class<? extends StuffException> throwIfInvalid();
}

@RequiresNonBlankDummy

@Retention (RetentionPolicy.RUNTIME)
@Target (value = {ElementType.METHOD})
@Documented
@RequiresNonBlank (
    value = "T(com.stuff.exceptions.annotation.TestDummyValueHolder).value",
    throwIfInvalid = TestDummyStuffException.class
)
@interface RequiresNonBlankDummy {
}

我有以下虚拟控制器:

TestDummyController

@Component
public class TestDummyController {
  @RequiresNonBlank (
      value = "T(com.stuff.exceptions.annotation.TestDummyValueHolder).value",
      throwIfInvalid = TestDummyStuffException.class
  )
  public boolean methodWithRequiresNonBlankAnnotation() {
    return true;
  }

  @RequiresNonBlankDummy
  public boolean methodWithRequiresNonBlankDummyAnnotation() {
    return true;
  }

  @RequiresNonBlankDummy
  @RequiresNonBlank (
      value = "T(com.stuff.exceptions.annotation.TestDummyValueHolder).anotherValue",
      throwIfInvalid = TestDummyStuffException.class
  )
  public boolean methodWithMultipleRequiresNonBlankAnnotation() {
    return true;
  }
}

我的TestDummyValueHolder是一个class,它只包含两个String(valueanotherValue)及其对应的getter和setter。

我想定义一个或多个切入点(@Pointcut),它将在一个方法上处理一个或多个/堆叠 @RequiresNonBlank(“派生”注释,例如 @RequiresNonBlankDummy 应该被考虑在内)。

我的方面处理程序目前看起来像这样:

RequiresNonBlankAspect

@Component
@Aspect
@Slf4j
public class RequiresNonBlankAspect {
  private static final String REQUIRES_NON_BLANK_FQPN =
      "com.stuff.exceptions.annotation.RequiresNonBlank";
  @Before("execution(@" + REQUIRES_NON_BLANK_FQPN + " * *(..)) && @annotation(annotation)")
  public void evaluatePreconditionItself(JoinPoint joinPoint, RequiresNonBlank annotation) {
    evaluatePrecondition(joinPoint, annotation);
  }

  @Before("execution(@(@" + REQUIRES_NON_BLANK_FQPN + " *) * *(..)) && @annotation(annotation)")
  public void evaluatePreconditionOnAnnotation(JoinPoint joinPoint, RequiresNonBlank annotation) {
    evaluatePrecondition(joinPoint, annotation);
  }

  private void evaluatePrecondition(JoinPoint joinPoint, RequiresNonBlank annotation) {
    try {
      Objects.requireNonNull(annotation);
    } catch (NullPointerException e) {
      log.error("No annotation found!", e);
    }

    ExpressionParser elParser = new SpelExpressionParser();
    Expression expression = elParser.parseExpression(annotation.value());

    String expressionToEvaluate = (String) expression.getValue(joinPoint.getArgs());
    log.info("value to check: {}", expressionToEvaluate);

    if (StringUtils.isEmpty(expressionToEvaluate)) {
      try {
        throw annotation.throwIfInvalid().getConstructor().newInstance();
      } catch (InstantiationException | IllegalAccessException | InvocationTargetException
          | NoSuchMethodException e) {
        log.error("Could not throw the exception configured!", e);
      }
    }
  }
}

但是:annotationOnAnnotation(...) 不起作用。 annotationItself 然而确实如此。

我有以下测试:

RequiresNonBlankTest

@SpringBootApplication
@ActiveProfiles (profiles = "test")
@RunWith (SpringRunner.class)
public class RequiresNonBlankTest {

  @Autowired
  private TestDummyController controller;

  @Test (expected = TestDummyStuffException.class)
  public void testRequiresNonBlank_valueIsNull() {
    TestDummyValueHolder.setValue(null);
    controller.methodWithRequiresNonBlankAnnotation();
  }

  @Test
  public void testRequiresNonBlank_valueIsNotNull() {
    TestDummyValueHolder.setValue("value: non-null");
    assertThat(controller.methodWithRequiresNonBlankAnnotation(), equalTo(true));
  }

  @Test (expected = TestDummyStuffException.class)
  public void testRequiresNonBlankDummy_valueIsNull() {
    TestDummyValueHolder.setValue(null);
    controller.methodWithRequiresNonBlankDummyAnnotation();
  }

  @Test
  public void testRequiresNonBlankDummy_valueIsNotNull() {
    TestDummyValueHolder.setValue("value: non-null");
    assertThat(controller.methodWithRequiresNonBlankDummyAnnotation(), equalTo(true));
  }
}

但是:测试 testRequiresNonBlankDummy_valueIsNull() 失败。


我还想知道如何不仅对方法上的注释上的注释做出反应(参见 @RequiresNonBlankDummy),而且还想知道 TestDummyController#methodWithMultipleRequiresNonBlankAnnotation 中的内容(堆叠/多个注释) .这可能吗?如果可能,怎么做?

我正在使用 Spring 引导和 Spring AOP。我尝试过使用 AnnotationUtilsAnnotationElementUtils,但至少据我所知它没有帮助。请帮助我或给我提示如何解决这个问题。


编辑 (15.08.2021):

TestDummyValueHolder

public class TestDummyValueHolder {
  private static String value;
  private static String anotherValue;

  public static String getValue() {
    return TestDummyValueHolder.value;
  }

  public static void setValue(String value) {
    TestDummyValueHolder.value = value;
  }

  public static String getAnotherValue() {
    return TestDummyValueHolder.anotherValue;
  }

  public static void setAnotherValue(String anotherValue) {
    TestDummyValueHolder.anotherValue = anotherValue;
  }
}

StuffException 非常通用。事实上,您可以将其替换为任何具有无参数构造函数的异常。 我还更新了方面处理程序 (RequiresNonBlankAspect) 就像@kriegaex 提到的那样。

在OP更新了他的问题后写了一个新的答案,纠正了开头的错误。现在旧答案不再适用了。

OK,剩下的问题是:

  • 现在我们从使用 || 链接两个切入点更改为具有两个单独的切入点,注释绑定工作更可靠。不过请注意:

    • 对于同时带有普通注释和元注释的方法methodWithMultipleRequiresNonBlankAnnotation,两个建议都会被触发,即您可能只希望发生一次的事情,现在实际上发生了两次。
    • OTOH,您实际上可能想要触发两个建议,因为有两个不同的 SpEL 表达式 T(de.scrum_master.spring.q68785567.TestDummyValueHolder).anotherValue(在 @RequiresNonBlank 中)和 T(de.scrum_master.spring.q68785567.TestDummyValueHolder).value(在 [=16= 的注释中) ]),那么这实际上更好。这取决于您的要求。
  • 您将 @annotation(annotation) 与参数绑定 RequiresNonBlank annotation 结合使用会阻止为 @RequiresNonBlankDummy 触发建议(方法 methodWithRequiresNonBlankDummyAnnotation),因为这两种注释类型是不兼容的,并且不存在一种注释类型扩展另一种注释类型或实现接口这样的事情。因此,在这种情况下,您所要做的就是使用没有参数绑定的点,并通过建议方法内部的反射找到注释。


更新: 好的,我假设在直接注释和元注释的情况下,您希望同时触发这两个建议。解决方案如下所示:

package de.scrum_master.spring.q68785567;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Objects;

@Component
@Aspect
//@Slf4j
public class RequiresNonBlankAspect {
  private static final Logger log = LoggerFactory.getLogger(RequiresNonBlankAspect.class.getName());
  private static final String REQUIRES_NON_BLANK_FQPN =
    "de.scrum_master.spring.q68785567.RequiresNonBlank";
  private ExpressionParser elParser = new SpelExpressionParser();

  @Before("execution(@" + REQUIRES_NON_BLANK_FQPN + " * *(..)) && @annotation(requiresNonBlank)")
  public void evaluatePreconditionItself(JoinPoint joinPoint, RequiresNonBlank requiresNonBlank) {
    log.info("[DIRECT] " + joinPoint + " -> " + requiresNonBlank);
    evaluatePrecondition(joinPoint, requiresNonBlank);
  }

  @Before("execution(@(@" + REQUIRES_NON_BLANK_FQPN + " *) * *(..))")
  public void evaluatePreconditionOnAnnotation(JoinPoint joinPoint) {
    RequiresNonBlank requiresNonBlank = getRequiresNonBlankMeta(joinPoint);
    log.info("[META]   " + joinPoint + " -> " + requiresNonBlank);
    evaluatePrecondition(joinPoint, requiresNonBlank);
  }

  private RequiresNonBlank getRequiresNonBlankMeta(JoinPoint joinPoint) {
    RequiresNonBlank requiresNonBlank = null;
    Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
    Annotation[] annotations = method.getAnnotations();
    for (Annotation annotation : annotations) {
      RequiresNonBlank requiresNonBlankMeta = annotation.annotationType().getAnnotation(RequiresNonBlank.class);
      if (requiresNonBlankMeta != null) {
        requiresNonBlank = requiresNonBlankMeta;
        break;
      }
    }
    return requiresNonBlank;
  }

  public void evaluatePrecondition(JoinPoint joinPoint, RequiresNonBlank requiresNonBlank) {
    try {
      Objects.requireNonNull(requiresNonBlank);
    }
    catch (NullPointerException e) {
      log.error("No annotation found!", e);
    }

    Expression expression = elParser.parseExpression(requiresNonBlank.value());
    String expressionToEvaluate = (String) expression.getValue(joinPoint.getArgs());
    log.info("Evaluated expression: " + expressionToEvaluate);
    if (StringUtils.isEmpty(expressionToEvaluate)) {
      try {
        throw requiresNonBlank.throwIfInvalid().getConstructor().newInstance();
      }
      catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
        log.error("Could not throw the exception configured!", e);
      }
    }
  }
}

更新 2: This gist 是我对可重复注释、多个元注释和混合它们的新要求的看法,但使用板载 Java 意味着代替 Spring 注释实用程序,以便还可以使其在 Spring 上下文之外的本机 AspectJ 应用程序中工作。顺便说一句,我重命名了一些 类 和方法,因为对我来说你们的名字太相似而且有点缺乏表达力。