Я пытаюсь выполнить нулевую проверку своих методов, используя простую пользовательскую аннотацию @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. Я мог исключить 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.
Согласен, так как можно проверить на JDoodle. Просто поиграйте с версией JDK и измените 8 на 9 или 10.
Пожалуйста, предоставьте MCVE, т. е. по крайней мере один или несколько полных классов приложений, воспроизводящих проблему, и, конечно, также ваши определения pointcut для
anyConstructorWithNotNullParam()
иanyMethodWithNotNullParam()
. Никто не сможет вам помочь, если не сможет понять и воспроизвести вашу проблему. Вы не знаете, почему ваши решения не работают, так как же вы можете знать, что они находятся в фрагменте кода, которым вы делитесь, а не где-либо еще?