آموزشدانستنی‌ها

JSON با Jackson (بخش چهارم-الف) Data Binding

در مقاله‌ی اول، دوم و سوم، به‌ترتیب سه روش Stream Method ،Data Binding و Tree Model معرفی شدند. در این مقاله امکانات روش Data Binding بیشتر شرح داده‌ می‌شود.

سومین روش تبدیل متقابل JSON و POJO به یکدیگر، یعنی Data Binding، آن چنان کار را ساده کرده که مثال بررسی‌شده در مقاله‌ی اول روش کار با آن را نشان می‌دهد و نیازی به کاری فراتر از آن نیست. آنچه در این نوشتار آمده جواب در‌خواست‌های ما برای چیزی فراتر از تبدیل این دو به یکدیگر است. سوال‌هایی از این دست: اگر نام فیلد در JSON و POJO متفاوت باشد، Jackson می‌تواند کار نسبت دادن این دو را انجام دهد؟ آیا Jackson می‌تواند پارامتر‌های سازنده (Constructor) موجود در POJO را از مقادیر موجود در JSON تغذیه کند؟ کار با Generic‌ها چگونه انجام می‌شود؟ و …

نام فیلد متفاوت در POJO و JSON

اگر نام فیلد در JSON و POJO متفاوت باشد چگونه می‌توان از Data Binding استفاده کرد؟ برای این کار از JsonProperty@ استفاده می‌کنیم. این annotation می‌تواند روی فیلد یا یکی از متد‌های getter یا setter استفاده شود.

در مثال زیر کلاس ساده‌ی Person تعریف شده‌است. به‌سادگی می‌توان نام فیلد firstName را به foreName و نام فیلد lastName را به surName در JSON تغییر‌داد.

public class Person {
	@JsonProperty("foreName")
	private String firstName;
	
	@JsonProperty("surName")
	private String lastName;
	
	//getters and setters
}

تکه کد زیر از کلاس Person استفاده کرده و نمونه‌ای از آن را در قالب JSON ذخیره می کند.

private static void writePerson() throws IOException, JsonGenerationException, JsonMappingException {
	ObjectMapper objectMapper = new ObjectMapper();
	Person person = new Person();
	person.setFirstName("John");
	person.setLastName("Smith");
	objectMapper.writeValue(new File("changeFieldName.json"), person);
}

خروجی کد زیر که در فایل نوشته شده است به شکل زیر است. به تغییر نام فیلد‌ها دقت کنید:

{"foreName":"John","surName":"Smith"}

سازنده‌های تغذیه‌کننده از فیلد‌های JSON

گونه‌ای از کلاس‌های جاوا که از آن‌ها به عنوان بسته‌ی داده استفاده می‌کنند به Bean مشهورند. این کلاس‌ها سازنده پیش‌فرض دارند که بدون پارامتر ورودی است. در‌نتیجه پس از ساخت شی با آن، مقدار‌دهی هر فیلد با استفاده از متد setter انجام می‌شود. اما گاهی استفاده از سازنده‌هایی که دارای پارامتر هستند بسیار کاربردی است. به این مثال دقت کنید: JSON زیر معرف یک شخص است. همان‌طور که مشاهده می‌کنید فیلدی به نام age در آن نیست.

{"firstName":"John","lastName":"Smith","birthYear":1990}

اگر POJO معادل دارای فیلدی به نام age باشد، به سادگی می‌توانیم در سازنده، سال تولد را به عنوان آرگومان گرفته و سن را از روی تفاوت تاریخ فعلی و تاریخ تولد حساب کنیم. در چنین مواقعی وجود سازنده‌ای که مقادیر پارامتر ورودی (سال تولد) را از فیلد‌های JSON دریافت کند بسیار راه‌گشاست. به این منظور از JsonCreator@ استفاده می‌شود.

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;

public class PersonWithAge {
	private String firstName;
	private String lastName;
	private int birthYear;
	@JsonIgnore
	private int age;
	
	@JsonCreator
	public PersonWithAge(@JsonProperty("birthYear") int birthYear) {
		this.birthYear = birthYear;
		age = LocalDate.now().getYear() - birthYear;
	}

