一个朋友的框架发布1.1版本,和1.0在运行时生成额外字节码不同,1.1在编译时通过maven插件生成额外字节码,因此避免了暴露微量的设计模式给用户,让用法更简单粗暴,性能也更高。很大一部分不太像框架了,而更像是java语言的扩展,是个重大升级。此外,充分吸取1.0的教训,给出架构图并,让所有文档中英文双版,避免别人不明全局。另外,不再兼容java7。
原文 - https://github.com/babyfish-ct/babyfish/blob/master/README_zh_CN.md
BabyFish是什么
BabyFish是一款让开发人员仅通过声明式代码的方式实现智能数据结构的Java框架; 其功能非常丰富,请阅读下面这张图片以对其大体架构有个粗略了解。
框架架构
在这张图中,有5个功能点被红色星号标注:
,它们是本框架最重要的5个功能点。
Java环境要求
BabyFish1.1需要Java8
离线文档
文档 | 描述 |
---|---|
tutorial.html(英文) | 图文并茂地详细讲述部分重要功能(请使用高版本IE或其它浏览器打开)。 |
tutorial_zh_CN.html(中文) | |
demo-guide.html(英文) | 逐个介绍每个demo,并给出建议的阅读顺序(请使用高版本IE或其它浏览器打开)。 |
demo-guide_zh_CN.html(中文) |
框架功能
-
1. Java部分
-
1.1. 基础功能
-
1.1.1. TypedI18N
提供一种更好的实现国际化的方法。和传统的使用在运行时刻报错的java.util.ResourceBundle进行开发不同, 请类型国际化的所有错误均在编译时刻报告给开发人员(此功能需要编译时字节码增强)。 -
1.1.2. Delegate
更好的事件通知模型, 如同.NET的委托一样,相比于Java Bean规范的建议更简单、更节约、更强大 (此功能需要编译时字节码增强)。 -
1.1.3. Equality
开发人员往往需要覆盖“public boolean equals(Object o)”方法, 通常他们有两种检查参数对象的类型是否和当前对象(this)的类型相同的办法。- 过于严格的办法: if (this.getClass() != o.getClass()) return false;
- 过于宽松的办法:if (!(o instanceof ${ThisClass})) return false;
-
1.1.4. Graph Traveler
此功能用于深度优先或广度优先的对象图遍历,遍历过程中提供丰富的遍历上下文变量。
-
-
1.2. BabyFish集合框架
-
1.2.1. X集合框架
“X”代表“eXtensible”, 为了实现一些不可思议的功能,X集合框架扩展了Java集合框架的接口并给出一套全新的实现。
这里仅仅讨论X集合框架最重要的两个功能(请参考demo-guide以了解所有功能)。
-
1.2.1.1. Bidi集合
和“org.apache.commons.collections4.BidiMap”类似, 为了让“java.util.Map” 支持值(非键)的唯一性; X集合框架支持BidMap;同理,也支持BidiList。 -
1.2.1.2. 不稳定集合元素
Java集合框架支持哈希结构和红黑树结构,例如HashMap, TreeSet and TreeMap; 它们的好处是高性能,但是有一个缺陷,当某个对象被作为元素添加到Set或被作为键添加到Map,它的数据就不能被修改了。
X集合框架支持一个“不稳定集合元素的功能“,即便某个对象被作为元素添加到Set或被作为键添加到Map, 它也可以被修改,因为与之相关的所有Set和Map集合均会在其被修改时自动修改。
集合类型 不稳定集合元素能力体现 Set 不稳定元素 Map 不稳定键 BidiList 不稳定元素 BidiMap 不稳定键 不稳定值
-
-
1.2.2. MA Collection Framework
“MA”代表“Modification Aware”,MA集合框架扩展至X集合框架,用户支持数据变更事件通知。 对于每一个被修改的元素或键值对,两个事件将会被触发,其中一个在修改之前触发,而另外一个在修改后触发。 这个功能和关系型数据库的行级触发器极为类似。
这里仅仅讨论MA集合框架最重要的两个功能(请参考demo-guide以了解所有功能)。
-
1.2.2.1 Implicit Event
在BabyFish集合框架中,集合对象的数据不仅会被显式的API调用修改,有时也会被系统自动修改。其中,最典型的代表就是“不稳定集合元素”导致的集合被自动修改的案例了。 这种不是由开发人员发起的、自动的、隐式的集合修改照样会导致事件的触发。 -
1.2.2.2 Bubble Event
Java集合框架是支持集合视图的,例如
- “java.util.NavigableMap”支持headMap、tailMap、subMap和descendingMap方法。
- “java.util.NavigableSet”支持headSet、tailSet、subSet、descendingSet和descendingIterator方法。
- “java.util.List”支持subList和listIterator方法。
- “java.util.Map”支持keySet、values和entrySet方法。
- “java.util.Collection”支持iterator方法。
MA集合框架支持一个叫“冒泡事件“的功能,当视图集合被修改的时候,事件将会在此视图对象上被触发;然后,事件向上冒泡,让上一层视图集合触发事件;以此类推,最终,冒泡到根部的原始集合,事件将在原始集合上被触发。
-
-
1.2.3. Lazy Collection Framework
我们知道,ORM框架常常支持一种叫延迟加载的技术,延迟集合被允许是虚假的,并不包含任何数据,直到第一次被开发人员使用时,才会通过IO操作从外界获取数据从而转变成真实集合。 这是一个很强大的功能,如此好的功能如果只能在ORM领域使用,就是太浪费了。
为了让这种强大的功能可以在任何场合被使用(而非仅仅在ORM实现中实现),BabyFish支持抽象而普适的延迟集合框架。
没有相应的demo,因为这是框架的SPI,并非API。
-
1.2.4. Collection Utils
Java集合框架附带了一个强大的工具类java.util.Collections, 此类提供很多静态方法以创建神奇的集合代理对象。
BabyFish集合框架扩展了Java集合框架的接口,类似的,它需要提供自己的工具类:org.babyfish.collection.MACollections"。
-
-
1.3. ObjectModel4Java
ObjectModel为BabyFish框架的核心功能,也是我开发此框架的原因。
ObjectModel用于构建智能数据结构,又可分为两个部分
- ObjectModel4Java:在Java语言层面支持智能数据结构,这是本章节将重点介绍的功能。
-
- ObjectModel4JPA:在JPA层面支持智能数据结构,将在后续章节中介绍。
ObjectModel4Java具备很多功能,本文仅仅讨论两个:1、双向关联的一致性;2、不稳定对象 (请参考demo-guide以了解其所有功能)。
1.3.1. 双向关联的一致性
Java开发人员往往将数据模型类声明成除了getter和setter访问器外没有任何逻辑的简单类 (说直白点,就是C语言结构体),这种简单的类所描述的数据结构的智能性和方便性非常有限。 举一简单的例子,对于两个对象之间的双向关联,开发人员其中一端时,另外一端没有对应地修改, 就会导致数据不一致,通过某个对象的关系能导航到另外一个对象,但反过来不成立。
ObjectModel4Java在为数据结构引入智能性的同时,并没有增加开发的复杂程度。毕竟,除了getter和setter访问器 之外无任何逻辑的数据类写起来是非常简单的,这种简单的代码书写方式早已深入人心。ObjectModel4Java 只需要开发人员在这些简单的数据类上添加一点注解,借助于编译时的字节码增强Maven插件,开发人员便可以得到功能强大的数据模型。
下面我们来看一个实际的例子,创建部门和员工之间的双向一对多关系。
- 一个部门可以包含多个员工,且这些员工是有先后顺序的。
@Model //该类使用ObjectModel4Java,需编译时字节码增强 public class Department { @Association(opposite = "department") //该字段和“Employee.department”字段互为镜像,构成双向关联。 private List<Employee> employees; //一个部门包含多个员工,该一对多集合字段有顺序,故采用List 此处略去Getter和Setter }
- 一个部门可以包含多个员工,且这些员工是有先后顺序的。
-
每个员工均属于某个部门,且它知道自己在其所属部门中的位置。
@Model //该类使用ObjectModel4Java,需编译时字节码增强 public class Employee { @Association(opposite = "employees") //该字段和“Department.employees”字段互为镜像,构成双向关联。 private Department department; //员工属于一个部门,如果无,为null // 该字段依附于“Employee.department”字段,表示当前员工在department字段指向的父对象集合中的索引, // 如果department为null,此索引为-1 @IndexOf("department") private int index; 此处略去Getter和Setter }
假设现有6个对象,其变量名分别为department1, department2, employee1, employe2, employee3和employee4,它们的数据如下:
部门对象 员工对象 变量名 employees 变量名 department index department1 [ employee1, employee2 ] employee1 department1 0 employee2 department1 1 department2 [ employee3, employee4 ] employee3 department2 0 employee4 department2 1 此处,仅举一例,用户使用如下代码修改department1对象的employees集合
department1.getEmployees().add(0, employee3);
此语句视图将employee3插入到department1.employees集合的最前面, 为了维持数据结构一致性,ObjectModel4Java将会执行如下调整。- 自动将employee3从department2.employees集合中删除。
- 自动将employee3.department设置为department1。
- 自动将employee1的index从0变成1。
- 自动将employee2的index从1变成2。
- 自动将employee4的index从1变成0。
最终,数据结构变成这样
部门对象 员工对象 变量名 employees 变量名 department index department1 [ employee3, employee1, employee2 ] employee1 department1 1 employee2 department1 2 employee3 department1 0 department2 [ employee4 ] employee4 department2 0 -
1.3.2. 不稳定对象。
在上个例子中,关联字段使用了java.util.List集合,除此之外,集合关联属性还可以使用java.util.Set或java.util.Map。 接下来的例子中,我们使用java.util.Set来描述公司和投资人之间的双向多对多关系。
- 一个公司可以由多个投资人投资。
@Model //该类使用ObjectModel4Java,需编译时字节码增强 public class Company { @Scalar //标量字段,非关联。 private String shortName; @Association(opposite = "companies") //该字段和“Investor.companies”字段互为镜像,构成双向关联。 // @ComparatorRule注解表示该Set集合使用Investor.name字段 // 计算Investor对象的hashCode以及判断两个Investor对象是否相等。 @ComparatorRule(properties = @ComparatorProperty("name")) private Set<Investor> investors; 此处略去Getter和Setter }
- 一个公司可以由多个投资人投资。
-
一个投资者也可以由多个投资人投资。
@Model //该类使用ObjectModel4Java,需编译时字节码增强 public class Investor { @Scalar //标量字段,非关联。 private String name; @Association(opposite = "investors") //该字段和“Company.investors”字段互为镜像,构成双向关联。 private Set<Company> companies; 此处略去Getter和Setter }
-
一个投资者也可以由多个投资人投资。
假设现有3个对象,其中的一个Company类型的对象,变量名为apple,还有两个Investor对象,变量名为steve和sculley。 它们的数据如下:
公司对象 投资人对象 变量名 shortName investors 变量名 name companies apple Apple [ steve, sculley ] steve Steve [ apple ] sculley Sculley [ apple ] 现在开发人员执行如下一句代码:
sculley.setName("Steve");
执行这句代码后,sculley对象的name字段的值就和steve对象的name字段的值一样了,均为Steve; 但是Company类的investors字段 使用@ComparatorRule 注解限定该集合不允许出现name字段值相等的两个Investor对象。 这是一个矛盾的局面。
幸运的是,Company类的investors 字段支持”不稳定集合元素“;所以将会导致如下的效应。
- 为了保证Set集合的唯一性,在“不稳定集合元素”机制的作用下,steve对象将会从apple对象的 investors集合字段中被自动排挤出去。
- 由于steve对象不再是apple对象的投资人,为了维护双向关联的一致性,apple对象也将会从steve对象的companies集合中被自动删除。
最终,数据结构变成这样
公司对象 投资人对象 变量名 shortName investors 变量名 name companies apple Apple [ sculley ] steve Steve [] sculley Steve [ apple ]
-
-
2. JPA部分
-
2.1. ObjectModel4JPA
ObjectModel4JPA是ObjectModel4Java的扩展, 其功能和ObjectModel4Java一样, 但二者的目的不同,它允许在JPA实体类上使用ObjectModel,而非针对普通的Java类; 同样,二者的声明方式也有不同,更多的细节请参照demo-guide。
和ObjectModel4Java一样, ObjectModel4JPA也需要基于Maven插件的编译时字节码增强。
-
2.2. BabyFish Hibernate Collection Framework
- 在ObjectModel4Java中, 作为关联字段的集合支持双向关联一致性以及X集合框架和MA集合框架的所有功能。 ObjectModel4JPA既然功能与前者一样,其关联字段的集合当然也要具备这些功能。
- 由于ObjectModel4JPA是为JPA/Hibernate设计的, 所以,关联字段的集合必须继承原生Hibernate集合的延迟加载能力。
为了让实体对象的集合字段能具备以上所有能力,BabyFish扩展Lazy集合框架,给出了 自己的Hibernate集合实现。
此功能并无相关demo,因为这是BabyFish Hibernate功能扩展内部模块的一部分。 作为开发人员,只需要知道ObjectModel4JPA中的集合既有ObjectModel4Java中集合的功能 也有原生的Hibernate中集合的功能即可。
-
2.3. JPA & Hibernate Extension
BabyFish对JPA和Hibernate API进行了扩展,提供了一些新功能, 本文仅讨论最重要的两个:QueryPath和Oracle Optimization (请参考demo-guide以了解所有功能)。
-
2.3.1. Query Path
为了在查询业务支持支持动态的贪懒加载(关联对象的抓取方式由业务层和表示层的通过传递参数动态决定,而非在数据层内部硬编码实现), BabyFish支持一个叫“Query Path”的功能,它看起来非常类似于
此功能比从JPA2.1开始支持的 javax.persistence.EntityGraph 更简单,更优雅,更强大。请忘记后者并在实际项目中使用QueryPath。和 Active Record的的贪婪加载能力 以及 ADO.NET Entity Framework的贪婪加载能力 相比,QueryPath具备如下三个优势。
- Active Record和ADO.NET Entity Framework的包含路径是类型不安全的字符串, 错误路径的错误要等到运行或测试程序才会被报告。 而Query Path则通过Maven插件在编译时生成Query Path元模型源码的方式让路径强类型化,错误的路径会在编译时报错。
- QueryPath不仅仅可以以当前被查询对象为核心对任意深度和广度的的关联属性进行贪婪加载,还可以对延迟的标量属性(不一定但往往是大字段)进行贪婪加载。
- QueryPath还可以对被查询对象本身以及对象之间的集合关联进行排序。
-
2.3.2. Oracle Optimization
Hibernate有一个性能缺陷。当某个查询既包含对集合关联的抓取(join fetch)又具备分页条件时, Hibernate无法通过SQL语句实现分页查询,而是先不分页地查询出所有满足条件的对象,然后在内存中完成分页筛选, 同时在日志文件中打印如下警告:
"firstResult/maxResults specified with collection fetch; applying in memory!"针对Oracle数据库,BabyFish能解决此问题,只需要开发使用BabyFish扩展过的Oracle方言即可,如下:
-
-
许可:LPGL3.0
BabyFish采用the LGPL-3.0授权,可以在商业应用中免费试用, 请参考http://opensource.org/licenses/LGPL-3.0以了解更多。
鸣谢
历史
- 2008年8月: 我有了某些点子呢, 开始利用工作外的业余时间开发此框架。
- 2015年10月: 完成第一个版本1.0.0并上传至github
- 2016年6月: 完成1.1.0版本。
联系我,提出意见和期望
2016年6月25日于成都。