Как получить значение аннотаций jackson
Перейти к содержимому

Как получить значение аннотаций jackson

  • автор:

Мой первый опыт работы с Jackson’ом.

Java-университет

Всем привет! Как-то раз в далекой-далекой галактике нашелся очень длинный JSON. И стало лень создавать под него POJO. И задался я вопросом: Представим ситуацию, в которой я получаю ответ в виде JSON с, например, курсами валют. В самом JSON’е очень много полей, а мне нужно 2 из них. И вот хотелось бы узнать — могу ли я создать класс с нужными мне полями и попытаться запарсить этот джсон в объект класса? Поймет джексон, что я от него хочу? И, соответственно, если поймет и так сделать можно — как сделать правильнее и что будет работать быстрее? Итак, касательно вопроса, который возник у меня по поводу JSON и восприятия его джексоном: джексон все поймет. Он умный. Только нужно ему чуть-чуть помочь в этом. Создаем POJO — обычный джава класс, в котором будут описаны нужные нам переменные из JSON’a. Тут же скажу, что для этого крайне желательно изучить сам JSON, для которого мы пишем класс (скорее всего в нем будут вложенные классы, которые тоже нужно создать). Далее с помощью аннотации @JsonCreator над конструктором мы показываем, что поля этого класса нужно заполнить из JSON’a. В параметрах конструктора мы можем указать какие именно поля джсона присваивать полям класса с помощью аннотации @JsonParam(«ИмяПоляИзДжсон»). Если полей в джсоне больше чем нам нужно (а изначально это и был мой вопрос) — мы должны об этом предупредить и объяснить, что остальные поля нам не нужны. Для этого мы используем аннотацию @JsonIgnoreProperties(ignoreUnknown=true). Тогда при встрече с неизвестными полями программа не будет падать. И, барабанная дробь. Мы имеем объект класса с заполненными полями (да-да, именно теми, которые нам нужны) и можем его использовать. Изначально я писал свой отдельный класс со статик методами, которые вынимали нужные мне значения и, при необходимости, приводили их в божеский вид. Это мутарно. Это кустарно. Это костыли. Вот собственно сам класс с методами:

 public class Methods < public static double findRes(String body, String need)< int begin = body.indexOf(need); int end = body.indexOf(",", begin); String res = body.substring(begin, end); String res2 = res.substring(res.indexOf(":")+2); double finalRes = Double.parseDouble(res2); return finalRes; >public static String findUrl(String body, String need) < int begin = body.indexOf(need) + need.length(); int end = body.indexOf(",", begin); String res = body.substring(begin, end); String res2 = res.substring(res.indexOf(":")+1); return res2; >> 

