问题复现
假设实体类和Controller分别如下:
然后在postman中发送一个请求:
可以看到返回的是null,而且在后台的debug中也能看到pId在入参之后已经是null了,并不是return时产生的问题。
产生原因
在测试中发现,如果第二个字母大写,则会导致反序列化失败,该属性变为null。
具体是原因是SpringBoot默认使用Jackson进行json的序列化与反序列化,代码如下:
protected static String legacyManglePropertyName(final String basename, final int offset)
{
final int end = basename.length();
if (end == offset) { // empty name, nope
return null;
}
// next check: is the first character upper case? If not, return as is
char c = basename.charAt(offset);
char d = Character.toLowerCase(c);
if (c == d) {
return basename.substring(offset);
}
// otherwise, lower case initial chars. Common case first, just one char
StringBuilder sb = new StringBuilder(end - offset);
sb.append(d);
int i = offset+1;
for (; i < end; ++i) {
c = basename.charAt(i);
d = Character.toLowerCase(c);
if (c == d) {
sb.append(basename, i, end);
break;
}
sb.append(d);
}
return sb.toString();
}
Jackson在反序列化时主要依靠对象的setter方法。例如sName属性,实际需要调用的是setSName,在最后的for循环中,第二字母小写后,与第二个字母比较,如果都是小写,则直接拼接上并返回,
如果第二字母大写,就拼接上转为小写的结果,再去找下一个字母,直到找到小写字母为止。
这里也不是故意留下bug,而是为了满足驼峰命名规则,要规范输出,换言之,对于像pId,sName这种命名,它不认为这是一个有get/set方法的属性。
我们平时都知道,在JavaBean规范中,第一个字母必须要小写,但是很多时候不知道,第二个字母也需要小写,而造成一些影响,比如取到null,导致空指针异常。
解决方法
方法一
修改属性名称,将名称修改为符合规范的,也就是前两个字母均为小写的命名。但是如果半路发现这个问题,可能改动比较大,又涉及到前端的修改,可以采用方法二。
方法二
在字段前加入@JsonProperty
这种方法是比较推荐的一种方法,即你不认为这是一个属性,那么明确地我告诉你这是一个属性,避免歧义。
将实体类代码修改为如下再进行尝试:
@Data
public class TestEntity {
@JsonProperty("pId")
private String pId;
private String qid;
}
已经可以正常进行数据转换。
方法三
重写ObjectMapper中的转换方法PropertyNamingStrategy,这种方法比较不推荐,因为有可能会对其他用到ObjectMapper的地方产生不可预估的影响。
具体方式如下:
@Test
public void contextLoads() throws IOException {
Student test = new Student();
test.setBName("234234");
//objectMapper.configure(MapperFeature.USE_STD_BEAN_NAMING, true);
objectMapper.setPropertyNamingStrategy(new PropertyNamingStrategy() {
private static final long serialVersionUID = 1L;
// 反序列化时调用
@Override
public String nameForSetterMethod(MapperConfig<?> config,
AnnotatedMethod method, String defaultName) {
return method.getName().substring(3);
}
// 序列化时调用
@Override
public String nameForGetterMethod(MapperConfig<?> config,
AnnotatedMethod method, String defaultName) {
return method.getName().substring(3);
}
});
String s = objectMapper.writeValueAsString(test);
Assert.assertEquals("{\"BName\":\"2342344\"}", s);
}