	//getters and setters

	@Override
	public String toString() {
		return "PersonWithAge [firstName=" + firstName + ", lastName=" + lastName + ", birthYear=" + birthYear
				+ ", age=" + age + "]";
	}
}

JsonIgnore@ در مثال فوق به این منظور استفاده شده که به ObjectMapper بگوییم هنگام تولید JSON از این شیء، فیلد age را ننویسد. همچنین هنگام تولید شیء از JSON نیازی به خواندن فیلدی به این نام نیست. در آینده بیشتر به این annotation خواهیم پرداخت.

داده‌ی فوق را که معرف یک شخص دارای یک تاریخ تولد در فرمت JSON است، توسط تکه‌کد زیر خوانده و تبدیل به نمونه‌ای از کلاس PersonWithAge می‌کنیم:

private static void readPersonWithAge() throws JsonParseException, JsonMappingException, IOException {
	ObjectMapper objectMapper = new ObjectMapper();
	PersonWithAge pwa = objectMapper.readValue(new File("personWithAge.json"), PersonWithAge.class);
	System.out.println(pwa);
}

که خروجی زیر را در کنسول چاپ می‌کند:

PersonWithAge [firstName=John, lastName=Smith, birthYear=1990, age=27]

دریافت و محاسبه‌ی سن در سازنده، با موفقیت انجام گرفته است.

کار با Generic‌ها

در این بخش دو موضوع متفاوت را مورد بررسی قرار خواهیم داد. در ابتدا یک شیء از List را به عنوان یک Generic به شکل JSON خواهیم نوشت. در بخش دوم یک شیء را که دارای فیلدی از نوع List است نوشته و بازیابی می‌کنیم.

تولید JSON از لیست

جاوا برای پیاده‌سازی Generic‌ها از ویژگی‌ای به نام Type Erasure استفاده کرده است. بدین معنا که نوع Generic‌هایی که در کد مشخص می‌کنیم هنگام اجرا در دسترس نیست. به مثال زیر دقت کنید.

package com.rhotiz.jacksonGenerics;

public class Book {
	private int ISBN;
	private String title;
	
	public Book() {
	}
	public Book(int ISBN,String title) {
		this.ISBN = ISBN;
		this.title = title;
	}
	// getters and setters
	@Override
	public String toString() {
		return "Book [ISBN=" + ISBN + ", title=" + title + "]";
	}
}

با اجرای کد زیر می‌توانیم عملکرد Type Erasure را مشاهده کنیم.

private static void showTypeEraser() {
	Book book = new Book(1000, "my Book");
	System.out.println("Class of book is: "+book.getClass());
	
	List<Book> bookList = new ArrayList<>();
	bookList.add(book);
	System.out.println("Class of bookList is: "+bookList.getClass());
}

که منجر به تولید خروجی زیر می‌شود.

Class of book is: class com.rhotiz.jacksonGenerics.Book
Class of bookList is: class java.util.ArrayList

باید بدانیم هنگام اجرا با پاک شدن نوع Generic‌ها، تفاوتی بین لیستی از String با لیستی از Book نیست (از لحاظ نوع المان‌ها)، چرا که هر دو تبدیل به لیستی از Object شده‌اند. این نوع عملکرد جاوا سوالی را ایجاد می‌کند:

تا کنون هنگام استفاده از ObjectMapper برای خواندن JSON از متد زیر استفاده می کردیم:

<T> T readValue(!!SourceType!! src, Class<T> valueType)

به عنوان مثال برای بازیابی یک شیء Book می نوشتیم:

‌Book book = objectMapper.readValue(new File(“db.json”), Book.class);

در شرایطی که بخواهیم «لیستی از Book» را از یک فایل بازیابی کنیم، چگونه می‌توانیم به objectMapper بگوییم آنچه باید بخواند یک لیست از Book است نه لیستی از Ojbect؟ یا لیستی از چیز دیگر؟

به این منظور از کلاس TypeReference استفاده می‌شود. این کلاس یک نسخه از نوع المان را ذخیره می‌کند تا پس از Type Erasure قادر به بازیابی نوع Generic‌ها باشیم.

