Java Developer | digipay
Java Object Mapper
توی این آموزش میخوایم انواع روش هایی که میشه دوتا Object رو به هم Map کنیم رو توضیح بدیم .
چنتا Dependency معرفی میکنیم. و از یکیش که بهتر و خفن تره استفاده میکنیم . (هدف این آموزش اینه که از Dependency استفاده کنیم.)
چند مدل کد بدون استفاده از Dependency مینویسیم . (روش های خیلی ساده و پیچیده)

چنتا روش برای Map کردن کلاس های مختلف داریم
1- استفاده از dependency :
1- MapStruct
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>1.5.5.Final</version>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>1.5.5.Final</version>
<scope>provided</scope>
</dependency>
2-ModelMapper
<dependency>
<groupId>org.modelmapper</groupId>
<artifactId>modelmapper</artifactId>
<version>3.1.0</version>
</dependency>
2- استفاده از روش های دستی و طاقت فرسا :
توی این حالت میتونید از reflection استفاده کنید
میتونید برید دستی setter , getter کنید
میتونید براش builder بنویسید
میتونید constructor بنویسید.
میتونید از objectMepper های جاوا استفاده کنید یا از Gson استفاده کنید البته باید فیلد ها هم نام باشند .
بیاید توی هر کلاس mapper ایجاد کنید .
هر کدوم از این روش ها یه سری مشکلات به همراه داره ، نگهداری کد رو دچار مشکل میکنه مثلا اگر constructor بنویسید به ازای هر فیلد جدید باز باید تغییر بدید و یه ور کد میترکه و تست دوچار مشکل میشه و ... که خودتون بهتر از میدونید .
خوب حالا مزیت هر روش چیه :
1- اگر بیایم از mapStruct استفاده کنیم
سرعت : بسیار بالا
خوانایی کد : عالی یعنی خیلی خیلی عالی
زمان بندی اجرا : compile-time
انعطاف پذیریش هم بالاست
2- اگر بیایم از ModelMapper استفاده کنیم
سرعت : متوسط
خوانایی کد : خوب
زمان بندی اجرا : Runtime
انعطاف پذیریش بسیار بسیار بالاست
3- روش های دستی هم :
سرعت : بالا بستگی به کدی که میزنی داره
خوانایی کد : بستگی به کد داره
زمان بندی اجرا : دستی (custom)
انعطاف پذیریش بالا - بستگی به کدت داره
حالا چرا همش گفتیم بستگی به کدت داره یکی اینکه از چه روشی استفاده کنی و اینکه چقدر کثیف کد بزنی میتونه تاثیر گذار باشه .
برای اینکه بهتر درک کنید که چرا ما میایم از dependency استفاده میکنیم ، بیایم اول یه چنتا کد دستی بزنیم برای map کردن . (ممکنه کدی که اینجا میزنم خطا داشته باشه چون وقت کافی برای تست نداشتم اخر شب داشتم مینوشتم اینو ، خودتون یه دستی به سروگوشش بکشید. )
خوب بیام دوتا کلاس داشته باشیم که میخوایم اینارو به هم map کنیم و بعد بیایم از reflection برای map کردن این دوتا کلاس استفاده کنیم .
class Employee {
private String fullName;
private int age;
public String getFullName() {
return fullName;
}
public void setFullName(String fullName) {
this.fullName = fullName;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
class Person {
private String name;
private int yearsOld;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getYearsOld() {
return yearsOld;
}
public void setYearsOld(int yearsOld) {
this.yearsOld = yearsOld;
}
}
حالا شاید بگید خوب اینجا دوتا فیلد هست میایم اینارو با constructor یا با همون setter , getter یا اصلا یه builder مینویسیم و میریزیم رو هم ، ولی هر کدوم از این روش ها ایراداتی داره که خودتون بهتر از من میدونید.
حالا این کدی که داریم میزنیم هم خیلی خفن نیستا در نهایت میخوایم به dependency برسیم ولی گفتیم از لطف خارج نیست که چنتا کد هم بزنیم .
public class CustomObjectMapper {
public static <S, T> T map(S source, Class<T> targetClass) {
if (source == null || targetClass == null) {
throw new IllegalArgumentException("Source and target class must not be null");
}
try {
// ایجاد یک نمونه از کلاس هدف
T target = targetClass.getDeclaredConstructor().newInstance();
// دریافت فیلدهای کلاس مبدا
Field[] sourceFields = source.getClass().getDeclaredFields();
for (Field sourceField : sourceFields) {
sourceField.setAccessible(true); // دسترسی به فیلد خصوصی
// مقدار فیلد را بخوانید
Object value = sourceField.get(source);
// فیلد همنام را در کلاس هدف پیدا کنید
Field targetField;
try {
targetField = targetClass.getDeclaredField(sourceField.getName());
} catch (NoSuchFieldException e) {
continue; // اگر فیلدی وجود ندارد، به فیلد بعدی بروید
}
targetField.setAccessible(true);
// بررسی کنید آیا نوع دادهها یکسان است
if (targetField.getType().isAssignableFrom(sourceField.getType())) {
targetField.set(target, value); // مقداردهی به فیلد
}
}
return target;
} catch (Exception e) {
throw new RuntimeException("Error while mapping objects", e);
}
}
}
public class Main {
public static void main(String[] args) {
Employee employee = new Employee();
employee.setFullName("John Doe");
employee.setAge(30);
// Map کردن
Person person = CustomObjectMapper.map(employee, Person.class);
// مقداردهی را بررسی کنید
// خروجی: null (چون نامها متفاوتاند)
System.out.println(
"Name: " + person.getName()
);
// خروجی: 0 (چون فیلدها همنام نیستند)
System.out.println(
"Years Old: " + person.getYearsOld()
);
}
}
کدی که زدیم شبیه همون ObjectMapper خود جاواست سعی کردم کامنت فارسی بزارم براتون که راحت درکش کنید . اگر به این خط دقت کنید میبینید که برای مقدار دهی دنبال فیلد های هم نام میگرده و توی مثال ما چون فیلد ها هم نام نیست خطا میخوره و مقادیر خالیه .
targetField = targetClass.getDeclaredField(sourceField.getName());
توی این خط هم بررسی میکنه حتما نوع داده فیلد ها هم یکی باشه .
if (targetField.getType().isAssignableFrom(sourceField.getType())) {
targetField.set(target, value); // مقداردهی به فیلد
}
پس اگر به این دوتا نکته دقت کنید متوجه میشید که نام فیلد ها و نوع فیلد ها مهمه .
البته میشد توی همین reflection بالا هم بیایم یه mapping table ایجاد کنیم و فیلد های غیر هم نام رو هم پیدا کنیم . ولی حی نیاز به کد بیشتر هست.
حالا به dependency میرسیم و از این مشکلات خلاص میشید. :)
خوب حالا بریم ببینیم MapStruct چیکار میکنه و چی داره برامون :
اول از همه که برای استفاده بیاید dependency که بالا گفتیم رو اضافه کنید.
من همیشه عادت دارم بیام annotation ها و متدها رو بگم بعد مثال بزنم و نمیدونم این کار درسته یا نه شاید شما ترجیح بدید که بیام توی یه مثال راجب annotation صحبت کنیم . خلاصه اینکه نظرتون رو بگید.
ولی اینجا میام ترکیبی میرم هم توضیح میدم و همونجا مثالش رو میزنم .
من چیزایی که باهاش کار کردم رو میگم اگر چیز بیشتری بود بگید اضافه کنم .
1- @Mapper
- این انوتیشن یه کلاس یا درواقع یه interface را به عنوان یک Mapper معرفی میکند.
- باید آن را روی Interface قرار دهیم.
- چنتا attribute مهم داره که همینجا میگم بهتون :
componentModel
برای مدیریت bean ها استفادش میکنیم مثلا اگر از spring استفاده مکنید و مقدارش رو برابر
componentModel = "spring"
قرار بدید مشخص میکنه که مدیریت Bean توسط Spring کنترل بشه .
چند مقدار دیگه هم میگیره اینجا مینویسم خودتون راجبش بخونید :
"default" "spring" "cdi" "jsr330"
unmappedTargetPolicy
این مقدار تعیین میکند که اگر فیلدی در DTO یا Entity وجود داشت که MapStruct نتوانست مقداردهی کند، چه اتفاقی بیفتد.
مثلا من میام میگم فیلدی موجود نبود کامپایلر error بده :
unmappedTargetPolicy = ReportingPolicy.ERROR
اگر مقدار رو برابر WARN بزارید فقط یه هشدار نمایش میده اگر هم برابر IGNORE باشه هیچ خطا یا هشداری نمیده .
imports
این ویژگی برای وارد کردن کلاسهای خارجی به Mapper استفاده میشود.
اگر میخواهی در متدهای Custom Mapping از کلاسهایی استفاده کنی که خارج از این کلاس Mapper هستند، باید آنها را اینجا import کنی.
مثالا فرض کنید من میخوام از یه Utils خاصی توی کارم استفاده کنم .
@Mapper(componentModel = "spring", imports = { CustomUtils.class })
public interface PaymentMapper {
@Mapping(
target = "formattedAmount", expression =
"java(CustomUtils.valueOf(payment.getAmount()).setScale(100))"
)
PaymentDto toDto(Payment payment);
}
ناراحت نباشید جولو تر مثال میزنیم یاد میگیرید.
2-@Mapping
یکی از مهمترین Annotationها در MapStruct است که برای تبدیل فیلدهای یک کلاس به کلاس دیگر استفاده میشود.
مهم ترین انوتیشن همینه ، خوب بریم attribute هاشو بگیم :
Source & Target
دوتا attribute داریم که مکمل همن که معمولا باهم میان target , source :
به خط کد زیر توجه کنید :
@Mapping(target = "name", source = "fullName")
Person toPerson(Employee empl);
همون طور که متوجه شدید target به فیلد مقصد اشاره دارد و source به فیلد مبدا یا همون فیلد ورودی.
ignore:
نادیده گرفتن یک فیلد خاص ، اگر بخواهیم یک فیلد از کلاس مقصد مقداردهی نشود، مقدار ignore = true را تنظیم میکنیم .
@Mapper(componentModel = "spring")
public interface PersonMapper {
@Mapping(target = "yearsOld", ignore = true)
Person toPerson(Employee empl);
}
expression:
بعضی جاها ممکنه لازم داشته باشید یه فیلد target رو با دستورات جاوا مقدار دهی کنید ، چه میدونم مثلا شاید لازم داشته باشید یه UUID به فیلد userId بدید یا اینکه یه Date یا Systerm.currentTime بدید یه فیلد updated_at و یا مثلا بخواید یه فیلد ورودی رو یه تغییری در لحظه بدید و بعد ذخیره کنید و حالا هرچی ...
اینجاست که این attribute به کمکتون میاد.
بیاید اول کلاس Person رو یکم ارتقا بدیم :
@Setter @Getter
@AllArgsConstructor
@NoArgsConstructor
class Person {
private long id;
private String uuid;
private String name;
private int yearsOld;
private Boolean enabled;
private Date date;
}
حالا بریم پرش کنیم:
@Mapper(componentModel = "spring")
public interface PersonMapper {
@Mapping(target = "yearsOld", source="empl.age")
@Mapping(target = "id", ignore= true)
@Mapping(target = "uuid", expression="java(UUID.randomUUID()")
@Mapping(target = "date", expression="java(new Date())")
@Mapping(target = "name", source="empl.fullName")
@Mapping(target = "enabled",source="enabled")
Person toPerson(Employee empl, boolean enabled);
}
اگر دقت کنید میبینید که مقادیر uuid , date رو با استفاده از متدهای جاوا پر کردیم .
و همچنین اینکه الان به دلیل اینکه در ورودی متد toPerson دوتا پارامتر داریم بنابر این باید در source مشخص کنیم از کدام پارامتر میخوایم استفاده کنیم .
source="empl.fullName"
source="enabled"
میتونید در java() بیاید یه validate برای فیلد های خاص بنویسید و خیلی کارهای باحال دیگه و ازین جانگولر بازیا.
3- @Named
برای نام گذاری متد ها استفاده میشه ، نه متد های معمولی متدهایی برای تبدیل ها و بررسی های اختصاصی.
خوب حالا یعنی چی :
فرض کنید میخواید بعد از اینکه یه source به target تبدیل شد بیایم یه متد سوم رو کال کنیم که بیاد روی نتیجه نهایی یعنی target یه تغییری یا یه بررسی انجام بده .
بریم یه مثال ببینیم :
قبلش باید یچی دیگه هم توضیح بدم که قاطی نکنید .
qualifiedByName
به ما اجازه میدهد تا یک متد خاص را در @Mapping
با qualifiedByName
مشخص کنیم و از آن در تبدیل فیلدها استفاده کنیم.
دوتا کلاس جدید نیاز داریم :
public class User {
private String name;
private String email;
private String categoryType; // مقدار اصلی که باید تبدیل شود
}
public class UserDto {
private String name;
private String email;
private String subCategoryType; // مقدار نهایی بعد از تبدیل
}
حالا میخوام مثل قبل یه mapping داشته باشیم و با یه تفاوت که بعد از map شدن بیاد روی category یه کاری انجام بده .
@Mapper(componentModel = "spring")
public interface UserMapper {
@Mapping(
source = "categoryType",
target = "subCategoryType", qualifiedByName = "toSubCategoryType"
)
UserDto toDto(User user);
@Named("toSubCategoryType")
default String toSubCategoryType(String type) {
if (type == null) return "UNKNOWN"
switch (type) {
case "HOUSE": return "Real Estate"
case "CAR": return "Vehicle"
default: return "Other"
}
}
}
خوب چه اتفاقی افتاد :
وقتی مقدار categoryType به subCategoryType تبدیل و Map میشود. mapStruct میاد متد toSubCategoryType رو صدا میزنه ، این کار باعث میشه پیچیدگی های اضافه کاهش پیدا کنه و کد تمیز تر بشه و یه سری validate شدن هارو میتونیم در متدهای مختلف پیاده سازی کنیم .
خوب حالا که این dependecy رو یادگرفتیم ، میشه گفت مزیت هاش اینه که هر فیلدی با هر نامی بود رو میتونید به هم map کنید ، میتونید ignore کنید ، میتونید validator بنویسید ، میتونید چندین متد mapper بنویسید بین دوتا کلاس ، میتونید متدهای مجزا بنویسید و استفاده کنید و میتونید از کدها و متدهای جاوا برای مقدار دهی استفاده کنید و ....
تا درودی دیگر بدرود.
امیدوار لذت برده باشید و براتون مفید بوده باشه .
منتظر نگاه های قشنگتون هستم .
مراقب خودتون باشید . 🌹🌹
مطلبی دیگر از این انتشارات
Mutable VS Immutable in java
مطلبی دیگر از این انتشارات
StringBuilder vs StringBuffer vs String in java
مطلبی دیگر از این انتشارات
Integer vs int in java