SpringBoot JSON序列化自定义对象研究
- 1. 问题描述
- 1.1 工程代码
- 1.2 测试
- 2. 问题分析
- 2.1 初步分析结论
- 2.2 实验验证结论
- 2.3 问题拓展
- 3. 结论
摘要(干货):在使用SpringBoot中通过RestController返回自定义对象时,虽然SpringBoot会将自定义的类自动转换为JSON对象并返回给客户端。在转换过程中,Spring Boot会调用自定义类中所有的getXXX方法(XXX为任意名字),如果该方法返回值不为void,那么XXX将作为该自定义的类成员返回给客户端,而该类成员的值即为getXXX方法的返回值。如果类中未出现满足条件的方法,将会抛异常。
最近在工作中需要用到Kubernetes官方提供的client-Java来操作Kubetnetes集群。但是写bug的过程,遇到了Spring Boot巨大的坑(也可能是自己太菜了。。),自己在解决过程中,逐渐总结了一个知识点,同时也是易犯的点,特此记录并分享。
1. 问题描述
1.1 工程代码
首先看一下我demo工程的部分代码(为了能够说明问题,我自己搭建了一个简单的SpringBoot工程)。
代码中涉及到几个类主要有;
- ReturnVO — 自定义的通用返回类,主要用于返回数据
- TestController — 自定义的Rest Controller,主要用于处理请求。
- MyData — 自定义的数据类,会序列化为JSON对象,并返回给客户端。
- 首先我在pom文件中引入了几个常见的依赖(都是些常规依赖,可忽略)
<dependencies>
<!-- 引入SpringBoot web-starter的依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.6.4</version>
</dependency>
<!-- 引入lombok工具-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.22</version>
</dependency>
</dependencies>
- 代码中编写了自定义的一个返回类(ReturnVO),代码如下:
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@AllArgsConstructor
@NoArgsConstructor
@Data
public class ReturnVO {
private int code; // 返回码
private String message; // 返回的message
private Object data; // 返回的数据
}
- 代码中中的自定义类代码如下(MyData):
@Data
@NoArgsConstructor
@AllArgsConstructor
public class MyData {
// 该类中有2个成员变量分别为 name, address
private String name;
private String address;
public String getdemo(){
return "hello world";
}
}
- 代码中的Controller类(TestController)如下
@RestController
public class TestController {
@PostMapping("/test")
public ReturnVO test(){
ReturnVO result = new ReturnVO();
MyData data = new MyData();
result.setCode(200);
result.setMessage("success");
result.setData(data);
return result;
}
}
1.2 测试
将Spring Boot代码运行,我们使用postman发送POST请求对代码进行测试。我们可以看到如下返回值:
{
"code": 200,
"message": "success",
"data": {
"name": null,
"address": null,
"demo": "hello world"
}
}
可以看到,服务端返回值中增加了一个名字为demo的属性,但是我们的MyData类病没有该成员,是Spring Boot给我们自动加上了这个成员。
2. 问题分析
2.1 初步分析结论
出现了1.2中的情况,虽然没有看Spring boot的源代码,但是初步形成一下结论:
- Spring Boot在将Java类转换为JSON对象时,是通过读取属性值的get方法来获取属性值的,如果未出现某个成员变量的get方法,那么该类成员将不会出现在JSON对象中。
- Spring Boot在将Java类转换为JSON对象时,会读取所有以getXXX命名的成员方法(前提是该方法名的返回值不能为void),并将XXX作为JSON的属性,方法的返回值作为该属性的属性值,处理完成后返回给请求端。
2.2 实验验证结论
首先,我们来验证2.1章节中的第一个结论。
在 1.1章节的代码中,我们在MyIntOrString这个类的代码中,我们用了lombok自带的@Data注解,这个注解相当于@Setter和@Getter,为我们自动创建了类成员的set和get方法,为了验证我们2.1章节中的第一个结论,我们改写一下MyData类的代码,修改后如下:
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Setter // 将@Data注解替换为@Setter注解
@NoArgsConstructor
@AllArgsConstructor
public class MyData {
private String name;
private String address;
public String getname(){ // name属性的get方法
return "hello world";
}
}
其他代码不用更改,我们重新运行代码,并对TestController进行测试,可以看到服务端返回如下信息:
{
"code": 200,
"message": "success",
"data": {
"name": "hello world"
}
}
通过上面的返回值我们可以看到,虽然address是My Data类的成员变量,但是因为类中没有定义它的get方法,因此在最终的返回结果中,并未出现该成员。印证了2.1中的结论。
接着,我们来验证2.1章节中的第2个结论
在 1.1章节的代码中,我们在MyIntOrString这个类的代码中,我们用了lombok自带的@Data注解,这个注解相当于@Setter和@Getter,为我们自动创建了类成员的set和get方法,为了验证我们2.1章节中的第一个结论,我们改写一下MyData类的代码,修改后如下:
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class MyData {
private String name;
private String address;
public String getdemo(){
return "hello world";
}
public String getname(){
return "user";
}
public int gettime(){
return 2022;
}
public void getAge(){
}
}
上面的代码中,我们增加个getdemo方法,gettime方法,getAge方法。其中getAge方法的返回值为空。工程中其他代码不变,我们重新运行代码,并对TestController进行测试。可以得到如下的返回结果:
{
"code": 200,
"message": "success",
"data": {
"name": "user",
"demo": "hello world",
"time": 2022
}
}
通过上面的返回值我们可以看到,MyData类中所有getXXX命名的方法会被读取,但是由于getAge方法返回值为空,并没有出现在返回的JSON对象中。印证了2.1中的结论。
2.3 问题拓展
我们将这个问题的结论继续拓展:假设该类中没有get方法,那么程序的运行结果会是怎样的呢? ,为了验证这个结论,我们将MyData类进行继续进行改造,改造后的代码如下:
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class MyData {
private String name;
private String address;
}
重新运行代码,并对TestController进行测试,得出下面的结果:
{
"timestamp": "2022-03-27T08:43:05.115+00:00",
"status": 500,
"error": "Internal Server Error",
"path": "/test"
}
我们可以看到程序直接出错,查看程序输出的异常信息显示:
com.fasterxml.jackson.databind.exc.InvalidDefinitionException: No serializer found for class com.sodalife.blog.eneity.MyData and no properties discovered to create BeanSerializer (to avoid exception, disable SerializationFeature.FAIL_ON_EMPTY_BEANS) (through reference chain: com.sodalife.blog.eneity.ReturnVO["data"])
at com.fasterxml.jackson.databind.exc.InvalidDefinitionException.from(InvalidDefinitionException.java:77) ~[jackson-databind-2.13.1.jar:2.13.1]
at com.fasterxml.jackson.databind.SerializerProvider.reportBadDefinition(SerializerProvider.java:1300) ~[jackson-databind-2.13.1.jar:2.13.1]
at com.fasterxml.jackson.databind.DatabindContext.reportBadDefinition(DatabindContext.java:400) ~[jackson-databind-2.13.1.jar:2.13.1]
at com.fasterxml.jackson.databind.ser.impl.UnknownSerializer.failForEmpty(UnknownSerializer.java:46) ~[jackson-databind-2.13.1.jar:2.13.1]
at com.fasterxml.jackson.databind.ser.impl.UnknownSerializer.serialize(UnknownSerializer.java:29) ~[jackson-databind-2.13.1.jar:2.13.1]
at com.fasterxml.jackson.databind.ser.BeanPropertyWriter.serializeAsField(BeanPropertyWriter.java:728) ~[jackson-databind-2.13.1.jar:2.13.1]
上述的错误提示告诉我们,没有用来创建BeanSerializer的serializer,进而我们可以推断,get方法是必须存在的,否则程序将会出现异常。
3. 结论
通过问题的描述与分析,我们可以初步形成一下结论:
在使用SpringBoot中返回自定义对象时,虽然SpringBoot会将自定义的类自动转换为JSON对象并返回给客户端。在转换过程中,Spring Boot会调用自定义类中所有的getXXX方法(XXX为任意名字),如果该方法返回值不为void,那么XXX将作为该自定义的类成员返回给客户端,而该类成员的值即为getXXX方法的返回值。如果类中未出现满足任何满足条件的get方法,将会抛出异常。