Использовать джексон гораздо более удобно и читабельно(как минимум это действительно красивее). В разговоре с братьями нашими старшими — они подтвердили: использование библиотек, в нашем случае джексона — предпочтительнее.

 import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; import org.springframework.stereotype.Component; @Component @JsonIgnoreProperties(ignoreUnknown = true) public class CurrencyPojo < private Rates rates; public CurrencyPojo() < >@JsonCreator public CurrencyPojo(@JsonProperty("rates") Rates rates) < this.rates = rates; >public Rates getRates() < return rates; >public void setRates(Rates rates) < this.rates = rates; >@Override public String toString() < return "CurrencyPojo'; > @Component @JsonIgnoreProperties(ignoreUnknown = true) public static class Rates < private double rub; private double inr; private double eur; public Rates() < >@JsonCreator public Rates(@JsonProperty("RUB") double rub, @JsonProperty("INR") double inr, @JsonProperty("EUR") double eur) < this.rub = rub; this.inr = inr; this.eur = eur; >public double getRub() < return rub; >public void setRub(double rub) < this.rub = rub; >public double getInr() < return inr; >public void setInr(double inr) < this.inr = inr; >public double getEur() < return eur; >public void setEur(double eur) < this.eur = eur; >@Override public String toString() < return "Rates  < "disclaimer": "Usage subject to terms: https://openexchangerates.org/terms", "license": "https://openexchangerates.org/license", "timestamp": 1638143999, "base": "USD", "rates": < "AED": 3.672934, "AFN": 95.889778, "ALL": 107.277494, "AMD": 483.27152, "ANG": 1.802446, "AOA": 585, "ARS": 100.99011, "AUD": 1.400119, "AWG": 1.80025, "AZN": 1.700805, "BAM": 1.734322, "BBD": 2, "BDT": 85.804468, "BGN": 1.727247, "BHD": 0.377164, "BIF": 1994.344572, "BMD": 1, "BND": 1.370223, "BOB": 6.905713, "BRL": 5.593606, "BSD": 1, "BTC": 0.000017488138, "BTN": 74.895631, "BWP": 11.828585, "BYN": 2.560653, "BZD": 2.015918, "CAD": 1.27258, "CDF": 2003.492833, "CHF": 0.92439, "CLF": 0.030154, "CLP": 831.315923, "CNH": 6.395085, "CNY": 6.393, "COP": 3975.845415, "CRC": 639.731775, "CUC": 1, "CUP": 25.75, "CVE": 97.95, "CZK": 22.7447, "DJF": 178.031664, "DKK": 6.585348, "DOP": 56.599119, "DZD": 139.135508, "EGP": 15.756894, "ERN": 15.000155, "ETB": 47.819833, "EUR": 0.885541, "FJD": 2.12473, "FKP": 0.749595, "GBP": 0.749595, "GEL": 3.095, "GGP": 0.749595, "GHS": 6.142755, "GIP": 0.749595, "GMD": 52.425, "GNF": 9472.013443, "GTQ": 7.738789, "GYD": 209.235741, "HKD": 7.7981, "HNL": 24.17051, "HRK": 6.661782, "HTG": 98.81349, "HUF": 327.09539, "IDR": 14379.716018, "ILS": 3.185445, "IMP": 0.749595, "INR": 75.050444, "IQD": 1458.680982, "IRR": 42275, "ISK": 130.231848, "JEP": 0.749595, "JMD": 155.740793, "JOD": 0.709, "JPY": 113.7185, "KES": 112.535405, "KGS": 84.774702, "KHR": 4069.37439, "KMF": 436.000041, "KPW": 900, "KRW": 1195.716418, "KWD": 0.30268, "KYD": 0.833396, "KZT": 436.292325, "LAK": 10839.499888, "LBP": 1520.868483, "LKR": 202.516227, "LRD": 142.25, "LSL": 16.236278, "LYD": 4.615464, "MAD": 9.244198, "MDL": 17.763696, "MGA": 3988.128848, "MKD": 54.637275, "MMK": 1790.896161, "MNT": 2854.559306, "MOP": 8.033255, "MRO": 356.999828, "MRU": 36.094075, "MUR": 43.067396, "MVR": 15.45, "MWK": 816.475065, "MXN": 21.738389, "MYR": 4.239, "MZN": 63.857001, "NAD": 16.26, "NGN": 410.875846, "NIO": 35.230131, "NOK": 9.0605, "NPR": 119.833306, "NZD": 1.465193, "OMR": 0.385109, "PAB": 1, "PEN": 4.033921, "PGK": 3.51889, "PHP": 50.480705, "PKR": 176.598456, "PLN": 4.168379, "PYG": 6826.299832, "QAR": 3.646364, "RON": 4.37388, "RSD": 103.877366, "RUB": 75.58127, "RWF": 1024.40338, "SAR": 3.7514, "SBD": 8.064563, "SCR": 14.654883, "SDG": 438, "SEK": 9.148279, "SGD": 1.370086, "SHP": 0.749595, "SLL": 11119.30017, "SOS": 580.721202, "SRD": 21.52, "SSP": 130.26, "STD": 21187.940504, "STN": 22.195, "SVC": 8.750748, "SYP": 2512.5, "SZL": 15.967534, "THB": 33.757117, "TJS": 11.286041, "TMT": 3.51, "TND": 2.882, "TOP": 2.277258, "TRY": 12.378954, "TTD": 6.78112, "TWD": 27.866934, "TZS": 2302.544214, "UAH": 27.094403, "UGX": 3563.214629, "USD": 1, "UYU": 44.148288, "UZS": 10783.399861, "VES": 4.57705, "VND": 22678.30849, "VUV": 111.998805, "WST": 2.563531, "XAF": 580.876668, "XAG": 0.04289544, "XAU": 0.00055691, "XCD": 2.70255, "XDR": 0.714635, "XOF": 580.876668, "XPD": 0.00055962, "XPF": 105.673123, "XPT": 0.00101782, "YER": 250.249937, "ZAR": 16.1344, "ZMW": 17.776133, "ZWL": 322 >> 

