You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Spring Boot中Jackson无中间对象序列化扁平Map为嵌套JSON树的最优方式及接口触发

问题与解决方案:Spring Boot中无中间POJO序列化扁平Map为嵌套JSON树

问题背景

我正在构建Spring Boot REST API,受严格架构约束:必须仅使用标准Java集合(如Map、List)实现并暴露树状数据结构,禁止创建自定义Node、Tree或中间DTO类。

OpenAPI契约要求返回深度嵌套的JSON响应,但树在内存中以扁平Map形式存储。

预期JSON输出

{
  "id": 1,
  "name": "Root",
  "children": [
    {
      "id": 2,
      "name": "Child A",
      "children": []
    },
    {
      "id": 3,
      "name": "Child B",
      "children": [
        {
          "id": 4,
          "name": "Grandchild",
          "children": []
        }
      ]
    }
  ]
}

已实现的自定义序列化方案

通过自定义Jackson StdSerializer,使用JsonGenerator递归遍历扁平Map生成嵌套JSON,完整可运行示例:

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.ser.std.StdSerializer;

import java.io.IOException;
import java.util.*;

public class Main {
    public static void main(String[] args) throws Exception {
        // 1. 内存中的扁平存储结构
        Map<Integer, List<Integer>> adjacencyList = new HashMap<>();
        Map<Integer, String> nodeData = new HashMap<>();

        // 填充示例树数据
        nodeData.put(1, "Root");
        nodeData.put(2, "Child A");
        nodeData.put(3, "Child B");
        nodeData.put(4, "Grandchild");

        adjacencyList.put(1, List.of(2, 3));
        adjacencyList.put(2, Collections.emptyList());
        adjacencyList.put(3, List.of(4));
        adjacencyList.put(4, Collections.emptyList());

        // 2. 配置Jackson ObjectMapper与自定义序列化器
        ObjectMapper mapper = new ObjectMapper();
        
        // 传入节点数据与根节点ID初始化序列化器
        TreeMapSerializer serializer = new TreeMapSerializer(nodeData, 1);
        
        // 通过SimpleModule注册自定义序列化器
        SimpleModule module = new SimpleModule();
        module.addSerializer(Map.class, serializer);
        mapper.registerModule(module);

        // 输出格式化后的JSON
        String jsonResult = mapper.writerWithDefaultPrettyPrinter()
                                  .writeValueAsString(adjacencyList);

        System.out.println(jsonResult);
    }
}

class TreeMapSerializer extends StdSerializer<Map<Integer, List<Integer>>> {
    private final Map<Integer, String> nodeData;
    private final int rootId;

    @SuppressWarnings({"unchecked", "rawtypes"})
    public TreeMapSerializer(Map<Integer, String> nodeData, int rootId) {
        super((Class<Map<Integer, List<Integer>>>) (Class) Map.class);
        this.nodeData = nodeData;
        this.rootId = rootId;
    }

    @Override
    public void serialize(Map<Integer, List<Integer>> adjacencyList, 
                          JsonGenerator gen, 
                          SerializerProvider provider) throws IOException {
        
        // 从根节点开始递归序列化
        serializeNode(rootId, adjacencyList, gen);
    }

    private void serializeNode(int nodeId, 
                               Map<Integer, List<Integer>> adjacencyList, 
                               JsonGenerator gen) throws IOException {
        gen.writeStartObject();
        gen.writeNumberField("id", nodeId);
        gen.writeStringField("name", nodeData.getOrDefault(nodeId, "Unknown"));
        
        gen.writeArrayFieldStart("children");
        List<Integer> children = adjacencyList.getOrDefault(nodeId, Collections.emptyList());
        for (Integer childId : children) {
            // 递归序列化子节点
            serializeNode(childId, adjacencyList, gen);
        }
        gen.writeEndArray();
        
        gen.writeEndObject();
    }
}

疑问解答

1. 该实现是否是Jackson无中间对象序列化的地道方式?

是的,直接使用JsonGenerator进行递归序列化是Jackson中无需中间POJO时的标准做法。这种方式绕开了Jackson的自动数据绑定逻辑,完全手动控制JSON生成流程,适合受严格约束无法创建中间类的场景。

相比动态构建嵌套Map/List再序列化的方式,直接操作JsonGenerator的优势在于:

  • 内存效率更高:无需构建完整的嵌套集合对象,直接流式生成JSON
  • 逻辑更直观:递归遍历扁平结构的逻辑与JSON生成逻辑紧密结合,减少转换步骤

2. 如何仅为特定API端点触发该序列化(避免全局注册风险)?

全局注册Map.class的序列化器会影响所有Map类型的序列化,风险很高。可以通过以下几种方式实现局部生效:

方式一:使用@JsonSerialize注解指定序列化器

创建一个仅用于标记的空类(非业务DTO,仅作为Jackson的标记载体),在Controller方法返回值上指定自定义序列化器:

// 仅用于标记的空类,无业务属性
public class TreeMapMarker {}

// Controller实现
@RestController
@RequestMapping("/tree")
public class TreeController {
    private final Map<Integer, List<Integer>> adjacencyList;
    private final Map<Integer, String> nodeData;

