มาใช้ MapStruct ใน Java กันเถอะ

Phayao Boonon
6 min readJun 16, 2019

--

Photo by Andrew Stutesman on Unsplash

MapStruct เป็นเครื่องมือ code generator ที่จะสร้าง implemenation code สำหรับ mapping ระหว่าง Java Type โดยใช้ convention ด้วยวิธี configuration ระหว่าง field ของ Type (Class) ซึ่ง mapping code ที่ได้สามารถเรียกใช้ method ได้เลย และมีความเร็ว, type-safe และ ง่ายต่อการทำความเข้าใจ

Multi-layer application บ่อยครั้งจะต้องมีการ map ระหว่าง Object model (เช่น entity กับ DTO) การเขียน method แบบนี้เป็นงานที่ น่าเบื่อ และ มีโอกาสเกิด error ได้ ซึ่ง MapStruct จะมาช่วยให้งานนี้ง่ายขึ้น โดยการสร้างโค้ดให้อัตโนมัติ

MapStruct แตกต่างจาก mapping framework อื่น ด้วยการ generate code ในตอน compile-time ซึ่งแน่ใจว่าได้ code ที่มีประสิทธิภาพสูง โดยที่ feedback developer ได้เร็ว และ error checking

MapStruct เป็น Annotation processor ซึ่งใช้ร่วมกับ Java compiler และสามารถใช้กับ command-line build อย่าง Maven หรือ Gradle ได้ด้วย จาก IDE ที่ใช้ และสามารถเพิ่ม implementation ที่ต้องการได้อีกด้วย

Install

MapStruct สามารถใช้ dependency management ทั้ง Maven และ Gradle

Maven

<properties>
<java.version>1.8</java.version>
<maven.compiler.plugin>3.8.1</maven.compiler.plugin>
<org.mapstruct.version>1.3.0.Final</org.mapstruct.version>
</properties>
<dependencies>
.....
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>${org.mapstruct.version}</version>
</dependency>
.....
</dependencies>

