Spring Boot中Jackson无中间对象序列化扁平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

