Java 如何優(yōu)雅的拷貝對象屬性
場景
在 Java 項目中,經常遇到需要在對象之間拷貝屬性的問題。然而,除了直接使用 Getter/Stter 方法,我們還有其他的方法么?
當然有,例如 Apache Common Lang3 的 BeanUtils,然而 BeanUtils 卻無法完全滿足吾輩的需求,所以吾輩便自己封裝了一個,這里分享出來以供參考。
- 需要大量復制對象的屬性
- 對象之間的屬性名可能是不同的
- 對象之間的屬性類型可能是不同的
目標
簡單易用的 API
- copy: 指定需要拷貝的源對象和目標對象
- prop: 拷貝指定對象的字段
- props: 拷貝指定對象的多個字段
- exec: 執(zhí)行真正的拷貝操作
- from: 重新開始添加其他對象的屬性
- get: 返回當前的目標對象
- config: 配置拷貝的一些策略
思路
- 定義門面類 BeanCopyUtil 用以暴露出一些 API
- 定義每個字段的操作類 BeanCopyField,保存對每個字段的操作
- 定義 BeanCopyConfig,用于配置拷貝屬性的策略
- 定義 BeanCopyOperator 作為拷貝的真正實現(xiàn)
圖解

實現(xiàn)
注:反射部分依賴于 joor, JDK1.8 請使用 joor-java-8
定義門面類 BeanCopyUtil 用以暴露出一些 API
/**
* java bean 復制操作的工具類
*
* @author rxliuli
*/
public class BeanCopyUtil<F, T> {
/**
* 源對象
*/
private final F from;
/**
* 目標對象
*/
private final T to;
/**
* 拷貝的字段信息列表
*/
private final List<BeanCopyField> copyFieldList = new LinkedList<>();
/**
* 配置信息
*/
private BeanCopyConfig config = new BeanCopyConfig();
private BeanCopyUtil(F from, T to) {
this.from = from;
this.to = to;
}
/**
* 指定需要拷貝的源對象和目標對象
*
* @param from 源對象
* @param to 目標對象
* @param <F> 源對象類型
* @param <T> 目標對象類型
* @return 一個 {@link BeanCopyUtil} 對象
*/
public static <F, T> BeanCopyUtil<F, T> copy(F from, T to) {
return new BeanCopyUtil<>(from, to);
}
/**
* 拷貝指定對象的字段
*
* @param fromField 源對象中的字段名
* @param toField 目標對象中的字段名
* @param converter 將源對象中字段轉換為目標對象字段類型的轉換器
* @return 返回 {@code this}
*/
public BeanCopyUtil<F, T> prop(String fromField, String toField, Function<? super Object, ? super Object> converter) {
copyFieldList.add(new BeanCopyField(fromField, toField, converter));
return this;
}
/**
* 拷貝指定對象的字段
*
* @param fromField 源對象中的字段名
* @param toField 目標對象中的字段名
* @return 返回 {@code this}
*/
public BeanCopyUtil<F, T> prop(String fromField, String toField) {
return prop(fromField, toField, null);
}
/**
* 拷貝指定對象的字段
*
* @param field 源對象中與目標對象中的字段名
* @param converter 將源對象中字段轉換為目標對象字段類型的轉換器
* @return 返回 {@code this}
*/
public BeanCopyUtil<F, T> prop(String field, Function<? super Object, ? super Object> converter) {
return prop(field, field, converter);
}
/**
* 拷貝指定對象的字段
*
* @param field 源對象中與目標對象中的字段名
* @return 返回 {@code this}
*/
public BeanCopyUtil<F, T> prop(String field) {
return prop(field, field, null);
}
/**
* 拷貝指定對象的多個字段
*
* @param fields 源對象中與目標對象中的多個字段名
* @return 返回 {@code this}
*/
public BeanCopyUtil<F, T> props(String... fields) {
for (String field : fields) {
prop(field);
}
return this;
}
/**
* 執(zhí)行真正的拷貝操作
*
* @return 返回 {@code this}
*/
public BeanCopyUtil<F, T> exec() {
new BeanCopyOperator<>(from, to, copyFieldList, config).copy();
return this;
}
/**
* 重新開始添加其他對象的屬性
* 用于在執(zhí)行完 {@link #exec()} 之后還想復制其它對象的屬性
*
* @param from 源對象
* @param <R> 源對象類型
* @return 一個新的 {@link BeanCopyUtil} 對象
*/
public <R> BeanCopyUtil<R, T> from(R from) {
return new BeanCopyUtil<>(from, to);
}
/**
* 返回當前的目標對象
*
* @return 當前的目標對象
*/
public T get() {
return to;
}
/**
* 配置拷貝的一些策略
*
* @param config 拷貝配置對象
* @return 返回 {@code this}
*/
public BeanCopyUtil<F, T> config(BeanCopyConfig config) {
this.config = config;
return this;
}
}
定義每個字段的操作類 BeanCopyField,保存對每個字段的操作
/**
* 拷貝屬性的每一個字段的選項
*
* @author rxliuli
*/
public class BeanCopyField {
private String from;
private String to;
private Function<? super Object, ? super Object> converter;
public BeanCopyField() {
}
public BeanCopyField(String from, String to, Function<? super Object, ? super Object> converter) {
this.from = from;
this.to = to;
this.converter = converter;
}
public String getFrom() {
return from;
}
public BeanCopyField setFrom(String from) {
this.from = from;
return this;
}
public String getTo() {
return to;
}
public BeanCopyField setTo(String to) {
this.to = to;
return this;
}
public Function<? super Object, ? super Object> getConverter() {
return converter;
}
public BeanCopyField setConverter(Function<? super Object, ? super Object> converter) {
this.converter = converter;
return this;
}
}
定義 BeanCopyConfig,用于配置拷貝屬性的策略
/**
* 拷貝屬性的配置
*
* @author rxliuli
*/
public class BeanCopyConfig {
/**
* 同名的字段自動復制
*/
private boolean same = true;
/**
* 覆蓋同名的字段
*/
private boolean override = true;
/**
* 忽略 {@code null} 的源對象屬性
*/
private boolean ignoreNull = true;
/**
* 嘗試進行自動轉換
*/
private boolean converter = true;
public BeanCopyConfig() {
}
public BeanCopyConfig(boolean same, boolean override, boolean ignoreNull, boolean converter) {
this.same = same;
this.override = override;
this.ignoreNull = ignoreNull;
this.converter = converter;
}
public boolean isSame() {
return same;
}
public BeanCopyConfig setSame(boolean same) {
this.same = same;
return this;
}
public boolean isOverride() {
return override;
}
public BeanCopyConfig setOverride(boolean override) {
this.override = override;
return this;
}
public boolean isIgnoreNull() {
return ignoreNull;
}
public BeanCopyConfig setIgnoreNull(boolean ignoreNull) {
this.ignoreNull = ignoreNull;
return this;
}
public boolean isConverter() {
return converter;
}
public BeanCopyConfig setConverter(boolean converter) {
this.converter = converter;
return this;
}
}
定義 BeanCopyOperator 作為拷貝的真正實現(xiàn)
/**
* 真正執(zhí)行 copy 屬性的類
*
* @author rxliuli
*/
public class BeanCopyOperator<F, T> {
private static final Logger log = LoggerFactory.getLogger(BeanCopyUtil.class);
private final F from;
private final T to;
private final BeanCopyConfig config;
private List<BeanCopyField> copyFieldList;
public BeanCopyOperator(F from, T to, List<BeanCopyField> copyFieldList, BeanCopyConfig config) {
this.from = from;
this.to = to;
this.copyFieldList = copyFieldList;
this.config = config;
}
public void copy() {
//獲取到兩個對象所有的屬性
final Map<String, Reflect> fromFields = Reflect.on(from).fields();
final Reflect to = Reflect.on(this.to);
final Map<String, Reflect> toFields = to.fields();
//過濾出所有相同字段名的字段并進行拷貝
if (config.isSame()) {
final Map<ListUtil.ListDiffState, List<String>> different = ListUtil.different(new ArrayList<>(fromFields.keySet()), new ArrayList<>(toFields.keySet()));
copyFieldList = Stream.concat(different.get(ListUtil.ListDiffState.common).stream()
.map(s -> new BeanCopyField(s, s, null)), copyFieldList.stream())
.collect(Collectors.toList());
}
//根據拷貝字段列表進行拷貝
copyFieldList.stream()
//忽略空值
.filter(beanCopyField -> !config.isIgnoreNull() || fromFields.get(beanCopyField.getFrom()).get() != null)
//覆蓋屬性
.filter(beanCopyField -> config.isOverride() || toFields.get(beanCopyField.getTo()).get() == null)
//如果沒有轉換器,則使用默認的轉換器
.peek(beanCopyField -> {
if (beanCopyField.getConverter() == null) {
beanCopyField.setConverter(Function.identity());
}
})
.forEach(beanCopyField -> {
final String fromField = beanCopyField.getFrom();
final F from = fromFields.get(fromField).get();
final String toField = beanCopyField.getTo();
try {
to.set(toField, beanCopyField.getConverter().apply(from));
} catch (ReflectException e) {
log.warn("Copy field failed, from {} to {}, exception is {}", fromField, toField, e.getMessage());
}
});
}
}
使用
使用流程圖

