场景分析

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 {};
}

这里在更新操作时,titlecontentindex都有可能为空,且前端有可能会传递空字符串过来。我们需要把它转换为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。从而完成了需求。

  • 优点:在校验阶段之前,就完成了对象的映射。流程上更加合理,最主要的是。更加的优雅