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

實(shí)現(xiàn)
注:反射部分依賴于 joor, JDK1.8 請(qǐng)使用 joor-java-8
定義門(mén)面類 BeanCopyUtil 用以暴露出一些 API
/**
* java bean 復(fù)制操作的工具類
*
* @author rxliuli
*/
public class BeanCopyUtil<F, T> {
/**
* 源對(duì)象
*/
private final F from;
/**
* 目標(biāo)對(duì)象
*/
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;
}
/**
* 指定需要拷貝的源對(duì)象和目標(biāo)對(duì)象
*
* @param from 源對(duì)象
* @param to 目標(biāo)對(duì)象
* @param <F> 源對(duì)象類型
* @param <T> 目標(biāo)對(duì)象類型
* @return 一個(gè) {@link BeanCopyUtil} 對(duì)象
*/
public static <F, T> BeanCopyUtil<F, T> copy(F from, T to) {
return new BeanCopyUtil<>(from, to);
}
/**
* 拷貝指定對(duì)象的字段
*
* @param fromField 源對(duì)象中的字段名
* @param toField 目標(biāo)對(duì)象中的字段名
* @param converter 將源對(duì)象中字段轉(zhuǎn)換為目標(biāo)對(duì)象字段類型的轉(zhuǎn)換器
* @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;
}
/**
* 拷貝指定對(duì)象的字段
*
* @param fromField 源對(duì)象中的字段名
* @param toField 目標(biāo)對(duì)象中的字段名
* @return 返回 {@code this}
*/
public BeanCopyUtil<F, T> prop(String fromField, String toField) {
return prop(fromField, toField, null);
}
/**
* 拷貝指定對(duì)象的字段
*
* @param field 源對(duì)象中與目標(biāo)對(duì)象中的字段名
* @param converter 將源對(duì)象中字段轉(zhuǎn)換為目標(biāo)對(duì)象字段類型的轉(zhuǎn)換器
* @return 返回 {@code this}
*/
public BeanCopyUtil<F, T> prop(String field, Function<? super Object, ? super Object> converter) {
return prop(field, field, converter);
}
/**
* 拷貝指定對(duì)象的字段
*
* @param field 源對(duì)象中與目標(biāo)對(duì)象中的字段名
* @return 返回 {@code this}
*/
public BeanCopyUtil<F, T> prop(String field) {
return prop(field, field, null);
}
/**
* 拷貝指定對(duì)象的多個(gè)字段
*
* @param fields 源對(duì)象中與目標(biāo)對(duì)象中的多個(gè)字段名
* @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;
}
/**
* 重新開(kāi)始添加其他對(duì)象的屬性
* 用于在執(zhí)行完 {@link #exec()} 之后還想復(fù)制其它對(duì)象的屬性
*
* @param from 源對(duì)象
* @param <R> 源對(duì)象類型
* @return 一個(gè)新的 {@link BeanCopyUtil} 對(duì)象
*/
public <R> BeanCopyUtil<R, T> from(R from) {
return new BeanCopyUtil<>(from, to);
}
/**
* 返回當(dāng)前的目標(biāo)對(duì)象
*
* @return 當(dāng)前的目標(biāo)對(duì)象
*/
public T get() {
return to;
}
/**
* 配置拷貝的一些策略
*
* @param config 拷貝配置對(duì)象
* @return 返回 {@code this}
*/
public BeanCopyUtil<F, T> config(BeanCopyConfig config) {
this.config = config;
return this;
}
}
定義每個(gè)字段的操作類 BeanCopyField,保存對(duì)每個(gè)字段的操作
/**
* 拷貝屬性的每一個(gè)字段的選項(xiàng)
*
* @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 {
/**
* 同名的字段自動(dòng)復(fù)制
*/
private boolean same = true;
/**
* 覆蓋同名的字段
*/
private boolean override = true;
/**
* 忽略 {@code null} 的源對(duì)象屬性
*/
private boolean ignoreNull = true;
/**
* 嘗試進(jìn)行自動(dòng)轉(zhuǎn)換
*/
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 作為拷貝的真正實(shí)現(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() {
//獲取到兩個(gè)對(duì)象所有的屬性
final Map<String, Reflect> fromFields = Reflect.on(from).fields();
final Reflect to = Reflect.on(this.to);
final Map<String, Reflect> toFields = to.fields();
//過(guò)濾出所有相同字段名的字段并進(jìn)行拷貝
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());
}
//根據(jù)拷貝字段列表進(jìn)行拷貝
copyFieldList.stream()
//忽略空值
.filter(beanCopyField -> !config.isIgnoreNull() || fromFields.get(beanCopyField.getFrom()).get() != null)
//覆蓋屬性
.filter(beanCopyField -> config.isOverride() || toFields.get(beanCopyField.getTo()).get() == null)
//如果沒(méi)有轉(zhuǎn)換器,則使用默認(rèn)的轉(zhuǎn)換器
.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());
}
});
}
}
使用
使用流程圖

