استفاده از mapstruct برای تبدیل object ها به یکدیگر

گاهی در پروژه ها لازم است در لایه های مختلف یک object به object دیگری تبدیل شود، نوشتن کد برای تبدیل شی های مختلف به هم کار سخت و خسته کننده است و احتمال خطا را نیز زیاد می کند، یکی از روش ها استفاده از MapStruct است که با استفاده از annotaion های موجود، تبدیل انواع objectها را با هم انجام می دهد. از متد های setter/getter که در شی های sourceو target قرار دارد کمک می گیرد. کد ایجاد شده خوانا نیز می باشد و یک جا این تبدیلات توسط MapStruct انجام می شود و ما در تمام پروژه به راحتی از آن استفاده می کنیم.کلاس های تبدیل map در زمان کامپایل اجرا می شوند و در زمان runtime هیچ کاری را انجام نمی دهند بنابراین سرعت بالایی داردبرای تبدیل شی ها در یک پروژه ی spring boot تنها کافی ایست مقادیر زیر را به pom فایل خودتون اضافه کنید:<dependencies> <dependency> <groupId>org.mapstruct</groupId> <artifactId>mapstruct</artifactId> <version>1.3.1.Final</version> </dependency> </dependencies> ... <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.5.1</version> <configuration> <source>1.8</source> <target>1.8</target> <annotationProcessorPaths> <path> <groupId>org.mapstruct</groupId> <artifactId>mapstruct-processor</artifactId> <version>1.3.1.Final</version> </path> </annotationProcessorPaths> </configuration> </plugin> </plugins> </build>در ابتدا برای تبدیلات به یک اینترفیس نیاز داریم(متد های موجود در این اینترفیس شی های مختلف را به هم تبدیل می کند) @Mapper را بالا سر آن قرار می دهیم. در زمان کامپایل خودش پیاده سازی مربوطه را بر اساس متد های نوشته شده ایجاد می کند.@Mapper public interface CustomerDtoMapper {}در صورتی که بخواهید مقدار null بودن را نیز چک کند تا اگر فیلدی مقدارش Null است را در object نهایی set نکند کافی ایست nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS را در بالا سر اینترفیس که جزی از @Mapper می باشد قرار دهید.نکته:در ورژن های قدیمی که در داخل یک شی متغییر لیستی وجود داشت در زمان تبدیل لیست به هم ، دچار خطای NullpointerException می شد چون کدی که ایجاد می کرد شبیه کد زیر بود(فرض کنید در شی countryOutput متغییری با نام OtherLanguagesList با دیتاتایپ لیستی داریم و مقدار getOtherLanguagesList به دلیل null بودن دچار خطا می گردید)countryOutput.getOtherLanguagesList().addAll( list )بنابراین برای برطرف نمودن این خطا کافی بود در بخش Mapper مقدار زیر را وراد نمایید. کد پیاده سازی شده تغییر می کند .collectionMappingStrategy = CollectionMappingStrategy.ADDER_PREFERREDدر صورتی که نام متغییر ها در هر دو طرف یکسان باشد دیگر در اینترفیس ایجاد شده نیازی نیست نظیر به نظیر اعلام کنیم کدام فیلد با کدام فیلد باید مقدار دهی شود و خودش این تبدیل را به صورت پیش فرض انجام می دهد. اما اگر نام ها متفاوت باشد باید با @Mapping در بالا سر متد مربوطه نظیر به نظیر اعلام کنیم کدام فیلد را با کدام فیلد map کند .حتی می توانید چند object متفاوت را به یک object تبدیل کنید.@Mappings({ @Mapping( target = &quotforename&quot,source = &quotcustomer.firstName&quot), @Mapping(target = &quotsurname&quot,source = &quotcustomer.lastName&quot), @Mapping( target = &quotstreet&quot,source = &quotaddress.street&quot), @Mapping(target = &quotpostalCode&quot,source = &quotaddress.postalCode&quot), @Mapping(target = &quotcounty&quot,source = &quotaddress.county&quot) }) DeliveryAddress from(Customer customer, Address address);در صورتی که بخواهید در زمان map کردن عملیات خاصی را انجام دهید می توانید با تعریف یک متد و پیاده سازی آن در اینترفیس و معرفی آن با qualifiedByName استفاده کنید. مثلا در اینجا برای تبدیل تاریخ رشته با جداکننده(1399/01/20) به عددی(13990120)@Mappings({ @Mapping(source = &quotbirthDate&quot, target = &quotbirthDate&quot, qualifiedByName = &quotconvertToNumberDate&quot), @Mapping(target = &quotstartDt&quot, source = &quotcustomerDto.customerStartDt&quot, dateFormat = &quotdd-MM-yyyy HH:mm:ss&quot) }) Customer fromCustomerDoToCustomer(CustomerDto customerDto);@Named(&quotconvertToNumberDate&quot) default Integer removeSlashFromDate(String dateStr) { return Integer.parseInt(dateStr.replaceAll(&quot/&quot, &quot&quot)); }حتی از dateFormat و numberFormat نیز می توانید در زمان تبدیلات استفاده کنید. در مثال بالا برای تاریخ نیز از dateFormat استفاده کردیم.برای تبدیل enum به String و بالعکس خودش مقدارها را تبدیل می کند : 1. if ( customer.getCustomerType() != null ) { customerDto.customerType( customer.getCustomerType().name() ); } 2. if ( customerDto.getCustomerType() != null ) { customer.customerType( Enum.valueOf( CustomerType.class, customerDto.getCustomerType() ) ); }ویژگی ignore :اگر در برابر پارامتری قرار دهیم یعنی در زمان تبدیل این متغییر را در نظر نگیرد.همچنین می خواهیم در زمان تبدیل مقدار شناسه با کمک کلاس UUID برای ما ایجاد کند ، تنها کافی است برای شناسایی کلاس در زمان پیاده سازی متدها نام کلاس را در بخش Mapper با import وارد نمایید. و بخش کد مورد نظر را نیز در بخش Mapping متغییر مورد نظر با expression بیان کنیم.@Mapper(imports = UUID.class) public interface CustomerDtoMapper{ @Mapping(target=&quotcustomerId&quot,expression=&quotjava( UUID.randomUUID().toString() )&quot) Customer fromCustomerDoToCustomer(CustomerDto customerDto) }برای این که این اینترفیس توسط spring ایجاد شود در داخل Mapper بالا سر اینترفیس می توانیم از مقدار “spring” در componentModel استفاده کنیم.@Mapper(componentModel = &quotspring&quot) public interface CustomerDtoMapper {} ............................................ @Autowired private CustomerDtoMapper mapper;راه حل دیگر برای دسترسی به متد ها :CustomerDtoMapper customerDtoMapper = Mappers.getMapper(CustomerDtoMapper.class);Customer customer =customerDtoMapper .fromCustomerDoToCustomer(dto);نمونه کد