    // 构造方法注入扁平结构数据
    public TreeController(Map<Integer, List<Integer>> adjacencyList, Map<Integer, String> nodeData) {
        this.adjacencyList = adjacencyList;
        this.nodeData = nodeData;
    }

    @GetMapping
    @JsonSerialize(using = TreeMapSerializer.class)
    public TreeMapMarker getTree() {
        return new TreeMapMarker();
    }
}

// 修改序列化器泛型为TreeMapMarker
class TreeMapSerializer extends StdSerializer<TreeMapMarker> {
    private final Map<Integer, List<Integer>> adjacencyList;
    private final Map<Integer, String> nodeData;
    private final int rootId;

    public TreeMapSerializer(Map<Integer, List<Integer>> adjacencyList, Map<Integer, String> nodeData, int rootId) {
        super(TreeMapMarker.class);
        this.adjacencyList = adjacencyList;
        this.nodeData = nodeData;
        this.rootId = rootId;
    }

    @Override
    public void serialize(TreeMapMarker marker, JsonGenerator gen, SerializerProvider provider) throws IOException {
        serializeNode(rootId, adjacencyList, gen);
    }

    // 保留原有的serializeNode方法
    private void serializeNode(int nodeId, Map<Integer, List<Integer>> adjacencyList, JsonGenerator gen) throws IOException {
        gen.writeStartObject();
        gen.writeNumberField("id", nodeId);
        gen.writeStringField("name", nodeData.getOrDefault(nodeId, "Unknown"));
        
        gen.writeArrayFieldStart("children");
        List<Integer> children = adjacencyList.getOrDefault(nodeId, Collections.emptyList());
        for (Integer childId : children) {
            serializeNode(childId, adjacencyList, gen);
        }
        gen.writeEndArray();
        
        gen.writeEndObject();
    }
}

在Spring配置中注册序列化器Bean:

@Configuration
public class JacksonConfig {
    @Bean
    public TreeMapSerializer treeMapSerializer(Map<Integer, List<Integer>> adjacencyList, Map<Integer, String> nodeData) {
        return new TreeMapSerializer(adjacencyList, nodeData, 1);
    }
}

方式二:使用自定义ObjectMapper实例处理该端点

为目标端点单独配置ObjectMapper,手动序列化后返回String类型:

@RestController
@RequestMapping("/tree")
public class TreeController {
    private final Map<Integer, List<Integer>> adjacencyList;
    private final Map<Integer, String> nodeData;
    private final ObjectMapper treeObjectMapper;

    public TreeController(Map<Integer, List<Integer>> adjacencyList, Map<Integer, String> nodeData) {
        this.adjacencyList = adjacencyList;
        this.nodeData = nodeData;
        
        // 为该端点单独配置ObjectMapper
        this.treeObjectMapper = new ObjectMapper();
        TreeMapSerializer serializer = new TreeMapSerializer(nodeData, 1);
        SimpleModule module = new SimpleModule();
        module.addSerializer(Map.class, serializer);
        treeObjectMapper.registerModule(module);
        treeObjectMapper.enable(SerializationFeature.INDENT_OUTPUT);
    }

    @GetMapping(produces = MediaType.APPLICATION_JSON_VALUE)
    public String getTree() throws JsonProcessingException {
        return treeObjectMapper.writeValueAsString(adjacencyList);
    }
}

这种方式无需修改序列化器,直接在端点内手动控制序列化流程,完全隔离其他端点的序列化逻辑。

方式三:使用@ControllerAdvice结合ResponseBodyAdvice局部拦截

通过ResponseBodyAdvice拦截指定端点的响应,替换为自定义序列化后的JSON:

@ControllerAdvice(basePackages = "com.yourpackage.controller")
public class TreeResponseBodyAdvice implements ResponseBodyAdvice<Object> {
    private final ObjectMapper treeObjectMapper;

    public TreeResponseBodyAdvice(Map<Integer, List<Integer>> adjacencyList, Map<Integer, String> nodeData) {
        this.treeObjectMapper = new ObjectMapper();
        TreeMapSerializer serializer = new TreeMapSerializer(nodeData, 1);
        SimpleModule module = new SimpleModule();
        module.addSerializer(Map.class, serializer);
        treeObjectMapper.registerModule(module);
        treeObjectMapper.enable(SerializationFeature.INDENT_OUTPUT);
    }

    @Override
    public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
        // 仅拦截指定的Controller方法
        return returnType.getMethod().getName().equals("getTree") &&
               returnType.getContainingClass().equals(TreeController.class);
    }

    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
        if (body instanceof Map) {
            try {
                return treeObjectMapper.writeValueAsString(body);
            } catch (JsonProcessingException e) {
                throw new RuntimeException("Failed to serialize tree", e);
            }
        }
        return body;
    }
}

这种方式适合需要对多个端点应用相同序列化逻辑的场景,且无需修改Controller代码。

内容的提问来源于stack exchange,提问作者JOSUE RAFAEL PEREZ AGUIRRE

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.02 05:57:27