Backend Development 14 min read

Why MapStruct Outperforms Spring BeanUtils for Object Mapping

MapStruct dramatically outperforms Spring's BeanUtils for Java object mapping by generating compile-time code that avoids reflection, achieving roughly ten-to-thirty times faster conversions—even across dozens of fields—while requiring only simple PO/Entity definitions and optional @Mapping annotations, making it a superior, low-learning-curve replacement.

DaTaobao Tech
DaTaobao Tech
DaTaobao Tech
Why MapStruct Outperforms Spring BeanUtils for Object Mapping

If you are still using Spring's BeanUtils, this article explains why you should switch to MapStruct for object-to-object mapping in Java.

Performance test results show that with 50 million conversions, BeanUtils takes 14 s (6 fields), 36 s (15 fields) and 55 s (25 fields), while MapStruct consistently finishes in about 1 s regardless of field count.

MapStruct dependencies (Maven): <dependency> <groupId>org.mapstruct</groupId> <artifactId>mapstruct</artifactId> <version>1.5.0.Final</version> </dependency> <dependency> <groupId>org.mapstruct</groupId> <artifactId>mapstruct-processor</artifactId> <version>1.5.0.Final</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.12</version> </dependency>

Simple property copy example :

Define PO and Entity with identical fields:

package mapstruct;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Date;

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class UserPo {
    private Long id;
    private Date gmtCreate;
    private Date createTime;
    private Long buyerId;
    private Long age;
    private String userNick;
    private String userVerified;
}
package mapstruct;

import lombok.Data;
import java.util.Date;

@Data
public class UserEntity {
    private Long id;
    private Date gmtCreate;
    private Date createTime;
    private Long buyerId;
    private Long age;
    private String userNick;
    private String userVerified;
}

Define a mapper interface:

package mapstruct;

import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;

@Mapper
public interface IPersonMapper {
    IPersonMapper INSTANCE = Mappers.getMapper(IPersonMapper.class);
    UserEntity po2entity(UserPo userPo);
}

Test class:

package mapstruct;

import java.util.Date;

public class MapStructTest {
    public static void main(String[] args) {
        UserPo userPo = UserPo.builder()
                .id(1L)
                .gmtCreate(new Date())
                .buyerId(666L)
                .userNick("test mapstruct")
                .userVerified("ok")
                .age(18L)
                .build();
        UserEntity userEntity = IPersonMapper.INSTANCE.po2entity(userPo);
        System.out.println(userEntity);
    }
}

MapStruct generates an implementation (e.g., IPersonMapperImpl ) that simply calls getters on the source and setters on the target, avoiding reflection and thus achieving the speed shown in the table.

Why BeanUtils is slower : its source code uses reflection to discover properties, make methods accessible, and invoke them in a loop. The core method looks like:

private static void copyProperties(Object source, Object target, Class
editable, String... ignoreProperties) throws BeansException {
    Assert.notNull(source, "Source must not be null");
    Assert.notNull(target, "Target must not be null");
    Class
actualEditable = target.getClass();
    // ... resolve editable class
    PropertyDescriptor[] targetPds = getPropertyDescriptors(actualEditable);
    for (PropertyDescriptor targetPd : targetPds) {
        Method writeMethod = targetPd.getWriteMethod();
        if (writeMethod != null && (ignoreList == null || !ignoreList.contains(targetPd.getName()))) {
            PropertyDescriptor sourcePd = getPropertyDescriptor(source.getClass(), targetPd.getName());
            if (sourcePd != null) {
                Method readMethod = sourcePd.getReadMethod();
                if (readMethod != null && ClassUtils.isAssignable(writeMethod.getParameterTypes()[0], readMethod.getReturnType())) {
                    Object value = readMethod.invoke(source);
                    writeMethod.invoke(target, value);
                }
            }
        }
    }
}

Reflection incurs a high overhead, especially when the conversion is executed many times.

Handling different property names uses @Mapping :

@Mapper
public interface IPersonMapper {
    IPersonMapper INSTANCE = Mappers.getMapper(IPersonMapper.class);
    @Mapping(target = "userNick1", source = "userNick")
    UserEntity po2entity(UserPo userPo);
}

Entity class must declare userNick1 instead of userNick .

Custom conversions (e.g., JSON string to object) are done by a helper class annotated with @Named and referenced in the mapper:

package mapstruct;

import com.alibaba.fastjson.JSONObject;
import org.apache.commons.lang3.StringUtils;
import org.mapstruct.Named;

public class AttributeConvertUtil {
    @Named("jsonToObject")
    public Attributes jsonToObject(String jsonStr) {
        if (StringUtils.isEmpty(jsonStr)) return null;
        return JSONObject.parseObject(jsonStr, Attributes.class);
    }
}
@Mapper(uses = AttributeConvertUtil.class)
public interface IPersonMapper {
    IPersonMapper INSTANCE = Mappers.getMapper(IPersonMapper.class);
    @Mapping(target = "attributes", source = "attributes", qualifiedByName = "jsonToObject")
    @Mapping(target = "userNick1", source = "userNick")
    UserEntity po2entity(UserPo userPo);
}

Performance comparison test runs 50 million iterations of both BeanUtils and MapStruct conversions and prints the elapsed seconds. The result confirms MapStruct is roughly 10‑30× faster.

public static void testTime() {
    int times = 50_000_000;
    long springStart = System.currentTimeMillis();
    for (int i = 0; i < times; i++) {
        UserPo po = UserPo.builder().id(1L).gmtCreate(new Date()).buyerId(666L).userNick("test").userVerified("ok").build();
        UserEntity ue = new UserEntity();
        BeanUtils.copyProperties(po, ue);
    }
    long springEnd = System.currentTimeMillis();
    for (int i = 0; i < times; i++) {
        UserPo po = UserPo.builder().id(1L).gmtCreate(new Date()).buyerId(666L).userNick("test").userVerified("ok").build();
        UserEntity ue = IPersonMapper.INSTANCE.po2entity(po);
    }
    long mapstructEnd = System.currentTimeMillis();
    System.out.println("BeanUtils use time=" + (springEnd - springStart) / 1000 + "s; MapStruct use time=" + (mapstructEnd - springEnd) / 1000 + "s");
}

Conclusion : MapStruct provides compile‑time generated mapping code that is orders of magnitude faster than BeanUtils' reflection‑based approach. The learning curve is low—just define PO/Entity classes, a mapper interface, and optional custom converters—making it a highly recommended replacement for BeanUtils in backend Java projects.

JavaPerformanceBeanUtilsMapStructObject Mapping
DaTaobao Tech
Written by

DaTaobao Tech

Official account of DaTaobao Technology

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.