Understanding DTO, VO, PO and Object Mapping with BeanUtils and MapStruct in Java
This article explains the roles of DTO, VO, and PO in Java applications, compares manual property copying with BeanUtils and MapStruct, provides complete code examples for both approaches, and offers guidance on choosing the appropriate method based on performance and project needs.
In Java projects, DTO (Data Transfer Object), VO (Value Object), and PO (Persistent Object) each serve distinct purposes: DTOs carry data between client and server or between micro‑services, VOs represent data presented to the user, and POs map directly to database tables.
When converting between these objects, developers often start with manual set() calls, which becomes tedious and error‑prone as the number of fields grows.
Spring's BeanUtils offers a quick way to copy matching properties via reflection, but it incurs runtime overhead due to repeated type checks and field traversal.
MapStruct generates type‑safe mapper implementations at compile time, producing plain set() calls that run much faster. The article includes visual comparisons of BeanUtils source code and the compiled code produced by MapStruct.
Below is a custom utility class that extends BeanUtils to provide fluent conversion methods for single objects and lists:
import org.springframework.beans.BeanUtils;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Supplier;
/**
* Conversion utility extending BeanUtils
*/
public class BeanConvertUtils extends BeanUtils {
public static <S, T> T convertTo(S source, Supplier<T> targetSupplier) {
return convertTo(source, targetSupplier, null);
}
public static <S, T> T convertTo(S source, Supplier<T> targetSupplier, ConvertCallBack<S, T> callBack) {
if (source == null || targetSupplier == null) {
return null;
}
T target = targetSupplier.get();
copyProperties(source, target);
if (callBack != null) {
callBack.callBack(source, target);
}
return target;
}
public static <S, T> List<T> convertListTo(List<S> sources, Supplier<T> targetSupplier) {
return convertListTo(sources, targetSupplier, null);
}
public static <S, T> List<T> convertListTo(List<S> sources, Supplier<T> targetSupplier, ConvertCallBack<S, T> callBack) {
if (sources == null || targetSupplier == null) {
return null;
}
List<T> list = new ArrayList<>(sources.size());
for (S source : sources) {
T target = targetSupplier.get();
copyProperties(source, target);
if (callBack != null) {
callBack.callBack(source, target);
}
list.add(target);
}
return list;
}
@FunctionalInterface
public interface ConvertCallBack<S, T> {
void callBack(S s, T t);
}
}A test class demonstrates converting a User object and a list of users to UserDTO using both the simple and lambda‑style APIs.
@Test
public void test04() {
// copy Object
User user = new User().setAge("20").setName("xxw");
System.out.println("复制前的数据:" + user);
UserDTO userDTO = BeanConvertUtils.convertTo(user, UserDTO::new);
System.out.println("复制后的数据:" + userDTO);
UserDTO dto = BeanConvertUtils.convertTo(user, UserDTO::new, (u, d) -> d.setAge(Integer.valueOf(u.getAge())));
System.out.println("复制后的数据:" + dto);
// copy List<Object>
User user1 = new User().setAge("22").setName("xw");
List<User> users = Lists.newArrayList(user1);
System.out.println("复制前的数据:" + users);
List<UserDTO> dtos = BeanConvertUtils.convertListTo(users, UserDTO::new);
System.out.println("复制后的数据:" + dtos);
List<UserDTO> dtoList = BeanConvertUtils.convertListTo(users, UserDTO::new, (u, d) -> d.setAge(Integer.valueOf(u.getAge())));
System.out.println("复制后的数据:" + dtoList);
}For compile‑time mapping, the article shows how to add the MapStruct dependency (version 1.4.2.Final) and the required Maven compiler plugin configuration for JDK 11+, then defines DTO and PO classes annotated with Lombok.
@Data
@Accessors(chain = true)
public class BeanDto {
private String name;
private Integer age;
private String time;
private List<Integer> list;
private Set<String> set;
}
@Data
@Accessors(chain = true)
public class BeanPo {
private String name;
private String age;
private String newTime;
private List<String> list1;
private Set<String> set1;
}The mapper interface uses @Mapper and @Mappings to map fields with different names and to convert types via custom @Named methods.
@Mapper
public interface BeanConvert {
BeanConvert INSTANCE = Mappers.getMapper(BeanConvert.class);
BeanDto poToDto(BeanPo po);
@Mappings({
@Mapping(source = "time", target = "newTime"),
@Mapping(source = "set", target = "set1"),
@Mapping(target = "age", qualifiedByName = "intToString"),
@Mapping(source = "list", target = "list1", qualifiedByName = "intToStringOnList")
})
BeanPo dtoToPo(BeanDto dto);
@Named("intToString")
static String intToString(Integer num) { return String.valueOf(num); }
@Named("intToStringOnList")
static List<String> intToString(List<Integer> nums) { return nums.stream().map(String::valueOf).collect(Collectors.toList()); }
static void main(String[] args) {
BeanDto dto = new BeanDto().setName("dto").setAge(1).setTime("2022").setList(Lists.newArrayList(1,2)).setSet(Sets.newSet("2"));
BeanPo beanPo = BeanConvert.INSTANCE.dtoToPo(dto);
System.out.println(beanPo);
}
}The final recommendation is: use BeanUtils for quick, low‑performance‑critical scenarios without extra dependencies; use MapStruct when runtime efficiency matters or when you need compile‑time safety, remembering to verify that source and target field names and types match to avoid mapping errors.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Selected Java Interview Questions
A professional Java tech channel sharing common knowledge to help developers fill gaps. Follow us!
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.