測(cè)試
代碼寫(xiě)完了,讓我們測(cè)試一下!
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() {
//簡(jiǎn)單的復(fù)制(類似于 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("靈夢(mèng)")
.setAge(17);
//測(cè)試 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() {
//測(cè)試 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() {
//不自動(dòng)復(fù)制同名屬性
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);
//不忽略源對(duì)象不為空的屬性
assertThat(BeanCopyUtil.copy(new Student(), student)
.config(new BeanCopyConfig().setIgnoreNull(false))
.exec()
.get())
.extracting("realname", "age", "sex", "grade")
.containsOnlyNulls();
}
/**
* 測(cè)試學(xué)生類
*/
private static class Student {
/**
* 姓名
*/
private String realname;
/**
* 年齡
*/
private Integer age;
/**
* 性別,男/女
*/
private String sex;
/**
* 年級(jí),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);
}
}
/**
* 測(cè)試教師類
*/
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);
}
}
}
如果沒(méi)有發(fā)生什么意外,那么一切將能夠正常運(yùn)行!
好了,那么關(guān)于在 Java 中優(yōu)雅的拷貝對(duì)象屬性就到這里啦
以上就是Java 如何優(yōu)雅的拷貝對(duì)象屬性的詳細(xì)內(nèi)容,更多關(guān)于Java 拷貝對(duì)象屬性的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
JDK1.8中的ConcurrentHashMap使用及場(chǎng)景分析
這篇文章主要介紹了JDK1.8中的ConcurrentHashMap使用及場(chǎng)景分析,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-01-01
RocketMQ?NameServer架構(gòu)設(shè)計(jì)啟動(dòng)流程
這篇文章主要為大家介紹了RocketMQ?NameServer架構(gòu)設(shè)計(jì)啟動(dòng)流程,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-02-02
spring mvc 使用kaptcha配置生成驗(yàn)證碼實(shí)例
本篇文章主要介紹了spring mvc 使用kaptcha生成驗(yàn)證碼實(shí)例,詳細(xì)的介紹了使用Kaptcha 生成驗(yàn)證碼的步驟,有興趣的可以了解一下2017-04-04
SpringBoot?@Transactional事務(wù)不生效排查方式
這篇文章主要介紹了SpringBoot?@Transactional事務(wù)不生效排查方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-01-01
Spring Boot 集成 Mybatis Plus 自動(dòng)填充字段的實(shí)例詳解
這篇文章主要介紹了Spring Boot 集成 Mybatis Plus 自動(dòng)填充字段,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-03-03
在Java的Struts中判斷是否調(diào)用AJAX及用攔截器對(duì)其優(yōu)化
這篇文章主要介紹了在Java的Struts中判斷是否調(diào)用AJAX及用攔截器對(duì)其優(yōu)化的方法,Struts框架是Java的SSH三大web開(kāi)發(fā)框架之一,需要的朋友可以參考下2016-01-01

