Spring提供的BeanUtils源码剖析目录
前言
前段时间刚入职xxx公司,由于公司内部有一套自己的技术架构,并且很多东西都是自研、封装的。身为小菜鸡的在业余时间抽空看了一下里面的源码,感觉这些人基础挺扎实的哈哈哈哈,这些个基础架构工具包全是自研的,牛逼。一下子激发了我看 BeanUtils 源码的兴趣,想着靠自己的摸索,结合之前研究的JDK动态代理技术,自己也尝试着写一套功能类似的基架组件出来(结合代理技术的以后再写,这玩意有点繁琐)。
先导知识之JAVA内省
不清楚JAVA内省的小伙伴,先来了解一下这方面的知识,何为内省?内省是JAVA提供的一组可以操纵 JavaBean 属性的一组 Api,掌握了这些个 Api 我们也可以写一个自己的 BeanUtils 出来。下面先来介绍一波这些个 Api。
- Introspector.getBeanInfo(o.getClass()) 获取当类的一个BeanInfo对象,和Spring中的 BeanDefinition 对象有点类似,BeanDefinition 是封装了 Bean 信息的一个信息对象,BeanInfo 是封装了 Java 对象一系列 Api 方法的对象。
- BeanInfo.getPropertyDescriptors() 获取 PropertyDescriptor 对象,可以通过此对象获取类字段属性 name、value、get和set方法名字、对字段进行属性填充。等操作
山寨版 BeanUtils.copyProperties()
恭喜你已经通过了入职考核,有资格进入 zzh 盗版集团来,我们集团这里的人都是人中翘楚、人中龙凤、国之栋梁、梁之中流砥柱、柱中之无懈可击的存在了。下面列举一个 我司开发的山寨版BeanUtils.copyProperties()软件 给读者参观(),温馨提示:前方500米有宝箱,请大家仔细翻阅。大概流程为:遍历操作对源对象与目标对象进行属性名的匹配,匹配上了,随即进行对应属性名的value值填充。
public class IntrospectionTest {
public static List<Object> init() {
List<Object> zzhSources = new ArrayList<>();
ZzhSource zzhSource = new ZzhSource();
zzhSource.setAge("1");
zzhSource.setName("zzh1");
ZzhSource zzhSource2 = new ZzhSource();
zzhSource2.setAge("2");
zzhSource2.setName("zzh2");
zzhSources.add(zzhSource);
zzhSources.add(zzhSource2);
return zzhSources;
}
@SneakyThrows
public static void main(String[] args) {
List<Object> zzhTargets = new ArrayList<>();
System.err.println("copy before: " + zzhTargets);
copyProperties(init(), zzhTargets, ZzhTarget.class);
System.err.println("copy after:: " + zzhTargets);
}
public static void copyProperties(List<Object> sources, List<Object> targets, Class targetClass) {
sources.stream().forEach(source -> {
try {
Object o = targetClass.newInstance();
Arrays.stream(Introspector.getBeanInfo(source.getClass()).getPropertyDescriptors()).forEach(sp -> {
try {
/**
* 遍历操作:targetClass 对应的对象与 sources 集合中对应的对象进行属性匹配
*/
Arrays.stream(Introspector.getBeanInfo(o.getClass()).getPropertyDescriptors()).forEach(op -> {
/**
* 名称匹配上了之后进行属性填充
*/
if (sp.getName().equals(op.getName()) && !sp.getName().equals("class")) {
try {
/**
* 对我们新创建的 o 对象进行属性填充,填充数据的来源为 sources
*/
op.getWriteMethod().invoke(o, sp.getReadMethod().invoke(source));
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
});
} catch (IntrospectionException e) {
e.printStackTrace();
}
});
/**
* o 对象属性填充完毕,添加至 targets集合中
*/
targets.add(o);
} catch (IntrospectionException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
});
}
@Data
static class ZzhSource {
String name;
String age;
String sex;
}
@Data
static class ZzhTarget {
String name;
String age;
String sex;
}
}
效果展示
可以看到数据成功的拷贝到了我们指定的 List 中了
正版 BeanUtils.copyProperties() 源码剖析
步骤流程总是惊人的相似,获取对象的 PropertyDescriptor ,然后调用内省为我们提供的各种操作 字段 属性的Api,最终达到属性拷贝的目的。
private static void copyProperties(Object source, Object target, @Nullable Class<?> editable, @Nullable String... ignoreProperties) throws BeansException {
Assert.notNull(source, "Source must not be null");
Assert.notNull(target, "Target must not be null");
//获取目标对象Class
Class<?> actualEditable = target.getClass();
if (editable != null) {
if (!editable.isInstance(target)) {
throw new IllegalArgumentException("Target class [" + target.getClass().getName() + "] not assignable to Editable class [" + editable.getName() + "]");
}
actualEditable = editable;
}
//获取目标对象的所有 PropertyDescriptor
PropertyDescriptor[] targetPds = getPropertyDescriptors(actualEditable);
List<String> ignoreList = ignoreProperties != null ? Arrays.asList(ignoreProperties) : null;
PropertyDescriptor[] var7 = targetPds;
int var8 = targetPds.length;
//遍历目标对象的 PropertyDescriptor
for(int var9 = 0; var9 < var8; ++var9) {
PropertyDescriptor targetPd = var7[var9];
//获取目标对象的写方法,通过此方法可以对指定字段赋值
Method writeMethod = targetPd.getWriteMethod();
if (writeMethod != null && (ignoreList == null || !ignoreList.contains(targetPd.getName()))) {
//获取源对象 PropertyDescriptor
PropertyDescriptor sourcePd = getPropertyDescriptor(source.getClass(), targetPd.getName());
if (sourcePd != null) {
//获取源对象的读方法,通过此方法可以获取对应字段的 value值
Method readMethod = sourcePd.getReadMethod();
if (readMethod != null && ClassUtils.isAssignable(writeMethod.getParameterTypes()[0], readMethod.getReturnType())) {
try {
//设置一下方法操作权限,为可操作
if (!Modifier.isPublic(readMethod.getDeclaringClass().getModifiers())) {
readMethod.setAccessible(true);
}
//获取源对象字段中的 value值
Object value = readMethod.invoke(source);
if (!Modifier.isPublic(writeMethod.getDeclaringClass().getModifiers())) {
writeMethod.setAccessible(true);
}
//为目标对象进行属性填充
writeMethod.invoke(target, value);
} catch (Throwable var15) {
throw new FatalBeanException("Could not copy property '" + targetPd.getName() + "' from source to target", var15);
}
}
}
}
}
}
上班一年的时候补充:Map转对象(补充)
最近在写 Es 相关的业务,但是懒得用框架,主要是 Es 6 和 Es 7 和 Es 8 之间的 api 语法相差过大,实在是垃圾,于是自己基于原始 Api 封装了一套基于组内同时使用,其中就包括 Map 转 对象,传统写法需要一个个 Set 实在是垃圾,于是用反射封转了如下一个抽象方法。
public interface Trans<T> {
abstract Class getTargetClass();
default Object trans(Map<String, Object> map) throws IntrospectionException, InstantiationException, IllegalAccessException {
Object o = getTargetClass().newInstance();
BeanInfo beanInfo = Introspector.getBeanInfo(o.getClass());
Arrays.stream(beanInfo.getPropertyDescriptors()).forEach(propertyDescriptor -> {
if (!"this".equals(propertyDescriptor.getName())&&!Objects.isNull(map.get(propertyDescriptor.getName()))) {
try {
propertyDescriptor.getWriteMethod().invoke(o, map.get(propertyDescriptor.getName()));
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
});
return o;
}
}
抽象接口 Trans 使用
使用很简单每个业务对象实现 Trans 中的抽象方法即可
后续 Es 查到数据直接 new ArticleResponse().trans(hit.getSourceAsMap()) 即可得到填充好的对象,简单方便!
初版工具类的 Trans 接口存在的 bug优化!加入高亮字段映射
之前没考虑到父类属性赋值的问题,以及 Es 查到的数据源全是字符串,需按照类型进行映射,比如 Es 查到的日期是 2023-12-12 字符串类型的,但是映射实体类是 Date 接受的,这种情况优化了一下代码 如下。
主要逻辑就是根据字段类型做一个匹配映射,匹配类型对不上的进行强转,以及加入高亮字段替换!!!
/**
* map 转对象
* author:zzh
*
* @param <T>
*/
public interface Trans<T> {
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
abstract Class getTargetClass();
default PropertyDescriptor[] mergeAndDeduplicate(PropertyDescriptor[]... arrays) {
Set<PropertyDescriptor> set = new HashSet<>();
for (PropertyDescriptor[] array : arrays) {
set.addAll(Arrays.asList(array));
}
return set.toArray(new PropertyDescriptor[0]);
}
default Object trans(Map<String, Object> map, Map<String, HighlightField> highlightFields) throws IntrospectionException, InstantiationException, IllegalAccessException {
Object o = getTargetClass().newInstance();
Class tclass = getTargetClass();
HashMap<String, Class> nameTypeMap = new HashMap<>();
//找到父类的所有字段
do {
Arrays.stream(tclass.getDeclaredFields()).forEach(field -> {
field.setAccessible(true);
//key:字段名称,value:字段类型
nameTypeMap.put(field.getName(), field.getType());
});
tclass = tclass.getSuperclass();
} while (!tclass.equals(Object.class));
PropertyDescriptor[] propertyDescriptors = Introspector.getBeanInfo(o.getClass()).getPropertyDescriptors();
Arrays.stream(propertyDescriptors).forEach(propertyDescriptor -> {
if (!"targetClass".equals(propertyDescriptor.getName()) && !Objects.isNull(map.get(propertyDescriptor.getName()))) {
try {
Method writeMethod = propertyDescriptor.getWriteMethod();
if (null != writeMethod) {
if (!Modifier.isPublic(writeMethod.getDeclaringClass().getModifiers())) {
writeMethod.setAccessible(true);
}
Object sourceValue = map.get(propertyDescriptor.getName());
Class aClass = nameTypeMap.get(propertyDescriptor.getName());
//类型一致,直接赋值
if (sourceValue.getClass().equals(aClass)) {
writeMethod.invoke(o, highlightFields.get(propertyDescriptor.getName()) != null ?
(highlightFields.get(propertyDescriptor.getName()).getFragments()[0]).toString() :
map.get(propertyDescriptor.getName()));
}
/**
* 类型不一致强转,这里可以搞个策略模式优化优化
*/
else {
if (aClass.equals(Date.class)) {
Date parse = simpleDateFormat.parse(String.valueOf(map.get(propertyDescriptor.getName())));
writeMethod.invoke(o, parse);
}
if (aClass.equals(Integer.class)) {
writeMethod.invoke(o, Integer.valueOf(String.valueOf(map.get(propertyDescriptor.getName()))));
}
if (aClass.equals(Long.class)) {
writeMethod.invoke(o, Long.valueOf(String.valueOf(map.get(propertyDescriptor.getName()))));
}
if (aClass.equals(int.class)) {
writeMethod.invoke(o, Integer.parseInt(String.valueOf(map.get(propertyDescriptor.getName()))));
}
}
} else throw new RuntimeException(propertyDescriptor.getName() + "~ writeMethod is null!!!!!");
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (Exception e) {
//类型不匹配
// System.err.println(propertyDescriptor.getWriteMethod());
// System.err.println(propertyDescriptor.getName());
// System.err.println(o);
// System.err.println(map.get(propertyDescriptor.getName()));
e.printStackTrace();
}
}
});
return o;
}
}
抽象接口 Trans 优化
又优化了一下复杂对象下高亮字段的映射!!!!!最终代码如下
/**
* map 转对象
* author:zzh
*
* @param <T>
*/
public interface Trans<T> {
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
abstract Class getTargetClass();
default PropertyDescriptor[] mergeAndDeduplicate(PropertyDescriptor[]... arrays) {
Set<PropertyDescriptor> set = new HashSet<>();
for (PropertyDescriptor[] array : arrays) {
set.addAll(Arrays.asList(array));
}
return set.toArray(new PropertyDescriptor[0]);
}
default Object trans(Map<String, Object> map, Map<String, HighlightField> highlightFieldsSource, List<String> highLightFields) throws IntrospectionException, InstantiationException, IllegalAccessException {
Object o = getTargetClass().newInstance();
Class tclass = getTargetClass();
HashMap<String, Class> nameTypeMap = new HashMap<>();
//找到父类的所有字段
do {
Arrays.stream(tclass.getDeclaredFields()).forEach(field -> {
field.setAccessible(true);
//key:字段名称,value:字段类型
nameTypeMap.put(field.getName(), field.getType());
});
tclass = tclass.getSuperclass();
} while (!tclass.equals(Object.class));
PropertyDescriptor[] propertyDescriptors = Introspector.getBeanInfo(o.getClass()).getPropertyDescriptors();
Arrays.stream(propertyDescriptors).forEach(propertyDescriptor -> {
if (!"targetClass".equals(propertyDescriptor.getName()) && !Objects.isNull(map.get(propertyDescriptor.getName()))) {
try {
Method writeMethod = propertyDescriptor.getWriteMethod();
if (null != writeMethod) {
if (!Modifier.isPublic(writeMethod.getDeclaringClass().getModifiers())) {
writeMethod.setAccessible(true);
}
Object sourceValue = map.get(propertyDescriptor.getName());
//父类以及自己所有字段类型
Class aClass = nameTypeMap.get(propertyDescriptor.getName());
//字符串类型以及高亮直接赋值
if (sourceValue.getClass().equals(aClass)) {
writeMethod.invoke(o, highlightFieldsSource.get(propertyDescriptor.getName()) != null ?
(highlightFieldsSource.get(propertyDescriptor.getName()).getFragments()[0]).toString() :
map.get(propertyDescriptor.getName()));
}
/**
* 类型不一致强转,这里可以搞个策略模式优化优化
*/
else {
if (aClass.equals(Date.class)) {
Date parse = simpleDateFormat.parse(String.valueOf(map.get(propertyDescriptor.getName())));
writeMethod.invoke(o, parse);
}
if (aClass.equals(Integer.class)) {
writeMethod.invoke(o, Integer.valueOf(String.valueOf(map.get(propertyDescriptor.getName()))));
}
if (aClass.equals(Long.class)) {
writeMethod.invoke(o, Long.valueOf(String.valueOf(map.get(propertyDescriptor.getName()))));
}
if (aClass.equals(List.class)) {
ArrayList<Map<String, Object>> oraginSources = (ArrayList<Map<String, Object>>) map.get(propertyDescriptor.getName());
if (null != oraginSources &&
0 != highlightFieldsSource.size()) {
for (int i = 0; i < oraginSources.size(); i++) {
for (int j = 0; j < highLightFields.size(); j++) {
try {
if (highlightFieldsSource.containsKey(highLightFields.get(j))) {
oraginSources.get(i).put(highLightFields.get(j).split("\\.")[1],
highlightFieldsSource.get(highLightFields.get(j)).getFragments()[j].toString());
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
writeMethod.invoke(o, oraginSources);
}
if (aClass.equals(int.class)) {
writeMethod.invoke(o, Integer.parseInt(String.valueOf(map.get(propertyDescriptor.getName()))));
}
}
} else throw new RuntimeException(propertyDescriptor.getName() + "~ writeMethod is null!!!!!");
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (Exception e) {
//类型不匹配
// System.err.println(propertyDescriptor.getWriteMethod());
// System.err.println(propertyDescriptor.getName());
// System.err.println(o);
// System.err.println(map.get(propertyDescriptor.getName()));
e.printStackTrace();
}
}
});
return o;
}
}
抽象接口 Trans 最终效果
使用方法如下
可以看到复杂对象也成功加上高亮标签
总结
Spring为我们提供的 BeanUtils 工具类其实也就是对 内省 Api中的一些方法的包转而已,思考:内省的实现原理是什么呢?我们是否可以从 Jdk 或者 CgLib动态代理入手,自己写一套 山寨版 内省 Api呢?答案肯定是可以的,以后有时间再去研究一下这个,读者感兴趣的话,也可以尝试着自己写一套内省 Api哦
小咸鱼的技术窝
关注不迷路,日后分享更多技术干货,B站、CSDN、微信公众号同名,名称都是(小咸鱼的技术窝)更多详情在主页