如何使用MapStruct智能映射JSON返回结果到自定义DTO
优雅实现方案
1. 修正反序列化DTO
原定义的Wrapper类字段类型和实际JSON结构不匹配:JSON中entities是二维数组结构,因此需要先修正字段类型,保证WebClient集成的Jackson能正常反序列化响应。
// 修正后的外层响应包装类 public class Wrapper { private int error; private String message; private int totalItems; // 原List<PairEntity>类型错误,改为二维List匹配JSON结构 private List<List<PairEntity>> entities; // 补全全字段getter/setter、无参构造,开发中可直接用Lombok @Data注解简化 } // 键值对实体类保持原有定义即可 public class PairEntity { private String entityName; private String entityValue; // 同上补全getter/setter、无参构造 }
2. 定义目标业务DTO
按需求定义接收转换结果的DTO:
import java.util.Date; public class BusinessFieldDto { private String field1; private Date field2; // 补全getter/setter、无参构造 }
3. 编写MapStruct映射接口
利用MapStruct的@Named和qualifiedByName特性,封装按字段名取值、类型转换的逻辑,无需在业务代码中手动遍历键值对集合,所有映射逻辑统一收敛在Mapper中,编译期自动生成实现类,无反射性能损耗。
import org.mapstruct.Mapper; import org.mapstruct.Mapping; import org.mapstruct.Named; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; import java.util.List; // 指定componentModel = spring,直接将Mapper注册为Spring Bean,可直接@Autowired注入使用 @Mapper(componentModel = "spring") public interface ExternalResponseMapper { /** * 单条结果转换入口,适配totalItems=1的场景 */ default BusinessFieldDto toSingleDto(Wrapper wrapper) { if (wrapper == null || wrapper.getEntities() == null || wrapper.getEntities().isEmpty()) { return null; } // 取第一条数据的键值对集合做映射 return pairListToDto(wrapper.getEntities().get(0)); } /** * 批量结果转换入口,适配totalItems>1的场景 */ default List<BusinessFieldDto> toDtoList(Wrapper wrapper) { List<BusinessFieldDto> result = new ArrayList<>(); if (wrapper == null || wrapper.getEntities() == null || wrapper.getEntities().isEmpty()) { return result; } for (List<PairEntity> pairList : wrapper.getEntities()) { result.add(pairListToDto(pairList)); } return result; } // 配置字段映射规则 @Mapping(target = "field1", source = "pairList", qualifiedByName = "extractField1") @Mapping(target = "field2", source = "pairList", qualifiedByName = "extractField2AsDate") BusinessFieldDto pairListToDto(List<PairEntity> pairList); // 提取Field1对应的字符串值 @Named("extractField1") default String extractField1(List<PairEntity> pairList) { return pairList.stream() .filter(pair -> "Field 1".equals(pair.getEntityName())) .findFirst() .map(PairEntity::getEntityValue) .orElse(null); } // 提取Field2对应的值并转换为指定格式的Date @Named("extractField2AsDate") default Date extractField2AsDate(List<PairEntity> pairList) { String dateValue = pairList.stream() .filter(pair -> "Field 2".equals(pair.getEntityName())) .findFirst() .map(PairEntity::getEntityValue) .orElse(null); if (dateValue == null) { return null; } // 格式提示:dd-mm-yyyy中mm代表分钟,若要解析月份请使用大写MM,即dd-MM-yyyy,可根据实际业务调整 SimpleDateFormat dateFormat = new SimpleDateFormat("dd-mm-yyyy"); try { return dateFormat.parse(dateValue); } catch (ParseException e) { // 按需处理解析异常,可抛自定义业务异常、记录日志或返回null throw new IllegalArgumentException("Field2日期格式非法: " + dateValue, e); } } }
使用方式
直接在Service层注入Mapper,调用对应转换方法即可:
@Service public class ExternalCallService { @Autowired private ExternalResponseMapper responseMapper; @Autowired private WebClient webClient; public BusinessFieldDto callExternalService() { Wrapper response = webClient.get() .uri("外部接口地址") .retrieve() .bodyToMono(Wrapper.class) .block(); // 直接转换,无需手动遍历键值对 return responseMapper.toSingleDto(response); } }
方案优势
- 无冗余手动遍历逻辑:所有键值对匹配、转换逻辑都由MapStruct编译期生成实现,业务代码中不需要写循环遍历
entities的逻辑 - 易维护:后续新增映射字段,只需要在Mapper中新增对应
@Mapping规则和取值方法即可,不需要修改公共遍历代码 - 高性能:编译期生成纯Java代码赋值,和手写硬编码逻辑性能一致,无反射开销
- 低侵入:适配Spring生态,直接注入即可使用
注意:
SimpleDateFormat是线程不安全类,高并发场景建议将field2类型改为Java 8+提供的LocalDate,使用线程安全的DateTimeFormatter做日期转换,避免并发问题。
内容的提问来源于stack exchange,提问作者user1436883
相关产品推荐
相关产品推荐