<build>
<plugins>
....
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>${maven.compiler.plugin}</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
<annotationProcessorPaths>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${org.mapstruct.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>

Gradle

plugins {
...
id 'net.ltgt.apt' version "0.8"
}

....

dependencies {
...
compile 'org.mapstruct:mapstruct:1.3.0.Final'
apt 'org.mapstruct:mapstruct-processor:1.3.0.Final'
}

Mapper

หลังจากที่เพิ่ม MapStruct dependency เข้าไปใน project เรียบร้อยแล้วก็ลองสร้าง Mapper เพื่อ map ข้อมูลจาก DTO ไปเป็น Entity ซึ่งในบทความนี้จะสร้างตัวอย่างง่ายๆ ด้วย class FooDto (DTO type) และ class Foo (Entity) โดยจะ mapp ไปกลับระหว่าง 2 class นี้ (จะซ่อน Constructor/Getter/Setter ของ class ไว้)

public class FooDto {
private String br;
private int bz;

// Constructor/Getter/Setter
}
public class Foo {
private String bar;
private String baz;

// Constructor/Getter/Setter
}

สร้าง Mapper ด้วยการสร้าง interface FooMapper โดยใช้ @Mapper annotation ซึ่งเป็น interface ที่กำหนดให้ MapStruct สร้าง generated code โดยกำหนดว่าจะ map field ไหนของ Source type กับ Target type บ้าง

@Mapper
public interface
FooMapper {

FooMapper INSTANCE = Mappers.getMapper(FooMapper.class);

@Mapping(source = "br", target = "bar")
@Mapping(source = "bz", target = "baz")
Foo fooDtoToFoo(FooDto dto);

@Mapping(source = "bar", target = "br")
@Mapping(source = "baz", target = "bz")
FooDto fooToFooDto(Foo foo);
}

ในตัวอย่างนี้จะ map field “br” และ “bz” ของ class FooDto กับ field “bar” และ “baz” ของ class Foo ตามลำดับ ด้วย @Mapping annotation ซึ่ง field “bz” ของ class FooDto เป็นชนิด Integer แต่ “baz” ของ class Foo เป็นชนิด String เพื่อแสดงให้เห็นว่า MapStruct สามารถสร้าง mapper สำหรับชนิดของข้อมูลที่แตกต่างกันได้

เมื่อ compile project ซึ่งในบทความนี้ใช้ maven จึงใช้คำสั่ง mvn clean compile maven compiler จะเรียกใช้ annotation processor ของ MapStruct เพื่อสร้าง generated code เมื่อ compile สำเร็จจะได้ code ใน

/target/generated-sources/annotations/{package-name}/

ดังนี้

@Generated(
value = "org.mapstruct.ap.MappingProcessor",
date = "2019-06-16T13:46:04+0700",
comments = "version: 1.3.0.Final, compiler: javac, environment: Java 11.0.2 (Oracle Corporation)"
)
public class FooMapperImpl implements FooMapper {

@Override
public
Foo fooDtoToFoo(FooDto dto) {
if ( dto == null ) {
return null;
}

Foo foo = new Foo();

foo.setBar( dto.getBr() );
foo.setBaz( String.valueOf( dto.getBz() ) );

return foo;
}

@Override
public
FooDto fooToFooDto(Foo foo) {
if ( foo == null ) {
return null;
}

FooDto fooDto = new FooDto();

fooDto.setBr( foo.getBar() );
if ( foo.getBaz() != null ) {
fooDto.setBz( Integer.parseInt( foo.getBaz() ) );
}

return fooDto;
}
}

จะเห็นได้ว่า MapStruct จะ convert type ให้ด้วย ลองทดสอบ Mapper ด้วย JUnit

public class FooMapperTest {

@Test
public void
shouldMapFooDtoToFoo() {
// given
FooDto fooDto = new FooDto("BAR", 8888);

// when
Foo foo = FooMapper.INSTANCE.fooDtoToFoo(fooDto);

// then
assertEquals
("BAR", foo.getBar());
assertEquals("8888", foo.getBaz());
}

@Test
public void
shouldMapFooToFooDto() {
// given
Foo foo = new Foo("BAR", "8888");

// when
FooDto fooDto = FooMapper.INSTANCE.fooToFooDto(foo);

// then
assertEquals
("BAR", fooDto.getBr());
assertEquals(8888, fooDto.getBz());
}
}

แต่ถ้า 2 type มีชื่อของ field เหมือนกัน ก็ไม่จำเป็นต้องใช้ @Mapping annotation โดยที่ MapStruct จะทำการ map ชื่อ field ให้เอง

เปลี่ยนชื่อ field ของ class FooDto ให้เหมือนกับ class Foo

public class FooDto {
private String bar;
private String baz;

// Constructor/Getter/Setter
}

แล้วลบ @Mapping annotation ทิ้ง

@Mapper
public interface
FooMapper {
FooMapper INSTANCE = Mappers.getMapper(FooMapper.class);

Foo fooDtoToFoo(FooDto dto);

}

จะได้ generated code ดังนี้ จะเห็นได้ว่า MapStruct map field ที่มีชื่อเดียวกันให้เองอัตโนมัติ

public class FooMapperImpl implements FooMapper {

@Override
public
Foo fooDtoToFoo(FooDto dto) {
if ( dto == null ) {
return null;
}

Foo foo = new Foo();

foo.setBar( dto.getBar() );
foo.setBaz( dto.getBaz() );

return foo;
}

}

แต่ถ้าไม่ต้อง map บาง field แต่ field ที่เหลือ map ทั้งหมด สามารถกำหนด ignore ของ target field ให้เป็น true

@Mapper
public interface
FooMapper {
FooMapper INSTANCE = Mappers.getMapper(FooMapper.class);

@Mapping(target = "baz", ignore = true)
Foo fooDtoToFoo(FooDto dto);

}

จะเห็นได้ว่าใน generated code จะละเว้น filed ที่กำหนด ignore และเหลือแต่ field ที่เหลือที่จะทำการ map โดย MapStruct

public class FooMapperImpl implements FooMapper {

@Override
public
Foo fooDtoToFoo(FooDto dto) {
if ( dto == null ) {
return null;
}

Foo foo = new Foo();

foo.setBar( dto.getBar() );

return foo;
}
}

แต่ถ้าต้องการ map ระหว่าง sub-field ของ source กับ field ของ target ก็สามารถทำได้โดยกำหนด reference อ้างอิงไปถึง sub-field นั้น

@Mapper
public interface
FooMapper {
FooMapper INSTANCE = Mappers.getMapper(FooMapper.class);

@Mapping(source = "dto.bar.etc", target = "bar")
Foo fooDtoToFoo(FooDto dto);

}

จากตัวอย่างเป็นกรณีที่ field bar ของ FooDto เปลี่ยนเป็น type Bar ซึ่งมี field etc เป็น sub-field

public class FooDto {
private Bar bar;
private String baz;

// Constructor/Getter/Setter
}
public class Bar {
private String etc;

// Constructor/Getter/Setter
}

เมื่อ compile แล้วจะได้ generated code ที่ map field bar ของ FooDto

public class FooMapperImpl implements FooMapper {

@Override
public
Foo fooDtoToFoo(FooDto dto) {
if ( dto == null ) {
return null;
}

Foo foo = new Foo();

foo.setBar( dtoBarEtc( dto ) );
foo.setBaz( dto.getBaz() );

return foo;
}

private String dtoBarEtc(FooDto fooDto) {
if ( fooDto == null ) {
return null;
}
Bar bar = fooDto.getBar();
if ( bar == null ) {
return null;
}
String etc = bar.getEtc();
if ( etc == null ) {
return null;
}
return etc;
}
}

Mapping Collection

นอกจาก MapStruct จะใช้สำหรับ map type เดี่ยวๆ ได้แล้ว ก็ยังสามารถ map field ที่เป็น collection ได้อีกด้วย โดยกำหนดให้ parameter ของ mapping method เป็น collection ซึ่งในตัวอย่างคือ List และ method return เป็น collection ด้วย

@Mapper
public interface
FooMapper {
FooMapper INSTANCE = Mappers.getMapper(FooMapper.class);

List<Foo> fooDtosToFoos(List<FooDto> dtos);

}

จะได้ generated code ซึ่งจะเห็นได้ว่า MapStruct สร้าง protected method เพื่อ map field ของ 2 type และ วนลูป collection เพื่อ map ทีละ element ของ collection

public class FooMapperImpl implements FooMapper {

@Override
public
List<Foo> fooDtosToFoos(List<FooDto> dtos) {
if ( dtos == null ) {
return null;
}

List<Foo> list = new ArrayList<Foo>( dtos.size() );
for ( FooDto fooDto : dtos ) {
list.add( fooDtoToFoo( fooDto ) );
}

return list;
}

protected Foo fooDtoToFoo(FooDto fooDto) {
if ( fooDto == null ) {
return null;
}

Foo foo = new Foo();

foo.setBar( fooDto.getBar() );
foo.setBaz( fooDto.getBaz() );

return foo;
}
}

นอกจาก List แล้ว MapStruct ก็สามารถ map Map ได้ด้วย

@Mapper
public interface
FooMapper {
FooMapper INSTANCE = Mappers.getMapper(FooMapper.class);

Map<Integer, Foo> fooDtoMapToFooMap(Map<String, FooDto> dtoMap);
}

จะได้ generated code ที่วนลูป mapping แต่ละ element ของ Map และแปลง type ของ Key ให้ด้วย

public class FooMapperImpl implements FooMapper {

@Override
public
Map<Integer, Foo> fooDtoMapToFooMap(Map<String, FooDto> dtoMap) {
if ( dtoMap == null ) {
return null;
}

Map<Integer, Foo> map = new HashMap<Integer, Foo>( Math.max( (int) ( dtoMap.size() / .75f ) + 1, 16 ) );

for ( java.util.Map.Entry<String, FooDto> entry : dtoMap.entrySet() ) {
Integer key = Integer.parseInt( entry.getKey() );
Foo value = fooDtoToFoo( entry.getValue() );
map.put( key, value );
}

return map;
}

protected Foo fooDtoToFoo(FooDto fooDto) {
if ( fooDto == null ) {
return null;
}

Foo foo = new Foo();

foo.setBar( fooDto.getBar() );
foo.setBaz( fooDto.getBaz() );

return foo;
}
}

Custom Mapping

บางครั้งอาจจะต้องการด้วย logic พิเศษแต่ถ้าใช้ MapStruct อาจจะไม่รองรับเงื่อนไขนั้น ก็สามารถทำได้โดย mapping customization ด้วย Decorator ซึ่งสร้าง abstract class decorator ที่ implement interface Mapper และ delegate mapper มาเพิ่มเติม logic ที่ต้องการ

public abstract class FooMapperDecorator implements FooMapper {

private final FooMapper delegate;

public FooMapperDecorator(FooMapper delegate) {
this.delegate = delegate;
}

@Override
public
Foo fooDtoToFoo(FooDto dto) {
Foo foo = delegate.fooDtoToFoo(dto);
foo.setBar(dto.getBar() + "/" + dto.getBar());
return foo;
}
}

และกำหนด decorator class ให้กับ interface Mapper

@Mapper
@DecoratedWith
(FooMapperDecorator.class)
public interface FooMapper {
FooMapper INSTANCE = Mappers.getMapper(FooMapper.class);

Foo fooDtoToFoo(FooDto dto);

}

เมื่อ compile แล้วจะได้ generated code เพิ่มมา 1 class คือ FooMapperImpl_ ที่เป็น class ที่ใช้ในการ map ในขั้นแรก

public class FooMapperImpl_ implements FooMapper {

@Override
public
Foo fooDtoToFoo(FooDto dto) {
if ( dto == null ) {
return null;
}

Foo foo = new Foo();

foo.setBar( dto.getBar() );
foo.setBaz( dto.getBaz() );

return foo;
}
}

และส่วน FooMapperImpl นั้นจะ extents จาก abstract class Decorator และ implement interface Mapper

public class FooMapperImpl extends FooMapperDecorator
implements FooMapper {

private final FooMapper delegate;

public FooMapperImpl() {
this( new FooMapperImpl_() );
}

private FooMapperImpl(FooMapperImpl_ delegate) {
super( delegate );
this.delegate = delegate;
}
}

Use with Lombok

MapStruct จะมีปัญหาถ้าใช้ร่วมกับ Lombok ดังนั้นใน StackOverflow แนะวิธีแก้ปัญหาไว้ใน maven 2 วิธีคือ

  1. เพิ่ม Lombok dependency ใน annotationProcessorPaths ของ maven compiler (เลือกใช้วิธีนี้)
  2. เพิ่ม mapstruct-processor dependency ใน dependencies tag และลบ annotationProcessorPaths ซึ่งวิธีนี้ maven compiler จะใช้ annotation processor ทั้งหมดที่อยู่ใน dependencies
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>${maven.compiler.plugin}</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
<annotationProcessorPaths>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${org.mapstruct.version}</version>
</path>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${org.lombok.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>

หลังจากเพิ่ม Lombok ใน annotationProcessorPaths ก็จะทำให้ใช้ MapStruct ร่วมกับ Lombok ได้แล้ว

สรุป

MapStruct เป็นเครื่องมือที่ช่วยให้ Java Developer ได้สร้าง mapping class ระหว่าง Type ของ Java ได้อย่างง่าย และมีความสามารถที่หลากหลาย และยังมี feature อื่นอีกของ MapStruct ที่บทความนี้ไม่ครอบคลุม ซึ่งสามารถอ่านได้ที่ reference guide

--

--

Phayao Boonon
Phayao Boonon

Written by Phayao Boonon

Software Engineer 👨🏻‍💻 Stay Hungry Stay Foolish

No responses yet