[Java] Bean copy using MapStruct

6 minute read

1. Why MapStruct

There are several libraries that are commonly used when copying beans using Java.

  • org.apache.commons.beanutils.BeanUtils.copyProperties
  • org.apache.commons.beanutils.PropertyUtils.copyProperties
  • org.springframework.beans.BeanUtils.copyProperties
  • org.springframework.cglib.beans.BeanCopier.copy
  • org.mapstruct

Of these, the one I recommend most is mapstruct. The reason is that mapstruct is ** fastest **. In the following source, the time taken for each bean copy using the above five libraries is statistic.

@Slf4j
public class CopyDemoTest {

    public UserMainBO bo;

    public static int count = 1000000;

    @Before
    public void init(){
        bo = new UserMainBO();
        bo.setId(1L);
    }

    @Test
    public void mapstruct() {
        UserMainVOMapping INSTANCE = Mappers.getMapper( UserMainVOMapping.class );
        log.info("star------------");
        for (int i = 1; i <=count; i++) {
            UserMainVO vo = INSTANCE.toVO(bo);
        }
        log.info("end------------");
    }

    @Test
    public void beanCopier() {
        log.info("star------------");
        BeanCopier copier = BeanCopier.create(UserMainBO.class, UserMainVO.class, false);
        for (int i = 1; i <=count; i++) {
            UserMainVO vo = new UserMainVO();
            copier.copy(bo, vo, null);
        }
        log.info("end------------");
    }

    @Test
    public void springBeanUtils(){
        log.info("star------------");
        for (int i = 1; i <=count; i++) {
            UserMainVO vo = new UserMainVO();
            BeanUtils.copyProperties(bo, vo);
        }
        log.info("end------------");
    }

    @Test
    public void apacheBeanUtils() throws InvocationTargetException, IllegalAccessException {
        for (int i = 1; i <=count; i++) {
            UserMainVO vo = new UserMainVO();
            org.apache.commons.beanutils.BeanUtils.copyProperties(bo, vo);
        }
        log.info("end------------");
    }

    @Test
    public void apachePropertyUtils() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        log.info("star------------");
        for (int i = 1; i <=count; i++) {
            UserMainVO vo = new UserMainVO();
            PropertyUtils.copyProperties(bo, vo);
        }
        log.info("end------------");
    }

}

inspection result:

  1,000 times 10,000 times 100,000 times 1,000,000 times
apache.BeanUtils 550ms 1085ms 4287ms 32088ms
apache.PropertyUtils 232ms 330ms 2080ms 20681ms
cglib.BeanCopier 73ms 106ms 102ms 99ms
mapstruct 91ms 5ms 7ms 12ms
spring.BeanUtils 5ms 188ms 336ms 844ms

From the above verification results, it can be confirmed that mapstruct has far better performance than other libraries.

2. To use MapStruct

To use MapStruct, you need to import the MapStruct library into your project.
The following is an example when using maven.

pom.xml


<properties>
    <org.mapstruct.version>1.3.1.Final</org.mapstruct.version>
</properties>

<dependencies>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>${org.projectlombok.version}</version>
    </dependency>
</dependencies>

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>3.8.1</version>
            <configuration>
                <source>1.8</source> <!-- depending on your project -->
                <target>1.8</target> <!-- depending on your project -->
                <annotationProcessorPaths>
                    <path>
                        <groupId>org.mapstruct</groupId>
                        <artifactId>mapstruct-processor</artifactId>
                        <version>${org.mapstruct.version}</version>
                    </path>
                </annotationProcessorPaths>
            </configuration>
        </plugin>
    </plugins>
</build>

2.1 Combined use with lombok

When using MapStruct with 1.2.0.Final version or earlier and lombok together, the following settings are required:

pom.xml


<properties>
    <org.mapstruct.version>1.1.0.Final</org.mapstruct.version>
    <org.projectlombok.version>1.18.12</org.projectlombok.version>
</properties>

