我们在编写实体类的过程中经常性地会使用lombok框架的@Data注解来帮我们生成get/set方法而不是自己手动生成.代码上看着也比较简洁.今天遇到的一个坑就是由于@Data产生的.
一个Post请求封装的一个对象,只接收到了部分属性,另一部分却没有接收到?
我们先来看两个简单的实体类
@Data
class Person {
private boolean male;
private List<DDog> dDogs;
}
@Data
class DDog {
private String name;
private Integer age;
}
Post请求如下:
@PostMapping("/test/objectOfObject/dog")
public void test(@RequestBody Person person) {
System.out.println("person = " + person);
}
{
"sex": true,
"dDogs": [
{
"age": 0,
"name": "0"
},
{
"age":1,
"name":1
}
]
}
这是我发送请求的方式以及body里面的内容,端点调试
可以清楚地看到,dDogs明明在body中进行了传递,但是没有接收到.郁闷了很久想不出原因是什么,但是如果我把变量名改一改,就会发生奇妙的事情.
@Data
class Person {
private boolean male;
private List<DDog> dogs;
}
{
"sex": true,
"dogs": [
{
"age": 0,
"name": "0"
},
{
"age":1,
"name":1
}
]
}
相信不少小伙伴看到这里时内心是多么的郁闷.那么我们来分析下问题是如何产生的呢
相信小伙伴已经仔细核对过参数命名的问题,首先排除参数名称不对应.显然这个理由不成立.于是我们可以猜测JSON解析时在对象的属性set方法中出现了问题.我们知道当一个请求向Server发送过来时,是由Servlet的service方法进行处理的.在SpringMVC中是由DispatcherServlet#doService()方法处理的.在doService()方法中又调用到了doDispatch()方法.接着由请求的url获取到相应的HandlerAdapter.在这里就不赘述url和handler建立映射的关系了.不是本文的重点.HandlerAdapter负责该请求的完整处理流程.在这里由于是post请求,RequestBody是一个对象,结合参数的解析转换底层是由jackson完成的,所以我们可以将问题定位到JSON数据set对象数据的那个地方.这个地方就是MethodProperty#deserializeAndSet()
public void deserializeAndSet(JsonParser p, DeserializationContext ctxt,
Object instance) throws IOException
{
Object value;
if (p.hasToken(JsonToken.VALUE_NULL)) {
if (_skipNulls) {
return;
}
value = _nullProvider.getNullValue(ctxt);
} else if (_valueTypeDeserializer == null) {
value = _valueDeserializer.deserialize(p, ctxt);
// 04-May-2018, tatu: [databind#2023] Coercion from String (mostly) can give null
if (value == null) {
if (_skipNulls) {
return;
}
value = _nullProvider.getNullValue(ctxt);
}
} else {
value = _valueDeserializer.deserializeWithType(p, ctxt, _valueTypeDeserializer);
}
try {
// 这里就是json数据解析出来并设置到对象
_setter.invoke(instance, value);
} catch (Exception e) {
_throwAsIOE(p, e, value);
}
}
上面只是得到了一个大概的猜想.现在是为了确定原因是什么
接下来我们可以手动生成下get/set方法
我们可以看到在set方法调用时调用了setDogs().我们也知道@Data会帮我们生成该方法.那么在出现问题的场景中我们再来进行断点调试,看下究竟发生了什么事情.
dDogs这个属性并没有经过jackson的解析set这个步骤,有点奇怪,我们将断点打在手动生成的set方法中,因为dDogs是一个对象JSON化的字符串,中间肯定是要有一个JSON解析的过程解析并set设值的过程.这一点是毋庸置疑的.
@Data
class Person {
private boolean male;
private List<DDog> dDogs;
public List<DDog> getdDogs() {
return dDogs;
}
public void setdDogs(List<DDog> dDogs) {
this.dDogs = dDogs;
}
}
我们发现手动生成了set方法,jackson却调用到了.没有生成之前就不会走.我们知道在set方法调用时是会反射遍历到对象的所有method.应该是set方法出了问题.于是我们将手动生成的set方法去掉,再看一下@Data帮我们生成的get/set方法是什么样的.在IDEA中我们可以打开这样的Structure查看下这个Person类中的dDogs的get/set方法.
上图中我们可以看到,get/set方法竟然是setDDogs(),而jackson在反序列化的时候调用的方法是setdDogs(),jackson没有扫描到合适的set方法于是就没有进行解析赋值.到这里已经真相大白了.
出现问题的原因在于属性命名不规范导致@Data注解生成的get/set方法与原生的get/set方法出现了偏差。在开发过程中尽量遵循一些规范,这样能规避一些问题.
Java属性命名规范:
一个拥有Property(域/类变量)及其setter/getter的普通Java类,
一般情况下,Java的属性变量名都以小写字母开头,如:userName,showMessage等。
特殊情况下,一些特定的英文缩略词如(USA,XML等),JavaBean也允许大写字母起头的属性变量名
,不过必须满足“变量的前两个字母要么全部大写,要么全部小写”。