Java自动映射嵌套递归对象:如何自动识别循环递归免手动注解?
模型转DTO时的循环递归自动处理方案
问题场景
在进行Model到DTO的映射时,由于实体间的双向关联(比如Father包含Child列表,Child又持有Father引用),会导致映射过程中出现循环递归问题。以下是示例代码:
实体类代码
public class Father { private int id; private String firstName; private String lastName; private List<Child> children; // Constructor, getters and setters }
public class Child { private int id; private String firstName; private String lastName; private Father father; // Constructor, getters and setters }
DTO类代码
public class FatherDto { private int id; private String firstName; private String lastName; private List<ChildDto> children; // Constructor, getters and setters }
public class ChildDto { private int id; private String firstName; private String lastName; private FatherDto father; // Constructor, getters and setters }
已知使用MapStruct时,可以通过添加@Mapping(target = "father", ignore = true)这类注解手动忽略递归字段来解决,但想知道是否存在无需为每个模型添加自定义注解、能自动识别递归并处理的方法?
可行方案
1. MapStruct内置循环引用自动处理(1.4+版本支持)
MapStruct从1.4版本开始,内置了循环引用检测逻辑,无需手动添加忽略注解。只需在映射接口上正常定义转换方法,框架会自动识别双向关联的递归链,自动截断循环:
@Mapper(componentModel = "spring") public interface FamilyMapper { FamilyMapper INSTANCE = Mappers.getMapper(FamilyMapper.class); FatherDto toFatherDto(Father father); List<FatherDto> toFatherDtos(List<Father> fathers); ChildDto toChildDto(Child child); List<ChildDto> toChildDtos(List<Child> children); }
比如映射Father到FatherDto时,框架会正常映射children列表,但每个ChildDto中的father字段会被自动跳过,避免无限递归。
2. Jackson序列化配置间接处理
如果是通过Jackson的ObjectMapper直接将实体转成DTO(比如用convertValue方法),可以配置Jackson的循环引用处理规则,自动识别并截断递归:
ObjectMapper mapper = new ObjectMapper(); // 关闭自引用失败抛出异常的规则 mapper.configure(SerializationFeature.FAIL_ON_SELF_REFERENCES, false); // 配置循环引用时只序列化ID字段(自定义处理逻辑) mapper.addHandler(new SerializerProvider.ReferenceReserializer(null) { @Override public JsonSerializer<Object> findReferenceSerializer(SerializerProvider provider, Object reference) throws JsonMappingException { return new JsonSerializer<Object>() { @Override public void serialize(Object value, JsonGenerator gen, SerializerProvider serializers) throws IOException { if (value instanceof Father) { gen.writeNumber(((Father) value).getId()); } else if (value instanceof Child) { gen.writeNumber(((Child) value).getId()); } } }; } });
这种方式不需要修改映射逻辑,Jackson会自动识别循环并按自定义规则处理。
3. 自定义通用映射工具类
可以自己实现一个基于反射的通用映射工具,用ThreadLocal记录已映射的对象ID,检测到重复对象时自动截断递归:
public class GenericMapper { private static final ThreadLocal<Set<Object>> MAPPED_OBJECTS = ThreadLocal.withInitial(HashSet::new); public static <S, T> T map(S source, Class<T> targetClass) throws IllegalAccessException, InstantiationException { Set<Object> mapped = MAPPED_OBJECTS.get(); if (mapped.contains(source)) { // 已映射过,返回null或仅序列化ID return null; } mapped.add(source); T target = targetClass.newInstance(); // 遍历字段完成映射 Field[] sourceFields = source.getClass().getDeclaredFields(); for (Field field : sourceFields) { field.setAccessible(true); Object value = field.get(source); if (value == null) continue; Field targetField = targetClass.getDeclaredField(field.getName()); targetField.setAccessible(true); if (value instanceof Collection) { // 处理集合类型 Collection<?> sourceCol = (Collection<?>) value; Collection<Object> targetCol = new ArrayList<>(); for (Object item : sourceCol) { targetCol.add(map(item, getGenericType(field))); } targetField.set(target, targetCol); } else if (!isBasicType(value.getClass())) { // 处理关联对象 targetField.set(target, map(value, targetField.getType())); } else { // 基本类型直接赋值 targetField.set(target, value); } } mapped.remove(source); return target; } // 判断是否为基本类型或包装类、字符串 private static boolean isBasicType(Class<?> clazz) { return clazz.isPrimitive() || clazz.equals(Integer.class) || clazz.equals(String.class) || clazz.equals(Long.class) || clazz.equals(Double.class) || clazz.equals(Boolean.class); } // 获取集合泛型类型(简化实现) private static Class<?> getGenericType(Field field) { ParameterizedType pt = (ParameterizedType) field.getGenericType(); return (Class<?>) pt.getActualTypeArguments()[0]; } }
这个工具类通用性强,无需为每个模型加注解,但反射实现的性能比MapStruct这类编译期生成代码的工具差。
内容的提问来源于stack exchange,提问作者Denver
相关产品推荐
相关产品推荐

