前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >java之通过反射生成并初始化对象

java之通过反射生成并初始化对象

作者头像
一灰灰blog
发布2018-02-06 15:37:50
3.9K0
发布2018-02-06 15:37:50
举报
文章被收录于专栏:小灰灰小灰灰

java之通过反射生成并初始化对象

在博文 《java之的读取文件大全》 中读取csv文件后,需要自己将csv文件的对象转为自己的DO对象,那么有没有办法我直接穿进去一个DO的class对象,内部实现生成对象,并利用 CSVRecord 对象对其进行初始化呢 ?

本篇主要是为了解决上面的这个问题,实现了一个非常初级转换方法,然后会分析下大名鼎鼎的BeanUtils是如何实现这种功能的

1. CSVRecord对象转xxxBO对象

在做之前,先把csv的读取相关代码贴出来,具体的实现逻辑详解可以参考 《java之的读取文件大全》

CsvUtil.java

代码语言:javascript
复制
/**
 * 读取文件
 */
public static InputStream getStreamByFileName(String fileName) throws IOException {
        if (fileName == null) {
            throw new IllegalArgumentException("fileName should not be null!");
        }

        if (fileName.startsWith("http")) { // 网络地址
            URL url = new URL(fileName);
            return url.openStream();
        } else if (fileName.startsWith("/")) { // 绝对路径
            Path path = Paths.get(fileName);
            return Files.newInputStream(path);
        } else  { // 相对路径
            return FileUtil.class.getClassLoader().getResourceAsStream(fileName);
        }
    }

/**
* 读取csv文件, 返回结构话的对象
* @param filename csv 路径 + 文件名, 支持绝对路径 + 相对路径 + 网络文件
* @param headers  csv 每列的数据
* @return
* @throws IOException
*/
public static List<CSVRecord> read(String filename, String[] headers) throws IOException {
   try (Reader reader = new InputStreamReader(getStreamByFileName(fileName), Charset.forName("UTF-8"))) {
       CSVParser csvParser = new CSVParser(reader,
               CSVFormat.INFORMIX_UNLOAD_CSV.withHeader(headers)
       );

       return csvParser.getRecords();
   }
}

word.csv 文件

代码语言:javascript
复制
dicId,"name",rootWord,weight
1,"质量",true,0.1
2,"服务",true,0.2
3,"发货",,0.1
4,"性价比",false,0.4
5,"尺码",true,0.4

测试用例

代码语言:javascript
复制
@Getter
@Setter
@ToString
static class WordDO {
   long dicId;

   String name;

   Boolean rootWord;

   Float weight;

   public WordDO() {
   }
}

@Test
public void testCsvRead() throws IOException {
   String fileName = "word.csv";
   List<CSVRecord> list = CsvUtil.read(fileName, new String[]{"dicId", "name", "rootWord", "weight"});
   Assert.assertTrue(list != null && list.size() > 0);

   List<WordDO> words = list.stream()
           .filter(csvRecord -> !"dicId".equals(csvRecord.get("dicId")))
           .map(this::parseDO).collect(Collectors.toList());
   logger.info("the csv words: {}", words);
}


private WordDO parseDO(CSVRecord csvRecord) {
   WordDO wordDO = new WordDO();
   wordDO.dicId = Integer.parseInt(csvRecord.get("dicId"));
   wordDO.name = csvRecord.get("name");
   wordDO.rootWord = Boolean.valueOf(csvRecord.get("rootWord"));
   wordDO.weight = Float.valueOf(csvRecord.get("weight"));
   return wordDO;
}

输出结果

代码语言:javascript
复制
16:17:27.145 [main] INFO  c.h.h.q.file.test.FileUtilTest - the csv words: CsvUtilTest.WordDO(dicId=1, name=质量, rootWord=true, weight=0.1)
16:17:27.153 [main] INFO  c.h.h.q.file.test.FileUtilTest - the csv words: CsvUtilTest.WordDO(dicId=2, name=服务, rootWord=true, weight=0.2)
16:17:27.154 [main] INFO  c.h.h.q.file.test.FileUtilTest - the csv words: CsvUtilTest.WordDO(dicId=3, name=发货, rootWord=false, weight=0.1)
16:17:27.154 [main] INFO  c.h.h.q.file.test.FileUtilTest - the csv words: CsvUtilTest.WordDO(dicId=4, name=性价比, rootWord=false, weight=0.4)
16:17:27.154 [main] INFO  c.h.h.q.file.test.FileUtilTest - the csv words: CsvUtilTest.WordDO(dicId=5, name=尺码, rootWord=true, weight=0.4)

从上面的使用来看,每次都要自己对解析出来的 CsvRecord 进行对象转换, 我们的目标就是把这个集成在 CsvUtil 内部去实现

设计思路

