Spring Boot REST接口JSON序列化重复对象转ID问题解决
Spring Boot REST接口JSON序列化异常:关联对象仅首项显示完整内容
问题场景
我开发了一个返回List<Map<String,Object>>的Spring Boot REST接口,接口代码如下:
public List<Map<String,Object>> getAll(@Valid DataRequestDto dto) { List<Map<String, Object>> data = (List<Map<String, Object>>) dataOperationMap.getAllData(dto); return data; }
期望输出
期望所有列表项中的acckind和accbalance都显示完整对象:
{ "id": 7581, "serialid": 24, "parentid": 7576, "rowno": 5, "code": "15005", "acckind": { "id": 10080001, "title": "بودجه ای", "systemDefine": 1, "typeValueId": 1, "isActive": true, "isDefault": true, "engTitle": "" }, "accbalance": { "id": 10160001, "title": "خنثی و بدون کنترل", "systemDefine": 1, "typeValueId": 1, "isActive": true, "isDefault": true, "engTitle": null } }, { "id": 7582, "serialid": 24, "parentid": 7576, "rowno": 6, "code": "15006", "acckind": { "id": 10080001, "title": "بودجه ای", "systemDefine": 1, "typeValueId": 1, "isActive": true, "isDefault": true, "engTitle": "" }, "accbalance": { "id": 10160001, "title": "خنثی و بدون کنترل", "systemDefine": 1, "typeValueId": 1, "isActive": true, "isDefault": true, "engTitle": null } }
实际输出
实际JSON中从第二个数据开始,acckind和accbalance仅显示ID:
{ "id": 7581, "serialid": 24, "parentid": 7576, "rowno": 5, "code": "15005", "acckind": { "id": 10080001, "title": "بودجه ای", "systemDefine": 1, "typeValueId": 1, "isActive": true, "isDefault": true, "engTitle": "" }, "accbalance": { "id": 10160001, "title": "خنثی و بدون کنترل", "systemDefine": 1, "typeValueId": 1, "isActive": true, "isDefault": true, "engTitle": null } }, { "id": 7582, "serialid": 24, "parentid": 7576, "rowno": 6, "code": "15006", "acckind": 10080001, "accbalance": 10160001 }
排查信息
- 调试确认接口返回前数据是完整的,问题出在JSON转换环节
- 项目使用Spring Boot 2.3.6.RELEASE,pom.xml配置如下:
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.3.6.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.hushaerp</groupId> <artifactId>form-manager</artifactId> <version>1.0-RELEASE</version> <name>form-manager</name> <description>Demo project for Spring Boot</description> <properties> <java.version>1.8</java.version> <spring-cloud.version>Hoxton.SR9</spring-cloud.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- <dependency>--> <!-- <groupId>org.springframework.cloud</groupId>--> <!-- <artifactId>spring-cloud-starter-config</artifactId>--> <!-- </dependency>--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency> <dependency> <groupId>org.modelmapper</groupId> <artifactId>modelmapper</artifactId> <version>3.1.0</version> </dependency> <dependency> <groupId>org.postgresql</groupId> <artifactId>postgresql</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-oauth2</artifactId> </dependency> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger2</artifactId> <version>2.9.2</version> </dependency> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger-ui</artifactId> <version>2.9.2</version> </dependency> </dependencies> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>${spring-cloud.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
原因分析
这个问题由Jackson的对象引用检测机制导致:当同一个对象被多次引用时,Jackson默认会在第一次序列化时输出完整对象,后续引用仅输出对象标识(ID),以此优化序列化性能。同时,使用Map<String, Object>作为返回类型会丢失类型信息,Jackson无法明确重复对象的序列化规则,进一步触发该问题。
解决方案
方法1:禁用Jackson对象引用检测
通过配置关闭Jackson的引用检测逻辑,强制序列化所有重复对象:
代码配置方式
import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class JacksonConfig { @Bean public ObjectMapper objectMapper() { ObjectMapper mapper = new ObjectMapper(); // 关闭引用检测,强制序列化所有重复对象 mapper.disable(com.fasterxml.jackson.databind.SerializationFeature.USE_ID_FOR_REFERENCE); mapper.disable(com.fasterxml.jackson.databind.MapperFeature.USE_ANNOTATIONS); return mapper; } }
配置文件方式
在application.properties中添加:
spring.jackson.serialization.use-id-for-reference=never spring.jackson.mapper.use-annotations=false
方法2:使用具体DTO类替代Map
创建明确的DTO类,避免Map的类型模糊问题,这也是最规范的长期解决方案:
import lombok.Data; import java.util.List; import java.util.stream.Collectors; import org.modelmapper.ModelMapper; @Data public class DataItemDto { private Long id; private Integer serialid; private Long parentid; private Integer rowno; private String code; private AccKindDto acckind; private AccBalanceDto accbalance; @Data public static class AccKindDto { private Long id; private String title; private Integer systemDefine; private Integer typeValueId; private Boolean isActive; private Boolean isDefault; private String engTitle; } @Data public static class AccBalanceDto { private Long id; private String title; private Integer systemDefine; private Integer typeValueId; private Boolean isActive; private Boolean isDefault; private String engTitle; } }
修改接口返回类型:
public List<DataItemDto> getAll(@Valid DataRequestDto dto) { List<Map<String, Object>> dataMap = (List<Map<String, Object>>) dataOperationMap.getAllData(dto); ModelMapper modelMapper = new ModelMapper(); return dataMap.stream() .map(map -> modelMapper.map(map, DataItemDto.class)) .collect(Collectors.toList()); }
方法3:序列化前深拷贝重复对象
如果无法修改返回类型,可在返回前对重复的关联对象进行深拷贝,确保每个Map中的对象都是独立实例:
import org.springframework.beans.BeanUtils; public List<Map<String,Object>> getAll(@Valid DataRequestDto dto) { List<Map<String, Object>> data = (List<Map<String, Object>>) dataOperationMap.getAllData(dto); for (Map<String, Object> item : data) { // 深拷贝acckind对象 Object accKind = item.get("acckind"); if (accKind != null) { AccKind newAccKind = new AccKind(); BeanUtils.copyProperties(accKind, newAccKind); item.put("acckind", newAccKind); } // 深拷贝accbalance对象 Object accBalance = item.get("accbalance"); if (accBalance != null) { AccBalance newAccBalance = new AccBalance(); BeanUtils.copyProperties(accBalance, newAccBalance); item.put("accbalance", newAccBalance); } } return data; }
内容的提问来源于stack exchange,提问作者F Goli
相关产品推荐
相关产品推荐

