背景之前在做一个老项目重构的时候,由于数据库不能改动,所以还是继续沿用之前的老数据库。保护安全险公司嘛,哪怕加了个互联网保护安全险的 title,业务和系统还是偏经典的,数据模型不会轻易地更新;所以这个系统年代比较久远,而且它的数据库表命名方式选用的还是匈牙利命名法,导致在重构时因为这个命名方式让人作呕了我好久……
匈牙利命名法匈牙利命名法(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 -> CNamen_age -> NAget_birthday -> TBirthday当匈牙利命名法接触 JAVA咱们这次的重构目标,是要保持老系统表不动的情况下,完全重写。可是新系统是 Java 语言来研究,Java 可是驼峰命名标准的,当这个匈牙利命名法的表迁移到驼峰命名法的 Java 语言会怎么样?
比如 c_name 这个命名,到 Java 里之后,是改为 CName 呢,还是 cName 呢?好像怎么都有点奇怪,不过最后咱们还是选择了 CName ,将类型的前缀完全大写,至少看着稍微正常一点,没那么反人类
c_name -> CNamen_age -> NAget_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 的规范。
总结Java 里访问私有属性值,标准的方式是通过 getter 方法,但还是提供了一个 makeAccessible 操作,可以让咱们直接访问私有属性或者私有方法。
一直不太明白 JDK 为什么要这么设计,既然已经指定了规范,为什么还要开个后门呢?如果限制死了这个功能,那么所有序列化的库不就可以统一了,再也没有这种让人作呕的不一致问题!
但对比以上三个序列化库,我觉得都没错,Jackson/FastJson 遵从规范的方式来,老老实实地通过 getter 方法来获取,而 Gson 就有点暴力,直接访问私有属性,各有优势。
介绍浏览:Google一面挂,疯刷1000道JAVA面试题,上岸华为