java之通过反射生成并初始化对象
在博文 《java之的读取文件大全》 中读取csv文件后,需要自己将csv文件的对象转为自己的DO对象,那么有没有办法我直接穿进去一个DO的class对象,内部实现生成对象,并利用
CSVRecord
对象对其进行初始化呢 ?
本篇主要是为了解决上面的这个问题,实现了一个非常初级转换方法,然后会分析下大名鼎鼎的BeanUtils
是如何实现这种功能的
1. CSVRecord
对象转xxxBO
对象
在做之前,先把csv的读取相关代码贴出来,具体的实现逻辑详解可以参考 《java之的读取文件大全》
CsvUtil.java
/**
* 读取文件
*/
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
文件
dicId,"name",rootWord,weight
1,"质量",true,0.1
2,"服务",true,0.2
3,"发货",,0.1
4,"性价比",false,0.4
5,"尺码",true,0.4
测试用例
@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;
}
输出结果
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);
- 方法名:
实现代码
基本结构如上,先贴出实现的代码,并对其中的几点做一下简短的说明
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
对象就没办法通过反射创建对象
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
中的对象名相同
测试一发
@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);
}
}
输出结果
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
@Getter
@Setter
@ToString
@AllArgsConstructor
@NoArgsConstructor
public class DoA {
private String name;
private long phone;
}
DoB.java
@Getter
@Setter
@ToString
@AllArgsConstructor
@NoArgsConstructor
public class DoB {
private String name;
private long phone;
}
测试case
@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);
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到各种属性,以及属性的设置方法了
内部的几个关键属性
// bean 的成员类型
private Reference<? extends Class<?>> propertyTypeRef;
// bean 的成员读方法
private final MethodRef readMethodRef = new MethodRef();
// bean 的成员写方法
private final MethodRef writeMethodRef = new MethodRef();
MethodRef.java
, 包含了方法的引用
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<?>)
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);
最后属性设置的源码如下, 删了很多不关心的代码,基本上和我们上面的实现相差不大
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)》