ArrayIndexOutOfBoundsException при попытке доступа к аннотациям параметров для конструктора внутреннего класса через отражение

Я пытаюсь выполнить нулевую проверку своих методов, используя простую пользовательскую аннотацию @NotNull, т.е. Я объявляю метод как myMethod(@NotNull String name, String description), и когда кто-то вызывает этот метод с нулевым значением, переданным в качестве аргумента «имя», возникает исключение.

У меня уже есть реализация простого аспекта с использованием aspectj. Это решение работает очень хорошо для меня. Единственным исключением являются конструкторы внутренних классов. В таком случае аспект вылетает из-за исключения внутри java.lang.reflect.Parameter:

Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 1
    at java.lang.reflect.Parameter.getDeclaredAnnotations(Parameter.java:305)
    at java.lang.reflect.Parameter.declaredAnnotations(Parameter.java:342)
    at java.lang.reflect.Parameter.getAnnotation(Parameter.java:287)
    at java.lang.reflect.Parameter.getDeclaredAnnotation(Parameter.java:315)
    at ValidationAspect.checkNotNullArguments(ValidationAspect.java:22)
    at OuterClass$InnerClass.<init>(OuterClass.java:4)
    at OuterClass.constructInnerClass(OuterClass.java:14)
    at Main.main(Main.java:5)

Упрощенная реализация:

Аспект:

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.ConstructorSignature;

import java.lang.reflect.Parameter;

@Aspect
public class ValidationAspect {

  @Pointcut("execution(*.new(.., @NotNull (*), ..))")
  private void anyConstructorWithNotNullParam() {}

  @Before("anyConstructorWithNotNullParam()")
  public void checkNotNullArguments(JoinPoint joinPoint) {
    ConstructorSignature signature = (ConstructorSignature) joinPoint.getSignature();
    Object[] args = joinPoint.getArgs();
    Parameter[] params = signature.getConstructor().getParameters();

    for(int i = 0; i < args.length; i++) {
      if (params[i].getDeclaredAnnotation(NotNull.class) != null) {
        if (args[i] == null) {
          throw new IllegalArgumentException("Illegal null argument");
        }
      }
    }
  }
}

Аннотация:

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.PARAMETER})
public @interface NotNull { }

Тестовый класс:

public class OuterClass {

  public class InnerClass {
    public InnerClass(
        @NotNull String name
    ) {
      System.out.println(String.format("Construct inner class with name: %s", name));
    }
  }

  public InnerClass constructInnerClass(
      String name
  ) {
    return new InnerClass(name);
  }
}

Применение:

public class Main {

  public static void main(String[] args) {
    OuterClass outObj = new OuterClass();
    outObj.constructInnerClass("myName");
  }
}

Насколько я могу судить, это вызвано тем, что java передает объект окружающего класса в качестве первого аргумента конструктору внутреннего класса (что, как мне сказали, является стандартным поведением). Проблема в том, что params[i].executable.getParameterAnnotations() похоже не знает о дополнительном аргументе и возвращает аннотации только для «обычных» параметров.

Я чувствую, что это ошибка либо в аспекте, либо в java.lang.reflection. Но поскольку я не могу найти отчет об ошибке для этого, мне кажется более вероятным, что я делаю что-то не так. Приложение работает на Java 8 (пробовал несколько разных сборок оракула jdk и последнюю сборку openjkd) и аспекте 1.8.13 (но пробовал и 1.9.4).

Итак, мой вопрос (ы): это известная ошибка? Есть ли какой-то недостаток в моей реализации? Есть ли обходной путь? (Думаю, было бы не так сложно сопоставить аннотации с параметрами вручную. Но, поскольку у меня очень ограниченные знания об отражении Java, я не могу предвидеть последствия).

Отредактировано: предоставлен рабочий пример

Пожалуйста, предоставьте MCVE, т. е. по крайней мере один или несколько полных классов приложений, воспроизводящих проблему, и, конечно, также ваши определения pointcut для anyConstructorWithNotNullParam() и anyMethodWithNotNullParam(). Никто не сможет вам помочь, если не сможет понять и воспроизвести вашу проблему. Вы не знаете, почему ваши решения не работают, так как же вы можете знать, что они находятся в фрагменте кода, которым вы делитесь, а не где-либо еще?

kriegaex 28.05.2019 04:53

ОК, я изменил вопрос, чтобы он содержал упрощенную реализацию с той же проблемой.

fonadius 28.05.2019 11:53
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
0
2
570
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Ладно, мне было слишком любопытно, и я поиграл со своей собственной MCVE. Я мог исключить AspectJ как виновника и свести проблему к проблеме JDK/JRE:

Суть внутренних (нестатических) конструкторов классов в том, что их первый параметр всегда является экземпляром внешнего объекта. Java 8 — я пробовал как с 1.8.0_152, так и с 1.8.0_211 — содержит ошибку отражения по одному. В основном он перемещает аннотации реальных внутренних параметров конструктора на один индекс вверх, например. параметры аннотации для первого аргумента конструктора хранятся в индексе 0, который фактически должен содержать аннотации для экземпляра внешнего объекта. Мой пример кода объясняет это лучше, я думаю:

package de.scrum_master.app;

import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

import java.lang.annotation.Retention;
import java.lang.annotation.Target;

@Retention(RUNTIME)
@Target(PARAMETER)
public @interface NotNull {}
package de.scrum_master.app;

import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.lang.reflect.Parameter;

public class Application {
  class Inner {
    public Inner(@NotNull String text) {
      System.out.println("Constructing inner with " + text);
    }
  }

  public static void main(String[] args) throws NoSuchMethodException, SecurityException {
      Constructor<Inner> constructor = Inner.class.getConstructor(Application.class, String.class);
      System.out.println(constructor);
      for (Parameter parameter : constructor.getParameters()) {
        System.out.println("  " + parameter);
        for (Annotation annotation : parameter.getAnnotations())
          System.out.println("    " + annotation);
      }
  }
}

Это воспроизводит вашу проблему для JDK 8:

public de.scrum_master.app.Application$Inner(de.scrum_master.app.Application,java.lang.String)
  de.scrum_master.app.Application arg0
    @de.scrum_master.app.NotNull()
  java.lang.String arg1
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 1
    at java.lang.reflect.Parameter.getDeclaredAnnotations(Parameter.java:305)
    at java.lang.reflect.Parameter.getAnnotations(Parameter.java:333)
    at de.scrum_master.app.Application.main(Application.java:19)

Но если вы работаете с JDK 11 (я использовал 11.0.2), все работает, как и ожидалось, даже если я использую аспект с советом, подобным вашему:

public de.scrum_master.app.Application$Inner(de.scrum_master.app.Application,java.lang.String)
  de.scrum_master.app.Application arg0
  java.lang.String arg1
    @de.scrum_master.app.NotNull()

Я не удосужился просмотреть все заметки о выпуске JDK, чтобы узнать, было ли это исправлено намеренно или случайно и в какой версии JDK (9, 10, 11), но, по крайней мере, я могу сказать вам, что после обновления до JDK 11 все должно быть в порядке.

Кажется, исправлено с JDK 9.

Holger 28.05.2019 09:33

Согласен, так как можно проверить на JDoodle. Просто поиграйте с версией JDK и измените 8 на 9 или 10.

kriegaex 28.05.2019 10:58

Другие вопросы по теме