Для преобразования джсона в объект класса используем маппер:

 ObjectMapper mapper = new ObjectMapper(); currencyPojo(объект нашего класса) = mapper.readValue(jsonResponse(Джсон в стринге), CurrencyPojo.class(класс, который нам нужен)); 

Код рабочий, вытягивает из этой громадины именно то, что нужно мне. Вот вывод объекта класса после парса: CurrencyPojo> Я в восторге. Второй день хлопаю во все лицо и улыбаюсь в ладоши. Или как-то так. Значения курса могут различаться т.к. текст JSON’a и принт объекта от разных дат, но в целом результат должен быть понятен. P.S. Пишу статью в первый раз, поэтому не судите строго. И я только начинающий «хацкер» =) Критику люблю и принимаю 😉 P.S.2 Прощу прощения за «многабукв», но я постарался изложить то, до чего шел не один день.

тут блог

тут блог

Общественные обязательства интроверта.
Сообщения на ИТ тематику, но не обязательно.

О Lombok

На одном проекте, который, ой, уже как почти год идёт, мы не уломали технарей от заказчика на Kotlin. На наши попытки рассказать, чем Kotlin хорош, нам говорили: ну вот же, берёте Lombok, и будет так же лаконично. Так мы узнали, что такое Lombok, Project Lombok.

На самом деле, Lombok — это остров в Индонезии. Рядом с островом Java. Там ещё рядом есть город Jakarta. Ну вы поняли, да?

Индонезия

Lombok — это библиотека кодогенерации. В Maven достаточно просто добавить зависимость, и вот у вас уже работает плугин, который творит магию. В Gradle Lombok нужно подключить либо как плугин, либо как специальную annotationProcessor зависимость.

Что за код генерирует Lombok? Ну вот простенький пример.

import lombok.Data; @Data public class Location  private double latitude; private double longitude; > 

Аннотация @Data добавляет нам геттеры, сеттеры, toString() , equals() , hashCode() и конструктор для «обязательных» аргументов. В данном случае и latitude , и longitude являются double , у которых есть дефолтное значение 0.0 , и они не объявлены final . С точки зрения Lombok это «необязательные» аргументы, поэтому создался конструктор без аргументов.

Location location = new Location(); location.setLatitude(1.23); location.setLongitude(4.56); location.getLatitude(); location.getLongitude(); location.toString(); // Location(latitude=1.23, longitude=4.56) location.hashCode(); // -819911996 Location anotherLocation = new Location(); location.equals(anotherLocation); // false 

Аналогичный data class на Kotlin будет выглядеть так:

data class Location( var latitude: Double = 0.0, var longitude: Double = 0.0 ) 

Чтобы можно было изменять свойства класса, они объявлены как var . Чтобы можно было вызывать конструктор без аргументов, добавлены значения по умолчанию. Не вполне котлиновый подход.

val location = Location() location.latitude = 1.23 location.longitude = 4.56 location.latitude location.longitude println(location) // Location(latitude=1.23, longitude=4.56) println(location.hashCode()) // 1091536083 val anotherLocation = Location() location == anotherLocation // false 

Но мы ведь все за иммутабельные данные? Поэтому отходим от JavaBean и используем аннотацию @Value .

import lombok.Value; @Value public class Location  double latitude; double longitude; > 

@Value делает все поля класса private final , поэтому явно писать private не нужно. Нам сгенерируют геттеры, toString() , equals() и hashCode() . Сеттеров не будет, иммутабельный класс же. И будет конструктор со всеми аргументами, который создаёт наш объект раз и навсегда.

Location location = new Location(1.23, 4.56); location.getLatitude(); location.getLongitude(); location.toString(); // Location(latitude=1.23, longitude=4.56) location.hashCode(); // -819911996 Location anotherLocation = new Location(1.23, 4.56); location.equals(anotherLocation); // true 