با کمک کلاس TypeReference و نسخه‌‌ی زیر از متد readValue در کلاس ObjectMapper، می‌توانیم بگوییم «لیستی از Book‌ها» را از فایل بخوان:

<T> T readValue(File src, TypeReference valueTypeRef)

مثال زیر روش کار را نشان می‌دهد:

private static void wrokWithGenericsRightWay() throws JsonGenerationException, JsonMappingException, IOException {
	Book book = new Book(12345, "Book1");
	List<Book> bookList = new ArrayList<>();
	bookList.add(book);
	TypeReference<List<Book>> bookListTypeReference = new TypeReference<List<Book>>() {};
	System.out.println("TypeReference of bookList is: "+bookListTypeReference.getType());
	objectMapper.writeValue(new File("db.json"), bookList);
	
	List<Book> readBookList = objectMapper.readValue(new File("db.json"), bookListTypeReference);
	System.out.println(readBookList.get(0));
}

که منجر به تولید خروجی زیر می‌گردد:

TypeReference of bookList is: java.util.List<com.rhotiz.jacksonGenerics.Book>
Book [ISBN=12345, title=Book1]

تولید JSON از POJO دارای یک لیست Generic

بر اساس فایل README ماژول jackson-databind در گیت‌هاب، در شرایطی که یک POJO دارای لیستی از POJO‌های دیگر (به عنوان فیلد) باشد نیازی به کار اضافی (مانند تعریف TypeReference و …) برای خواندن و نوشتن آن نیست.

کلاس Library به صورت زیر تعریف شده تا دارای لیست‌ای از Book باشد.

package com.rhotiz.jacksonGenerics;

import java.util.List;

public class Library {
	private String name;
	private List<Book> bookList;
	
	public Library() {
	}
	
	public Library(String name, List<Book> bookList) {
		this.name = name;
		this.bookList = bookList;
	}

	//getters and setters

	@Override
	public String toString() {
		return "Library [name=" + name + ", bookList=" + bookList + "]";
	}
}

تکه‌کد زیر نمونه‌ای از کلاس Library ساخته، آن‌را در فایل می‌نویسد، و سپس داده را از فایل خوانده و آن را تبدیل به شیء ای از کلاس Library می‌کند. پس از آن برای ارزیابی نکته‌ای که به آن اشاره کردیم کلاس فیلد مربوطه (bookList) و المان‌های موجود در لیست (Book) را بررسی می‌کنیم تا ببینیم نوع آن‌ها با وجود مسئله‌ی Type Erasure به درستی بازیابی شده یا خیر.

private static void workWithGenericsAsProperty() throws JsonGenerationException, JsonMappingException, IOException {
	List<Book> bookList = new ArrayList<>();
	bookList.add(new Book(1, "Book1"));
	bookList.add(new Book(2, "Book2"));
	
	Library library = new Library("library", bookList);
	objectMapper.writeValue(new File("libraryDB.json"), library);
	
	Library readLibrary = objectMapper.readValue(new File("libraryDB.json"), Library.class);
	System.out.println(readLibrary);
	System.out.println(readLibrary.getBookList().getClass());
	System.out.println(readLibrary.getBookList().get(0).getClass());
}

خروجی در کنسول به شکل زیر قابل مشاهده است:

Library [name=library, bookList=[Book [ISBN=1, title=Book1], Book [ISBN=2, title=Book2]]]
class java.util.ArrayList
class com.rhotiz.jacksonGenerics.Book

که نشان می‌دهد بدون این که کاری برای ذخیره و بازیابی Type مروبوط به bookList انجام شود، نوع المان‌های آن به درستی تشخیص داده شده است.

در مقاله‌ی بعد به چند امکان دیگر که می توان در کنار Data Binding از آن‌ها استفاده کرد، اشاره خواهیم‌کرد.

بخش چهارم-ب

.

.

.

با ما همراه باشید

آدرس کانال تلگرام: JavaCupIR@

آدرس اکانت توییتر: JavaCupIR@

آدرس صفحه اینستاگرام: javacup.ir

آدرس گروه لینکدین: Iranian Java Developers

نوشته های مشابه

یک دیدگاه

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *

دکمه بازگشت به بالا