场景分析 在Restful 风格的接口中,参数经常需要以JSON 的形式通过RequestBody 传递到后端。后端接收到数据进行反序列化的过程中,有一类参数是很棘手的。那就是JSON串里的 “”
空字符串。
项目中使用的是MybatisPlus,在进行Update操作时,不更新的字段需要设置为null。若字符串字段的内容为“”
则会触发更新操作,把原有的值设置为空字符串。这一定是和预期不符的。所以对于JSON串里的 “”
空字符串,它们大多数情况下需要被反序列化为null而非空字符串。避免在后续进行冗余的判断。
本文将本文通过不同类型的场景,分别进行说明。
案例 在项目中有这样一个DTO ,用来接收前端传来的参数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 @Data @NoArgsConstructor public class KnowledgePointDTO { @Schema(description = "知识点id,添加时必须为空,修改时不能为空") @Null(message = "id必须为空", groups = {Add.class}) @NotNull(message = "id不能为空", groups = {Update.class}) private Integer id; @Schema(description = "标题,长度1-32") @NotNull(message = "标题不能为空", groups = {Add.class}) @Length(min = 1, max = 32, message = "标题长度必须在1-32之间") private String title; @Schema(description = "内容(md),需要经过Base64编码") @NotNull(message = "内容不能为空", groups = {Add.class}) @Length(min = 1, max = 8192, message = "内容长度必须在1-8192之间") private String content; @Schema(description = "所在的目录,长度1-32。") @NotNull(message = "目录不能为空", groups = {Add.class}) @Length(min = 1, max = 32, message = "目录长度必须在1-32之间") private String index; public KnowledgePoint toEntity () { KnowledgePoint knowledgePoint = new KnowledgePoint (); knowledgePoint.setTitle(this .title); knowledgePoint.setContent(this .content); knowledgePoint.setIndex(this .index); knowledgePoint.setId(this .id); return knowledgePoint; } public interface Add extends Default {}; public interface Update extends Default {}; }
这里在更新操作时,title
、content
、index
都有可能为空,且前端有可能会传递空字符串过来。我们需要把它转换为null。
解决方案 toEntity() 这是我之前一直使用的方案,因为操控数据库的对象是它的实体类,所以在后续我们会通过toEntity()方法把它转换为对应的实体类。
那么就代表着我们可以在这里进行一层判断。把“”
转换为null。
1 2 3 4 5 6 7 8 public KnowledgePoint toEntity () { KnowledgePoint knowledgePoint = new KnowledgePoint (); knowledgePoint.setTitle(StringUtils.isEmpty(this .title) ? null : this .title); knowledgePoint.setContent(StringUtils.isEmpty(this .content) ? null : this .content); knowledgePoint.setIndex(StringUtils.isEmpty(this .index) ? null : this .index); knowledgePoint.setId(this .id); return knowledgePoint; }
优点:最容易想到的方法,实现简单
缺点:
只适用于这一种场景,若这个DTO在转换为实体类之前被使用,则会出现预料外的情况
“”
会通过JSR303 的 @NotNull
校验,这问题就很麻烦
虽然@NotBlank
注解可以解决,它会判断null和空白字符串。但是这其实是架构上的问题,参数的校验应该在转换之前 ,我有强迫症,你知道的。
convertEmptyStringToNull() 这是我临时想的方案,除了使用场景更通用外,跟toEntity()
没有本质区别。为了这个需求还需要为每个方法都编写一遍。此乃下策
1 2 3 4 5 public void convertEmptyStringToNull () { this .title = StringUtils.isEmpty(this .title) ? null : this .title this .content = StringUtils.isEmpty(this .content) ? null : this .content this .index = StringUtils.isEmpty(this .index) ? null : this .index }
优点:emmm硬说也就是使用场景更通用
缺点:
需要在业务方法中额外调用,太不优雅
没有解决参数的校验应该在转换之前 这个问题
修改ObjectMapper 这个方案是通过修改ObjectMapper来实现需求。
ObjectMapper
在反序列化过程中负责把Json映射成对象。可以说是各种JSON库中最核心的对象之一了。我们可以通过编辑这个对象,把空字符串映射为null。
但是我们一般不直接修改ObjectMapper或者是它的Builder。而是修改一个 Jackson2ObjectMapperBuilderCustomizer
的对象,来控制ObjectMapper的Builder从而达到修改ObjectMapper的目的。
这里就直接上代码了:
1 2 3 4 5 6 7 8 9 10 11 12 @Bean public Jackson2ObjectMapperBuilderCustomizer customizeJackson () { return builder -> { builder.serializationInclusion(JsonInclude.Include.NON_EMPTY); builder.serializationInclusion(JsonInclude.Include.NON_NULL); builder.featuresToEnable(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT); builder.modulesToInstall(emptyStringToNullModule()); builder.dateFormat(new SimpleDateFormat ("yyyy-MM-dd HH:mm:ss" )); builder.featuresToEnable(SerializationFeature.INDENT_OUTPUT); }; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 private static SimpleModule emptyStringToNullModule () { SimpleModule module = new SimpleModule (); module .addDeserializer(String.class, new StdDeserializer <String>(String.class) { @Override public String deserialize (JsonParser p, DeserializationContext ctxt) throws IOException { String result = StringDeserializer.instance.deserialize(p, ctxt); if (StringUtils.isEmpty(result)) { return null ; } return result; } }); return module ; }
通过注册Jackson2ObjectMapperBuilderCustomizer来“定制”一个ObjectMapperBuilder对象。
这里真正完成这个需求的是builder.modulesToInstall(emptyStringToNullModule());
这是为ObjectMapper
注册了一个Model
。这个model中定义了反序列化String类型时的逻辑。在那里把空字符串映射成了null。从而完成了需求。
其中 builder.featuresToEnable(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT);
这一行的作用是:空字符串反序列化为POJO对象时,若字符串为空串则映射为null。注意,它并不作用于String 文档中对于POJO对象的描述是这样的:
Feature that can be enabled to allow JSON empty String value (“”) to be bound as null
for POJOs and other structured values (java.util.Maps, java.util.Collections).
这样修改完之后,空字符串将被反序列化为null。从而完成了需求。
优点:在校验阶段之前,就完成了对象的映射。流程上更加合理,最主要的是。更加的优雅