反射创建对象,获取对象的所有属性,然后在属性前面加 set 表示设置属性的方法(boolea类型的属性可能是 isXXX格式), 通过反射设置方法的属性值

  • 创建对象: T obj = clz.newInstance();
  • 获取所有属性: Field[] fields = clz.getDeclaredFields();
  • 设置属性值
    • 方法名: fieldSetMethodName = "set" + upperCase(field.getName());
    • 属性值,需要转换对应的类型: fieldValue = this.parseType(value, field.getType());
    • 获取设置属性方法 : Method method = clz.getDeclaredMethod(fieldSetMethodName, field.getType());
    • 设置属性: method.invoke(obj, fieldValue);

实现代码

基本结构如上,先贴出实现的代码,并对其中的几点做一下简短的说明

代码语言:javascript
复制
private <T> T parseBO(CSVRecord csvRecord, Class<T> clz) throws IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {

        // 创建BO对象
        T obj = clz.newInstance();


        // 获取声明的所有成员变量
        Field[] fields = clz.getDeclaredFields();


        // 保存属性对应的csvRecord中的值
        String value;
        String fieldSetMethodName;
        Object fieldValue;
        for (Field field : fields) {
            // 设置为可访问
            field.setAccessible(true);

            // 将value转换为目标类型
            value = csvRecord.get(field.getName());
            if (value == null) {
                continue;
            }
            fieldValue = this.parseType(value, field.getType());


            // 获取属性对应的设置方法名
            fieldSetMethodName = "set" + upperCase(field.getName());
            Method method = clz.getDeclaredMethod(fieldSetMethodName, field.getType());
            

            // 设置属性值
            method.invoke(obj, fieldValue);
        }


        return obj;
    }


    // 首字母变大写
    private String upperCase(String str) {
        char[] ch = str.toCharArray();
//      也可以直接用下面的记性转大写
//      ch[0] = Character.toUpperCase(ch[0]);
        if (ch[0] >= 'a' && ch[0] <= 'z') {
            ch[0] = (char) (ch[0] - 32);
        }
        return new String(ch);
    }


    /**
     * 类型转换
     * 
     * @param value 原始数据格式
     * @param type 期待转换的类型
     * @return 转换后的数据对象
     */
    private Object parseType(String value, Class type) {

        if (type == String.class) {
            return value;
        } else if (type == int.class) {
            return value == null ? 0 : Integer.parseInt(value);
        } else if (type == float.class) {
            return value == null ? 0f : Float.parseFloat(value);
        } else if (type == long.class) {
            return value == null ? 0L : Long.parseLong(value);
        } else if (type == double.class) {
            return value == null ? 0D : Double.parseDouble(value);
        } else if (type == boolean.class) {
            return value != null && Boolean.parseBoolean(value);
        } else if (type == byte.class) {
            return value == null || value.length() == 0 ? 0 : value.getBytes()[0];
        } else if (type == char.class) {
            if (value == null || value.length() == 0) {
                return 0;
            }

            char[] chars = new char[1];
            value.getChars(0, 1, chars, 0);
            return chars[0];
        }

        // 非基本类型,
        if (StringUtils.isEmpty(value)) {
            return null;
        }


        if (type == Integer.class) {
            return Integer.valueOf(value);
        } else if (type == Long.class) {
            return Long.valueOf(value);
        } else if (type == Float.class) {
            return Float.valueOf(value);
        } else if (type == Double.class) {
            return Double.valueOf(value);
        } else if (type == Boolean.class) {
            return Boolean.valueOf(value);
        } else if (type == Byte.class) {
            return value.getBytes()[0];
        } else if (type == Character.class) {
            char[] chars = new char[1];
            value.getChars(0, 1, chars, 0);
            return chars[0];
        }


        throw new IllegalStateException("argument not basic type! now type:" + type.getName());
    }
1. 字符串的首字母大写

最直观的做法是直接用String的内置方法

return str.substring(0,1).toUpperCase() + str.substring(1);

因为substring内部实际上会新生成一个String对象,所以上面这行代码实际上新生成了三个对象(+号又生成了一个),而我们的代码中, 则直接获取String对象的字符数组,修改后重新生成一个String返回,实际只新生成了一个对象,稍微好一点

2. string 转基本数据类型

注意一下将String转换为基本的数据对象,封装对象时, 需要对空的情况进行特殊处理

3. 几个限制

BO对象必须是可实例化的

举一个反例, 下面的这个 WordBO对象就没办法通过反射创建对象

代码语言:javascript
复制
public class CsvUtilTest {
    @Getter
    @Setter
    @ToString
    private static class WordBO {
        long dicId;

        String name;

        Boolean rootWord;

        Float weight;

//        public WordDO() {
//        }
    }
}

解决办法是加一个默认的无参构造方法即可


BO对象要求

  • 显示声明无参构造方法
  • 属性 abc 的设置方法命名为 setAbc(xxx)
  • 属性都是基本的数据结构 (若对象是以json字符串格式存csv文件时,可利用json工具进行反序列化,这样可能会更加简单)
  • BO对象的属性名与CsvRecord中的对象名相同

测试一发

代码语言:javascript
复制
@Test
public void testCsvReadV2() throws IOException {
   String fileName = "word.csv";
   List<CSVRecord> list = CsvUtil.read(fileName, new String[]{"dicId", "name", "rootWord", "weight"});
   Assert.assertTrue(list != null && list.size() > 0);
   
   try {
       List<WordDO> words = new ArrayList<>(list.size() - 1);
       for (int i = 1; i < list.size(); i++) {
           words.add(parseDO(list.get(i), WordDO.class));
       }

       words.stream().forEach(
               word -> logger.info("the csv words: {}", word)
       );
   } catch (Exception e) {
       logger.error("parse DO error! e: {}", e);
   }
}

输出结果

代码语言:javascript
复制
17:17:14.640 [main] INFO  c.h.h.q.file.test.FileUtilTest - the csv words: CsvUtilTest.WordDO(dicId=1, name=质量, rootWord=true, weight=0.1)
17:17:14.658 [main] INFO  c.h.h.q.file.test.FileUtilTest - the csv words: CsvUtilTest.WordDO(dicId=2, name=服务, rootWord=true, weight=0.2)
17:17:14.658 [main] INFO  c.h.h.q.file.test.FileUtilTest - the csv words: CsvUtilTest.WordDO(dicId=3, name=发货, rootWord=null, weight=0.1)
17:17:14.659 [main] INFO  c.h.h.q.file.test.FileUtilTest - the csv words: CsvUtilTest.WordDO(dicId=4, name=性价比, rootWord=false, weight=0.4)
17:17:14.659 [main] INFO  c.h.h.q.file.test.FileUtilTest - the csv words: CsvUtilTest.WordDO(dicId=5, name=尺码, rootWord=true, weight=0.4)

注意这里发货这一个输出的 rootWord为null, 而上面的是输出false, 主要是因为解析逻辑不同导致


2. BeanUtils 分析

顶顶大名的BeanUtils, 目前流行的就有好多个 Apache的两个版本:(反射机制) org.apache.commons.beanutils.PropertyUtils.copyProperties(Object dest, Object orig) org.apache.commons.beanutils.BeanUtils.copyProperties(Object dest, Object orig) Spring版本:(反射机制) org.springframework.beans.BeanUtils.copyProperties(Object source, Object target, Class editable, String[] ignoreProperties) cglib版本:(使用动态代理,效率高) net.sf.cglib.beans.BeanCopier.copy(Object paramObject1, Object paramObject2, Converter paramConverter)

本篇分析的目标放在 BeanUtils.copyProperties

先看一个使用的case

DoA.java

代码语言:javascript
复制
@Getter
@Setter
@ToString
@AllArgsConstructor
@NoArgsConstructor
public class DoA {

    private String name;

    private long phone;
}

DoB.java

代码语言:javascript
复制
@Getter
@Setter
@ToString
@AllArgsConstructor
@NoArgsConstructor
public class DoB {
    private String name;

    private long phone;
}

测试case

代码语言:javascript
复制
@Test
public void testBeanCopy() throws InvocationTargetException, IllegalAccessException {
   DoA doA = new DoA();
   doA.setName("yihui");
   doA.setPhone(1234234L);

   DoB doB = new DoB();
   BeanUtils.copyProperties(doB, doA);
   log.info("doB: {}", doB);

   BeanUtils.setProperty(doB, "name", doA.getName());
   BeanUtils.setProperty(doB, "phone", doB.getPhone());
   log.info("doB: {}", doB);
}

1, 属性拷贝逻辑

实际看下属性拷贝的代码,

  • 获取对象的属性描述类 PropertyDescriptor,
  • 然后遍历可以进行赋值的属性 getPropertyUtils().isReadable(orig, name) && getPropertyUtils().isWriteable(dest, name)
  • 获取orgi属性名 + 属性值,执行赋值 copyProperty(dest, name, value);
代码语言:javascript
复制
PropertyDescriptor[] origDescriptors =
      getPropertyUtils().getPropertyDescriptors(orig);
for (int i = 0; i < origDescriptors.length; i++) {
 String name = origDescriptors[i].getName();
 if ("class".equals(name)) {
     continue; // No point in trying to set an object's class
 }
 if (getPropertyUtils().isReadable(orig, name) &&
     getPropertyUtils().isWriteable(dest, name)) {
     try {
         Object value =
             getPropertyUtils().getSimpleProperty(orig, name);
        // 获取源对象的 属性名 + 属性值, 调用 copyProperty方法实现赋值
         copyProperty(dest, name, value);
     } catch (NoSuchMethodException e) {
         // Should not happen
     }
 }

2. PropertyDescriptor

jdk说明: A PropertyDescriptor describes one property that a Java Bean exports via a pair of accessor methods.

根据class得到这个属性之后,基本上就get到各种属性,以及属性的设置方法了

内部的几个关键属性

代码语言:javascript
复制
// bean 的成员类型
private Reference<? extends Class<?>> propertyTypeRef;
// bean 的成员读方法
private final MethodRef readMethodRef = new MethodRef();
// bean 的成员写方法
private final MethodRef writeMethodRef = new MethodRef();

MethodRef.java, 包含了方法的引用

代码语言:javascript
复制
final class MethodRef {
    // 方法签名 , 如 : public void com.hust.hui.quicksilver.file.test.dos.DoA.setName(java.lang.String)
    private String signature;
    private SoftReference<Method> methodRef;
    // 方法所在的类对应的class
    private WeakReference<Class<?>> typeRef;
}

一个实例的截图如下

如何获取 PropertyDescriptor 对象呢 ? 通过 java.beans.BeanInfo#getPropertyDescriptors 即可, 顺着 PropertyDescriptor[] origDescriptors = getPropertyUtils().getPropertyDescriptors(orig); , 一路摸到如何根据 class 获取 BeanInfo对象, 贴一下几个重要的节点

  • org.apache.commons.beanutils.PropertyUtilsBean#getPropertyDescriptors(java.lang.Class<?>) <--
  • org.apache.commons.beanutils.PropertyUtilsBean#getIntrospectionData <--
  • org.apache.commons.beanutils.PropertyUtilsBean#fetchIntrospectionData <--
  • org.apache.commons.beanutils.DefaultBeanIntrospector#introspect <--
  • java.beans.Introspector#getBeanInfo(java.lang.Class<?>)
代码语言:js
复制
beanInfo = new Introspector(beanClass, null, USE_ALL_BEANINFO).getBeanInfo();

在创建 `Introspector` 对象时, 会递归获取class的超类,也就是说超类中的属性也会包含进来, 构造方法中,调用了下面的方法 `findExplicitBeanInfo` , 这里实际上借用的是jdk的 `BeanInfoFinder#find()` 方法

/**
 * 
 */
private static BeanInfo findExplicitBeanInfo(Class<?> beanClass) {
    return ThreadGroupContext.getContext().getBeanInfoFinder().find(beanClass);
}

3. 属性拷贝

上面通过内省获取了Bean对象的基本信息(成员变量 + 读写方法), 剩下的一个点就是源码中的 copyProperty(dest, name, value); 实际的属性值设置

看代码中,用了很多看似高大上的东西,排除掉一些不关心的,主要干的就是这么几件事情

  • 属性描述对象 descriptor = getPropertyUtils().getPropertyDescriptor(target, name);
  • 参数类型 type = descriptor.getPropertyType();
  • 属性值的类型转换 value = convertForCopy(value, type);
  • 属性值设置 getPropertyUtils().setSimpleProperty(target, propName, value);

最后属性设置的源码如下, 删了很多不关心的代码,基本上和我们上面的实现相差不大

代码语言:javascript
复制
public void setSimpleProperty(Object bean,
                                         String name, Object value)
            throws IllegalAccessException, InvocationTargetException,
            NoSuchMethodException {

        // Retrieve the property setter method for the specified property
        PropertyDescriptor descriptor =
                getPropertyDescriptor(bean, name);

        Method writeMethod = getWriteMethod(bean.getClass(), descriptor);

        // Call the property setter method
        Object[] values = new Object[1];
        values[0] = value;

        invokeMethod(writeMethod, bean, values);

    }

4. 小结

apache的BeanUtils实现属性拷贝的思路和我们上面的设计相差不多,那么差距在哪 ? 仔细看 BeaUtils 源码,发现有很多优化点

  • 获取 clas对应的 BeanInfo 用了缓存,相当于一个class只用反射获取一次即可,避免每次都这么干
  • 类型转换,相比较我们上面原始到爆的简陋方案,BeanUtils使用的是专门做类型转换的 Converter 来实现,所有你可以自己定义各种类型的转换,注册进去后可以实现各种鬼畜的场景了
  • 各种异常边界的处理 (单反一个开源的成熟产品,这一块真心没话说)
  • DynaBean Map Array 这几个类型单独进行处理,上面也没有分析
  • 用内省来操作JavaBean对象,而非使用反射 参考博文《深入理解Java:内省(Introspector)》
本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • java之通过反射生成并初始化对象
    • 1. CSVRecord对象转xxxBO对象
      • 设计思路
      • 实现代码
      • 测试一发
    • 2. BeanUtils 分析
      • 1, 属性拷贝逻辑
      • 2. PropertyDescriptor
      • 3. 属性拷贝
      • 4. 小结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档