JSON با Jackson (بخش چهارم-ب) Data Binding
شرح بیشتر توانمندیهای روش Data Binding در این مقاله ادامه پیدا کرده و تکمیل میشود.
نوشتهی حاضر، در ادامه مقاله قبل، امکانات روش Data Binding را بیشتر شرح داده و به پاسخ سوالهای زیر خواهد پرداخت: تولید و بازیابی نمونه از کلاسهای یک ساختار 1hierarchy به شکل 2polymorphic امکان پذیر است؟ آیا میتوان هنگام تولید و بازیابی از یک سری فیلدها صرف نظر کرده و آنها را نادیده گرفت؟ آیا میتوان انعطافی را که به کمک annotationها در عملکرد Jackson ایجاد میکنیم، بر روی کلاسهای کتابخانههای third-party نیز اعمال کنیم؟ در انتها به این سوال خواهیم پرداخت: «در چه مواردی Data Binding Method بهترین گزینه است؟»
Josn Polymorhpism
Jackson روشی برای نگهداری نوع زیرکلاسها (Sub-Class)، هنگام ساخت JSON از اشیای جاوا در اختیار قرار میدهد. با استفاده از این تسهیلات هنگام ساخت دوبارهی اشیاء جاوا از JSON میتوان نوع دقیق زیرکلاس را بازیابی کرد. اطلاعات نوع زیرکلاس میتواند در داخل JSON به عنوان یک property ذخیره شود. در مثال زیر، مثال Library باز طراحی شده تا کار با کلاسهای polymorphic قابل بررسی شود. به این منظور کلاس Library را تغییر دادیم تا لیستی از Documentها را نگهداری کند. Document یک کلاس abstract است. زیرکلاسهای آن عبارتند از Journal و Book. تعریف مربوط به هر کدام از کلاسها در ادامه آورده میشود:
public abstract class Document { int isbn; String title; public Document() { } public Document(int isbn, String title) { this.isbn = isbn; this.title = title; } //getters and setters }
Document توسط دو کلاس زیر به ارث برده می شود. کلاس Journal:
public class Journal extends Document { private int issue; public Journal(int isbn, String title, int issue) { super(isbn, title); this.issue = issue; } //getter and setter for issue @Override public String toString() { return "Journal [issue=" + issue + ", isbn=" + isbn + ", title=" + title + "]"; } }
و کلاس Book:
public class Book extends Document { private String author; public Book(int isbn, String title, String author) { super(isbn, title); this.author = author; } // getter and setter for author @Override public String toString() { return "Book [author=" + author + ", isbn=" + isbn + ", title=" + title + "]"; } }
کلاس Library به شکل زیر تعریف شده است، به فیلد docList در این کلاس دقت کنید.
public class Library { private String title; private List<Document> docList; public Library() { } public Library(String title, List<Document> docList) { this.title = title; this.docList = docList; } // getters and setters }
با کمک تابع زیر شیءای از کلاس Library را ساخته و برای تولید JSON از آن استفاده خواهیم کرد:
private static Library generateLibrary() { //Create Documents Book book = new Book(1, "myBook", "John"); Journal journal = new Journal(2, "myJournal", 1000); //Create a List of Documents List<Document> docList = new ArrayList<>(); docList.add(book); docList.add(journal); //Create an instance of Library Library lib = new Library("national Lib.", docList); return lib; }
با اجرای دو سطر زیر، JSON تولید شده و در فایل ذخیره میشود:
Library library = generateLibrary(); objectMapper.writeValue(new File("polymorhpismDB.json"), library);
JSON تولید شده به شکل زیر است:
{"title":"national Lib.","docList":[ {"isbn":1,"title":"myBook","author":"John"}, {"isbn":2,"title":"myJournal","issue":1000} ]}
همان طور که میبینید هیچ اطلاعاتی در این مورد که شیء با isbn=1 از نوع Book است در این رشته وجود ندارد. هنگامی که میخواهیم این داده را تبدیل به POJO کنیم، در ابتدا Jackson تلاش میکند اشیاء داخل docList را تبدیل به نمونههایی از Document کند. اما چون Document یک کلاس abstract است، یک exception پرتاب میکند.
این موضوع را با اجرای کد زیر بررسی میکنیم:
Library readLib = objectMapper.readValue(new File("polymorhpismDB.json"), Library.class);
خروجی زیر تولید میشود:
"Exception in thread 'main' com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Can not construct instance of com.rhotiz.jacksonPolymorphism.Document (no Creators, like default construct, exist): abstract types either need to be mapped to concrete types, have custom deserializer, or contain additional type information"
پیشنهاد Jackson این است که اطلاعاتی در مورد نوع اشیا (additional type information) در کنار کلاس Document عرضه شود.
برای ذخیرهی «اطلاعات نوع» از دو annotation با نامهای JsonTypeInfo و JsonSubTypes استفاده میکنیم. اطلاعات مربوط به زیرکلاسهای Document به شکل زیر در کد نوشته میشود. دقت کنید که این اطلاعات در کنار کد ابَرکلاس (یعنی Document) ذکر شده است.
import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonSubTypes.Type; import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.annotation.JsonTypeInfo.As; @JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = As.PROPERTY, property = "@class") @JsonSubTypes({ @Type(value = Book.class, name = "book"), @Type(value = Journal.class, name = "jouranl") }) public abstract class Document { int isbn; String title; // Constructors // getters and seters }
پس از اعمال تغییرات فوق، با تبدیل شیءای از کلاس Library به JSON، خروجی زیر تولید میشود:
{"title":"national Lib.","docList": [{"@class":"com.rhotiz.jacksonPolymorphism.Book","isbn":1,"title":"myBook","author":"John"} ,{"@class":"com.rhotiz.jacksonPolymorphism.Journal","isbn":2,"title":"myJournal","issue":1000}]}
همانطور که میبینیم، به هر کدام از نمونههای Document یک فیلد با نام class@ اضافه شده و نام کامل کلاس که شامل نام package هم هست به عنوان value نوشته شده است. در صورتی که داده JSON به این صورت نوشته شود قادر به بازیابی کامل library خواهیم بود.
بررسی دقیقتر annotationهای مربوط به «اطلاعات نوع»
JsonTypeInfo@ که در کد فوق مورد استفاده قرار گرفت دارای سه پارامتر است.
- پارامتر use دارای مقداری از نوع JsonTypeInfo.Id Enum است. این پارامتر بیان میکند کلاس مربوط به این شیء چگونه معرفی شود. به عنوان مثال اگر این پارامتر از نوع Id.CLASS باشد نام کامل کلاس (همراه با نام پکیج) و اگر از نوع Id.MINIMAL_CLASS باشد، نام کلاس بدون نام پکیج نوشته میشود.
- پارامتر دوم بیان میکند اطلاعات مربوط به نوع کلاس به چه شکل در JSON نوشته شود، این پارامتر به صورت یک مقدار از Enum JsonTypeInfo.As نوشته میشود. به عنوان مثال اگر include برابر با As.PROPERTY باشد نوع کلاس به عنوان یک فیلد نوشته می شود:
{"@class":"com.rhotiz.jacksonPolymorphism.Book","isbn":1,"title":"myBook","author":"John"}
و اگر از نوع As.WRAPPER_OBJECT نوشته شود، شکل ذخیرهی داده به شکل زیر تغییر می کند:
{"com.rhotiz.jacksonPolymorphism.Book":{"isbn":1,"title":"myBook","author":"John"}
- پارامتر سوم یا property، نام فیلدی که نوع کلاس را در JSON ذخیره خواهد کرد، نشان میدهد.
JsonSubTypes@ لیستی از زیرکلاسها را به شکل Type@ دریافت میکند.
مدیریت فیلدهایی که باید Serialize و Deserialize شوند
در مثالهای قبل برای تبدیل یک شیء به JSON آنرا در اختیار ObjectMaper قرار میدادیم، این کلاس فیلدهای شیء را به صورت «اتوماتیک» «شناسایی» کرده و بعد نام و مقدار آنها را در محل مناسب مینوشت. در این قسمت میخواهیم روی «شناسایی اتوماتیک» کنترل بیشتری داشته باشیم. به این منظور از annotation ای به نام JsonAutoDetect@ استفاده خواهیم کرد.
از کلاس زیر به منظور بررسی عملکرد این annotation استفاده خواهیم کرد. کلاس، دارای فیلدی از هر چهار نوع Access Control است. getter و setter هر فیلد نیز هم نوع با خود فیلد تعریف شدهاند. (هر چند در برنامه نویسی معمول چنین کلاسی کاربردی نخواهد داشت ولی به درک بحث کمک میکند)
public class FieldyClass { private int privateField; protected int protectedField; int pakcageField; public int publicField; public FieldyClass() { privateField = 0; protectedField = 0; pakcageField = 0; publicField = 0; } private int getPrivateField() { return privateField; } private void setPrivateField(int privateField) { this.privateField = privateField; } protected int getProtectedField() { return protectedField; } protected void setProtectedField(int protectedField) { this.protectedField = protectedField; } int getPakcageField() { return pakcageField; } void setPakcageField(int pakcageField) { this.pakcageField = pakcageField; } public int getPublicField() { return publicField; } public void setPublicField(int publicField) { this.publicField = publicField; } }
اگر شیء از کلاس فوق ایجاد کرده و طبق روال معمول آن را به JSON تبدیل کنیم خروجی زیر تولید میشود:
{"publicField":0}
JsonAutoDetect@ دارای ورودیهایی مثل getterVisibility یا fieldVisibility است که مقداری از نوع JsonAutoDetect.Visibility دریافت میکند. اگر annotation زیر را به FieldyClass اضافه کنیم:
@JsonAutoDetect(fieldVisibility = Visibility.NONE , getterVisibility = Visibility.NONE )
و با این کلاس یک شیء ساخته و تبدیل به JSON کنیم، خروجی زیر تولید میشود.
{}
مثال بعدی حالتی است که تمام getterها را قابل مشاهده کنیم تا مقدار فیلد معادل در خروجی نوشته شود:
@JsonAutoDetect(fieldVisibility = Visibility.NONE , getterVisibility = Visibility.ANY )
خروجی زیر را تولید می کند:
{"privateField":0,"protectedField":0,"pakcageField":0,"publicField":0}
در آخرین مثال این بخش دسترسی توسط getterها را حذف کرده و فیلدهای غیر private را نمایان خواهیم کرد.
@JsonAutoDetect(fieldVisibility = Visibility.NON_PRIVATE , getterVisibility = Visibility.NONE ) public class FieldyClass { // Class Content }
حاصل کار به شکل زیر قابل مشاهده است:
{"protectedField":0,"pakcageField":0,"publicField":0}
علاوه بر تنظیم فوق annotationهایی مانند JsonIgnore@ و JsonIgnoreProperties@ در دسترس است. مثالی از کاربرد اولی در بخش مربوط به JsonCreator، نشان داده شد. مثال زیر عملکرد آنها را به صورت سادهای نشان می دهد. این کلاس دارای چهار فیلد است، فیلد اول و دوم را به کمک JsonIgnoreProperties@ و فیلد سوم را به کمک JSonIgnore@، از لیست مواردی که در رشتهی JSON نوشته میشوند، حذف میکنیم.
import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; @JsonIgnoreProperties(value = {"a","b"}) public class ClassWithIgnoredFields { private int a; private int b; @JsonIgnore private int c; private int d; public ClassWithIgnoredFields() { a = 0; b = 1; c = 2; d = 3; } //getters }
خروجی تولید شیء ای از جنس این کلاس و نوشتن آن به شکل JSON توسط ObjectMapper به شکل زیر است:
{"d":3}
JsonIgnore@ برای متد، Constructor و فیلدها قابل استفاده است. JsonIgnoreProperties@ علاوه بر موارد فوق بر روی Typeها (کلاسها و Interfaceها) هم استفاده میشود.
اعمال annotationهای Jackson روی یک کلاس، بدون تغییر کد آن
annotationها یک ابزار بسیار عالی برای مدیریت روال تولید و خواندن JSON یا (serialization and deserialization) هستند. اما اگر کدهای کلاسی که میخواهیم آن را تبدیل به JSON کنیم تحت اختیار ما نباشد، چه باید کرد؟
کلاسهای موجود در بستههای نرمافزاری از این سنخ هستند.
همچنین در بسیاری از موارد نوشته شدن annotationهای Jackson روی POJOهای پروژه مطلوب نیست. در مواردی گفته میشود باعث پیچیدگی کد و ناخوانا شدن آن است. همچنین موارد بسیاری وجود دارد که POJO به اشکال مختلفی مورد استفاده قرار میگیرد و «تبدیل شدن به JSON» تنها یکی از این موارد است. به عنوان مثال یک POJO میتواند همزمان Entity پایگاهداده هم باشد.
در این موارد از روش Mix-In Annotation استفاده خواهیمکرد. روش کار به این صورت است که annotationهایی از Jackson که باید روی بخشهای مختلف کلاس A اعمال میشدند، روی abstract class B اعمال میکنیم و سپس به کلاس ObjectMapper اعلام میشود، annotationها را از کلاس B دریافت کرده و روی A اعمال کند. این کار به کمک متد addMixInAnnotaions از ObjectMapper انجام میشود.
مثال ساده زیر کاربرد و روش پیادهسازی این مفهوم را نشان میدهد.
در این مثال یک دانش آموز به صورت نمونهای از کلاس Student بیان میشود. سه فیلد این کلاس عبارتند از lastName ،firstName و privateHealthCondition. مطلوب است JSON با فیلدهای foreName و surname تولید شود. همچنین وضعیت سلامتی دانشآموز هم محرمانه بوده و نباید تبدیل به JSON گردد. برای رسیدن به این هدف دو کلاس زیر را ایجاد میکنیم.
public class Student { private String firstName; private String lastName; private String privateHealthCondition; public Student() { } public Student(String firstName, String lastName) { this.firstName = firstName; this.lastName = lastName; } // getters and setters }
annotationهای مربوط به تنظیمات تولید JSON در کلاس زیر متمرکز شده اند:
import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; abstract public class StudentMixIn { public StudentMixIn( @JsonProperty("foreName") String firstName, @JsonProperty("surName")String lastName){} @JsonProperty("foreName") abstract String getFirstName(); @JsonProperty("surName") abstract String getLastName(); @JsonIgnore abstract String getPrivateHealthCondition(); }
با اجرای کد زیر نمونهای از کلاس Student ساخته میشود و پس از معرفی کلاس MixIn به ObjectMapper دادهی JSON ای در یک فایل نوشته میشود. تنظیماتی که در کلاس StudentMixIn اعمال کردهایم در تبدیل Student در نظر گرفته خواهند شد.
public static void main(String[] args) throws JsonGenerationException, JsonMappingException, IOException { Student student = new Student("John", "Smith"); student.setPrivateHealthCondition("Intellectual Disability"); System.out.println(student); objectMapper.addMixIn(Student.class, StudentMixIn.class); objectMapper.writeValue(new File("mixInAnnotation.json"), student); }
خروجی اجرای کد فوق، دادهی متنی زیر است:
{"foreName":"John","surName":"Smith"}
نتیجهی فوق نشان میدهد، فیلدهای مربوط به نام، تغییر کرده و همچنین فیلد مربوط به سلامتی دانش آموز هنگام تبدیل به JSON نادیده گرفته شدهاست.
ويژگیهای Data Binding
Data Binding با هدف تسهیل تبدیل POJO به JSON و بالعکس توسعه یافته است. در صورتی که در میان کلاسهای پروژه، کلاسی ای معادل داده ی JSON موجود باشد، این روش در میان دو گزینه ی دیگر یعنی Streaming و Tree Model برتری قابل ملاحظهای دارد و بدون شک بهترین انتخاب به شمار میرود.
1- hierarchy: به معنی سلسلهمراتبی، ساختاری که در آن، هر کدام از اعضا با شاخصهایی رتبهبندی و مرتب شدهاند. در مورد برنامه نویسی شیءگرایی، میتوان کلاسهایی که از هم ارثبری دارند را در یک ساختار مرتب کرد به شکلی که کلاسهای توسعه داده شده (یا ابرکلاسها) در جایگاه بالاتر، قرار داده شوند. از این ساختار به hierarchy یاد میشود.
2- polymorphism: به چندریختی ترجمه میشود. در یک ساختار hierarchy از کلاسها، یک اشارهگر به ابرکلاس میتواند به نمونههایی از زیرکلاسهای متفاوت اشاره کند. به این اصل و ویژگیهای رفتاری که از آن نشات میگیرد، polymorphism گفته میشود.
.
.
.
آدرس کانال تلگرام: JavaCupIR@
آدرس اکانت توییتر: JavaCupIR@
آدرس صفحه اینستاگرام: javacup.ir
آدرس گروه لینکدین: Iranian Java Developers
کد ها بعضیاشون به قالب اسیپب میزنه/؟ چطو اصلاح کنیم؟
مشکل امنیتی کدهای جیسون را چطور میشه حل کرد؟
ممنون که تک تک کد ها توضیح دادید
خیلی عالی بود
ممنون بابت سایت عالیتون
سایتی هم برای آموزش MVC سراغ دارید که به خوبی شما یاشد
ممنون میشم معرفی کنید
با تشکر حلقه ازدواج