Если, после Котлина, вам не нравится слово new , можно попробовать добавить @AllArgsConstructor(staticName=»of») . @Value уже неявно добавляет эту аннотацию, только без этого аргумента. Тогда можно будет так:

Location location = Location.of(1.23, 4.56); 

Так вот, навешивая всё больше и больше аннотаций, и меняется поведение Lombok.

Иммутабельное значение уже похоже на поведение типичных дата классов Котлина.

data class Location( val latitude: Double, val longitude: Double ) 

Конструктор без new . Возможность только читать свойства.

val location = Location(1.23, 4.56) location.latitude location.longitude println(location) // Location(latitude=1.23, longitude=4.56) println(location.hashCode()) // 1091536083 val anotherLocation = Location(1.23, 4.56) location == anotherLocation // true 

А что если не все свойства обязательны? Как конструировать такие объекты?

В Java нам либо придётся создавать больше конструкторов, в которых можно запутаться, особенно, если типы полей одинаковые. Либо создавать больше фабричных методов. Либо воспользоваться паттерном Builder.

В Lombok есть аннотация @Builder .

@Value @Builder public class Location  double latitude; double longitude; Double radius; > 

Теперь объекты можно конструировать через билдер. При этом не обязательно указывать все свойства.

Location location = Location.builder() .latitude(1.23) .longitude(4.56) .build(); location.toString(); // Location(latitude=1.23, longitude=4.56, radius=null) Location radiusLocation = Location.builder() .latitude(1.23) .longitude(4.56) .radius(5.0) .build(); radiusLocation.toString(); // Location(latitude=1.23, longitude=4.56, radius=5.0) 

А в Kotlin билдеры просто не нужны. Потому что там есть именованные аргументы и аргументы по умолчанию.

data class Location( val latitude: Double, val longitude: Double, val radius: Double? = null ) 

При вызове функции, метода или конструктора аргументы можно указать по именам, в любом порядке. А аргументы, у которых есть значение по умолчанию, можно не указывать. Очень гибко, удобно и просто.

val location = Location( latitude = 1.23, longitude = 4.56 ) println(location) // Location(latitude=1.23, longitude=4.56, radius=null) val radiusLocation = Location( latitude = 1.23, longitude = 4.56, radius = 5.0 ) println(radiusLocation) // Location(latitude=1.23, longitude=4.56, radius=5.0) 

А что, если нам нужно «поменять» иммутабельный класс? Ну чтобы не указывать заново 100500 свойств, а лишь поменять парочку при копировании.

В Lombok можно получить билдер из объекта. Нужно добавить toBuilder .

@Value @Builder(toBuilder = true) public class Location  double latitude; double longitude; Double radius; > 

Теперь у объекта есть метод toBuilder() .

Location location = Location.builder() .latitude(1.23) .longitude(4.56) .build(); location.toString(); // Location(latitude=1.23, longitude=4.56, radius=null) Location radiusLocation = location.toBuilder() .radius(5.0) .build(); radiusLocation.toString(); // Location(latitude=1.23, longitude=4.56, radius=5.0) 

В Kotlin ничего добавлять не нужно. У дата классов уже есть метод copy() с теми же аргументами, что у конструктора, только всеми необязательными.

val location = Location( latitude = 1.23, longitude = 4.56 ) println(location) // Location(latitude=1.23, longitude=4.56, radius=null) val radiusLocation = location.copy( radius = 5.0 ) println(radiusLocation) // Location(latitude=1.23, longitude=4.56, radius=5.0) 

Билдеры, это, конечно, хорошо. Если у вас Java, а не Kotlin. Но @Value класс с @Builder — это не JavaBean. От этого возникают проблемы с десериализацией.

С сериализацией проблем нет. У нас есть геттеры. А значит, сериализатор может узнать имена и значения свойств.

А вот для десериализации нам нужно создать объект по сериализованному описанию. Конструктор у нас без аргументов. Сеттеров нет. Где-то там есть билдер, но про него десериализатору ещё нужно сообщить.

Полностью обвешенный аннотациями для Jackson класс выглядит так:

