先点赞再看,养成好习惯
背景
之前在做一个老项目重构的时候,由于数据库不能改动,所以还是继续沿用之前的老数据库。保险公司嘛,哪怕加了个互联网保险的 title,业务和系统还是偏传统的,数据模型不会轻易的更新;所以这个系统年代比较久远,而且它的数据库表命名方式采用的还是匈牙利命名法,导致在重构时因为这个命名方式恶心了我好久……
作为阅读福利我也整理一下Java笔记(包含脑图、面试真题、手写pdf等)现在免费分享给阅读到本篇文章的Java程序员朋友们,需要的自行领取~
最全学习笔记大厂真题+微服务+MySQL+分布式+SSM框架+Java+Redis+数据结构与算法+网络+Linux+Spring全家桶+JVM+高并发+各大学习思维脑图+面试集合
匈牙利命名法
匈牙利命名法(Hungarian notation),由1972年至1981年在施乐帕洛阿尔托研究中心工作的-程序员 查尔斯·西蒙尼发明,这位前辈后面成了微软的总设计师。
这个命名法的特点是,在命名前面增加类型的前缀,就像这样:
- c_name - 姓名,字符串(Char)类型
- n_age - 年龄,数字(Number)类型
- t_birthday - 生日,日期/时间(Time)类型
可不要小看这个命名法,当年可是很流行的,而且直到今天还是有一些系统仍然在沿用这个命名标准,比如微软的 Win32 API:
况且这个命名法,也不是一无是处,还是有一定的优点的,至少我一眼就可以看出这个字段的类型。
只不过在今天看起来有点怪怪的,不太符合当今的设计风格,如果放到人名上就更有意思了:
- 男赵四
- 女谢大脚
- 男刘能
当匈牙利命名法遇到 JAVA
我们这次的重构目标,是要保持老系统表不动的情况下,完全重写。可是新系统是 Java 语言来开发,Java 可是驼峰命名标准的,当这个匈牙利命名法的表迁移到驼峰命名法的 Java 语言会怎么样?
比如 c_name 这个命名,到 Java 里之后,是改为 CName 呢,还是 cName 呢?好像怎么都有点奇怪,不过最后我们还是选择了 CName ,将类型的前缀完全大写,至少看着稍微正常一点,没那么反人类
- c_name -> CName
- n_age -> NAge
- t_birthday -> TBirthday
序列化的问题
刚确定了命名方式,还没开心多久,我就遇到了一个非常难受的问题……
由于是 Spring 全家桶,Web 层使用的也是 Spring MVC。Spring MVC 的默认 JSON 处理库是 Jackson,在 Web 层返回 JSON 后,数据就成了这个样子:
{
"nid":1,
"ctitle":"Effective JAVA"
}
可我这个 POJO 类是将匈牙利命名法的字段转了大写,它长这样啊:
public class Book {
private Integer NId;
private String CTitle;
public Integer getNId() {
return NId;
}
public void setNId(Integer NId) {
this.NId = NId;
}
public String getCTitle() {
return CTitle;
}
public void setCTitle(String CTitle) {
this.CTitle = CTitle;
}
}
大写字段名,在转 JSON 之后变成了小写……要是把这个小写的字段给了前端,前端命名肯定会用小写,那前端在发送到后端时后一定也是小写。
那由于我们出入参序列化都是 Jackson ,对于 Jackson 来说,怎么出就怎么进,还是能解析出来的,看似也没啥问题,只是恶心了一点,前后端一个大写一个小写。
不过……事情并没有那么简单。后端不会将所有的报文都作为 POJO 的字段,会存在一些动态的字段,用 Map 存储。可 Map 存储的这些字段,Jackson 在输出时不会转为小写,还是保留原有的大写形式,这样就会导致给前端的字段,虽然都是匈牙利风格,但有些大写有些小写……虽然前端不知道打不打人,但我可不敢这么玩
不同序列化库的处理机制不同
Jackson 的匈牙利命名法处理
其实 Jackson 序列化之后转小写的原因也很好解释,Java 的设计规范,就是 Bean 中的属性用 private 修饰,然后提供 getter/setter 来提供读取/写入。那么在序列化时,Jackson 通过字段的 Getter 来访问属性值,甚至用 Getter 方法来解析属性名。
Java 的 getter 方法命名标准是,将小写驼峰转大写驼峰,Jackson 在通过 getter 方法名解析字段名时,将 getNID 解析为 nid 了,所以导致最终输出的字段名为小写的 nid。
FastJson 的匈牙利命名法处理
本来是想定制化一下 Jackson 的命名处理的,但想了一下觉得没必要,毕竟是我们命名不标准,何必去改这个命名处理机制呢,划不来。
所以我又尝试着换一种 JSON 库去处理这个命名问题,先试试阿里的 FastJSON,看看这个国产库的处理怎么样:
{
"cTitle":"Effective JAVA",
"nId":1
}
看到这个序列化结果时,我差点把我键盘上的 Backspace 按断了……同样是通过 getter 方法解析属性名,两个库的解析规则还能不一样……
在 FastJson 里,c 是小写了,可 Title 里的 T 还是大写,@#¥%……&此处省略100字……
冷静一下之后,心里默念了几遍:“不怪别人,是我们自己的命名问题,不符合标准人家怎么解析都不关你事……”
不过 JAVA 的生态这么好,JSON 库也不止这两个,再换一个就是,Google 的 Gson 也很不错嘛!
Gson 的匈牙利命名法
于是我又换成了 Gson,配置完 Spring MVC Gson Converter 之后,输出结果:
{
"NId":1,
"CTitle":"Effective JAVA"
}
终于换到一个能正常显示原始字段名的 JSON 库了,不过它既然能保持原有字段名,而不是 getter 里解析的属性名,那么它肯定不是解析 getter 方法名的
于是我又去翻了下 Gson 的源码,发现它是直接 getDeclaredFields()
,然后makeAccessible
,最后直接通过 Field.getValue
的方式直接获取属性值的。
相比 Jackson 和 FastJson 里通过 getter 获取属性列表,然后通过调用 getter 方法来获取属性值的方法来说,强制访问私有属性这种做法还是太暴力了,不过我喜欢,至少它能轻松解决了我的问题
其他的序列化问题
除了 JSON 这种文本形式的序列化之外,一些二进制的序列化也会有这个尴尬的问题,获取属性列表/属性值,到底是用解析 getter 方法的方式,还是直接 makeAccessible 暴力访问私有属性呢?
这个我测试了一下,比如在 Dubbo 的默认序列化方式(Dubbo 简化的Hession2)中,仍然是 getDeclaredFields
,然后访问私有属性
在 JDK 的内置序列化 ObjectOutputStream 中,也是 getDeclaredFields
,然后访问私有属性。
不过这种 getDeclaredFields ,然后访问私有属性值的方式,也会有一些劣势。比如在遇到代码混淆时,私有属性的值会被全部打乱,而 public 的方法却不会,所以在遇到混淆的代码时,这种方式就会乱套了,而通过 getter 方法解析的方式就不会有问题。
所以吧,这个获取属性的方式并没有对错,怎么都可以,不过我认为还是应该通过 getter/setter 的方式来操作,符合 JAVA 的规范。
补充
感谢@用户3323102545477 的提醒,Jackson 很强大,支持配置属性的获取方式,可以配置 Visibility
来实现只通过 Field
而不通过 getter
来获取属性:
@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY,
getterVisibility = JsonAutoDetect.Visibility.NONE)
public class Book {
private Integer NId = 1;
private String CName = "John";
}
全局配置更方便:
ObjectMapper objectMapper = new ObjectMapper();
// 配置 field 为可见
objectMapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY);
// 配置 getter 不可见
objectMapper.setVisibility(PropertyAccessor.GETTER, JsonAutoDetect.Visibility.NONE);
总结
Java 里访问私有属性值,标准的方式是通过 getter 方法,但还是提供了一个 makeAccessible 操作,可以让我们直接访问私有属性或者私有方法。
一直不太明白 JDK 为什么要这么设计,既然已经指定了规范,为什么还要开个后门呢?如果限制死了这个功能,那么所有序列化的库不就可以统一了,再也没有这种恶心的不一致问题!
但对比以上三个序列化库,我觉得都没错,Jackson/FastJson 按照规范的方式来,老老实实的通过 getter 方法来获取,而 Gson 就有点暴力,直接访问私有属性,各有优势。
补充:Jackson 支持属性的获取方式,默认是通过 getter
获取,但也可以配置通过 Field
获取,配置方式见上面的补充部分