Ссылочные типы, null, @Nullable, @NotNull
Если передать значение примитивного типа в метод, оно будет скопировано:
public static void main(String[] args) < // объявляем переменную примитивного типа int voltage = 4100; // передаём add100mV(voltage); // 4100 — без изменений System.out.println(voltage); >private static void add100mV(int lowVoltage) < lowVoltage += 100; // Абсолютно бесполезное действие. // Переменная lowVoltage существует // только в пределах метода и уничтожается, // когда метод отработал. >
Если передать объект в метод, будет передана ссылка на этот объект, т. е. в методе будет доступен тот же объект, не копия.
public static void main(String[] args) < int[] voltages = ; // массивы в Java — это объекты. add100mVToFirstElement(voltages); // передаём System.out.println(voltages[0]); // 4200 > private static void add100mVToFirstElement(int[] lowVoltages) < lowVoltages[0] += 100; // Локальная переменная lowVoltages // указывает на массив, // мы прибавляем 100 мВ // к значению нулевого элемента // этого массива. >
Это называется «передача по значению» и «передача по указателю (ссылке)» соответственно.
Кроме того, что можно указывать на объекты (например, int[] voltages = ; или Phone myPhone = new Nexus5(); ), также можно указывать на пустое значение.
public static void main(String[] args) < int[] voltages = null; // пустой указатель add100mVToFirstElement(voltages); // передаём >private static void add100mVToFirstElement(int[] lowVoltages)
Exception in thread "main" java.lang.NullPointerException at JavaneseNull.add100mVToFirstElement(JavaneseNull.java:11) at JavaneseNull.main(JavaneseNull.java:8)
Выполнение программы прервалось, т. к. произошла попытка чтения элемента из null . Ещё один пример безнадёжного кода:
MobilePhone mp = null; mp.dial("*#06#");
null существует, чтобы показать отсутствие значения. Любая переменная объектного (ссылочного) типа, переданная извне, может оказаться null , поэтому в методах нужно проверять входные данные. Например:
private static void add100mVToFirstElement(int[] lowVoltages)
Если аргумент lowVoltages не пройдёт проверку, выполнение будет прервано с выбросом соответствующего исключения.
Если передать null :

Если передать пустой массив:


Аннотации для борьбы с нулевыми указателями (null pointers)
Чтобы показать, что поле или параметр может быть null или метод может вернуть null , используется аннотация @Nullable .
@Nullable private String toBeOrNotToBe; @Nullable public String toBeOrNotToBe()
Чтобы, наоборот, показать, что поле или параметр не может быть null или метод не может вернуть null , используется аннотация @NotNull или @NonNull .
@NotNull private final String name; public User(@NotNull String name)
Эти аннотации помогают среде разработки определить, где могут быть null , и предостеречь о возможных ошибках.
Nullable java что это
Ключевое слово null представляет специальный литерал, который указывает, что переменная не имеет как такового значения. То есть у нее по сути отсутствует значение.
val n = null println(n) // null
Подобное значение может быть полезно в ряде ситуациях, когда необходимо использовать данные, но при этом точно неизвестно, а есть ли в реальности эти данные. Например, мы получаем данные по сети, данные могут прийти или не прийти. Либо может быть ситуация, когда нам надо явным образом указать, что данные не установлены.
Однако переменным стандартных типов, например, типа Int или String или любых других классов, мы не можем просто взять и присвоить значение null:
val n : Int = null // ! Ошибка, переменная типа Int допускает только числа
Мы можем присвоить значение null только переменной, которая представляет тип Nullable. Чтобы превратить обычный тип в тип nullable, достаточно поставить после названия типа вопросительный знак:
// val n : Int = null //! ошибка, Int не допускает значение null val d : Int? = null // норм, Int? допускает значение null
При этом мы можем передавать переменным nullable-типов как значение null, так и конкретные значения, которые укладываются в диапазон значений данного типа:
var age : Int? = null age = 34 // Int? допускает null и числа var name : String? = null name = "Tom" // String? допускает null и строки
Nullable-типы могут представлять и создаваемые разработчиком классы:
fun main() < var bob: Person = Person("Bob") // bob = null // ! Ошибка - bob представляет тип Person и не допускает null var tom: Person? = Person("Tom") tom = null // норм - tom представляет тип Person? и допускает null >class Person(val name: String)
В то же время надо понимать, что String? и Int? — это не то же самое, что и String и Int. Nullable типы имеют ряд ограничений:
-
Значения nullable-типов нельзя присвоить напрямую переменным, которые не допускают значения null
var message : String? = "Hello" val hello: String = message // ! Ошибка - hello не допускает значение null
var message : String? = "Hello" // у типа String свойство length возвращает длину строки println("Message length: $") // ! Ошибка
Оператор ?:
Одним из преимуществ Kotlin состоит в том, что его система типов позволяет определять проблемы, связанные с использованием null, во время компиляции, а не во время выполнения. Например, возьмем следующий код:
var name : String? = "Tom" val userName: String = name // ! Ошибка
Переменная name хранит строку «Tom». Переменная userName представляет тип String и тоже может хранить строки, но тем не менее напрямую в данном случае мы не можем передать значение из переменной name в userName. В данном случае для компилятора неизвестно, каким значением инициализирована переменная name. Ведь переменная name может содержать и значение null, которое недопустимо для типа String.
В этом случае мы можем использовать оператор ?: , который позволяет предоставить альтернативное значение, если присваиваемое значение равно null:
var name : String? = "Tom" val userName: String = name ?: "Undefined" // если name = null, то присваивается "Undefined" var age: Int? = 23 val userAge: Int = age ?:0 // если age равно null, то присваивается число 0
Оператор ?: принимает два операнда. Если первый операнд не равен null, то возвращается значение первого операнда. Если первый операнд равен null, то возвращается значение второго операнда.
То есть это все равно, если бы мы написали:
var name : String? = «Tom» val userName: String if(name!=null)
Но оператор ?: позволяет сократить подобную конструкцию.
Оператор ?.
Оператор ?. позволяет объединить проверку значения объекта на null и обратиться к функциям или свойствам этого объекта.
Например, у строк есть свойство length, которое возвращает длину строки в символах. У объекта String? мы просто так не можем обратиться к свойству length, так как если объект String? равен null, то и строки как таковой нет, и соответственно длину строки нельзя определить. И в этом случае мы можем применить оператор ?. :
var message : String? = "Hello" val length: Int? = message?.length
Если переменная message вдруг равна null , то переменная length получит значение null . Если переменная name содержит строку, то возвращается длина этой строки. По сути выражение val length: Int? = message?.length эквивалентно следующему коду:
val length: Int? if(message != null) length = message.length else length = null
С помощью оператора ?. подобным образом можно обращаться к любым свойствам и функциям объекта.
Также в данном случае мы могли совместить оба выше рассмотренных оператора:
val message : String? = "Hello" val length: Int = message?.length ?:0
Теперь переменная length не допускает значения null. И если переменная name не определена, то length получает число 0.
Используя этот оператор, можно создавать цепочки проверок на null:
fun main() < var tom: Person? = Person("Tom") val tomName: String? = tom?.name?.uppercase() println(tomName) // TOM var bob: Person? = null val bobName: String? = bob?.name?.uppercase() println(bobName) // null var sam: Person? = Person(null) val samName: String? = sam?.name?.uppercase() println(samName) // null >class Person(val name: String?)
Здесь класс Person в первичном конструкторе принимает значение типа String? , то есть это можт быть строка, а может быть null .
Допустим, мы хотим получить переданное через конструктор имя пользователя в верхнем регистре (заглавными буквами). Для перевода текста в верхний регистр у класса String есть функция uppercase() . Однако может сложиться ситуация, когда либо объект Person равен null, либо его свойство name ( которое представляет тип String?) равно null. И в этом случае перед вызовом функции uppercase() нам надо проверять на null все эти объекты. А оператор ?. позволяет сократить код проверки:
val tomName: String? = tom?.name?.uppercase()
То есть если tom не равен null, то обращаемся к его свойству name . Далее если name не равен null, то обращаемся к ее функции uppercase() . Если какое-то звено в этой проверки возвратит null, переменная tomName тоже будет равна null.
Но здсь мы также можем избежать финального возвращения null и присвоить значение по умолчанию:
val tomName: String = tom?.name?.uppercase() ?: "Undefined"
Оператор !!
Оператор !! (not-null assertion operator) принимает один операнд. Если операнд равен null, то генерируется исключение. Если операнд не равен null, то возвращается его значение.
fun main() < try < val name : String? = "Tom" val id: String = name!! println(id) >catch (e: Exception) < println(e.message)>>
Поскольку данный оператор возвращает объект, который не представляет nullable-тип, то после применения оператора мы можем обратиться к методам и свойствам этого объекта:
val name : String? = null val length :Int = name. length
Spring Null-Safety аннотации
Хотя Java не позволяет выражать null-безопасность в своей системе типов, Spring Framework теперь содержит следующие аннотации в пакете org.springframework.lang , чтобы вы могли объявлять null-безопасность API-интерфейсов и полей:
- @Nullable : Аннотация, указывающая, что определенный параметр, возвращаемое значение или поле может быть null .
- @NonNull : Аннотация, указывающая, что определенный параметр, возвращаемое значение или поле не может быть null (не нужна для параметров/возвращаемых значений и полей, где применяются аннотации @NonNullApi и @NonNullFields соответственно).
- @NonNullApi : Аннотация на уровне пакета, которая объявляет значение, отличное от пустого (non-null), семантикой по умолчанию для параметров и возвращаемых значений.
- @NonNullFields : Аннотация на уровне пакета, которая объявляет значение, отличное от пустого (non-null), семантикой по умолчанию для полей.
Spring Framework сам эффективно задействует эти аннотации, но они также могут быть использованы в любом Java-проекте на базе Spring для объявления null-безопасных API-интерфейсов и, по желанию, null-безопасных полей. Допустимость пустого (null) значения для аргументов универсального типа, аргументов переменной длинны (varargs) и элементов массива пока не поддерживаются, но должна быть добавлена в следующем выпуске. Актуальную информацию см. в SPR-15942. Ожидается, что объявления переменных, допускающих пустое значение, будут дорабатываться между выпусками Spring Framework, включая минорные. Допустимость пустого значения для типов, используемых внутри тела метода, выходит за область видимости этой функции.
Другие распространенные библиотеки, такие как Reactor и Spring Data, содержат null-безопасные API-интерфейсы, которые используют аналогичную организацию допустимости пустого значения, обеспечивая согласованный общий опыт работы для разработчиков приложений на Spring.
Примеры использования
В дополнение к предоставлению явного объявления допустимости пустого значения для API-интерфейса Spring Framework, эти аннотации могут быть использованы IDE (например, IDEA или Eclipse) для полезных предупреждений, связанных с null-безопасностью, что позволит избежать NullPointerException во время выполнения.
Они также используются для того, чтобы сделать API-интерфейс Spring null-безопасным в проектах Kotlin, поскольку Kotlin изначально поддерживает null-безопасность. Более подробную информацию можно найти в документации по поддержке Kotlin.
Мета-аннотации JSR-305
Аннотации Spring мета-аннотируются с помощью аннотаций из JSR 305 (не осуществляемый в данный момент, но широко распространенный JSR). Мета-аннотации из JSR-305 позволяют поставщикам инструментов, таких как IDEA или Kotlin, обеспечивать поддержку null-безопасности в общем виде без необходимости жестко кодировать поддержку аннотаций в Spring.
Нет необходимости и не рекомендуется добавлять зависимость JSR-305 в путь классов проекта, чтобы пользоваться преимуществами null-безопасного API-интерфейса Spring. Только для таких проектов, как библиотеки на базе Spring, которые используют аннотации null-безопасности в своей кодовой базе, следует добавить com.google.code.findbugs:jsr305:3.0.2 с конфигурацией compileOnly из Gradle или областью видимости provided из Maven, чтобы избежать получения предупреждений о компиляции.
Тестирование аннотаций @NonNull/@Nullable
Описанная ниже задача не была новаторской или чертовски полезной, компания в которой я работаю не получит за нее прибыль, а я премию.
Но эта задача была, а значит ее пришлось решить.
Intro
В статье вы часто будете встречать слово Lombok, прошу хейтеров не торопиться с выводами.
Я не собираюсь «топить» за Lombok или его отсутствие, я как Геральт Сапковского, стараюсь хранить нейтралитет, и могу спокойно и без дрожи в веке читать код как с Lombok, так и без оного.
Но на текущем проекте упомянутая библиотека присутствует, и что-то подсказывает мне, что наш проект такой не единственный.
Так вот.
Последнее время в java безусловно есть тренд к анноташкам. Во славу концепции fast fail часто параметры методов аннотируются аннотацией @NonNull (чтоб если что, как зашло — так и вышло пало).
Вариантов импорта для данной(или похожей по идеологии аннотации) довольно много, мы же, как наверняка уже стало понятно, остановимся на версии
import lombok.NonNull;
Если вы используете эту(или подобную) аннотацию, то имеете некоторый контракт, который необходимо проверить тестом и любой статический анализатор кода любезно это подскажет(Sonar точно подсказывает).
Протестировать эту аннотацию unit-тестом достаточно просто, проблема в том что такие тесты будут размножаться в вашем проекте со скоростью кроликов по весне, а кролики, как известно, нарушают принцип DRY.
В статье мы напишем небольшой тестовый фреймворк, для тестирования контракта аннотаций @NonNull(и для того чтоб Sonar не светил вам в глаз противным красным светом).
P.S На написания названия меня вдохновила песня группы PowerWolf, которая заиграла(ей богу) когда я писал название(в оригинале название звучит более позитивно)
Основная часть
Изначально мы тестировали аннотацию как-то так:
@Test void methodNameWithNullArgumentThrowException() < try < instance.getAnyType(null); fail("Exception not thrown"); >catch (final NullPointerException e) < assertNotNull(e); >>
вызывали метод и подавали null в качестве параметра, аннотированного аннотацией @NonNull.
Получали NPE и оставались довольны(Sonar тоже радовался).
Потом стали делать то же самое, но с более модным assertThrow который работает через Supplier(мы же любим лямбды):
@TestUnitRepeatOnce void methodNameWithNullArgumentThrowException() < assertThrows(NullPointerException.class, () ->instance.getAnyType(null)); >
Стильно. Модно. Молодежно.
Казалось бы можно и закончить, аннотации протестированы, чего же боле?
Проблема (не то чтобы проблема, но все же) данного способа тестирования «всплыла» когда в один прекрасный день я написал тест на метод, он благополучно отработал, а потом я заметил, что аннотации @NonNull на параметре нет.
Оно и понятно: вы вызываете тестовый метод, при этом не описываете поведение моковых классов, через when()/then(). Исполняющий поток благополучно заходит внутрь метода, где то внутри ловит NPE, на незамоканном (или замоканном, но без when()/then()) объекте, и падает, впрочем с NPE, как вы и предупреждали, а значит тест зеленый
Получается что тестируем мы в таком случае уже не аннотацию, а непонятно что. При правильной работе теста мы не должны были вообще зайти вглубь метода(свалившись на пороге).
У @NonNull аннотации Lombok есть одна особенность: если мы падаем с NPE на аннотации, в ошибку записывается имя параметра.
На это мы и завяжемся, после того как упадем с NPE, дополнительно будем проверять текст stacktrace, вот так:
exception.getCause().getMessage().equals(parameter.getName())
А если вдруг.
На случай если вдруг Lombok обновится и перестанет писать в stacktrace имя параметра получившего null, то пересмотрим лекцию Андрея Пангина по JVM TI и напишем плагинчик для JVM, в котором один фиг-таки передадим имя параметра.
Все бы вроде ничего, сейчас мы действительно проверяем то что надо, но проблема «кроликов» не решена.
Хотелось бы иметь некий инструмент, которому можно было бы сказать, например так:
@TestUnitRepeatOnce @SneakyThrows void nonNullAnnotationTest()
а он бы сам пошел и просканировал все публичные методы указанного класса и проверил все их @NonNull параметры тестом.
Вы скажете, доставай рефлексию, и проверяй, есть ли на методе @NonNull и если есть пуляй в него null.
Все бы ничего, да RetentionPolicy не тот.
У всех аннотаций есть параметр RetentionPolicy, который может быть 3 типов: SOURCE, CLASS и RUNTIME, так вот у Lombok, по умолчанию RetentionPolicy.SOURCE, а это значит что в Runtime этой аннотации не видно и через reflection вы ее не найдете.
В нашем проекте аннотируются все параметры публичных методов(не считая примитивов), если подразумевается что параметр не может быть null, если подразумевается обратное — то параметр аннотируется спринговой @Nullable. На это можно завязаться, мы будем искать все публичные методы, и все параметр в них, не помеченные @Nullable и не являющиеся примитивами.
Подразумеваем, что для всех остальных случаев, на параметрах должна стоять аннотация @NonNull.
Для удобства по возможности будем расскидывать логику по приватным методам, для начала получим все публичные методы:
private List getPublicMethods(final Class clazz)
где METHOD_FILTER обычный предикат, в котором мы говорим что:
- Метод должен быть public
- Не должен быть syntetic(а такое случается когда у вас есть метод с raw параметром)
- Не должен быть абстрактный(про абстрактные классы отдельно и ниже)
- Имя метода не должно быть equals(на случай если какой то злой человек решит запулить на вход нашего фреймворка POJO класс с переопределенным equals())
if (method.getParameterCount() == 0)
Если параметры есть, нам надо понять, аннотированы ли они @NonNull(точнее должны ли быть, согласно
логике
- public method
- не @Nullable
- не примитив
Для этого сделаем мапку и положим в нее наши параметры по очередности следования в методе, а напротив них положим флаг, который будет говорить должна быть над параметром аннотация @NonNull или нет:
int nonNullAnnotationCount = 0; int index = 0; val parameterCurrentMethodArray = method.getParameters(); val notNullAnnotationParameterMap = new HashMap(); for (val parameter : parameterCurrentMethodArray) < if (isNull(parameter.getAnnotation(Nullable.class)) && isFalse(parameter.getType().isPrimitive())) < notNullAnnotationParameterMap.put(index++, true); nonNullAnnotationCount++; >else < notNullAnnotationParameterMap.put(index++, false); >> if (nonNullAnnotationCount == 0)
эта мапка пригодится нам чтобы потом вызывать метод и передавать ему null во все параметры с аннотацией @NonNull поочередно, а не только в первый попавшийся.
Параметр nonNullAnnotationCount считает сколько параметров в методе должно быть аннотировано @NonNull, по нему будет определено число интераций вызова каждого метода.
Кстати если аннотаций @NonNull нет(параметры есть, но все примитивные либо @Nullable), то и говорить не о чем:
if (nonNullAnnotationCount == 0)
Имеем на руках карту параметров. Знаем, сколько раз вызывать метод и в какие позиции пулять null, дело за малым (как я наивно полагал не разобравшись), нужно создавать instance класса и вызывать у них методы.
Проблемы начинаются когда понимаешь, насколько разные бывают instance: это может быть приватный класс, это может быть класс с одним дефолтным конструктором, с одним конструктором с параметрами, с таким и таким конструктором, абстрактный класс, интерфейс(со своими default методами, которые тоже public и которые тоже надо тестировать).
А когда мы соорудили-таки правдами и неправдами instance, нужно передать в метод invoke параметры и тут тоже раздолье: как создать instance финального класса? а Enum? а примитива? а массива примитивов(который тоже объект и тоже может быть аннотирован).
Ну давайте по порядку.
Первый случай это класс с одним приватным конструктором:
if (ONLY_ONE_PRIVATE_CONSTRUCTOR_FILTER.test(clazz))
тут все просто вызываем у нашего метода invoke, передаем ему clazz который пришел из вне в тест и массив параметров, в котором уже заряжен null на первую позицию с флагом на аннотацию @NonNull(помните, выше мы создали карту @NonNull-ов)мы начинаем бежать в цикле и создавать массив параметров, поочередно меняя позицию null параметра, и обнуляя флаг перед вызовом метода, чтобы в следующей интерации другой параметр стал null.
В коде это выглядит так:
val invokeMethodParameterArray = new Object[parameterCurrentMethodArray.length]; boolean hasNullParameter = false; int currentNullableIndex = 0; for (int i = 0; i < invokeMethodParameterArray.length; i++) < if (notNullAnnotationParameterMap.get(i) && isFalse(hasNullParameter)) < currentNullableIndex = i; invokeMethodParameterArray[i] = null; hasNullParameter = true; >else < mappingParameter(parameterCurrentMethodArray[i], invokeMethodParameterArray, i); >>
С первым вариантом инстанцирования разобрались.
Дальше интерфейсы, нельзя взять и создать instance интерфейса(у него даже конструктора нет).
Поэтому с интерфейсом это будет так:
if (INTERFACE_FILTER.test(clazz))
createInstanceByDynamicProxy позволяет нам создать instance на класс, если он реализует хотя бы один интерфейс, либо сам является интерфейсом
имейте ввиду, что тут принципиально какие именно интерфейсы реализует класс, важен типовой интерфейс(а не какой-нибудь Comparable), в котором есть методы, которые вы реализуете в целевом классе иначе instance удивит вас своим типом
а внутри он какой-то такой:
private Object createInstanceByDynamicProxy(final Class clazz, final Object[] invokeMethodParameterArray) < return newProxyInstance( currentThread().getContextClassLoader(), new Class[], (proxy, method1, args) -> < Constructorconstructor = Lookup.class .getDeclaredConstructor(Class.class); constructor.setAccessible(true); constructor.newInstance(clazz) .in(clazz) .unreflectSpecial(method1, clazz) .bindTo(proxy) .invokeWithArguments(invokeMethodParameterArray); return null; > ); >
Грабли
Кстати тут тоже были какие то грабли, уже не вспомню какие именно, их было много, но создавать проксю надо именно через Lookup.class
Следующий instance(мой любимый) это абстрактный класс. И тут Dynamic proxy нам уже не поможет, так как если абстрактный класс и реализует какой то интерфейс, то это явно не тот тип какой нам бы хотелось. И просто так взять и создать newInstance() у абстрактного класса мы не можем. Тут нам на помощь придет CGLIB, спринговая либа, которая создает прокси на основе наследования, но вот беда, целевой класс должен иметь default (без параметров) конструктор
Сплетня
Хотя судя по сплетням в интернете начиная со Spring 4 CGLIB умеет работать и без оного, так вот: Не работает!
Вариант для инстанцирования абстрактного класса будет такой:
if (isAbstract(clazz.getModifiers()))
makeErrorMessage() который встречался уже в примерах кода, роняет тест, если мы вызывали метод с аннотированным @NonNull параметром передав null и он не упал, значит тест не отработал, надо падать.
Для маппинга параметров у нас один общий метод, который умеет мэппировать и мокировать как параметры конструктора, так и метода, выглядит он так:
private void mappingParameter(final Parameter parameter, final Object[] methodParam, final int index) throws InstantiationException, IllegalAccessException < if (isFinal(parameter.getType().getModifiers())) < if (parameter.getType().isEnum()) < methodParam[index] = Enum.valueOf( (Class) (parameter.getType()), parameter.getType().getEnumConstants()[0].toString() ); > else if (parameter.getType().isPrimitive()) < mappingPrimitiveName(parameter, methodParam, index); >else if (parameter.getType().getTypeName().equals("byte[]")) < methodParam[index] = new byte[0]; >else < methodParam[index] = parameter.getType().newInstance(); >> else < methodParam[index] = mock(parameter.getType()); >>
Обратите внимание на создание Enum(вишенка на торте), вообщем нельзя просто так взять и создать Enum.
Здесь для финальных параметров свой маппинг, для нефинальных свой, а далее просто по тексту (кода).
Ну и после того как мы создали параметры для конструктора и для метода формируем наш instance:
val firstFindConstructor = clazz.getConstructors()[0]; val constructorParameterArray = new Object[firstFindConstructor.getParameters().length]; for (int i = 0; i < constructorParameterArray.length; i++) < mappingParameter(firstFindConstructor.getParameters()[i], constructorParameterArray, i); >notNullAnnotationParameterMap.put(currentNullableIndex, false); createAndInvoke(clazz, method, invokeMethodParameterArray, firstFindConstructor, constructorParameterArray); makeErrorMessage(method);
Мы уже точно знаем, что раз мы дошли до этого этапа кода, значит у нас есть минимум один конструктор, чтобы создать instance мы можем взять любой, поэтому берем первый попавшийся, смотрим, есть ли у него параметры в конструкторе и если нет то вызываем вот так:
method.invoke(spy(clazz.getConstructors()[0].newInstance()), invokeMethodParameterArray);
ну а если есть то так:
method.invoke(spy(clazz.getConstructors()[0].newInstance()), invokeMethodParameterArray);
Это та логика которая происходит в методе createAndInvoke() который вы видели чуть выше.
Полная версия тестового класса под спойлером, заливать на git я не стал, так как писал на рабочем проекте, но по сути это всего лишь один класс, который можно отнаследовать в ваших тестах и использовать.
Исходный код
public class TestUtil < private static final PredicateMETHOD_FILTER = method -> isPublic(method.getModifiers()) && isFalse(method.isSynthetic()) && isFalse(isAbstract(method.getModifiers())) && isFalse(method.getName().equals("equals")); private static final Predicate ONLY_ONE_PRIVATE_CONSTRUCTOR_FILTER = clazz -> clazz.getConstructors().length == 0 && isFalse(clazz.isInterface()); private static final Predicate INTERFACE_FILTER = clazz -> clazz.getConstructors().length == 0; private static final BiPredicate LOMBOK_ERROR_FILTER = (exception, parameter) -> isNull(exception.getCause().getMessage()) || isFalse(exception.getCause().getMessage().equals(parameter.getName())); protected void assertNonNullAnnotation(final Class clazz) throws Throwable < for (val method : getPublicMethods(clazz)) < if (method.getParameterCount() == 0) < continue; >int nonNullAnnotationCount = 0; int index = 0; val parameterCurrentMethodArray = method.getParameters(); val notNullAnnotationParameterMap = new HashMap(); for (val parameter : parameterCurrentMethodArray) < if (isNull(parameter.getAnnotation(Nullable.class)) && isFalse(parameter.getType().isPrimitive())) < notNullAnnotationParameterMap.put(index++, true); nonNullAnnotationCount++; >else < notNullAnnotationParameterMap.put(index++, false); >> if (nonNullAnnotationCount == 0) < continue; >for (int j = 0; j < nonNullAnnotationCount; j++) < val invokeMethodParameterArray = new Object[parameterCurrentMethodArray.length]; boolean hasNullParameter = false; int currentNullableIndex = 0; for (int i = 0; i < invokeMethodParameterArray.length; i++) < if (notNullAnnotationParameterMap.get(i) && isFalse(hasNullParameter)) < currentNullableIndex = i; invokeMethodParameterArray[i] = null; hasNullParameter = true; >else < mappingParameter(parameterCurrentMethodArray[i], invokeMethodParameterArray, i); >> try < if (ONLY_ONE_PRIVATE_CONSTRUCTOR_FILTER.test(clazz)) < notNullAnnotationParameterMap.put(currentNullableIndex, false); method.invoke(clazz, invokeMethodParameterArray); makeErrorMessage(method); >if (INTERFACE_FILTER.test(clazz)) < notNullAnnotationParameterMap.put(currentNullableIndex, false); method.invoke(createInstanceByDynamicProxy(clazz, invokeMethodParameterArray), invokeMethodParameterArray); makeErrorMessage(method); >if (isAbstract(clazz.getModifiers())) < createInstanceByCGLIB(clazz, method, invokeMethodParameterArray); makeErrorMessage(); >val firstFindConstructor = clazz.getConstructors()[0]; val constructorParameterArray = new Object[firstFindConstructor.getParameters().length]; for (int i = 0; i < constructorParameterArray.length; i++) < mappingParameter(firstFindConstructor.getParameters()[i], constructorParameterArray, i); >notNullAnnotationParameterMap.put(currentNullableIndex, false); createAndInvoke(clazz, method, invokeMethodParameterArray, firstFindConstructor, constructorParameterArray); makeErrorMessage(method); > catch (final Exception e) < if (LOMBOK_ERROR_FILTER.test(e, parameterCurrentMethodArray[currentNullableIndex])) < makeErrorMessage(method); >> > > > @SneakyThrows private void createAndInvoke( final Class clazz, final Method method, final Object[] invokeMethodParameterArray, final Constructor firstFindConstructor, final Object[] constructorParameterArray ) < if (firstFindConstructor.getParameters().length == 0) < method.invoke(spy(clazz.getConstructors()[0].newInstance()), invokeMethodParameterArray); >else < method.invoke(spy(clazz.getConstructors()[0].newInstance(constructorParameterArray)), invokeMethodParameterArray); >> @SneakyThrows private void createInstanceByCGLIB(final Class clazz, final Method method, final Object[] invokeMethodParameterArray) < MethodInterceptor handler = (obj, method1, args, proxy) ->proxy.invoke(clazz, args); if (clazz.getConstructors().length > 0) < val firstFindConstructor = clazz.getConstructors()[0]; val constructorParam = new Object[firstFindConstructor.getParameters().length]; for (int i = 0; i < constructorParam.length; i++) < mappingParameter(firstFindConstructor.getParameters()[i], constructorParam, i); >for (val constructor : clazz.getConstructors()) < if (constructor.getParameters().length == 0) < val proxy = Enhancer.create(clazz, handler); method.invoke(proxy.getClass().newInstance(), invokeMethodParameterArray); >> > > private Object createInstanceByDynamicProxy(final Class clazz, final Object[] invokeMethodParameterArray) < return newProxyInstance( currentThread().getContextClassLoader(), new Class[], (proxy, method1, args) -> < Constructorconstructor = Lookup.class .getDeclaredConstructor(Class.class); constructor.setAccessible(true); constructor.newInstance(clazz) .in(clazz) .unreflectSpecial(method1, clazz) .bindTo(proxy) .invokeWithArguments(invokeMethodParameterArray); return null; > ); > private void makeErrorMessage() < fail("Тестирование аннотации @NonNull в Абстрактных классах без DefaultConstructor не поддерживается"); >private void makeErrorMessage(final Method method) < fail("Параметр в публичном методе " + method.getName() + " не аннотирован @NonNull"); >private List getPublicMethods(final Class clazz) < return Arrays.stream(clazz.getDeclaredMethods()) .filter(METHOD_FILTER) .collect(toList()); >private void mappingParameter(final Parameter parameter, final Object[] methodParam, final int index) throws InstantiationException, IllegalAccessException < if (isFinal(parameter.getType().getModifiers())) < if (parameter.getType().isEnum()) < methodParam[index] = Enum.valueOf( (Class) (parameter.getType()), parameter.getType().getEnumConstants()[0].toString() ); > else if (parameter.getType().isPrimitive()) < mappingPrimitiveName(parameter, methodParam, index); >else if (parameter.getType().getTypeName().equals("byte[]")) < methodParam[index] = new byte[0]; >else < methodParam[index] = parameter.getType().newInstance(); >> else < methodParam[index] = mock(parameter.getType()); >> private void mappingPrimitiveName(final Parameter parameter, final Object[] methodParam, final int index) < val name = parameter.getType().getName(); if ("long".equals(name)) < methodParam[index] = 0L; >else if ("int".equals(name)) < methodParam[index] = 0; >else if ("byte".equals(name)) < methodParam[index] = (byte) 0; >else if ("short".equals(name)) < methodParam[index] = (short) 0; >else if ("double".equals(name)) < methodParam[index] = 0.0d; >else if ("float".equals(name)) < methodParam[index] = 0.0f; >else if ("boolean".equals(name)) < methodParam[index] = false; >else if ("char".equals(name)) < methodParam[index] = 'A'; >> >
Заключение
Данный код работает и тестирует аннотации в реальном проекте, на данный момент возможен только один вариант, когда все сказанное можно заколлапсить.
Объявить в классе ломбоковский setter (если найдется специалист, который ставит сеттер не в Pojo-классе, хотя чего только не бывает) и при этом поле на котором объявят сеттер будет не финальное.
Тогда фреймворк любезно скажет что мол есть публичный метод, а у него есть параметр на котором нет аннотации @NonNull, решение простое: объявить setter явно и аннотировать его параметр, исходя из контекста логики @NonNull/@Nullable.
Учтите, что если вы хотите как я, завязаться на имя параметра метода в своих тестах (или чем то еще), в Runtime по умолчанию недоступны имена переменных в методах, вы найдете там arg[0] и arg[1] и т.д.
Для включения отображения имен методов в Runtime используйте плагин Maven-а:
org.apache.maven.plugins maven-compiler-plugin $ $ $ $ -parameters
и в частности этот ключ:
-parameters