Java对象复制系列三: 手把手带你写一个 Spring BeanUtils
liebian365 2024-10-22 15:40 20 浏览 0 评论
前言
上一节我带大家手写了一个 Apache BeanUtils,并阅读了源码,大家应该都自己去阅读过源码了吧?
今天我大家手写一个 Spring BeanUtils,不像 Apache BeanUtils 我们就写了一个简单的案例,这次我们要写一个完整的 Spring BeanUtils 并带上和 Apache BeanUtils ,看看我们自己写的和 Apache BeanUtils 性能会有啥差别?
最佳实践
直接上案例
案例地址GitHub: https://github.com/zhuangjiaju/easytools/blob/main/easytools-test/src/test/java/com/github/zhuangjiaju/easytools/test/demo/beanutils/SpringBeanUtilsTest.java 案例地址gitee: https://gitee.com/zhuangjiaju/easytools/blob/main/easytools-test/src/test/java/com/github/zhuangjiaju/easytools/test/demo/beanutils/SpringBeanUtilsTest.java
需要上一期的教程自己去主页翻。
简单的复制对象
直接上代码:
/**
* Spring BeanUtils 使用的demo
*/
@Test
public void demo() throws Exception {
BeanUtilsDemoDTO beanUtilsDemo = new BeanUtilsDemoDTO();
beanUtilsDemo.setId("id");
beanUtilsDemo.setFirstName("firstName");
BeanUtilsDemoDTO newBeanUtilsDemo = new BeanUtilsDemoDTO();
//这里注意 spring 和 apache 的参数位置不一样 spring 是先 source 对象 后 target 对象
BeanUtils.copyProperties(beanUtilsDemo, newBeanUtilsDemo);
log.info("newBeanUtilsDemo: {}", newBeanUtilsDemo);
}
输出结果:
19:47:02.946 [main] INFO com.github.zhuangjiaju.easytools.test.demo.beanutils.SpringBeanUtilsTest -- newBeanUtilsDemo: BeanUtilsDemoDTO(id=id, firstName=firstName, lastName=null, age=null, email=null, phoneNumber=null, address=null, city=null, state=null, country=null, major=null, gpa=null, department=null, yearOfStudy=null, advisorName=null, enrollmentStatus=null, dormitoryName=null, roommateName=null, scholarshipDetails=null, extracurricularActivities=null)
可见已经复制对象成功了,输出里面有 firstName 的值。这里要注意 spring 和 apache 的参数位置不一样 spring 是先 source 对象 ,后 target 对象。
自己写一个完整的 Spring BeanUtils
Spring BeanUtils 源码相对比较简单,我们这里直接实现全部逻辑了,自己实现一遍在去看源码就会感觉怎么这么简单。
/**
* 自己写一个完整的 Spring BeanUtils
*/
@Test
public void custom() throws Exception {
BeanUtilsDemoDTO beanUtilsDemo = new BeanUtilsDemoDTO();
beanUtilsDemo.setId("id");
beanUtilsDemo.setFirstName("firstName");
BeanUtilsDemoDTO newBeanUtilsDemo = new BeanUtilsDemoDTO();
MySpringBeanUtils.copyProperties(beanUtilsDemo, newBeanUtilsDemo);
log.info("newBeanUtilsDemo: {}", newBeanUtilsDemo);
}
我们自己实现的工具类:
前置知识:
Introspector.getBeanInfo: 是 Java 自带的一个类,可以获取一个类的 BeanInfo 信息,然后获取属性的描述资料 PropertyDescriptor BeanInfo : bean 的描述信息 PropertyDescriptor: bean 的属性的资料信息 ,可以获取到属性的 get/set 方法 Method: 方法,用这个对象可以反射掉调用
public static class MySpringBeanUtils {
private static final ConcurrentMap<Class<?>, DescriptorCache> STRONG_CLASS_CACHE
= new ConcurrentHashMap<>();
/**
* 复制参数
*
* @param source
* @param target
* @throws Exception
*/
public static void copyProperties(Object source, Object target) throws Exception {
// 去缓存 获取目标对象的 PropertyDescriptor 属性资料
DescriptorCache targetDescriptorCache = getPropertyDescriptorFormClass(target.getClass());
// 去缓存 获取来源对象的 PropertyDescriptor 属性资料
DescriptorCache sourceDescriptorCache = getPropertyDescriptorFormClass(source.getClass());
// 循环目标对象
for (PropertyDescriptor targetPropertyDescriptor : targetDescriptorCache.getPropertyDescriptors()) {
// 获取属性名
String name = targetPropertyDescriptor.getName();
// 去Map 直接获取来源对象的属性资料 不用循环获取
PropertyDescriptor sourcePropertyDescriptor = sourceDescriptorCache.getPropertyDescriptorMap().get(
name);
// 反射直接调用方法
targetPropertyDescriptor.getWriteMethod().invoke(target,
sourcePropertyDescriptor.getReadMethod().invoke(source));
}
}
/**
* 我们在这里加一个缓存 这样不用每次都去解析 PropertyDescriptor
* @param beanClass
* @return
* @throws Exception
*/
private static DescriptorCache getPropertyDescriptorFormClass(Class<?> beanClass) throws Exception {
return STRONG_CLASS_CACHE.computeIfAbsent(beanClass, clazz -> {
try {
// 解析 PropertyDescriptor
PropertyDescriptor[] propertyDescriptors = Introspector.getBeanInfo(beanClass,
Object.class).getPropertyDescriptors();
// 转换成name的map 时间复杂度由 O(n) -> O(1) 也是用于加速
Map<String, PropertyDescriptor> propertyDescriptorMap = Arrays.stream(propertyDescriptors)
.collect(Collectors.toMap(PropertyDescriptor::getName, Function.identity(),
(oldValue, newValue) -> newValue));
return DescriptorCache.builder()
.propertyDescriptors(propertyDescriptors)
.propertyDescriptorMap(propertyDescriptorMap)
.build();
} catch (IntrospectionException ignore) {
}
return null;
});
}
}
缓存对象
@Data
@SuperBuilder
@AllArgsConstructor
@NoArgsConstructor
public static class DescriptorCache {
/**
* 存储所有的 PropertyDescriptor 属性资料
*/
private PropertyDescriptor[] propertyDescriptors;
/**
* 存储对应方法的 PropertyDescriptor 属性资料 用于加速
*/
private Map<String, PropertyDescriptor> propertyDescriptorMap;
}
这个和上一期的 Apache BeanUtils 我们加入了缓存,速度有了质的飞跃。
其实Spring BeanUtils 实现基本我们实现的一模一样,特别的容易。
性能测试
性能测试的代码 我就不贴了,大家有兴趣的 可以直接看案例代码。
1次 | 100次 | 1000次 | 10000次 | 100000次 | |
Apache BeanUtils | 33ms | 17ms | 52ms | 264ms | 1964ms |
自己写的BeanUtils | 5ms | 12ms | 18ms | 13ms | 116ms |
直接get/set | 0ms | 1ms | 1ms | 1ms | 13ms |
发现了没有 我们自己几行代码就实现了一个性能秒杀 Apache BeanUtils 的 BeanUtils。
很多源码没有大家想象的那么复杂,只要多去看看就行。
源码解析
org.apache.commons.beanutils.BeanUtils.copyProperties org.springframework.beans.BeanUtils.copyProperties(java.lang.Object, java.lang.Object, java.lang.Class<?>, java.lang.String...)
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<?> 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);
Set<String> ignoredProps = (ignoreProperties != null ? new HashSet<>(Arrays.asList(ignoreProperties)) : null);
CachedIntrospectionResults sourceResults = (actualEditable != source.getClass() ?
CachedIntrospectionResults.forClass(source.getClass()) : null);
// 循环目标的 PropertyDescriptor
for (PropertyDescriptor targetPd : targetPds) {
Method writeMethod = targetPd.getWriteMethod();
if (writeMethod != null && (ignoredProps == null || !ignoredProps.contains(targetPd.getName()))) {
PropertyDescriptor sourcePd = (sourceResults != null ?
sourceResults.getPropertyDescriptor(targetPd.getName()) : targetPd);
if (sourcePd != null) {
Method readMethod = sourcePd.getReadMethod();
if (readMethod != null) {
if (isAssignable(writeMethod, readMethod, sourcePd, targetPd)) {
try {
ReflectionUtils.makeAccessible(readMethod);
// 读取到source 中的值
Object value = readMethod.invoke(source);
ReflectionUtils.makeAccessible(writeMethod);
// 反射写到 target
writeMethod.invoke(target, value);
} catch (Throwable ex) {
throw new FatalBeanException(
"Could not copy property '" + targetPd.getName() + "' from source to target", ex);
}
}
}
}
}
}
}
org.springframework.beans.BeanUtils.getPropertyDescriptors org.springframework.beans.CachedIntrospectionResults.forClass
static CachedIntrospectionResults forClass(Class<?> beanClass) throws BeansException {
// 判断缓存中是否存在 存在则返回
CachedIntrospectionResults results = strongClassCache.get(beanClass);
if (results != null) {
return results;
}
results = softClassCache.get(beanClass);
if (results != null) {
return results;
}
// 封装缓存的对象 核心代码在 CachedIntrospectionResults 的构造方法
results = new CachedIntrospectionResults(beanClass);
ConcurrentMap<Class<?>, CachedIntrospectionResults> classCacheToUse;
if (ClassUtils.isCacheSafe(beanClass, CachedIntrospectionResults.class.getClassLoader()) ||
isClassLoaderAccepted(beanClass.getClassLoader())) {
classCacheToUse = strongClassCache;
} else {
if (logger.isDebugEnabled()) {
logger.debug("Not strongly caching class [" + beanClass.getName() + "] because it is not cache-safe");
}
classCacheToUse = softClassCache;
}
CachedIntrospectionResults existing = classCacheToUse.putIfAbsent(beanClass, results);
return (existing != null ? existing : results);
}
org.springframework.beans.CachedIntrospectionResults.CachedIntrospectionResults
private CachedIntrospectionResults(Class<?> beanClass) throws BeansException {
try {
if (logger.isTraceEnabled()) {
logger.trace("Getting BeanInfo for class [" + beanClass.getName() + "]");
}
// 获取bean 的信息 最终调用了:Introspector.getBeanInfo
this.beanInfo = getBeanInfo(beanClass);
if (logger.isTraceEnabled()) {
logger.trace("Caching PropertyDescriptors for class [" + beanClass.getName() + "]");
}
this.propertyDescriptors = new LinkedHashMap<>();
Set<String> readMethodNames = new HashSet<>();
// This call is slow so we do it once.
// 获取所有的 PropertyDescriptor
PropertyDescriptor[] pds = this.beanInfo.getPropertyDescriptors();
for (PropertyDescriptor pd : pds) {
// 忽略各种不需要的属性
if (Class.class == beanClass && !("name".equals(pd.getName()) ||
(pd.getName().endsWith("Name") && String.class == pd.getPropertyType()))) {
// Only allow all name variants of Class properties
continue;
}
if (URL.class == beanClass && "content".equals(pd.getName())) {
// Only allow URL attribute introspection, not content resolution
continue;
}
if (pd.getWriteMethod() == null && isInvalidReadOnlyPropertyType(pd.getPropertyType(), beanClass)) {
// Ignore read-only properties such as ClassLoader - no need to bind to those
continue;
}
if (logger.isTraceEnabled()) {
logger.trace("Found bean property '" + pd.getName() + "'" +
(pd.getPropertyType() != null ? " of type [" + pd.getPropertyType().getName() + "]" : "") +
(pd.getPropertyEditorClass() != null ?
"; editor [" + pd.getPropertyEditorClass().getName() + "]" : ""));
}
pd = buildGenericTypeAwarePropertyDescriptor(beanClass, pd);
// 生成一个 PropertyDescriptor 的map 提高调用速度
this.propertyDescriptors.put(pd.getName(), pd);
Method readMethod = pd.getReadMethod();
if (readMethod != null) {
readMethodNames.add(readMethod.getName());
}
}
// Explicitly check implemented interfaces for setter/getter methods as well,
// in particular for Java 8 default methods...
Class<?> currClass = beanClass;
while (currClass != null && currClass != Object.class) {
introspectInterfaces(beanClass, currClass, readMethodNames);
currClass = currClass.getSuperclass();
}
// Check for record-style accessors without prefix: e.g. "lastName()"
// - accessor method directly referring to instance field of same name
// - same convention for component accessors of Java 15 record classes
introspectPlainAccessors(beanClass, readMethodNames);
} catch (IntrospectionException ex) {
throw new FatalBeanException("Failed to obtain BeanInfo for class [" + beanClass.getName() + "]", ex);
}
}
Spring 源码就这么简单,总结下来就是 解析成PropertyDescriptor -> 缓存 -> 循环 target 的 method 去反射调用。
为什么Spring BeanUtils 速度比 Apache BeanUtils 快?
2个源码我们都看完了,实际上2个源码几乎是一模一样的, Apache BeanUtils 慢的原因如主要是:对象拷贝加了各种花里胡哨的校验。
是不是答案特别的神奇,不看源码我也没法下这个结论。
总结
我们已经学习了 Apache BeanUtils 和 Spring BeanUtils 的源码,虽然 Spring BeanUtils 的性能比 Apache BeanUtils 好很多,但是和原生的还是有一定差距。
我们有办法 进一步 的优化 BeanUtils 的性能呢?
还有不管是 Spring BeanUtils 还是 Apache BeanUtils 都只能拷贝相同的属性,如果属性名不一样怎么办?
下一节我就带大家来解决这个2个问题。
写在最后
给大家推荐一个非常完整的Java项目搭建的最佳实践,也是本文的源码出处,由大厂程序员&EasyExcel作者维护。
github地址:https://github.com/zhuangjiaju/easytools
gitee地址:https://gitee.com/zhuangjiaju/easytools
相关推荐
- 4万多吨豪华游轮遇险 竟是因为这个原因……
-
(观察者网讯)4.7万吨豪华游轮搁浅,竟是因为油量太低?据观察者网此前报道,挪威游轮“维京天空”号上周六(23日)在挪威近海发生引擎故障搁浅。船上载有1300多人,其中28人受伤住院。经过数天的调...
- “菜鸟黑客”必用兵器之“渗透测试篇二”
-
"菜鸟黑客"必用兵器之"渗透测试篇二"上篇文章主要针对伙伴们对"渗透测试"应该如何学习?"渗透测试"的基本流程?本篇文章继续上次的分享,接着介绍一下黑客们常用的渗透测试工具有哪些?以及用实验环境让大家...
- 科幻春晚丨《震动羽翼说“Hello”》两万年星间飞行,探测器对地球的最终告白
-
作者|藤井太洋译者|祝力新【编者按】2021年科幻春晚的最后一篇小说,来自大家喜爱的日本科幻作家藤井太洋。小说将视角放在一颗太空探测器上,延续了他一贯的浪漫风格。...
- 麦子陪你做作业(二):KEGG通路数据库的正确打开姿势
-
作者:麦子KEGG是通路数据库中最庞大的,涵盖基因组网络信息,主要注释基因的功能和调控关系。当我们选到了合适的候选分子,单变量研究也已做完,接着研究机制的时便可使用到它。你需要了解你的分子目前已有哪些...
- 知存科技王绍迪:突破存储墙瓶颈,详解存算一体架构优势
-
智东西(公众号:zhidxcom)编辑|韦世玮智东西6月5日消息,近日,在落幕不久的GTIC2021嵌入式AI创新峰会上,知存科技CEO王绍迪博士以《存算一体AI芯片:AIoT设备的算力新选择》...
- 每日新闻播报(September 14)_每日新闻播报英文
-
AnOscarstatuestandscoveredwithplasticduringpreparationsleadinguptothe87thAcademyAward...
- 香港新巴城巴开放实时到站数据 供科技界研发使用
-
中新网3月22日电据香港《明报》报道,香港特区政府致力推动智慧城市,鼓励公私营机构开放数据,以便科技界研发使用。香港运输署21日与新巴及城巴(两巴)公司签署谅解备忘录,两巴将于2019年第3季度,开...
- 5款不容错过的APP: Red Bull Alert,Flipagram,WifiMapper
-
本周有不少非常出色的app推出,鸵鸟电台做了一个小合集。亮相本周榜单的有WifiMapper's安卓版的app,其中包含了RedBull的一款新型闹钟,还有一款可爱的怪物主题益智游戏。一起来看看我...
- Qt动画效果展示_qt显示图片
-
今天在这篇博文中,主要实践Qt动画,做一个实例来讲解Qt动画使用,其界面如下图所示(由于没有录制为gif动画图片,所以请各位下载查看效果):该程序使用应用程序单窗口,主窗口继承于QMainWindow...
- 如何从0到1设计实现一门自己的脚本语言
-
作者:dong...
- 三年级语文上册 仿写句子 需要的直接下载打印吧
-
描写秋天的好句好段1.秋天来了,山野变成了美丽的图画。苹果露出红红的脸庞,梨树挂起金黄的灯笼,高粱举起了燃烧的火把。大雁在天空一会儿写“人”字,一会儿写“一”字。2.花园里,菊花争奇斗艳,红的似火,粉...
- C++|那些一看就很简洁、优雅、经典的小代码段
-
目录0等概率随机洗牌:1大小写转换2字符串复制...
- 二年级上册语文必考句子仿写,家长打印,孩子照着练
-
二年级上册语文必考句子仿写,家长打印,孩子照着练。具体如下:...
你 发表评论:
欢迎- 一周热门
- 最近发表
- 标签列表
-
- wireshark怎么抓包 (75)
- qt sleep (64)
- cs1.6指令代码大全 (55)
- factory-method (60)
- sqlite3_bind_blob (52)
- hibernate update (63)
- c++ base64 (70)
- nc 命令 (52)
- wm_close (51)
- epollin (51)
- sqlca.sqlcode (57)
- lua ipairs (60)
- tv_usec (64)
- 命令行进入文件夹 (53)
- postgresql array (57)
- statfs函数 (57)
- .project文件 (54)
- lua require (56)
- for_each (67)
- c#工厂模式 (57)
- wxsqlite3 (66)
- dmesg -c (58)
- fopen参数 (53)
- tar -zxvf -c (55)
- 速递查询 (52)