Spring Boot解析JSON生成可变列表,如何保障DTO不可变性?
不可变DTO在Spring Boot中的反序列化问题解答
问题背景
我尝试创建一个不可变DTO,为Pizza类添加了Lombok的@Builder和@Getter注解,同时使用@Singular注解防止ingredients字段被初始化为可变List。但通过Spring Boot的API端点接收JSON请求体时,反序列化后的ingredients是可变列表,可成功添加元素;而通过Builder构建的Pizza实例调用add方法时,会抛出java.lang.UnsupportedOperationException。
1. Spring Boot是如何对请求体/响应体进行序列化与反序列化的?
Spring Boot默认使用Jackson作为JSON序列化/反序列化工具,核心依赖ObjectMapper类,处理流程如下:
- 反序列化(请求体→Java对象):
- 优先通过无参构造器创建目标类实例;
- 存在对应字段的setter方法时,调用setter赋值;无setter则直接通过反射给字段赋值;
- 对于
List这类集合类型,Jackson默认实例化可变集合(如ArrayList)存储JSON数组元素,不会自动适配DTO定义的不可变集合类型。
- 序列化(Java对象→响应体):
通过反射读取对象getter方法的返回值,转换为JSON格式输出;不可变集合的元素会被正常序列化,不会改变其不可变特性。
2. 如何防止Spring Boot将ingredients字段初始化为可变列表?
提供三种可行方案,按需选择:
方案一:让Jackson复用Lombok Builder(推荐)
从Lombok 1.18.14版本开始,新增@Jacksonized注解,可让Jackson直接使用Lombok生成的Builder创建DTO实例,反序列化后的ingredients会继承@Singular注解生成的不可变列表特性:
@Getter @Builder @Singular @Jacksonized public class Pizza { private final String name; private final List<String> ingredients; }
此方案无需额外配置,能保证Builder构建和反序列化的实例行为一致。
方案二:自定义Jackson集合反序列化器
配置ObjectMapper,让Jackson将JSON数组反序列化为不可变列表,支持全局或字段级配置:
- 全局配置示例:
@Configuration public class JacksonConfig { @Bean public ObjectMapper objectMapper() { ObjectMapper mapper = new ObjectMapper(); SimpleModule module = new SimpleModule(); module.addDeserializer(List.class, new JsonDeserializer<List<?>>() { @Override public List<?> deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { List<?> mutableList = p.readValueAs(List.class); return List.copyOf(mutableList); // Java 9+生成不可变列表 // 若使用Guava可替换为:return ImmutableList.copyOf(mutableList); } }); mapper.registerModule(module); return mapper; } }
- 字段级配置:在
ingredients字段上添加@JsonDeserialize指定自定义反序列化器,仅对该字段生效。
方案三:在构造器中主动转为不可变列表
手动实现DTO构造器,将传入的集合转为不可变类型,同时用@JsonCreator告知Jackson使用该构造器反序列化:
@Getter @Builder @Singular public class Pizza { private final String name; private final List<String> ingredients; @JsonCreator public Pizza(@JsonProperty("name") String name, @JsonProperty("ingredients") List<String> ingredients) { this.name = name; this.ingredients = List.copyOf(ingredients); // 转为不可变列表 } }
这种方式不依赖特定Lombok版本,兼容性好,但需要手动维护构造器。
内容的提问来源于stack exchange,提问作者Moritz Kampfinger
相关产品推荐
相关产品推荐