@Value @Builder(builderClassName = "Builder", toBuilder = true) @JsonDeserialize(builder = Location.Builder.class) public class Location  double latitude; double longitude; @JsonPOJOBuilder(withPrefix = "") public static final class Builder  > > 

Не очень удобно, нужно навешивать аннотацию на генерируемый класс билдера. Зато Джексон теперь может использовать билдер для создания объекта.

ObjectMapper mapper = new ObjectMapper(); Location location = Location.builder() .latitude(1.23).longitude(4.56) .build(); String json = mapper.writeValueAsString(location); // Location fromJson = mapper.readValue(json, Location.class); location.equals(fromJson); // true 

Либо можно попросить Lombok сгенерировать публичный конструктор с аннотацией @ConstructorProperties . Эта аннотация сохраняет имена параметров конструктора. И Джексон может воспользоваться этим конструктором, чтобы создать объект.

Нужно снова добавить @AllArgsConstructor :

@Value @Builder @AllArgsConstructor public class Location  double latitude; double longitude; > 

Но также создать файл lombok.config в корне проекта. С таким содержимым:

lombok.addLombokGeneratedAnnotation = true lombok.anyConstructor.addConstructorProperties = true 

Расширение jackson-lombok , кстати, устарело.

Сложно. Более одного способа выстрелить себе в ногу. Нет никакой гарантии, что очередные правки аннотаций ничего не сломают.

Котлиновые дата классы тоже Джексону из коробки не по зубам. Но есть стабильный официальный поддерживаемый jackson-module-kotlin . Он работает.

import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.module.kotlin.readValue import com.fasterxml.jackson.module.kotlin.registerKotlinModule //. val mapper = ObjectMapper().registerKotlinModule() val location = Location( latitude = 1.23, longitude = 4.56 ) val json = mapper.writeValueAsString(location) // val fromJson: Location = mapper.readValue(json) println(location == fromJson) // true 

registerKotlinModule() и readValue() — это extension функции. Ещё одна приятная фича Котлина. А readValue() ещё и inline reified функция, благодаря чему можно не указывать тип, который мы пытаемся прочесть из JSON. Он будет выведен из типа переменной, которой здесь присваивается значение.

У Lombok есть проблемы не только с Jackson. Это — кодогенерация. И даже не столько кодогенерация, потому что нового кода-то и не создаётся, сколько вмешательство в процесс компиляции. Модификация AST, судя по тому, что гуглится.

А кодогенерация в Java так и не стала естественным этапом сборки, как, например, в Go. Раз в коде нет ни геттеров, ни сеттеров, ни билдеров, ни прочих нагенерированных методов, ни IDE, ни прочие инструменты их не увидят.

Без соответствующего плугина, работать с Lombok в IDE практически невозможно. А к IDEA плугин поддерживается не авторами Lombok, не JetBrains, а неким частным лицом с русской фамилией, проживающим в Германии. И за этот год этот плугин уже несколько раз отваливался, при обновлении IDEA. Причём в последний раз это, вроде как, было по вине JetBrains.

И не только IDE плохо. Всякие инструменты покрытия кода тестами, статистические анализаторы и прочие тоже спотыкаются о Lombok. Поэтому Lombok приходится разломбочивать. То есть генерировать тот самый отсутствующий код, который (не) создаёт Lombok.

С Kotlin таких проблем нет. Это отдельный язык со своим отдельным компилятором из исходного кода в JVM байткод. С полноценными плугинами для Maven, Gradle и IDE.

Что ещё мы используем от Lombok? Ну, например, аннотацию @Slf4j .

@Slf4j public class Main  public static void main(final String[] args)  log.info("Hello, World!"); > > 

Lombok добавляет нам в класс статическое свойство log .

В Kotlin можно добавить синтаксического сахара с помощью extension функций. Но оставить явное создание логера.

fun Any.logger(): Logger = LoggerFactory.getLogger(this.javaClass) class Main  private val log = logger() fun main()  log.info("Hello, World!") > > 

Lombok не нужен, если есть Kotlin.

Lombok очень странно встраивается в работу компилятора. Он требует сильно больших танцев с бубнами, чтобы все сопутствующие инструменты заработали. Kotlin надёжнее, стабильнее и удобнее.

