@RequestBody接收的部分属性为Null?

1.前言

我们在编写实体类的过程中经常性地会使用lombok框架的@Data注解来帮我们生成get/set方法而不是自己手动生成.代码上看着也比较简洁.今天遇到的一个坑就是由于@Data产生的.

2.问题描述

一个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);
    }

@RequestBody接收的部分属性为Null?_第1张图片

{
    "sex": true,
	"dDogs": [
		{
			"age": 0,
			"name": "0"
		},
		{
		    "age":1,
		    "name":1
		}
	]
}

这是我发送请求的方式以及body里面的内容,端点调试
@RequestBody接收的部分属性为Null?_第2张图片
可以清楚地看到,dDogs明明在body中进行了传递,但是没有接收到.郁闷了很久想不出原因是什么,但是如果我把变量名改一改,就会发生奇妙的事情.

@Data
class Person {
    private boolean male;
    private List<DDog> dogs;
}
{
    "sex": true,
	"dogs": [
		{
			"age": 0,
			"name": "0"
		},
		{
		    "age":1,
		    "name":1
		}
	]
}

@RequestBody接收的部分属性为Null?_第3张图片
相信不少小伙伴看到这里时内心是多么的郁闷.那么我们来分析下问题是如何产生的呢

3.问题排查

相信小伙伴已经仔细核对过参数命名的问题,首先排除参数名称不对应.显然这个理由不成立.于是我们可以猜测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);
        }
    }

4.验证猜想

上面只是得到了一个大概的猜想.现在是为了确定原因是什么
接下来我们可以手动生成下get/set方法
@RequestBody接收的部分属性为Null?_第4张图片

我们可以看到在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;
    }
}

@RequestBody接收的部分属性为Null?_第5张图片
我们发现手动生成了set方法,jackson却调用到了.没有生成之前就不会走.我们知道在set方法调用时是会反射遍历到对象的所有method.应该是set方法出了问题.于是我们将手动生成的set方法去掉,再看一下@Data帮我们生成的get/set方法是什么样的.在IDEA中我们可以打开这样的Structure查看下这个Person类中的dDogs的get/set方法.
@RequestBody接收的部分属性为Null?_第6张图片
上图中我们可以看到,get/set方法竟然是setDDogs(),而jackson在反序列化的时候调用的方法是setdDogs(),jackson没有扫描到合适的set方法于是就没有进行解析赋值.到这里已经真相大白了.

5.总结

出现问题的原因在于属性命名不规范导致@Data注解生成的get/set方法与原生的get/set方法出现了偏差。在开发过程中尽量遵循一些规范,这样能规避一些问题.

Java属性命名规范:

        一个拥有Property(域/类变量)及其setter/getter的普通Java类,

         一般情况下,Java的属性变量名都以小写字母开头,如:userName,showMessage等。

         特殊情况下,一些特定的英文缩略词如(USA,XML等)JavaBean也允许大写字母起头的属性变量名
         ,不过必须满足“变量的前两个字母要么全部大写,要么全部小写”。

你可能感兴趣的:(java,java)