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.
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.
DaTaobao Tech
Official account of DaTaobao Technology
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.