摘要(干货):在使用SpringBoot中通过RestController返回自定义对象时,虽然SpringBoot会将自定义的类自动转换为JSON对象并返回给客户端。在转换过程中,Spring Boot会调用自定义类中所有的getXXX方法(XXX为任意名字),如果该方法返回值不为void,那么XXX将作为该自定义的类成员返回给客户端,而该类成员的值即为getXXX方法的返回值。如果类中未出现满足条件的方法,将会抛异常。

  最近在工作中需要用到Kubernetes官方提供的client-Java来操作Kubetnetes集群。但是写bug的过程,遇到了Spring Boot巨大的坑(也可能是自己太菜了。。),自己在解决过程中,逐渐总结了一个知识点,同时也是易犯的点,特此记录并分享。

1. 问题描述

1.1 工程代码

首先看一下我demo工程的部分代码(为了能够说明问题,我自己搭建了一个简单的SpringBoot工程)。
代码中涉及到几个类主要有;

  1. ReturnVO — 自定义的通用返回类,主要用于返回数据
  2. TestController — 自定义的Rest Controller,主要用于处理请求。
  3. 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的源代码,但是初步形成一下结论

  1. Spring Boot在将Java类转换为JSON对象时,是通过读取属性值的get方法来获取属性值的,如果未出现某个成员变量的get方法,那么该类成员将不会出现在JSON对象中。
  2. 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方法,将会抛出异常。

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