上一篇博文常見Bean拷貝框架使用姿勢及性能對比 介紹了幾種bean拷貝框架的使用姿勢以及性能對比,主要適用的是屬性名一致、類型一致的拷貝,在實際的業務開發中,經常會用到駝峰和下划線的互轉,本文在之前的基礎上進行擴展
- cglib
- hutool
I. 駝峰下划線拷貝支持
上面的使用都是最基本的使用姿勢,屬性名 + 類型一致,都有getter/setter方法,我們實際的業務場景中,有一個比較重要的地方,就是下划線與駝峰的轉換支持,如果要使用上面的框架,可以怎樣適配?
1. cglib 下划線轉駝峰
spring cglib封裝 與 純凈版的cglib 實現邏輯差別不大,主要是spring里面做了一些緩存,所以表現會相對好一點;為了更加通用,這里以純凈版的cglib進行擴展演示
cglib實現轉換的核心邏輯在 net.sf.cglib.beans.BeanCopier.Generator.generateClass
public void generateClass(ClassVisitor v) {
// ... 省略無關代碼
PropertyDescriptor[] getters = ReflectUtils.getBeanGetters(source);
PropertyDescriptor[] setters = ReflectUtils.getBeanSetters(target);
// 掃描source的所有getter方法,寫入到map, key為屬性名;
// 為了支持駝峰,下划線,我們可以擴展一下這個map,如果屬性名為下划線的,額外加一個駝峰的kv進去
Map names = new HashMap();
for (int i = 0; i < getters.length; i++) {
names.put(getters[i].getName(), getters[i]);
}
// ...
for (int i = 0; i < setters.length; i++) {
PropertyDescriptor setter = setters[i];
// 這里根據target的屬性名,獲取source對應的getter方法,同樣適配一下,如果下划線格式的獲取不到,則改用駝峰的試一下
PropertyDescriptor getter = (PropertyDescriptor)names.get(setter.getName());
if (getter != null) {
// ....
}
}
// ...
}
改造邏輯,上面的注釋中已經貼出來了,核心實現就比較簡單了
提供一個下划線轉駝峰的工具了 StrUtil
public class StrUtil {
private static final char UNDER_LINE = '_';
/**
* 下划線轉駝峰
*
* @param name
* @return
*/
public static String toCamelCase(String name) {
if (null == name || name.length() == 0) {
return null;
}
if (!contains(name, UNDER_LINE)) {
return name;
}
int length = name.length();
StringBuilder sb = new StringBuilder(length);
boolean underLineNextChar = false;
for (int i = 0; i < length; ++i) {
char c = name.charAt(i);
if (c == UNDER_LINE) {
underLineNextChar = true;
} else if (underLineNextChar) {
sb.append(Character.toUpperCase(c));
underLineNextChar = false;
} else {
sb.append(c);
}
}
return sb.toString();
}
public static boolean contains(String str, char searchChar) {
return str.indexOf(searchChar) >= 0;
}
}
然后自定義一個 PureCglibBeanCopier, 將之前BeanCopier的代碼都拷貝進來,然后改一下上面注釋的兩個地方 (完整的代碼參考項目源碼)
public void generateClass(ClassVisitor v) {
// ... 省略無關代碼
PropertyDescriptor[] setters = ReflectUtils.getBeanSetters(target);
// 掃描source的所有getter方法,寫入到map, key為屬性名;
// 為了支持駝峰,下划線,我們可以擴展一下這個map,如果屬性名為下划線的,額外加一個駝峰的kv進去
Map<String, PropertyDescriptor> names = buildGetterNameMapper(source)
// ...
for (int i = 0; i < setters.length; i++) {
PropertyDescriptor setter = setters[i];
// 這里根據target的屬性名,獲取source對應的getter方法,同樣適配一下,如果下划線格式的獲取不到,則改用駝峰的試一下
PropertyDescriptor getter = loadSourceGetter(names, setter);
if (getter != null) {
// ....
}
}
// ...
}
/**
* 獲取目標的getter方法,支持下划線與駝峰
*
* @param source
* @return
*/
public Map<String, PropertyDescriptor> buildGetterNameMapper(Class source) {
PropertyDescriptor[] getters = org.springframework.cglib.core.ReflectUtils.getBeanGetters(source);
Map<String, PropertyDescriptor> names = new HashMap<>(getters.length);
for (int i = 0; i < getters.length; ++i) {
String name = getters[i].getName();
String camelName = StrUtil.toCamelCase(name);
names.put(name, getters[i]);
if (!name.equalsIgnoreCase(camelName)) {
// 支持下划線轉駝峰
names.put(camelName, getters[i]);
}
}
return names;
}
/**
* 根據target的setter方法,找到source的getter方法,支持下划線與駝峰的轉換
*
* @param names
* @param setter
* @return
*/
public PropertyDescriptor loadSourceGetter(Map<String, PropertyDescriptor> names, PropertyDescriptor setter) {
String setterName = setter.getName();
return names.getOrDefault(setterName, names.get(StrUtil.toCamelCase(setterName)));
}
使用姿勢和之前沒有什么區別,就是BeanCopier的創建這里稍稍修改一下即可(BeanCopier可以加緩存,避免頻繁的創建)
public <K, T> T copyAndParse(K source, Class<T> target) throws IllegalAccessException, InstantiationException {
// todo copier 可以緩存起來,避免每次重新創建
BeanCopier copier = PureCglibBeanCopier.create(source.getClass(), target, false);
T res = target.newInstance();
copier.copy(source, res, null);
return res;
}
2. hutool 下划線轉駝峰
hutool也支持下划線與駝峰的互轉,而且不需要修改源碼, 只用我們自己維護一個FieldMapper即可,改動成本較小;而且在map2bean, bean2map時,可以無修改的實現駝峰下划線互轉,這一點還是非常很優秀的
/**
* 駝峰轉換
*
* @param source
* @param target
* @param <K>
* @param <T>
* @return
*/
public <K, T> T copyAndParse(K source, Class<T> target) throws Exception {
T res = target.newInstance();
// 下划線轉駝峰
BeanUtil.copyProperties(source, res, getCopyOptions(source.getClass()));
return res;
}
// 緩存CopyOptions(注意這個是HuTool的類,不是Cglib的)
private Map<Class, CopyOptions> cacheMap = new HashMap<>();
private CopyOptions getCopyOptions(Class source) {
CopyOptions options = cacheMap.get(source);
if (options == null) {
// 不加鎖,我們認為重復執行不會比並發加鎖帶來的開銷大
options = CopyOptions.create().setFieldMapping(buildFieldMapper(source));
cacheMap.put(source, options);
}
return options;
}
/**
* @param source
* @return
*/
private Map<String, String> buildFieldMapper(Class source) {
PropertyDescriptor[] properties = ReflectUtils.getBeanProperties(source);
Map<String, String> map = new HashMap<>();
for (PropertyDescriptor target : properties) {
String name = target.getName();
String camel = StrUtil.toCamelCase(name);
if (!name.equalsIgnoreCase(camel)) {
map.put(name, camel);
}
String under = StrUtil.toUnderlineCase(name);
if (!name.equalsIgnoreCase(under)) {
map.put(name, under);
}
}
return map;
}
3. mapstruct
最后再介紹一下MapStruct,雖然我們需要手動編碼來實現轉換,但是好處是性能高啊,既然已經手動編碼了,那也就不介意補上下划線和駝峰的轉換了
@Mappings({
@Mapping(target = "userName", source = "user_name"),
@Mapping(target = "market_price", source = "marketPrice")
})
Target2 copyAndParse(Source source);
4. 測試
接下來測試一下上面三個是否能正常工作
定義一個Target2,注意它與Source有兩個字段不同,分別是 user_name/userName
, marketPrice/market_price
@Data
public class Target2 {
private Integer id;
private String userName;
private Double price;
private List<Long> ids;
private BigDecimal market_price;
}
private void camelParse() throws Exception {
Source s = genSource();
Target2 cglib = springCglibCopier.copyAndParse(s, Target2.class);
Target2 cglib2 = pureCglibCopier.copyAndParse(s, Target2.class);
Target2 hutool = hutoolCopier.copyAndParse(s, Target2.class);
Target2 map = mapsCopier.copy(s, Target2.class);
System.out.println("source:" + s + "\nsCglib:" + cglib + "\npCglib:" + cglib2 + "\nhuTool:" + hutool + "\nMapStruct:" + map);
}
輸出結果如下
source:Source(id=527180337, user_name=一灰灰Blog, price=7.9, ids=[-2509965589596742300, 5995028777901062972, -1914496225005416077], marketPrice=0.35188996791839599609375)
sCglib:Target2(id=527180337, userName=一灰灰Blog, price=7.9, ids=[-2509965589596742300, 5995028777901062972, -1914496225005416077], market_price=0.35188996791839599609375)
pCglib:Target2(id=527180337, userName=一灰灰Blog, price=7.9, ids=[-2509965589596742300, 5995028777901062972, -1914496225005416077], market_price=0.35188996791839599609375)
huTool:Target2(id=527180337, userName=一灰灰Blog, price=7.9, ids=[-2509965589596742300, 5995028777901062972, -1914496225005416077], market_price=0.35188996791839599609375)
MapStruct:Target2(id=527180337, userName=一灰灰Blog, price=7.9, ids=[-2509965589596742300, 5995028777901062972, -1914496225005416077], market_price=0.35188996791839599609375)
性能測試
private <T> void autoCheck2(Class<T> target, int size) throws Exception {
StopWatch stopWatch = new StopWatch();
runCopier(stopWatch, "apacheCopier", size, (s) -> apacheCopier.copy(s, target));
runCopier(stopWatch, "springCglibCopier", size, (s) -> springCglibCopier.copyAndParse(s, target));
runCopier(stopWatch, "pureCglibCopier", size, (s) -> pureCglibCopier.copyAndParse(s, target));
runCopier(stopWatch, "hutoolCopier", size, (s) -> hutoolCopier.copyAndParse(s, target));
runCopier(stopWatch, "springBeanCopier", size, (s) -> springBeanCopier.copy(s, target));
runCopier(stopWatch, "mapStruct", size, (s) -> mapsCopier.copyAndParse(s, target));
System.out.println((size / 10000) + "w -------- cost: " + stopWatch.prettyPrint());
}
對比結果如下,雖然cglib, hutool 支持了駝峰,下划線的互轉,最終的表現和上面的也沒什么太大區別
1w -------- cost: StopWatch '': running time = 754589100 ns
---------------------------------------------
ns % Task name
---------------------------------------------
572878100 076% apacheCopier yihui
017037900 002% springCglibCopier
031207500 004% pureCglibCopier
105254600 014% hutoolCopier
022156300 003% springBeanCopier
006054700 001% mapStruct
1w -------- cost: StopWatch '': running time = 601845500 ns
---------------------------------------------
ns % Task name
---------------------------------------------
494895600 082% apacheCopier
009014500 001% springCglibCopier
008998600 001% pureCglibCopier
067145800 011% hutoolCopier
016557700 003% springBeanCopier
005233300 001% mapStruct
10w -------- cost: StopWatch '': running time = 5543094200 ns
---------------------------------------------
ns % Task name
---------------------------------------------
4474871900 081% apacheCopier
089066500 002% springCglibCopier
090526400 002% pureCglibCopier
667986400 012% hutoolCopier
166274800 003% springBeanCopier
054368200 001% mapStruct
50w -------- cost: StopWatch '': running time = 27527708400 ns
---------------------------------------------
ns % Task name
---------------------------------------------
22145604900 080% apacheCopier
452946700 002% springCglibCopier
448455700 002% pureCglibCopier
3365908800 012% hutoolCopier
843306700 003% springBeanCopier
271485600 001% mapStruct
II. 其他
1. 一灰灰Blog: https://liuyueyi.github.io/hexblog
一灰灰的個人博客,記錄所有學習和工作中的博文,歡迎大家前去逛逛
2. 聲明
盡信書則不如,以上內容,純屬一家之言,因個人能力有限,難免有疏漏和錯誤之處,如發現bug或者有更好的建議,歡迎批評指正,不吝感激
- 微博地址: 小灰灰Blog
- QQ: 一灰灰/3302797840
3. 掃描關注
一灰灰blog