是时候丢掉BeanUtils了(丢掉了时间)
yuyutoo 2025-04-09 22:24 16 浏览 0 评论
前言
为了更好的进行开发和维护,我们都会对程序进行分层设计,例如常见的三层,四层,每层各司其职,相互配合。也随着分层,出现了VO,BO,PO,DTO,每层都会处理自己的数据对象,然后向上传递,这就避免不了经常要将一个对象的属性拷贝给另一个对象。
例如我有一个User对象和一个UserVO对象,要将User对象的10个属性赋值个UserVo的同名属性:
一种方式是手写,一个属性一个属性赋值,相信大家最开始学习时都是这么干的,这种方式就是太低效了。
在idea中可以安装插件帮我们快速生成set属性代码,虽然还是逐个属性赋值,但比一个个敲,效率提高了很多。
上面两种方式虽然最原始,做起来很麻烦,容易出错,但程序运行效率是最高的,现在仍有不少公司要求这么做,一是这样运行效率高,二是不需要引入其它的组件,避免出现其它问题。
但对于我们来说,这种操作要是多了,开发效率和代码可维护性都会受到影响,这种赋值属性代码很长,看起来很不舒服,所有有了下面几种方式。
bean copier
apache的BeanUtils,内部使用了反射,效率很低,在阿里java开发规范中命令禁止使用,这里就不过多讨论。
spring的BeanUtils,对apache BeanUtils做了优化,运行效率较高,可以使用。
bash复制代码BeanUtils.copyProperties(source, target);
BeanUtils.copyProperties(source, target, "id", "createTime"); //不拷贝指定的字段
cglib的BeanCopier,使用动态技术代替反射,在运行时生成一个子类,只有在第一次动态生成类时慢,后面基本就本接近原始的set,所以呀运行效率比上面两种要高很多。
ini复制代码BeanCopier beanCopier = BeanCopier.create(SourceData.class, TargetData.class, false);
beanCopier.copy(source, target, null);
我们使用的是spring BeanUtils,至少出现过两次问题:
一次是拷贝一方的对象类型变了,从int变成long,source.id int 拷贝到 target.id long 结果是空,因为类型不匹配,BeanUtils不会拷贝。由于是使用反射,所以当时修改类型时,只修改了编译报错的地方,忘记这种方式,导致结果都是空,这也很难怪开发,这种方式太隐蔽了。同样如果属性重命名,也会得到一个空,并且只能在运行时发现。
另一次拷贝的时候会把所有属性都拷过去,漏掉忽略主键id,结果在插入的时候报了唯一索引冲突。我们的场景比较特殊,id,createTime,updateTime这三个字段是表必须有的,通常也是不能被拷贝的,如果每个地方都手写忽略,代码比较麻烦也容易忘记。
上面3种方式都非常简单,意味着功能非常有限,如果你有一些复杂场景的拷贝,它们就无法支持,例如深拷贝,拷贝一个List。
另外一个最重要的点是,它们都是运行时的,这意味着你无法在编译时得到任何帮助,无法提前发现问题。
从标题可以看出我们本篇要讲的是另一个copier:mapstruct,接下来就看下它是解决我们问题的。
MapStruct
mapstruct是一个基于java注解处理器,用于生成类型安全且高性能的映射器。总结一下它有以下优点:
1.高性能。使用普通方法赋值,而非反射,mapstruct会在编译期间生成类,使用原生的set方法进行赋值,所以效率和手写set基本是一样的。
2.类型安全。mapstruct是编译时的,所以一旦有类型、名称等不匹配问题,就可以提前编译报错。
3.功能丰富。mapstruct的功能非常丰富,例如支持深拷贝,指定各种拷贝行为。
4.使用简单。你所需要做的就是定义接口和拷贝的行为,mapstruct会在编译期生成实现类。
示例
和学习其它组件一样,我们先用起来,准备两个类,SourceData,TargetData属性完全一样,其中TestData是另一个类。
typescript复制代码public class SourceData {
private String id;
private String name;
private TestData data;
private Long createTime;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public TestData getData() {
return data;
}
public void setData(TestData data) {
this.data = data;
}
public Long getCreateTime() {
return createTime;
}
public void setCreateTime(Long createTime) {
this.createTime = createTime;
}
}
导入包
xml复制代码
org.mapstruct
mapstruct
${org.mapstruct.version}
org.apache.maven.plugins
maven-compiler-plugin
3.8.1
1.8
1.8
org.mapstruct
mapstruct-processor
${org.mapstruct.version}
定义接口,这里的Mapper是mapstruct的,可不是mybatis的。
java复制代码@Mapper
public interface BeanMapper {
BeanMapper INSTANCE = Mappers.getMapper(BeanMapper.class);
TargetData map(SourceData source);
}
使用
ini复制代码SourceData source = new SourceData();
source.setId("123");
source.setName("abc");
source.setCreateTime(System.currentTimeMillis());
TestData testData = new TestData();
testData.setId("123");
TargetData target = BeanMapper.INSTANCE.map(source);
System.out.println(target.getId() + ":" + target.getName() + ":" + target.getCreateTime());
//true
System.out.println(source.getData() == target.getData());
可以看到使用非常简单,默认情况下mapstruct是浅拷贝,所以看到最后一个输出是true。编译后我们可以在target目录下找到帮我们生成的一个接口实现类BeanMapperImpl,如下:
深拷贝
可以看到它也是帮生成set代码,且默认是浅拷贝,所以上面最后一个输出是true。如果想变成深拷贝,在map方法上标记一下DeepClone即可:
python复制代码@Mapping(target = "data", mappingControl = DeepClone.class)
TargetData map(SourceData source);
重新编译一下,看到生成的代码变成如下,这次是深拷贝了。
集合拷贝
支持,新增一个接口方法即可。
scss复制代码List map(List source);
类型不一致
如果我将TargetData的createTime改成int类型,再编译一下,生成代码如下:
可以看到它会默认帮我们转换,但这是个隐藏的问题,如果我希望它能在编译时就提示,那么可以在Mapper注解上指定一些类型转换的策略是报错,如下:
ini复制代码@Mapper(typeConversionPolicy = ReportingPolicy.ERROR)
重新编译会提示错误:
vbnet复制代码java: Can't map property "Long createTime". It has a possibly lossy conversion from Long to Integer.
禁止隐式转换
如果我将类型改成string呢,编译又正常了,生成代码如下:
对于string和其它基础类型的包装类,它会隐式帮我们转换,这也是个隐藏问题,如果我希望它能在编译时就提示,可以定义一个注解,并在Mapper中指定它,如下:
less复制代码@Retention(RetentionPolicy.CLASS)
@MappingControl(MappingControl.Use.DIRECT)
@MappingControl(MappingControl.Use.MAPPING_METHOD)
@MappingControl(MappingControl.Use.COMPLEX_MAPPING)
public @interface ConversationMapping {
}
@Mapper(typeConversionPolicy = ReportingPolicy.ERROR, mappingControl = ConversationMapping.class)
重新编译会提示报错:
arduino复制代码java: Can't map property "Long createTime" to "String createTime". Consider to declare/implement a mapping method: "String map(Long value)".
这个可以参见issus上的讨论:issus1428 issus3186
忽略指定字段
忽略字段可以使用Mapping注解的ignore属性,如下:
python复制代码@Mapping(target = "id", mappingControl = DeepClone.class)
如果我想忽略某些字段,并且复用起来,就像我们的场景应用,可以定义一个IgnoreFixedField注解,然后打在方法上
less复制代码@Mapping(target = "id", ignore = true)
@Mapping(target = "createTime", ignore = true)
@Mapping(target = "updateTime", ignore = true)
@Target(METHOD)
@Retention(RUNTIME)
@Documented
@interface IgnoreFixedField {
}
@IgnoreFixedField
@Mapping(target = "data", mappingControl = DeepClone.class)
TargetData map(SourceData source);
这样只要打上这个注解,这3个字段就不会拷贝了。
与lombok集成
如果你的项目使用了lombok,上面的代码可能没法正常工作。需要在maven对lombok也做下配置,在上面的annotationProcessorPaths加入如下配置即可。
xml复制代码
org.projectlombok
lombok
1.18.24
上面只是结合本人的实际场景的一些例子,mapstruct还有更多的功能,参见官方文档。
总结
会用之后我们可以学习一下它的原理了,这也是我们平时学习一个新的东西的习惯,别一下子就扎到原理,源码里头,这样会严重打击学习热情,要先跑起来先,看到成果后你会更有激情学习下去。
其实mapstruct的原理和lombok是一样的,都是在编译期间生成代码,而不会影响运行时。例如我们最常见的@Data注解,查看源文件你会发现getter/setter生成了,源文件的类不会有@Data注解。
java代码编译和执行的整个过程包含三个主要机制:1.java源码编译机制 2.类加载机制 3.类执行机制。其中java源码编译由3个过程组成:1.分析和输入到符号表 2.注解处理 3.语义分析和生成class文件。如下:
其中annotation processing就是注解处理,jdk7之前采用APT技术,之后的版本使用了JSR 269 API。
JSR是什么?java Specification Requests,Java规范提案,是指向JCP(Java Community Process)提出新增一个标准化技术规范的正式请求。jsr 269是什么?在这里
注解我们非常熟悉,其实java里的注解有两种,一种是运行时注解,如常用@Resource,@Autowired,另一种是编译时注解,如lombok的@Data。
编译时注解主要作用是在编译期间生成代码,这样就可以避免在运行时使用反射。编译时注解处理核心接口是Processor,它有一个抽象实现类AbstractProcessor封装了许多功能,如果要实现继承它即可。
知道原理后,我们完全可以模仿lombok写一个简单的生成器,更多信息可以参考这篇文章:Java编译期注解处理器AbstractProcessor
关于性能,知道原理后其实你也知道根本不用担心mapstruct的性能问题了,可以参考这个:benchmark
如果要说它的缺点,就是得为了这个简单的拷贝功能导这个包,如果你的程序只有很少的拷贝,那手动写一下也未尝不可,如果有大量拷贝需求,那就推荐使用了。
相关推荐
- 《保卫萝卜2》安卓版大更新 壕礼助阵世界杯
-
《保卫萝卜2:极地冒险》本周不仅迎来了安卓版本的重大更新,同时将于7月4日本周五,带来“保卫萝卜2”安卓版本世界杯主题活动的火热开启,游戏更新与活动两不误。一定有玩家会问,激萌塔防到底进行了哪些更新?...
- 儿童手工折纸:胡萝卜,和孩子一起边玩边学carrot
-
1、准备两张正方形纸,一橙一绿,对折出折痕。2、橙色沿其中一条对角线如图折两三角形。3、把上面三角折平,如图。4、绿色纸折成三角形。5、再折成更小的三角形。6、再折三分之一如图。7、打开折纸,压平中间...
- 《饥荒》食物代码有哪些(饥荒最新版代码总汇食物篇)
-
饥荒游戏中,玩家们需要获取各种素材与食物,进行生存。玩家们在游戏中,进入游戏后按“~”键调出控制台使用代码,可以直接获得素材。比如胡萝卜的代码是carrot,玉米的代码是corn,南瓜的代码是pump...
- Skyscanner:帮你找到最便宜机票 订票不求人
-
你喜欢旅行吗?在合适的时间、合适的目的地,来一场说走就走的旅行?机票就是关键!Skyscanner这款免费的手机应用,在几秒钟内比较全球600多家航空公司的航班安排、价格和时刻表,帮你节省金钱和时间。...
- 小猪佩奇第二季50(小猪佩奇第二季英文版免费观看)
-
Sleepover过夜Itisnighttime.现在是晚上。...
- 我在民政局工作的那些事儿(二)(我在民政局上班)
-
时间到了1997年的秋天,经过一年多的学习和实践,我在处理结婚和离婚的事情更加的娴熟,也获得了领导的器重,所以我在处理平时的工作时也能得心应手。这一天我正在离婚处和同事闲聊,因为离婚处几天也遇不到人,...
- 夏天来了就你还没瘦?教你不节食13天瘦10斤的哥本哈根减肥法……
-
好看的人都关注江苏气象啦夏天很快就要来了你是否和苏苏一样身上的肉肉还没做好准备?真是一个悲伤的故事……下面这个哥本哈根减肥法苏苏的同事亲测有效不节食不运动不反弹大家快来一起试试看吧~DAY1...
- Pursuing global modernization for peaceful development, mutually beneficial cooperation, prosperity for all
-
AlocalworkeroperatesequipmentintheChina-EgyptTEDASuezEconomicandTradeCooperationZonei...
- Centuries-old tea road regains glory as Belt and Road cooperation deepens
-
FUZHOU/ST.PETERSBURG,Oct.2(Xinhua)--NestledinthepicturesqueWuyiMountainsinsoutheastChi...
- Ftrace function graph简介(flat function)
-
引言由于android开发的需要与systrace的普及,现在大家在进行性能与功耗分析时候,经常会用到systrace跟pefetto.而systrace就是基于内核的eventtracing来实...
- JAVA历史版本(java各版本)
-
JAVA发展1.1996年1月23日JDK1.0Java虚拟机SunClassicVM,Applet,AWT2.1997年2月19日JDK1.1JAR文件格式,JDBC,JavaBea...
- java 进化史1(java的进阶之路)
-
java从1996年1月第一个版本诞生,到2022年3月最新的java18,已经经历了27年,整整18个大的版本。很久之前有人就说java要被淘汰,但是java活到现在依然坚挺,不知道java还能活...
- 学习java第二天(java学完后能做什么)
-
#java知识#...
你 发表评论:
欢迎- 一周热门
- 最近发表
-
- 《保卫萝卜2》安卓版大更新 壕礼助阵世界杯
- 儿童手工折纸:胡萝卜,和孩子一起边玩边学carrot
- 《饥荒》食物代码有哪些(饥荒最新版代码总汇食物篇)
- Skyscanner:帮你找到最便宜机票 订票不求人
- 小猪佩奇第二季50(小猪佩奇第二季英文版免费观看)
- 我在民政局工作的那些事儿(二)(我在民政局上班)
- 夏天来了就你还没瘦?教你不节食13天瘦10斤的哥本哈根减肥法……
- Pursuing global modernization for peaceful development, mutually beneficial cooperation, prosperity for all
- Centuries-old tea road regains glory as Belt and Road cooperation deepens
- 15 THE NUTCRACKERS OF NUTCRACKER LODGE (CONTINUED)胡桃夹子小屋里的胡桃夹子(续篇)
- 标签列表
-
- mybatis plus (70)
- scheduledtask (71)
- css滚动条 (60)
- java学生成绩管理系统 (59)
- 结构体数组 (69)
- databasemetadata (64)
- javastatic (68)
- jsp实用教程 (53)
- fontawesome (57)
- widget开发 (57)
- vb net教程 (62)
- hibernate 教程 (63)
- case语句 (57)
- svn连接 (74)
- directoryindex (69)
- session timeout (58)
- textbox换行 (67)
- extension_dir (64)
- linearlayout (58)
- vba高级教程 (75)
- iframe用法 (58)
- sqlparameter (59)
- trim函数 (59)
- flex布局 (63)
- contextloaderlistener (56)