上一篇博文介绍了几种bean拷贝框架的使用姿势以及性能对比,主要适用的是属性名一致、类型一致的拷贝,在实际的业务开发中,经常会用到驼峰和下划线的互转,本文在之前的基础上进行扩展
I. 驼峰下划线拷贝支持
上面的使用都是最基本的使用姿势,属性名 + 类型一致,都有getter/setter方法,我们实际的业务场景中,有一个比较重要的地方,就是下划线与驼峰的转换支持,如果要使用上面的框架,可以怎样适配?
1. cglib 下划线转驼峰
spring cglib封装 与 纯净版的cglib 实现逻辑差别不大,主要是spring里面做了一些缓存,所以表现会相对好一点;为了更加通用,这里以纯净版的cglib进行扩展演示
cglib实现转换的核心逻辑在 net.sf.cglib.beans.BeanCopier.Generator.generateClass
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| public void generateClass(ClassVisitor v) { PropertyDescriptor[] getters = ReflectUtils.getBeanGetters(source); PropertyDescriptor[] setters = ReflectUtils.getBeanSetters(target); 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]; PropertyDescriptor getter = (PropertyDescriptor)names.get(setter.getName()); if (getter != null) { } } }
|
改造逻辑,上面的注释中已经贴出来了,核心实现就比较简单了
提供一个下划线转驼峰的工具了 StrUtil
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| public class StrUtil { private static final char UNDER_LINE = '_';
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的代码都拷贝进来,然后改一下上面注释的两个地方 (完整的代码参考项目源码)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
| public void generateClass(ClassVisitor v) { PropertyDescriptor[] setters = ReflectUtils.getBeanSetters(target); Map<String, PropertyDescriptor> names = buildGetterNameMapper(source)
for (int i = 0; i < setters.length; i++) { PropertyDescriptor setter = setters[i]; PropertyDescriptor getter = loadSourceGetter(names, setter); if (getter != null) { } } }
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; }
public PropertyDescriptor loadSourceGetter(Map<String, PropertyDescriptor> names, PropertyDescriptor setter) { String setterName = setter.getName(); return names.getOrDefault(setterName, names.get(StrUtil.toCamelCase(setterName))); }
|
使用姿势和之前没有什么区别,就是BeanCopier的创建这里稍稍修改一下即可(BeanCopier可以加缓存,避免频繁的创建)
1 2 3 4 5 6 7
| public <K, T> T copyAndParse(K source, Class<T> target) throws IllegalAccessException, InstantiationException { BeanCopier copier = PureCglibBeanCopier.create(source.getClass(), target, false); T res = target.newInstance(); copier.copy(source, res, null); return res; }
|
hutool也支持下划线与驼峰的互转,而且不需要修改源码, 只用我们自己维护一个FieldMapper即可,改动成本较小;而且在map2bean, bean2map时,可以无修改的实现驼峰下划线互转,这一点还是非常很优秀的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51
|
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; }
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; }
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,虽然我们需要手动编码来实现转换,但是好处是性能高啊,既然已经手动编码了,那也就不介意补上下划线和驼峰的转换了
1 2 3 4 5
| @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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| @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); }
|
输出结果如下
1 2 3 4 5
| 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)
|
性能测试
1 2 3 4 5 6 7 8 9 10
| 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 支持了驼峰,下划线的互转,最终的表现和上面的也没什么太大区别
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| 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. 其他
一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛
2. 声明
尽信书则不如,以上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现bug或者有更好的建议,欢迎批评指正,不吝感激
3. 扫描关注
一灰灰blog