Using MapStruct for Efficient Java Bean Copying and Mapping
This article explains why manual property copying in Java is inefficient, compares several runtime bean‑copy utilities, and demonstrates how MapStruct provides a compile‑time, type‑safe, high‑performance solution for shallow and deep object mapping, including handling of collections and ignored fields.
Preface
In layered architecture we often need to copy properties between objects such as VO, BO, PO, DTO. Manual copying is inefficient; IDE plugins can generate setters but still require repetitive code. Runtime copy utilities are fast but lack compile‑time safety, so we explore better alternatives.
Bean copier
Apache BeanUtils uses reflection and is discouraged by Alibaba's Java coding guidelines. Spring's BeanUtils improves performance and can be used directly.
BeanUtils.copyProperties(source, target);
BeanUtils.copyProperties(source, target, "id", "createTime"); // ignore specific fieldsCglib's BeanCopier generates a subclass at runtime, avoiding reflection and offering higher performance.
BeanCopier beanCopier = BeanCopier.create(SourceData.class, TargetData.class, false);
beanCopier.copy(source, target, null);Using Spring BeanUtils we encountered two issues: type mismatch (int → long) resulting in null values, and accidental copying of primary‑key fields causing unique‑key violations.
MapStruct
MapStruct is a Java annotation processor that generates type‑safe, high‑performance mappers at compile time. Its main advantages are:
High performance – uses plain setter calls instead of reflection.
Type safety – compilation errors are raised for mismatched types or names.
Rich features – supports deep copy, custom mapping behavior, collection mapping, etc.
Simple usage – define an interface and MapStruct generates the implementation.
Example
Define two identical classes, SourceData and TargetData , and a mapper interface.
public class SourceData {
private String id;
private String name;
private TestData data;
private Long createTime;
// getters and setters omitted for brevity
}Add the MapStruct dependency and Maven compiler plugin configuration (including annotation processor paths).
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>${org.mapstruct.version}</version>
</dependency>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<annotationProcessorPaths>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${org.mapstruct.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>Define the mapper interface (note that this is a MapStruct mapper, not MyBatis).
@Mapper
public interface BeanMapper {
BeanMapper INSTANCE = Mappers.getMapper(BeanMapper.class);
TargetData map(SourceData source);
}Use the mapper:
SourceData source = new SourceData();
source.setId("123");
source.setName("abc");
source.setCreateTime(System.currentTimeMillis());
TestData testData = new TestData();
testData.setId("123");
TargetData target = BeanMapper.INSTANCE.map(source);
System.out.println(target.getId() + ":" + target.getName() + ":" + target.getCreateTime()); // true
System.out.println(source.getData() == target.getData()); // true (shallow copy)MapStruct generates BeanMapperImpl in the target directory. By default it performs shallow copy; to enable deep copy, annotate the method with @Mapping(target = "data", mappingControl = DeepClone.class) .
Deep copy
After adding the @Mapping with DeepClone and recompiling, the generated code performs a deep copy.
Collection copy
MapStruct also supports collection mapping by adding a method like:
List<TestData> map(List<TestData> source);Type mismatch handling
If target field types differ (e.g., Long → int ), MapStruct will generate conversion code but you can enforce compile‑time errors with @Mapper(typeConversionPolicy = ReportingPolicy.ERROR) . The compiler will then report a lossy conversion error.
Disabling implicit conversion
For conversions like Long → String , MapStruct performs implicit conversion. To make such cases fail at compile time, define a custom annotation and reference it via mappingControl in the mapper.
Ignoring specific fields
Use @Mapping(target = "id", ignore = true) to skip fields. A reusable annotation (e.g., @IgnoreFixedField ) can group multiple ignored fields for convenience.
Integration with Lombok
If Lombok is used, add its dependency to annotationProcessorPaths so that MapStruct can see generated getters/setters.
Conclusion
MapStruct provides compile‑time generated, high‑performance mapping similar to Lombok's code generation. It eliminates runtime reflection overhead, offers type safety, and supports deep copy, collection mapping, and fine‑grained control over ignored fields. For projects with extensive object copying, MapStruct is a recommended solution.
The article also briefly outlines Java compilation, class loading, and execution mechanisms, emphasizing that annotation processing is a key part of compile‑time code generation.
Java Architect Essentials
Committed to sharing quality articles and tutorials to help Java programmers progress from junior to mid-level to senior architect. We curate high-quality learning resources, interview questions, videos, and projects from across the internet to help you systematically improve your Java architecture skills. Follow and reply '1024' to get Java programming resources. Learn together, grow together.
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.