Вот этими глюками плугинов и десериализаторов и буду пугать заказчика в следующий раз, если меня будут отговаривать от Kotlin, размахивая перед носом Lombok.

Как получить значение аннотаций jackson

Aннотации используются и при сериализации, и при десериализации

Aннотации классa
// 1. @JsonAutoDetect
// 2. @JsonWriteNullProperties
// 3. @JsonPropertyOrder

Aннотации переменных и методов
// 11. @JsonIgnore
// 12. @JsonProperty
// 13. @JsonDeserialize
// 14. @JsonAnyGetter // AVY
// 15. @JsonAnySetter // AVY

Аннотации Полиморфных типов
// 21. @JsonTypeInfo // Полиморфный тип
// 22. @JsonSubTypes // Полиморфный тип
// 23. @JsonTypeName // Полиморфный тип

Общий пример
// 33. Общий пример

Ставится перед классом
Помечает класс как готовый к сериализации в JSON
Сообщает Jackson, что необходимо использовать поля этого класса при записи или чтении. В скобках можно задать параметр (fieldVisibility = JsonAutoDetect.Visibility.ANY ), для настройки видимости полей, которые будут использоваться (по умолчанию используются только public поля).

Ставится перед классом
Поля объекта, которые равны null не будет игнорироваться

Ставится перед классом
Позволяет определить порядок, в котором поля Java объекта будут сериализованы в JSON
При десериализации будет неясно какой тип класса выбирать — родительский или дочерний
Сериализованные могут выглядеть одинаково : поле может быть null
Hужно сохранять информацию о типе сериализованного класса в JSON
Решение: К базовому классу иерархии надо добавлять аннотацию @JsonTypeInfo ( com.fasterxml.jackson.annotation.JsonTypeInfo )
Aтрибут property указывает имя свойства в JSON, в которое будет записано имя типа Java
Аннотацию @JsonTypeInfo можно указывать и у полей, для которых нужно сохранить информацию о типе при сериализации в JSON.

Ставится перед свойством ( полем )
Свойство игнорируется при Cериализации и Десериализации
Сообщает Jackson, что данное поле нужно игнорировать при чтении/записи

@JsonAutoDetect
class Cat

public String name;

@JsonIgnore
public int weight;

Ставится перед свойством ( полем ) или getter’ом или setter’ом
Позволяет задать другое имя поля при сериализации.

@JsonAutoDetect
class Cat

@JsonProperty(«klichka»)
public String name;

@JsonAutoDetect
class Cat

@JsonProperty(value=»klichka»)
public String name;

Ставится перед полем.
Используется для десериализации Интерфейсов-коллекций и Полиморфных классов ( наследуемых ). Когда неясно какой класс выбирать для десериализации : родителя или наследника
A. Интерфейсы-коллекции
B. Наследуемые классы

A.
Из Java массивы и списки сериализуются в массивы. И при десериализации можно выбрать, в какой именно тип класса надо десериализовать ( ArrayList или LinkedList или . )

@JsonDeserialize(as=ArrayList.class,contentAs=BasicClass.class)
static List set = new ArrayList();

public class Circle

@JsonDeserialize(as=PointImpl.class)
public Point center;

public int radius;

// 33. Общий пример

@JsonAutoDetect
@JsonWriteNullProperties
@JsonPropertyOrder
class Cat

@JsonProperty(value=»klichka»)
public String name;

@JsonIgnore
public int weight;

@JsonDeserialize(as=ArrayList.class,contentAs=BasicClass.class)
static List set = new ArrayList();

Jackson и неизменяемые объекты

Неизменяемые объекты в Java — это объекты, которые не имеют внутреннего состояния, или объекты, состояние которых не меняется после создания.

Пример неизменяемого класса:

public class Point < private final int x , y ; public Point ( int x , int y ) < this . x = x ; this . y = y ; public int getX ( ) < return x ; >
public int getY ( ) < return y ; >

У неизменяемого класса нет методов установки значений, вместо этого значения x и y устанавливаются в конструкторе. Преимущество подобных классов в том, что их можно без опасений применять в многопоточном приложении без всякой синхронизации. Неизменяемые объекты используются в качестве ключей java . util . Map .

В самом простом случае для поддержки неизменяемых объектов используется аннотация @JsonCreator :

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *