千家信息网

如何实现Bean拷贝框架下划线驼峰互转扩展支持

发表于:2025-01-20 作者:千家信息网编辑
千家信息网最后更新 2025年01月20日,这篇文章主要讲解了"如何实现Bean拷贝框架下划线驼峰互转扩展支持",文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习"如何实现Bean拷贝框架下划线驼峰互转
千家信息网最后更新 2025年01月20日如何实现Bean拷贝框架下划线驼峰互转扩展支持

这篇文章主要讲解了"如何实现Bean拷贝框架下划线驼峰互转扩展支持",文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习"如何实现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 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 buildGetterNameMapper(Class source) {    PropertyDescriptor[] getters = org.springframework.cglib.core.ReflectUtils.getBeanGetters(source);    Map 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 names, PropertyDescriptor setter) {    String setterName = setter.getName();    return names.getOrDefault(setterName, names.get(StrUtil.toCamelCase(setterName)));}

使用姿势和之前没有什么区别,就是BeanCopier的创建这里稍稍修改一下即可(BeanCopier可以加缓存,避免频繁的创建)

public  T copyAndParse(K source, Class 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  * @param  * @return */public  T copyAndParse(K source, Class target) throws Exception {    T res = target.newInstance();    // 下划线转驼峰    BeanUtil.copyProperties(source, res, getCopyOptions(source.getClass()));    return res;}// 缓存CopyOptions(注意这个是HuTool的类,不是Cglib的)private Map 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 buildFieldMapper(Class source) {    PropertyDescriptor[] properties = ReflectUtils.getBeanProperties(source);    Map 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

@Datapublic class Target2 {    private Integer id;    private String userName;    private Double price;    private List 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  void autoCheck2(Class 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 yihui017037900  002%  springCglibCopier031207500  004%  pureCglibCopier105254600  014%  hutoolCopier022156300  003%  springBeanCopier006054700  001%  mapStruct1w -------- cost: StopWatch '': running time = 601845500 ns---------------------------------------------ns         %     Task name---------------------------------------------494895600  082%  apacheCopier009014500  001%  springCglibCopier008998600  001%  pureCglibCopier067145800  011%  hutoolCopier016557700  003%  springBeanCopier005233300  001%  mapStruct10w -------- cost: StopWatch '': running time = 5543094200 ns---------------------------------------------ns         %     Task name---------------------------------------------4474871900  081%  apacheCopier089066500  002%  springCglibCopier090526400  002%  pureCglibCopier667986400  012%  hutoolCopier166274800  003%  springBeanCopier054368200  001%  mapStruct50w -------- cost: StopWatch '': running time = 27527708400 ns---------------------------------------------ns         %     Task name---------------------------------------------22145604900  080%  apacheCopier452946700  002%  springCglibCopier448455700  002%  pureCglibCopier3365908800  012%  hutoolCopier843306700  003%  springBeanCopier271485600  001%  mapStruct

感谢各位的阅读,以上就是"如何实现Bean拷贝框架下划线驼峰互转扩展支持"的内容了,经过本文的学习后,相信大家对如何实现Bean拷贝框架下划线驼峰互转扩展支持这一问题有了更深刻的体会,具体使用情况还需要大家实践验证。这里是,小编将为大家推送更多相关知识点的文章,欢迎关注!

0