測試
代碼寫完了,讓我們測試一下!
public class BeanCopyUtilTest {
private final Logger log = LoggerFactory.getLogger(getClass());
private Student student;
private Teacher teacher;
@Before
public void before() {
student = new Student("琉璃", 10, "女", 4);
teacher = new Teacher();
}
@Test
public void copy() {
//簡單的復制(類似于 BeanUtils.copyProperties)
BeanCopyUtil.copy(student, teacher).exec();
log.info("teacher: {}", teacher);
assertThat(teacher)
.extracting("age")
.containsOnlyOnce(student.getAge());
}
@Test
public void prop() {
//不同名字的屬性
BeanCopyUtil.copy(student, teacher)
.prop("sex", "sex", sex -> Objects.equals(sex, "男"))
.prop("realname", "name")
.exec();
assertThat(teacher)
.extracting("name", "age", "sex")
.containsOnlyOnce(student.getRealname(), student.getAge(), false);
}
@Test
public void prop1() {
//不存的屬性
assertThat(BeanCopyUtil.copy(student, teacher)
.prop("sex", "sex", sex -> Objects.equals(sex, "男"))
.prop("realname", "name2")
.exec()
.get())
.extracting("age", "sex")
.containsOnlyOnce(student.getAge(), false);
}
@Test
public void from() {
final Teacher lingMeng = new Teacher()
.setName("靈夢")
.setAge(17);
//測試 from 是否覆蓋
assertThat(BeanCopyUtil.copy(student, teacher)
.prop("sex", "sex", sex -> Objects.equals(sex, "男"))
.prop("realname", "name")
.exec()
.from(lingMeng)
.exec()
.get())
.extracting("name", "age", "sex")
.containsOnlyOnce(lingMeng.getName(), lingMeng.getAge(), false);
}
@Test
public void get() {
//測試 get 是否有效
assertThat(BeanCopyUtil.copy(student, teacher)
.prop("sex", "sex", sex -> Objects.equals(sex, "男"))
.prop("realname", "name")
.exec()
.get())
.extracting("name", "age", "sex")
.containsOnlyOnce(student.getRealname(), student.getAge(), false);
}
@Test
public void config() {
//不自動復制同名屬性
assertThat(BeanCopyUtil.copy(new Student().setAge(15), new Teacher())
.config(new BeanCopyConfig().setSame(false))
.exec()
.get())
.extracting("age")
.containsOnlyNulls();
//不覆蓋不為空的屬性
assertThat(BeanCopyUtil.copy(new Student().setAge(15), new Teacher().setAge(10))
.config(new BeanCopyConfig().setOverride(false))
.exec()
.get())
.extracting("age")
.containsOnlyOnce(10);
//不忽略源對象不為空的屬性
assertThat(BeanCopyUtil.copy(new Student(), student)
.config(new BeanCopyConfig().setIgnoreNull(false))
.exec()
.get())
.extracting("realname", "age", "sex", "grade")
.containsOnlyNulls();
}
/**
* 測試學生類
*/
private static class Student {
/**
* 姓名
*/
private String realname;
/**
* 年齡
*/
private Integer age;
/**
* 性別,男/女
*/
private String sex;
/**
* 年級,1 - 6
*/
private Integer grade;
public Student() {
}
public Student(String realname, Integer age, String sex, Integer grade) {
this.realname = realname;
this.age = age;
this.sex = sex;
this.grade = grade;
}
public String getRealname() {
return realname;
}
public Student setRealname(String realname) {
this.realname = realname;
return this;
}
public Integer getAge() {
return age;
}
public Student setAge(Integer age) {
this.age = age;
return this;
}
public String getSex() {
return sex;
}
public Student setSex(String sex) {
this.sex = sex;
return this;
}
public Integer getGrade() {
return grade;
}
public Student setGrade(Integer grade) {
this.grade = grade;
return this;
}
@Override
public String toString() {
return ToStringBuilder.reflectionToString(this);
}
}
/**
* 測試教師類
*/
private static class Teacher {
/**
* 姓名
*/
private String name;
/**
* 年齡
*/
private Integer age;
/**
* 性別,true 男,false 女
*/
private Boolean sex;
/**
* 職位
*/
private String post;
public Teacher() {
}
public Teacher(String name, Integer age, Boolean sex, String post) {
this.name = name;
this.age = age;
this.sex = sex;
this.post = post;
}
public String getName() {
return name;
}
public Teacher setName(String name) {
this.name = name;
return this;
}
public Integer getAge() {
return age;
}
public Teacher setAge(Integer age) {
this.age = age;
return this;
}
public Boolean getSex() {
return sex;
}
public Teacher setSex(Boolean sex) {
this.sex = sex;
return this;
}
public String getPost() {
return post;
}
public Teacher setPost(String post) {
this.post = post;
return this;
}
@Override
public String toString() {
return ToStringBuilder.reflectionToString(this);
}
}
}
如果沒有發(fā)生什么意外,那么一切將能夠正常運行!
好了,那么關于在 Java 中優(yōu)雅的拷貝對象屬性就到這里啦
以上就是Java 如何優(yōu)雅的拷貝對象屬性的詳細內容,更多關于Java 拷貝對象屬性的資料請關注腳本之家其它相關文章!
相關文章
Spring?JPA的實體屬性類型轉換器并反序列化工具類詳解
這篇文章主要介紹了Spring?JPA的實體屬性類型轉換器并反序列化工具類詳解,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-02-02
cmd中javac命令無法運行(java指令能運行)解決步驟
這篇文章主要介紹了在安裝JDK后,執(zhí)行javac命令沒有返回值的問題,可能是由于命令提示符窗口緩存問題、系統(tǒng)路徑優(yōu)先級問題、文件權限問題或命令行輸入問題,文中通過代碼將解決的步驟介紹的非常詳細,需要的朋友可以參考下2025-02-02
java利用CountDownLatch實現(xiàn)并行計算
這篇文章主要介紹了java利用CountDownLatch實現(xiàn)并行計算,具有一定的參考價值,感興趣的小伙伴們可以參考一下2018-10-10
JavaSE static final及abstract修飾符實例解析
這篇文章主要介紹了JavaSE static final及abstract修飾符實例解析,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友可以參考下2020-06-06