<dependencies>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>${org.projectlombok.version}</version>
    </dependency>
    <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>3.8.1</version>
            <configuration>
                <source>1.8</source> <!-- depending on your project -->
                <target>1.8</target> <!-- depending on your project -->
                <annotationProcessorPaths>
                    <path>
                        <groupId>org.projectlombok</groupId>
                        <artifactId>lombok</artifactId>
                        <version>${org.projectlombok.version}</version>
                    </path>
                    <path>
                        <groupId>org.mapstruct</groupId>
                        <artifactId>mapstruct-processor</artifactId>
                        <version>${org.mapstruct.version}</version>
                    </path>
                </annotationProcessorPaths>
            </configuration>
        </plugin>
    </plugins>
</build>

2.2 Combined use with swagger-ui

Since the inside of swagger-ui uses an old version of MapSruct, it is necessary to exclude the dependency on MapSruct from the swagger-ui library when using it together with swagger-ui.

pom.xml


<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger2</artifactId>
    <version>${springfox-swagger.version}</version>
    <exclusions>
        <exclusion>
            <groupId>org.mapstruct</groupId>
            <artifactId>mapstruct</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger-ui</artifactId>
    <version>${springfox-swagger.version}</version>
    <exclusions>
        <exclusion>
            <groupId>org.mapstruct</groupId>
            <artifactId>mapstruct</artifactId>
        </exclusion>
    </exclusions>
</dependency>

3. How to use MapStruct

3.1 Easy to use

There are two bean classes below.

Student.java


import lombok.Data;
import lombok.ToString;

@Data
@ToString
public class Student {
    private String name;
    private String address;
    private String phone;
}

Person.java


import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;

@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class Person {
    private String name;
    private String address;
    private String telephone;
}

From now on, bean copy from Person object to Student object. Before copying the bean, it is necessary to prepare the Mapper class in advance.

StudentMapper.java


import model.Person;
import model.Student;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;

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

    Student personToStudent(Person person); //Method name is arbitrary
}

Let’s copy the bean.

main_method


Person person = new Person("lisa", "Tokyo", "12345");
Student student = StudentMapper.INSTANCE.personToStudent(person);
System.out.println(student);

//output
// Student(name=Lisa, address=Tokyo, phone=null)

By default, any field with a name mismatch is automatically ** ignored ** and cannot be copied from telephone to phone.

3.2 Copy of name mismatch field

If the source and target field names do not match, you need to specify @Mapping when trying to copy.

StudentMapper.java


import model.Person;
import model.Student;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.factory.Mappers;

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

    @Mapping(source = "telephone", target = "phone")
    Student personToStudent(Person person);
}

Execution result:

Student(name=Lisa, address=Tokyo, phone=12345)

3.3 Exclude fields

If you don’t want to copy any fields, you can exclude them from copying through the @ Mapping ʻignore` attribute.

StudentMapper.java


import model.Person;
import model.Student;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.factory.Mappers;

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

    @Mapping(source = "telephone", target = "phone")
    @Mapping(target = "address", ignore = true)
    Student personToStudent(Person person);
}

Execution result:

Student(name=Lisa, address=null, phone=12345)

3.4 Copy from multiple beans to one bean

MapStruct can copy fields from multiple beans to one bean.

LoginInfo.java


import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class LoginInfo {
    private String id;
    private String password;
}

UserProfile.java


import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class UserProfile {
    private String email;
    private String address;
}

UserInfo.java


@ToString
@Data
public class UserInfo {
    private String id;
    private String password;
    private String email;
    private String address;
}

Prepare Mapper.

UserInfoMapper.java


import model.LoginInfo;
import model.UserInfo;
import model.UserProfile;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.factory.Mappers;

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

    @Mapping(target = "id", source = "loginInfo.id")
    @Mapping(target = "password", source = "loginInfo.password")
    @Mapping(target = "email", source = "userProfile.email")
    @Mapping(target = "address", source = "userProfile.address")
    UserInfo fromLoginInfoAndUserProfile(LoginInfo loginInfo, UserProfile userProfile);
}

make a copy.

main_method


LoginInfo loginInfo = new LoginInfo("12345", "54321");
UserProfile userProfile = new UserProfile("[email protected]", "Tokyo");

UserInfo userInfo = UserInfoMapper.INSTANCE.fromLoginInfoAndUserProfile(loginInfo, userProfile);
System.out.println(userInfo);

//output
// UserInfo(id=12345, password=54321, [email protected], address=Tokyo)

3.5 Update the created bean

MapStruct can not only copy and create beans, but also update created beans.

ʻAdd the following source to UserInfoMapper.java`:

UserInfoMapper.java


@Mapping(target = "email", source = "email")
@Mapping(target = "address", source = "address")
void updateUserProfile(UserProfile userProfile, @MappingTarget UserInfo userInfo);

Let’s update:

main_method


LoginInfo loginInfo = new LoginInfo("12345", "54321");
UserProfile userProfile = new UserProfile("[email protected]", "Tokyo");

UserInfo userInfo = UserInfoMapper.INSTANCE.fromLoginInfoAndUserProfile(loginInfo, userProfile);
System.out.println(userInfo);
//output
// UserInfo(id=12345, password=54321, [email protected], address=Tokyo)

userProfile = new UserProfile("[email protected]", "Fukuoka");
UserInfoMapper.INSTANCE.updateUserProfile(userProfile, userInfo);
System.out.println(userInfo);
//output
// UserInfo(id=12345, password=54321, [email protected], address=Fukuoka)

3.6 Format conversion

In MapStruct, when converting from Date / LocalDate to String, when converting from int to String, you can specify the format and convert.

Person.java


import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;
import java.time.LocalDate;

@Data
@ToString
@AllArgsConstructor
@NoArgsConstructor
public class Person {
    private String name;
    private LocalDate birthday;
    private int salary;
}

Employee.java


import lombok.Data;
import lombok.ToString;

@Data
@ToString
public class Employee {
    private String name;
    private String birthday;
    private String salary;
}

PersonMapper.java


import model.Employee;
import model.Person;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.factory.Mappers;

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

    @Mapping(source = "birthday", target = "birthday", dateFormat = "yyyy/MM/dd")
    @Mapping(source = "salary", target = "salary", numberFormat = "#,###")
    Employee personToEmployee(Person person);
}

main_method


Person person = new Person("lisa", LocalDate.of(1990, 1, 20), 1234567);
Employee employee = PersonMapper.INSTANCE.personToEmployee(person);
System.out.println(employee);
//output
// Employee(name=lisa, birthday=1990/01/20, salary=1,234,567)

3.7 expression

When copying a bean, if the complexity needs to be converted, it can be easily realized with ʻexpression`.

For example, capitalize the name field when copying from a Person object to an Employee object.

PersonMapper.java


import model.Employee;
import model.Person;
import org.apache.commons.lang3.StringUtils;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.factory.Mappers;

@Mapper(imports = {StringUtils.class}) //Import StringUtils
public interface PersonMapper {
    PersonMapper INSTANCE = Mappers.getMapper(PersonMapper.class);

    @Mapping(target = "name", expression = "java(StringUtils.upperCase(person.getName()))")
    Employee personToEmployee(Person person);
}

3.8 default method

Starting with Java8, interface can support the default method. MapStruct can also use the default method to create complex copy logic.

PersonMapper.java


import model.Employee;
import model.Person;
import org.apache.commons.lang3.StringUtils;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.factory.Mappers;

import java.time.format.DateTimeFormatter;

@Mapper(imports = {StringUtils.class})
public interface PersonMapper {
    PersonMapper INSTANCE = Mappers.getMapper(PersonMapper.class);

    default Employee personToEmployee(Person person, String dateFormat) {
        Employee employee = new Employee();
        //Uppercase name
        employee.setName(StringUtils.upperCase(person.getName()));
        //Convert birthday to specified format
        DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(dateFormat);
        employee.setBirthday(person.getBirthday().format(dateTimeFormatter));

        return employee;
    }
}

Execute the default method.

main_method


Person person = new Person("lisa", LocalDate.of(1990, 1, 20), 1234567);
Employee employee = PersonMapper.INSTANCE.personToEmployee(person, "yyyy year MM month dd day");
System.out.println(employee);
//output
// Employee(name=LISA, birthday=January 20, 1990, salary=null)