• 常见Bean拷贝框架下划线驼峰互转扩展支持


    上一篇博文常见Bean拷贝框架使用姿势及性能对比 介绍了几种bean拷贝框架的使用姿势以及性能对比,主要适用的是属性名一致、类型一致的拷贝,在实际的业务开发中,经常会用到驼峰和下划线的互转,本文在之前的基础上进行扩展

    • cglib
    • hutool

    常见Bean拷贝框架下划线驼峰互转扩展支持

    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 + "
    sCglib:" + cglib + "
    pCglib:" + cglib2 + "
    huTool:" + hutool + "
    MapStruct:" + 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. 一灰灰Bloghttps://liuyueyi.github.io/hexblog

    一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛

    2. 声明

    尽信书则不如,以上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现bug或者有更好的建议,欢迎批评指正,不吝感激

    3. 扫描关注

    一灰灰blog

    QrCode

  • 相关阅读:
    hbase
    spark-streaming
    spark-Scala
    经典台词二
    星爷电影经典台词一
    Hadoop第一阶段总结
    测试2
    POI 表格数据导出
    GC垃圾回收机制
    Java常见的200道面试题
  • 原文地址:https://www.cnblogs.com/yihuihui/p/14715459.html
Copyright © 2020-2023  润新知