领域对象转换库 RapidOOO v1.1.5
领域对象转换库 RapidOOO v1.1.5
注意:之前的版本弃用
RapidOOO Github: https://github.com/wangjiegulu/RapidOOO
什么是 RapidOOO
我们在领域驱动设计中经常会在不同层级之间传递数据,例如 VO, PO, DO, DTO, BO等。Android 的开发中也经常会遇到这些情况,比如在 Android-CleanArchitecture 的 UserModelDataMapper::transform, UserEntityDataMapper::transform 等。手工地进行拷贝转换的过程不但繁琐,而且错误的风险比较大,在新增、删除字段时也增加了维护的成本。Dozer 可以很好地解决这个问题,但是在 Android 上可能就不太适用了。
RapidOOO 根据 POJO 类编译时灵活地自动生成支持扩展互相绑定的领域对象。
怎么使用
在 build.gradle 中配置依赖 RapidOOO:
1 | implementation "com.github.wangjiegulu:rapidooo-api:x.x.x" |
注意:
x.x.x为版本号,最新版本点这里检查。
基本使用
存在 Person 类如下:
1 | public class Person { |
要自动生成 PersonBO 类,则创建 Generator 类并进行如下配置:
1 | (suffix = "BO", |
注意:
Generator类的类名可以任意。
构建之后,将会生成如下 PersonBO 与 Person 一样的类:
1 | public class PersonBO { |
注解配置自动生成
假设:通过
UserBO类自动生成UserVO类,则:
UserBO称为:源类UserVO称为:目标类
首先要创建 Generator 类,然后在该类上增加注解配置,该类表示 一次 自动生成转换,一次 自动生成转换中可以生成 多个 领域类。
重要的注解配置如下:
@OOOs 注解
@OOOs用于指定该次自动生成转换的所有配置。
- fromSuffix:
目标类后缀,比如从UserBO自动生成UserVO,则fromSuffix为UserBO的后缀:BO。如果源类没有后缀,则不填。 - suffix:
目标类后缀,比如从UserBO自动生成UserVO,则suffix为UserVO的后缀:VO。 - ooosPackages:
源类所在的包数组,RapidOOO将会把这些包中的类都会进行自动生成。 - ooos:
@OOO注解数组,目标类的自定义配置(后面会讲到)。
@OOO 注解
@OOOs用于指定目标类的自定义配置。
- id:该
目标类的id,id 必须以#号开头。 - fromSuffix:同
@OOOs中的fromSuffix,此优先级高。 - suffix:同
@OOOs中的suffix,此优先级高。 - from:指定
源类的Class。 - includes:指定该
目标类包含哪些字段(Field),不能与excludes同时使用。 - excludes:指定该
目标类不包含哪些字段(Field),不能与includes同时使用。 - targetSupperType:指定该
目标类的父类的 Class。 - targetSupperTypeId:指定该
目标类的父类的 id 表达式,id 必须以#号开头。 - pool:针对该
目标类类的对象池相关配置(后面会讲到)。 - parcelable:配置该
目标类类是否实现Parcelable接口(前提是源类是实现了Parcelable接口)。 - conversions:
@OOOConversion数组,表示该目标类中需要进行特殊转换的字段配置(后面会讲到)。
OOOConversion 注解
@OOOs用于指定目标类中某个字段的自定义配置。
- targetFieldName:该字段的名字。
- targetFieldType:指定该字段的 Class。
- targetFieldTypeId:指定该字段的 id 表达式。
- attachFieldName:表示字段模式(后面会讲到)为
attach,指定与源类的哪个字段 attach。 - bindMethodClass:表示字段模式(后面会讲到)为
bind,指定绑定转换的方法所在的类(后面会讲到)。 - bindMethodName:表示字段模式(后面会讲到)为
bind,指定绑定转换的方法(后面会讲到)。 - inverseBindMethodName:表示字段模式(后面会讲到)为
bind,指定绑定逆转换的方法(后面会讲到)。 - conversionMethodClass:表示字段模式(后面会讲到)为
conversion,指定转换的方法所在的类(后面会讲到)。 - conversionMethodName:表示字段模式(后面会讲到)为
conversion,指定转换的方法(后面会讲到)。 - inverseConversionMethodName:表示字段模式(后面会讲到)为
conversion,指定逆转换的方法(后面会讲到)。 - parcelable:是否可以实现
parcelable接口。 - controlDelegate:配置该字段的控制委托(后面会讲到)。
转换方法自动生成
通过注解配置,在构建后会自动生成如下两个方法:
- create() static 方法:
源类转为目标类,如:UserVO userVO = UserVO.create(userBO) - toXxx() 成员方法:
目标类转为源类,如:userVO.toUserBO()。
id 类型表达式
通过字符串来表示类型(包括
id)。
- ≈ 的
id必须以#号开头。 - 在
targetFieldTypeId等字段中可以使用 id 表达式来表达复杂类型(后面会提到)。
链式转换
通过配置支持链式的自动生成:从 Person 生成 PersonBO,从 PersonBO 生成 PersonVO…
1 | // 生成 PersonBO |
继承
如果 Abbey 类继承了 Person 类,如下:
1 | public class Abbey extends Person { |
可以通过以下配置来生成 AbbeyBO 类,并且继承同样是生成的 PersonBO:
1 | (suffix = "BO", |
- 先配置
Person类,然后设置它的id(任意字符,但是必须是#开头),如#id_bo_person(此配置会生成PersonBO类) - 然后配置
Abbey类,设置targetSupperTypeId为#id_bo_person(为Abbey类设置父类)
最终生成如下:
1 | public class AbbeyBO extends PersonBO { |
支持的接口
默认情况下,源类 实现的接口不会在生成的 目标类 中自动也去实现。
但是以下两个接口除外:
Serializable
如果 源类 实现了 Serializable 接口,在生成的 目标类 中也会自动默认实现该接口。如下:
1 | // 源类 |
Parcelable
如果 源类 实现了 Parcelable 接口,在生成的 目标类 中也会自动默认实现该接口(包括自动生成 构造方法、CREATOR、writeToParcel()、describeContents 等)。如下:
1 | // 源类 |
但是你可以通过 @OOO 注解中的 parcelable 设置为 false 来禁用所有字段不进行序列化。
如果你只需要生成类中的某个字段不进行 parcelable 序列化,可以通过 @OOOConversion 注解中的 parcelable 设置为 false 来禁用它。
包含排除字段
可以通过 @OOO 中的 include 和 excludes 两个属性来指定 源类 中的哪些字段 需要/不需要 生成在 目标类 中。如下:
1 | (suffix = "BO", ooos = { |
以上,在生成的 MessageBO 中不会包含来自 Message 的 chat 和 text 两个字段。
includes 的使用方式也是一样。
注意:
includes和excludes如果都没有设置,则默认为源类中的所有字段 都会 生成在目标类中。includes和excludes不能同时使用,否则编译会报错。
字段模式 Field Mode
可以在 @OOO 注解中通过 @OOOConversion 注解来对每个 目标类 中的每个字段通过不同的 字段模式(Field Mode) 来进行配置。
具体的配置方式如下:
字段增加
在 源类 生成 目标类 时,可以在 目标类 中增加一些 源类 中不存在的字段,如下:
1 | // `源类` |
以上通过 @OOOConversion 注解增加了一个字段名为 read、类型为 boolean 的字段,并生成了 getter/setter 方法。
注意:这个
read字段与Message类中的任何字段都没有任何关系。
字段连接 Attach Mode
如果我需要在 目标类 增加一个字段,并且把这个字段与 源类 中的相似类型的字段 连接 起来。
或者说用新的字段替换掉 源类 的相似类型的字段,比如:
1 | // `源类` |
如上注释:首先通过 excludes 排除掉要被关联的 content 字段,然后再用 @OOOConversion 注解配置新的 text 字段,并通过 attachFieldName 连接到 源类 的 content 字段。
最终的结果是,目标类 中的 content 被替换成了 text。
那怎么体现了 连接 这个特点呢?
如果是 连接(attach) 模式,则 目标类 中还会在某些方法中生成以下代码来确保新增字段与原来的字段是处于连接状态的:
1 | public class MessageBO { |
由此,Message 中的 content 与 MessageBO 中的 text 字段达成了连接,于是如下单元测试通过:
1 | final String CONTENT = "message content"; |
以上的例子是 源类 Message 的 content 与 目标类 MessageBO 的 text 之间的连接,两者都是同一种类型: String。
除了支持同一种类型的连接之外,其实还支持以下的类型的连接:
| 序号 | 源类字段 | 目标类字段 | 举例(源 -> 目标) |
|---|---|---|---|
| 1) | 某种任意类型 | 某种相同的任意类型 | FooBar -> FooBar |
| 2) | 某种 Map 类型 |
另一种相同或不同的 Map 类型(泛型相同) |
HashMap -> LinkedHashMap |
| 3) | 某种 List 类型 |
另一种相同或不同的 List 类型(泛型相同) |
ArrayList -> LinkedList |
| 4) | 某种数组类型 | 数组类型 | Foo[] -> Foo[] |
| 5) | 某种其它源类型 | 某种其它目标类型 | Chat -> ChatBO |
| 6) | 序号2)``3)``4)与5)的组合 |
序号2)``3)``4)与5)的组合 |
Map<String, Chat> -> HashMap<String, ChatBO> 或 ArrayList<Chat> -> List<ChatBO> |
字段转换 / 逆转换 Conversion Mode
使用 attach 这种 产生连接 以自动在 源 和 目标 字段中转换的方式,在有些实际场景可能无法满足需求,所以 RapidOOO 还提供了更为灵活的 字段转换 模式,你可以通过 @OOOConversion 注解的 conversionMethodName 和 inverseConversionMethodName 来指定转换 / 逆转换的方法,如下例子:
1 | // 源类 |
先看 gender 转换为 genderDesc 的配置:
通过指定 @OOOConversion 注解中的 conversionMethodName 方法,此方法可以编写在本次转换的 Generator 类中,也就是上面的 VOGenerator,方法名为 conversionGenderDesc,并且该方法必须满足以下几个条件:
- 方法必须是
public static - 方法的返回值必须为
targetFieldType指定的类型,方法返回值即为转换结果。 - 参数表示你本次转换需要使用到的对象,可以任意多个,但是必须满足以下几个条件之一:
- 参数名为对应的
目标类中的字段名,并且该参数类型与对应的字段要一致,生成的代码在调用时会把对应字段转入到该方法。 - 参数名为
self,类型为目标类,生成的代码在调用时会把当前的目标类对象传入到该方法。 - 参数名为
other,类型为源类,生成的代码在调用时会把当前的源类对象传入到该方法。
- 参数名为对应的
genderDesc 转换为 gender 的配置:
通过指定 @OOOConversion 注解中的 inverseConversionMethodName 方法,此方法可以编写在本次转换的 Generator 类中,也就是上面的 VOGenerator,方法名为 inverseConversionGender,并且该方法必须满足以下几个条件:
- 方法必须是
public static - 方法的返回值必须为
void - 参数表示你本次转换需要使用到的对象,可以任意多个,但是必须满足以下几个条件之一:
- 参数名为对应的
目标类中的字段名,并且该参数类型与对应的字段要一致,生成的代码在调用时会把对应字段转入到该方法。 - 参数名为
self,类型为目标类,生成的代码在调用时会把当前的目标类对象传入到该方法。 - 参数名为
other,类型为源类,生成的代码在调用时会把当前的源类对象传入到该方法。
- 参数名为对应的
转换结果通过调用 other 方法参数去进行设置(setter)。
注意:
conversionMethodName和inverseConversionMethodName也可以只设置其中一个。一般情况下使用 字段转换 模式的同时应该把对应的所有相关的字段(
conversionMethodName和inverseConversionMethodName中参数对应的所有字段)会exclude掉。
conversionMethodName和inverseConversionMethodName的对应的方法默认是在Generator中编写,但是你也可以通过指定@OOOConversion注解的conversionMethodClass来指定转换方法所在的类。
字段绑定 / 逆绑定 Bind Mode
如果需要把转换相应的 源类 字段继续保留,则可以使用 字段绑定(Bind) 模式。使用方式与 字段转换(Conversion Mode) 模式比较类似,同样提供了两个方法 bindMethodName 和 inverseBindMethodName 来指定绑定 / 逆绑定的方法,如下例子:
1 | // 源类 |
如上,通过设置 bindMethodName 和 inverseBindMethodName 来设置绑定和逆绑定的方法,并设置 bindMethodClass 来指定这些方法所在的类。
生成的 目标类 中,与 字段转换 Conversion Mode 不同的是,当某个字段发生改变时(setter 方法被调用),对应绑定的字段也会被更新(绑定和逆绑定方法被调)。也就是说,字段绑定 Bind Mode 时,字段和该字段对应绑定的字段将会实时双向同步数据。
age 转换为 ageDesc 的配置:
通过指定 @OOOConversion 注解中的 bindMethodName 方法,该方法必须满足以下几个条件:
- 方法必须是
public static - 方法的返回值必须为
targetFieldType指定的类型,方法返回值即为转换结果。 - 参数表示你本次转换需要使用到的对象,可以任意多个,但是必须满足以下几个条件之一:
- 参数名为对应的
目标类中的字段名,并且该参数类型与对应的字段要一致,生成的代码在调用时会把对应字段转入到该方法。 - 参数名为
self,类型为目标类,生成的代码在调用时会把当前的目标类对象传入到该方法。 - 参数名为
other,类型为源类,生成的代码在调用时会把当前的源类对象传入到该方法。
- 参数名为对应的
ageDesc 转换为 age 的配置:
通过指定 @OOOConversion 注解中的 inverseBindMethodName 方法,该方法必须满足以下几个条件:
- 方法必须是
public static - 方法的返回值必须为
void - 参数表示你本次转换需要使用到的对象,可以任意多个,但是必须满足以下几个条件之一:
- 参数名为对应的
目标类中的字段名,并且该参数类型与对应的字段要一致,生成的代码在调用时会把对应字段转入到该方法。 - 参数名为
self,类型为目标类,生成的代码在调用时会把当前的目标类对象传入到该方法。 - 参数名为
other,类型为源类,生成的代码在调用时会把当前的源类对象传入到该方法。
- 参数名为对应的
转换结果通过调用 other 方法参数去进行设置(setter)。
注意:
bindMethodName和inverseBindMethodName也可以只设置其中一个。一般情况下使用 字段绑定 模式的同时应该保留所有相关的字段(
bindMethodName和inverseBindMethodName中参数对应的所有字段)。
bindMethodName和inverseBindMethodName的对应的方法默认是在Generator中编写,但是你也可以通过指定@OOOConversion注解的bindMethodClass来指定转换方法所在的类。
对象池
为了解决在不同领域模型中对象频繁地互相转换所带来的性能问题,RapidOOO 也支持使用对象池来构建领域对象,方法如下:
1 | // 源类 |
如上,通过 @OOO 注解中的 pool 来配置对象池的 acquireMethod 和 releaseMethod 方法。
其中 acquireMethod 方法必须满足如下的要求:
- 方法必须是
public static - 方法无参数
- 方法返回值为
目标类类型
其中 releaseMethod 方法必须满足如下的要求:
- 方法必须是
public static - 方法参数必须是只有 1 个,并且类型为
目标类类型 - 方法返回值为
void
复杂类型
使用 @OOOConversion 扩展字段时,目前支持以下多种类型:
Array
使用数组时,需要按照如下设置 targetFieldTypeId:
1 | // ... |
如上:支持 id 和具体类型的权限定名,后面加上 [],表示数组。
List
使用 List 时,需要按照如下设置 targetFieldTypeId:
1 | // ... |
如上:泛型支持 id 和具体类型的权限定名,List 支持权限定名的 List, ArrayList, LinkedList 等等。
Map
使用 Map 时,需要按照如下设置 targetFieldTypeId:
1 | // ... |
如上:泛型(Key 和 Value)支持 id 和具体类型的权限定名,Map 支持权限定名的 Map, HashMap, TreeMap 等等。
控制委托 Control Delegate
如果你需要对转换的字段做一些额外的处理,你可以通过 控制委托 Control Delegate 来实现,比如通过 OOOLazyControlDelegate 来实现懒初始化字段。
注意:目前
控制委托 Control Delegate只支持bindMethod和conversionMethod,并不支持inverseBindMethod和inverseConversionMethod
OOOLazyControlDelegate
什么情况下可能需要使用到懒加载?
比如通过 Message 中的 videoUrl 转换为 MessageVO 中的 MediaPlayer 类,避免资源浪费,MediaPlayer 实例应该作为懒加载的对象,如下:
1 | ( |
以上,通过 @OOOConversion 中的 controlDelegate 配置对应的控制委托的 Class 类型即可,这里配置了 OOOLazyControlDelegate.class。
conversionLazyVideo 方法只会在 OOOLazy<MediaPlayer> 调用 getter 时才会被调用。
自定义 ControlDelegate
ControlDelegate 还支持自定义扩展,继承 OOOControlDelegate 类,实现 invoke 方法即可。
其中 invoke 中的两个参数:
Func0R<T> inputFunc:该方法调用call()就会直接调用bindMethod或conversionMethod方法,返回 T 类型结果Func1<R> outputFunc:该方法调用后,将会把输入的值赋值给目标类中的对应字段。
其中 inputFunc 和 outputFunc 的调用时机可以通过自定义的 ControlDelegate 进行控制。
比如以下是扩展的 OOONewThreadControlDelegate,表示字段在进行 bind / conversion 时,将会在新的线程中执行:
1 | public class OOONewThreadControlDelegate<T> implements OOOControlDelegate<T, T> { |
注意:以上
OOONewThreadControlDelegate只供自定义实现控制委托方式的参考。
本文链接:https://blog.wangjiegulu.com/2019/07/02/rapidooo-v115/
版权声明:本博客所有文章除特别声明外,均采用 CC BY 4.0 CN协议 许可协议。转载请注明出处。