本篇是 Java OSGL 工具库系列的第六篇, 前面五篇分别是:
1. API 一览
// shallow copy from `foo` to `bar`$.copy(foo).to(bar);// deep copy from `foo` to `bar$.deepCopy(foo).to(bar);// deep copy using loose name match$.deepCopy(foo).looseMatching().to(bar);// deep copy with filter$.deepCopy(foo).filter("-password,-address.streetNo").to(bar);// deep copy with special name mapping rule$.deepCopy(foo) .map("id").to("no") .map("subject").to("title") .to(bar);// merge data from `foo` to `bar`$.merge(foo).to(bar);// map data from `foo` to `bar`$.map(foo).to(bar);// map data from `foo` to `bar` using strict name match$.map(foo).strictMatching().to(bar);// merge map data from `foo` to `bar`$.mergeMap(foo).to(bar);
2. 概念
OSGL 依赖于 Java 反射来获得 Bean 的内部结构. 和很多其他工具不同, OSGL 使用字段而不是 Getter/Setter 来获取内部数据
2.1 语义
OSGL 提供一下五种不同的 Bean 拷贝语义:
- SHALLOW_COPY - 浅拷贝: 拷贝第一层字段的引用. API:
$.copy(foo).to(bar)
- DEEP_COPY- 深拷贝: 递归拷贝过程直到遇到不可变 (Immutable) 类型. API:
$.deepCopy(foo).to(bar)
- MERGE - 融合: 和深拷贝类似, 但碰到数组时容器的情况将源数据添加到目标容器. API:
$.merge(foo).to(bar)
- MAP - 印射: 和深拷贝类似, 并支持数据类型转换. API:
$.map(foo).to(bar)
- MERGE_MAP - 融合印射: 和融合类似, 并支持数据类型转换. API:
$.mergeMap(foo).to(bar)
2.1.1 不可变类型
不可变类型在 OSGL Bean 深度拷贝过程中是非常重要的概念. 当 OSGL 发现拷贝的数据类型为不可变时, 深度拷贝过程将终止, 并直接将数据引用拷贝到目标 Bean.
OSGL 认定以下类型为不可变类型:
- 所有的基本数据类型及其包装类型
- 字符串
- 枚举
- 所有使用
OsglConfig.registerImmutableClassNames
API 注册的类型 - 所有使用
OsglConfig.registerImmutableClassPredicate($.Predicate)
API 注册的判定函数返回true
的类型
2.2 名字匹配
OSGL 支持数据名字匹配
- 严格匹配, 源数据和目标数据的字段名必须完全一致. 这是默认匹配方式.
Keyword
匹配, 也叫loose
匹配. 当采用这种匹配方式的时候, 下面的名字被认定为相匹配的名字:
- foo_bar
- foo-bar
- fooBar
- FooBar
- Foo-Bar
- Foo_Bar
- 特殊匹配
下面是一段特殊匹配的示例代码:
$.deepCopy(foo) .map("id").to("no") .map("subject").to("title") .to(bar);
上面代码指示 OSGL 将 foo
对象的 id
字段拷贝到 bar
对象的 no
字段, 并将 foo
的 subject
字段拷贝到 bar
的 title
字段.
2.3 过滤器
过滤器用来指定在拷贝过程中忽略某些字段. 下面是过滤器的一些例子:
-email,-password
- 忽略email
和password
字段;其他所有字段都需要拷贝+email
- 仅拷贝email
字段, 其他所有字段都不拷贝-cc.cvv
- 忽略cc
字段对象的cvv
字段, 其他所有字段都拷贝-cc,+cc.cvv
- 对于cc
字段对象, 仅拷贝其cvv
字段, 忽略其他所有字段
使用过滤器的 API:
$.deepCopy(foo).filter("-password,-address.streetNo").to(bar);
注意 过滤器匹配目标对象, 而非源对象
2.4 根类型
上面我们有提到 OSGL 依赖于字段来获得拷贝数据. 因为 Java 类型继承的原因, 获取字段是一个递归过程直到遇到 Object.class
. 有时候我们希望递归过程更早结束, 这个时候可以指定根类型. 假设我们有下面的类:
public abstract class ModelBase { public Date _created;}
假设拷贝源的类型是 ModelBase
的子类, 而你的 Dao
使用 ModelBase._created
来判断某个 Entity 是新建记录, 还是从数据库中加载的老记录. 这种情况下, 当你需要拷贝某个老记录 Bean 到一个新记录 Bean, 并使用 Dao
来保存这个新建记录的时候就需要注意不能拷贝 ModelBase._created
字段. 因此需要指定根类型:
MyModel copy = $.copy(existing).rootClass(ModelBase.class).to(MyModel.class);
2.5 目标的泛型
当目标对象是一个泛型, 例如容器时, 需要提供 targetGenericType
来完成拷贝:
ListfooList = C.list(new Foo(), new Foo());List barList = C.newList();$.map(fooList).targetGenericType(new TypeReference
>(){}).to(barList);
2.6 类型转换
使用印射语义进行拷贝时, OSGL 自动在源数据类型和目标数据类型之间做转换. 假设源 Bean 定义为:
public class RawData { Calendar date; public RawData(long currentTimeMillis) { date = Calendar.getInstance(); date.setTimeInMillis(currentTimeMillis); }}
目标 Bean 定义为:
public static class ConvertedData { DateTime date;}
当我们需要将 RawData
拷贝到 ConvertedData
时, 需要将源数据 date
从 Calendar
类型转换到目标数据 date
的 DateTime
类型. 开发可以写一个类型转换器:
public static Lang.TypeConverterconverter = new Lang.TypeConverter () { @Override public DateTime convert(Calendar calendar) { return new DateTime(calendar.getTimeInMillis()); }};
并在 API 调用中指定类型转换器:
@Testpublic void testWithTypeConverter() { RawData src = new RawData($.ms()); ConvertedData tgt = $.map(src).withConverter(converter).to(ConvertedData.class); eq(tgt.date.getMillis(), src.date.getTimeInMillis());}
注意
- 在实际中你可能不需要定义很多类型转换器, 包括上面那个
Calendar
到DateTime
的, 因为 OSGL 已经默认提供了大部分需要用到的. 更多关于类型转换器的情况参见 - 类型转换仅适用于
MAP
和MERGE_MAP
语义,SHALLOW_COPY
,DEEP_COPY
和MERGE
语义不支持类型转换
2.6.1 转换提示
有的情况需要给出转换提示来帮助类型转换正确进行. 一个典型的例子是从字符串转换为日期, 这个过程需要提供日期格式作为转换提示. 另一个例子是从字符串转换为整型, 可以提供 radix
转换提示来调整转换过程. 下面是一个字符串到日期类型转换的案例.
源 Bean 定义:
public static class RawDataV2 { String date; public RawDataV2(String date) { this.date = date; }}
目标 Bean 定义:
public static class ConvertedDataV2 { Date date;}
使用转换提示进行源 Bean 到目标 Bean 拷贝:
RawDataV2 src = new RawDataV2("20180518");ConvertedDataV2 tgt = $.map(src).conversionHint(Date.class, "yyyyMMdd").to(ConvertedDataV2.class);
从代码中我们看到转换提示 yyyyMMdd
和目标数据类型 Date.class
绑定在一起, 这是告诉 OSGL, 当转换目标类型为 Date
的时候, 使用转换提示 yyyyMMdd
.
2.7 实例工厂
在拷贝过程中, 有可能需要就某个类型创建实例. 默认情况下 OSGL 使用 org.osgl.Lang.newInstance(Class)
API 来创建实例. 有的环境下, 例如当应用运行在 下的时候, 需要将实例创建交给容器 API Act.newInstance(Class)
. OSGL 提供 API 来注册实例工厂来替代默认创建实例的逻辑:
OsglConfig.registerGlobalInstanceFactory(new $.Function() { final App app = Act.app(); @Override public Object apply(Class aClass) throws NotAppliedException, $.Break { return app.getInstance(aClass); }});
4. 总结
OSGL 提供了一套功能完善的 API 支持 Bean 的拷贝操作, 包括 5 种拷贝语义. 如果您喜欢 OSGL, 这里是 maven 坐标:
org.osgl osgl-tool ${osgl-tool.version}
目前最新的 ${osgl-tool.version}
是 1.17.0