在做業務的時候,我們有時為了隔離變化,會將DAO查詢出來的Entity,和對外提供的DTO隔離開來。大概90%的時候,它們的結構都是類似的,但是我們很不喜歡寫很多冗長的b.setF1(a.getF1())這樣的代碼,於是我們需要BeanCopier來幫助我們。選擇Cglib的BeanCopier進行Bean拷貝的理由是,其性能要比Spring的BeanUtils,Apache的BeanUtils和PropertyUtils要好很多,尤其是數據量比較大的情況下。
BeanCopier基本用法
public class User {
private int age;
private String name;
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
public class UserDto {
private int age;
private String name;
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
public class UserWithDiffType {
private Integer age;
private String name;
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
1. 屬性名稱、類型都相同:
@Test
public void normalCopyTest() {
// create(Class source, Class target, boolean useConverter)
final BeanCopier beanCopier = BeanCopier.create(User.class, UserDto.class, false);
User user = new User();
user.setAge(10);
user.setName("zhangsan");
UserDto userDto = new UserDto();
beanCopier.copy(user, userDto, null);
Assert.assertEquals(10, userDto.getAge());
Assert.assertEquals("zhangsan", userDto.getName());
}
結論:屬性名稱相同類型相同的屬性拷貝OK。
2. 屬性名稱相同、類型不同:
@Test
public void normalCopyTest() {
// create(Class source, Class target, boolean useConverter)
final BeanCopier beanCopier = BeanCopier.create(User.class, UserWithDiffType.class, false);
User user = new User();
user.setAge(10);
user.setName("zhangsan");
UserWithDiffType userDto = new UserWithDiffType();
beanCopier.copy(user, userDto, null);
Assert.assertEquals(null, userDto.getAge());
Assert.assertEquals("zhangsan", userDto.getName());
}
結論:屬性名稱相同而類型不同的屬性不會被拷貝。
注意:即使源類型是原始類型(int, short和char等),目標類型是其包裝類型(Integer, Short和Character等),或反之:都 不會被拷貝。
總結:
BeanCopier只拷貝名稱和類型都相同的屬性。
自定義轉換器
當源和目標類的屬性類型不同時,不能拷貝該屬性,此時我們可以通過實現Converter接口來自定義轉換器
源類和目標類:
public class Account {
private int id;
private Date createTime;
private BigDecimal balance;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public Date getCreateTime() {
return createTime;
}
public void setCreateTime(Date createTime) {
this.createTime = createTime;
}
public BigDecimal getBalance() {
return balance;
}
public void setBalance(BigDecimal balance) {
this.balance = balance;
}
}
public class AccountDto {
private int id;
private String createTime;
private String balance;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getCreateTime() {
return createTime;
}
public void setCreateTime(String createTime) {
this.createTime = createTime;
}
public String getBalance() {
return balance;
}
public void setBalance(String balance) {
this.balance = balance;
}
}
1. 不使用Converter
@Test
public void noConverterTest() {
Account po = new Account();
po.setId(1);
po.setCreateTime(new Date());
po.setBalance(BigDecimal.valueOf(4000L));
BeanCopier copier = BeanCopier.create(Account.class, AccountDto.class, false);
AccountDto dto = new AccountDto();
copier.copy(po, dto, null);
// 類型不同,未拷貝
Assert.assertNull(dto.getBalance());
// 類型不同,未拷貝
Assert.assertNull(dto.getCreateTime());
}
2. 使用Converter
基於目標對象的屬性出發,如果源對象有相同名稱的屬性,則調一次convert方法:
public class TestCase {
@Test
public void noConverterTest() {
Account po = new Account();
po.setId(1);
po.setCreateTime(new Date());
po.setBalance(BigDecimal.valueOf(4000L));
BeanCopier copier = BeanCopier.create(Account.class, AccountDto.class, true);
AccountDto dto = new AccountDto();
AccountConverter converter = new AccountConverter();
copier.copy(po, dto, converter);
// 類型不同,未拷貝
Assert.assertEquals("4000", dto.getBalance());
// 類型不同,未拷貝
Assert.assertEquals("2018-12-13", dto.getCreateTime());
}
}
class AccountConverter implements Converter {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
@SuppressWarnings("rawtypes")
@Override
public Object convert(Object source, Class target, Object context) {
if (source instanceof Integer) {
return (Integer) source;
} else if (source instanceof Date) {
Date date = (Date) source;
return sdf.format(date);
} else if (source instanceof BigDecimal) {
BigDecimal bd = (BigDecimal) source;
return bd.toPlainString();
}
return null;
}
}
注:一旦使用Converter,BeanCopier只使用Converter定義的規則去拷貝屬性,所以在convert方法中要考慮所有的屬性。
封裝BeanCopier
@Test
public void costTest() {
List<User> list1 = new ArrayList<>(100);
for (int i = 0; i < 100; i++) {
User po = new User();
po.setId(1);
po.setCreateTime(new Date());
po.setBalance(BigDecimal.valueOf(4000L));
list1.add(po);
}
BeanCopier copier = BeanCopier.create(User.class, UserDto.class, false);
long start = System.currentTimeMillis();
List<UserDto> list2 = new ArrayList<>(100);
for (User user : list1) {
UserDto dto = new UserDto();
//BeanUtils.beanCopy(user, dto);
copier.copy(user, dto, null);
list2.add(dto);
}
System.out.printf("took time: %d(ms)%n",System.currentTimeMillis() - start);
}
經過測試,BeanCopier性能是BeanUtils10倍左右。
BeanCopier拷貝速度快,性能瓶頸出現在創建BeanCopier實例的過程中。 所以,把創建過的BeanCopier實例放到緩存中,下次可以直接獲取,提升性能:
依賴:
<dependencies>
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.2.10</version>
</dependency>
<dependency>
<groupId>com.esotericsoftware</groupId>
<artifactId>reflectasm</artifactId>
<version>1.11.7</version>
</dependency>
</dependencies>
封裝工具
public class WrapperBeanCopier {
private WrapperBeanCopier() {
//do nothing
}
private static final Map<String, BeanCopier> BEAN_COPIER_CACHE = new ConcurrentHashMap<>();
private static final Map<String, ConstructorAccess> CONSTRUCTOR_ACCESS_CACHE = new ConcurrentHashMap<>();
public static void copyProperties(Object source, Object target) {
BeanCopier copier = getBeanCopier(source.getClass(), target.getClass());
copier.copy(source, target, null);
}
private static BeanCopier getBeanCopier(Class sourceClass, Class targetClass) {
String beanKey = generateKey(sourceClass, targetClass);
BeanCopier copier = null;
if (!BEAN_COPIER_CACHE.containsKey(beanKey)) {
copier = BeanCopier.create(sourceClass, targetClass, false);
BEAN_COPIER_CACHE.put(beanKey, copier);
} else {
copier = BEAN_COPIER_CACHE.get(beanKey);
}
return copier;
}
/**
* 兩個類的全限定名拼接起來構成Key
*
* @param sourceClass
* @param targetClass
* @return
*/
private static String generateKey(Class<?> sourceClass, Class<?> targetClass) {
return sourceClass.getName() + targetClass.getName();
}
public static <T> T copyProperties(Object source, Class<T> targetClass) {
T t = null;
try {
t = targetClass.newInstance();
} catch (InstantiationException | IllegalAccessException e) {
throw new RuntimeException(format("Create new instance of %s failed: %s", targetClass, e.getMessage()));
}
copyProperties(source, t);
return t;
}
public static <T> List<T> copyPropertiesOfList(List<?> sourceList, Class<T> targetClass) {
if (sourceList == null || sourceList.isEmpty()) {
return Collections.emptyList();
}
ConstructorAccess<T> constructorAccess = getConstructorAccess(targetClass);
List<T> resultList = new ArrayList<>(sourceList.size());
for (Object o : sourceList) {
T t = null;
try {
t = constructorAccess.newInstance();
copyProperties(o, t);
resultList.add(t);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
return resultList;
}
private static <T> ConstructorAccess<T> getConstructorAccess(Class<T> targetClass) {
ConstructorAccess<T> constructorAccess = CONSTRUCTOR_ACCESS_CACHE.get(targetClass.getName());
if (constructorAccess != null) {
return constructorAccess;
}
try {
constructorAccess = ConstructorAccess.get(targetClass);
constructorAccess.newInstance();
CONSTRUCTOR_ACCESS_CACHE.put(targetClass.toString(), constructorAccess);
} catch (Exception e) {
throw new RuntimeException(format("Create new instance of %s failed: %s", targetClass, e.getMessage()));
}
return constructorAccess;
}
}

