Java OSGL 工具库-Bean 拷贝的艺术

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 拷贝语义:

  1. SHALLOW_COPY - 浅拷贝: 拷贝第一层字段的引用. API:
  • $.copy(foo).to(bar)
  1. DEEP_COPY- 深拷贝: 递归拷贝过程直到遇到不可变 (Immutable) 类型. API:
  • $.deepCopy(foo).to(bar)
  1. MERGE - 融合: 和深拷贝类似, 但碰到数组时容器的情况将源数据添加到目标容器. API:
  • $.merge(foo).to(bar)
  1. MAP - 印射: 和深拷贝类似, 并支持数据类型转换. API:
  • $.map(foo).to(bar)
  1. 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 支持数据名字匹配

  1. 严格匹配, 源数据和目标数据的字段名必须完全一致. 这是默认匹配方式.
  2. Keyword 匹配, 也叫 loose 匹配. 当采用这种匹配方式的时候, 下面的名字被认定为相匹配的名字:
  • foo_bar
  • foo-bar
  • fooBar
  • FooBar
  • Foo-Bar
  • Foo_Bar
  1. 特殊匹配

下面是一段特殊匹配的示例代码:

$.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 来完成拷贝:

List fooList = 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.TypeConverter converter = 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 来创建实例. 有的环境下, 例如当应用运行在 ActFramework 下的时候, 需要将实例创建交给容器 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}作者:罗格林


分享到:


相關文章: