前言
- 最近在使用
spring boot mvc
实现 HTTP 接口时出现了大小写异常转换的神秘现象,比如下面的案例:
java
@Data
public class User {
private int id;
private String name;
private String aTest;
}
请求参数:
{
"name": "小明",
"aTest": "测试"
}
响应参数:
{
"id": 1,
"name": "小明",
"atest": null // aTest 未成功接收
}
1、前端字段序列化异常
2、aTest 字段被序列化为了 atest
代码准备
- Spring-boot-parent 2.6.4
java
@Data
public class User {
private int id;
private String name;
private String aTest;
public User(int id, String name, String aTest) {
this.id = id;
this.name = name;
this.aTest = aTest;
}
}
@Repository
public class UserRepository {
public User createUser(User user) {
System.out.println(user);
return user;
}
}
@RestController
public class UserController {
@Autowired
private UserRepository userRepository;
@PostMapping("/users")
public User createUser(@RequestBody User user) {
return userRepository.createUser(user);
}
}
问题排查
- 经过一系列排查发现是对象序列化和反序列化导致的问题,一个是使用
lombok 生成 get、set
方法,一个使用自定义生成get、set
方式实现,下面我们来看一下两种情况的差异:
lombok
- User 对象使用
lombok 生成 get、set
方法
java
@Data
public class User {
private int id;
private String name;
private String aTest;
}
- 测试结果:
txt
POST http://localhost:8080/users
Content-Type: application/json
{
"name": "小明",
"aTest": "测试"
}
// 打印日志 aTest 字段未被成功接收
User(id=0, name=小明, aTest=null)
// 响应日志 aTest 字段被转换为 atest
{
"id": 1,
"name": "小明",
"atest": null
}
- 可以发现接口请求传递过来的
aTest
没有被正常反序列,响应时aTest
字段被序列为了atest
。
自定义生成 get、set
- 自定义生成
user 对象 get、set
方法。
java
public class User {
private int id;
private String name;
private String aTest;
public User(int id, String name, String aTest) {
this.id = id;
this.name = name;
this.aTest = aTest;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getaTest() {
return aTest;
}
public void setaTest(String aTest) {
this.aTest = aTest;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", name='" + name + '\'' +
", aTest='" + aTest + '\'' +
'}';
}
}
- 测试结果:
java
POST http://localhost:8080/users
Content-Type: application/json
{
"name": "小明",
"aTest": "测试"
}
// 打印日志 aTest 字段被成功接收
User{id=0, name='小明', aTest='测试'}
// 响应日志 aTest 字段被转为预期的 aTest
{
"id": 1,
"name": "小明",
"aTest": "测试"
}
- 可以发现请求时
aTest
被正常解析,响应时aTest
被序列化为预期的aTest
。
结合源码解析
- 这里我们可以对比
lombok
和我们自定义生成get、set
方法的差异:
java
// lombok
public String getATest() {
return this.aTest;
}
public void setATest(final String aTest) {
this.aTest = aTest;
}
// 自定义
public String getaTest() {
return aTest;
}
public void setaTest(String aTest) {
this.aTest = aTest;
}
- 我们知道
Spring
默认使用jackson
进行序列化和反序列,在构建BeanDeserializer
时会通过方法和字段获取对应的属性properties
,由于Spring 和 lombok
对JavaBeans
规范的定义理解并不一致导致识别字段结果不同,具体可以参考:https://github.com/projectlombok/lombok/issues/757
。
使用 lombok
- 我们先看看,
lombok
生成的BeanDeserializer
: com.fasterxml.jackson.databind.deser.BeanDeserializerFactory#buildBeanDeserializer
中我们可以看到调用了buildBeanDeserializer 生成 BeanDeserializer
:
- 一直断点,我们可以来到
com.fasterxml.jackson.databind.introspect.POJOPropertiesCollector#collectAll
获取properties map
,这里是重点:
- 执行
_addFields、_addMethods
我们推断出了来 4 个字段:
- 这里为什么是4个呢?因为
lombok
和Spring jackson
对JavaBeans
规范的定义理解不一致,导致从方法中推断出了atest
字段。 - 然后执行
_removeUnwantedProperties
字段去除了aTest
字段,因为它是不可见的。
使用 lombok
自定义生成 user 对象 get、set
方法
- 我们采取自定义的写法生成
user 对象 get、set
方法,对象属性可以正常被识别:
如何解决
- 知道了问题产生原因,解决就很简单了,只要让我们字段属性被正常推断即可。
使用注解 @JsonProperty("aTest")
自定义实现符合 Spring
规范的 get set
方法
java
public String getaTest() {
return aTest;
}
public void setaTest(String aTest) {
this.aTest = aTest;
}
个人简介
👋 你好,我是 Lorin 洛林,一位 Java 后端技术开发者!座右铭:Technology has the power to make the world a better place.
🚀 我对技术的热情是我不断学习和分享的动力。我的博客是一个关于Java生态系统、后端开发和最新技术趋势的地方。
🧠 作为一个 Java 后端技术爱好者,我不仅热衷于探索语言的新特性和技术的深度,还热衷于分享我的见解和最佳实践。我相信知识的分享和社区合作可以帮助我们共同成长。
💡 在我的博客上,你将找到关于Java核心概念、JVM 底层技术、常用框架如Spring和Mybatis 、MySQL等数据库管理、RabbitMQ、Rocketmq等消息中间件、性能优化等内容的深入文章。我也将分享一些编程技巧和解决问题的方法,以帮助你更好地掌握Java编程。
🌐 我鼓励互动和建立社区,因此请留下你的问题、建议或主题请求,让我知道你感兴趣的内容。此外,我将分享最新的互联网和技术资讯,以确保你与技术世界的最新发展保持联系。我期待与你一起在技术之路上前进,一起探讨技术世界的无限可能性。
📖 保持关注我的博客,让我们共同追求技术卓越。