责编:乐乐 | 来源:jtea
链接:cnblogs.com/jtea/p/17592696.html
编程技术圈(ID:study_tech)第 3037 期推文
往日回顾:ChatGPT 4o 国内直接用!
正文
大家好,我是小乐。
前言
为了更好的进行开发和维护,我们都会对程序进行分层设计,例如常见的三层,四层,每层各司其职,相互配合。也随着分层,出现了 VO,BO,PO,DTO,每层都会处理自己的数据对象,然后向上传递,这就避免不了经常要将一个对象的属性拷贝给另一个对象。
例如我有一个 User 对象和一个 UserVO 对象,要将 User 对象的10个属性赋值个 UserVO 的同名属性:
一种方式是手写,一个属性一个属性赋值,相信大家最开始学习时都是这么干的,这种方式就是太低效了。
在 idea 中可以安装插件帮我们快速生成 set 属性代码,虽然还是逐个属性赋值,但比一个个敲,效率提高了很多。
上面两种方式虽然最原始,做起来很麻烦,容易出错,但程序运行效率是最高的,现在仍有不少公司要求这么做,一是这样运行效率高,二是不需要引入其它的组件,避免出现其它问题。
但对于我们来说,这种操作要是多了,开发效率和代码可维护性都会受到影响,这种赋值属性代码很长,看起来很不舒服,所以有了下面几种方式。
bean copier
apache 的 BeanUtils,内部使用了反射,效率很低,在《阿里java开发规范中》明令禁止使用,这里就不过多讨论。
spring的BeanUtils,对 apache BeanUtils 做了优化,运行效率较高,可以使用。
BeanUtils.copyProperties(source, target);
BeanUtils.copyProperties(source, target, "id", "createTime"); //不拷贝指定的字段
cglib 的 BeanCopier,使用动态技术代替反射,在运行时生成一个子类,只有在第一次动态生成类时慢,后面基本就本接近原始的set,所以呀运行效率比上面两种要高很多。
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 注解处理器,用于生成类型安全且高性能的映射器。总结一下它有以下优点:
高性能。 使用普通方法赋值,而非反射,MapStruct 会在编译期间生成类,使用原生的 set 方法进行赋值,所以效率和手写 set 基本是一样的。 类型安全。 MapStruct 是编译时的,所以一旦有类型、名称等不匹配问题,就可以提前编译报错。 功能丰富。 MapStruct 的功能非常丰富,例如支持深拷贝,指定各种拷贝行为。 使用简单。 你所需要做的就是定义接口和拷贝的行为,MapStruct 会在编译期生成实现类。
示例
和学习其它组件一样,我们先用起来,准备两个类,SourceData
,TargetData
属性完全一样,其中 TestData
是另一个类。
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;
}
}
导入包 pom
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>${org.mapstruct.version}</version>
</dependency>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<annotationProcessorPaths>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${org.mapstruct.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
定义接口
这里的 Mapper 是 MapStruct 的,可不是 Mybatis 的。
@Mapper
public interface BeanMapper {
BeanMapper INSTANCE = Mappers.getMapper(BeanMapper.class);
TargetData map(SourceData source);
}
使用
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 即可:
@Mapping(target = "data", mappingControl = DeepClone.class)
TargetData map(SourceData source);
重新编译一下,看到生成的代码变成如下,这次是深拷贝了。
集合拷贝
支持,新增一个接口方法即可。
List<TestData> map(List<TestData> source);
类型不一致
如果我将 TargetData 的 createTime 改成 int 类型,再编译一下,生成代码如下:
可以看到它会默认帮我们转换,但这是个隐藏的问题,如果我希望它能在编译时就提示,那么可以在 Mapper 注解上指定一些类型转换的策略是报错,如下:
@Mapper(typeConversionPolicy = ReportingPolicy.ERROR)
重新编译会提示错误:
java: Can't map property "Long createTime". It has a possibly lossy conversion from Long to Integer.
禁止隐式转换
如果我将类型改成 String 呢,编译又正常了,生成代码如下:
对于 String 和其它基础类型的包装类,它会隐式帮我们转换,这也是个隐藏问题,如果我希望它能在编译时就提示,可以定义一个注解,并在 Mapper 中指定它,如下:
@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)
重新编译会提示报错:
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 属性,如下:
@Mapping(target = "id", ignore = true)
如果我想忽略某些字段,并且复用起来,就像我们的场景应用,可以定义一个IgnoreFixedField
注解,然后打在方法上
@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
加入如下配置即可。
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.24</version>
</path>
上面只是结合本人的实际场景的一些例子,MapStruct 还有更多的功能,参见官方文档。
总结
会用之后我们可以学习一下它的原理了,这也是我们平时学习一个新的东西的习惯,别一下子就扎到原理,源码里头,这样会严重打击学习热情,要先跑起来先,看到成果后你会更有激情学习下去。
其实 MapStruct 的原理和 lombok 是一样的,都是在编译期间生成代码,而不会影响运行时。例如我们最常见的 @Data
注解,查看源文件你会发现 getter/setter
生成了,源文件的类不会有 @Data 注解。
java 代码编译和执行的整个过程包含三个主要机制:
java源码编译机制 类加载机制 类执行机制。
其中 java 源码编译由3个过程组成:
分析和输入到符号表 注解处理 语义分析和生成class文件。
如下:
其中 annotation processing
就是注解处理,jdk7 之前采用 APT技术,之后的版本使用了 JSR 269 API。
JSR 是什么?java Specification Requests,Java 规范提案,是指向 JCP(Java Community Process
)提出新增一个标准化技术规范的正式请求。jsr 269 是什么?在这里[1]
注解我们非常熟悉,其实java里的注解有两种,一种是运行时注解,如常用 @Resource
, @Autowired
,另一种是编译时注解,如 lombok 的 @Data
。
编译时注解主要作用是在编译期间生成代码,这样就可以避免在运行时使用反射。编译时注解处理核心接口是 Processor
,它有一个抽象实现类 AbstractProcessor
封装了许多功能,如果要实现继承它即可。
知道原理后,我们完全可以模仿 lombok 写一个简单的生成器。
关于性能,知道原理后其实你也知道根本不用担心mapstruct的性能问题了,可以参考这个:benchmark[2]
如果要说它的缺点,就是得为了这个简单的拷贝功能导这个包,如果你的程序只有很少的拷贝,那手动写一下也未尝不可,如果有大量拷贝需求,那就推荐使用了。
jsr 269: https://jcp.org/en/jsr/detail?id=269
[2]benchmark: https://github.com/arey/java-object-mapper-benchmark
你还有什么想要补充的吗?
最后给大家推荐一个ChatGPT 4.0国内网站,是我们团队一直在使用的,我们对接是OpenAI官网的账号,给大家打造了一个一模一样ChatGPT,很多粉丝朋友现在也都通过我拿这种号,价格不贵,关键还有售后。
一句话说明:用官方一半价格的钱,一句话说明:用跟官方 ChatGPT4.0 一模一样功能,无需魔法,无视封号,不必担心次数不够。
最大优势:可实现会话隔离!突破限制:官方限制每个账号三小时可使用40次4.0本网站可实现次数上限之后,手动切换下一个未使用的账号【相当于一个4.0帐号,同享受一百个账号轮换使用权限】
为了跟上AI时代我干了一件事儿,我创建了一个知识星球社群:ChartGPT与副业。想带着大家一起探索ChatGPT和新的AI时代。
有很多小伙伴搞不定ChatGPT账号,于是我们决定,凡是这三天之内加入ChatPGT的小伙伴,我们直接送一个正常可用的永久ChatGPT独立账户。
不光是增长速度最快,我们的星球品质也绝对经得起考验,短短一个月时间,我们的课程团队发布了8个专栏、18个副业项目:
简单说下这个星球能给大家提供什么:
1、不断分享如何使用ChatGPT来完成各种任务,让你更高效地使用ChatGPT,以及副业思考、变现思路、创业案例、落地案例分享。
2、分享ChatGPT的使用方法、最新资讯、商业价值。
3、探讨未来关于ChatGPT的机遇,共同成长。
4、帮助大家解决ChatGPT遇到的问题。
5、提供一整年的售后服务,一起搞副业
星球福利:
1、加入星球4天后,就送ChatGPT独立账号。
2、邀请你加入ChatGPT会员交流群。
3、赠送一份完整的ChatGPT手册和66个ChatGPT副业赚钱手册。
其它福利还在筹划中... 不过,我给你大家保证,加入星球后,收获的价值会远远大于今天加入的门票费用 !
本星球第一期原价399,目前属于试运营,早鸟价149,每超过50人涨价10元,星球马上要来一波大的涨价,如果你还在犹豫,可能最后就要以更高价格加入了。。
早就是优势。建议大家尽早以便宜的价格加入!
PS:欢迎在留言区留下你的观点,一起讨论提高。如果今天的文章让你有新的启发,欢迎转发分享给更多人。
版权申明:内容来源网络,版权归原创者所有。除非无法确认,我们都会标明作者及出处,如有侵权烦请告知,我们会立即删除并表示歉意。谢谢!
最近面试BAT,整理一份面试资料《Java面试BAT通关手册》,覆盖了Java核心技术、JVM、Java并发、SSM、微服务、数据库、数据结构等等。在这里,我为大家准备了一份2021年最新最全BAT等大厂Java面试经验总结。
别找了,想获取史上最简单的Java大厂面试题学习资料
扫下方二维码回复「面试」就好了
猜你还想看
牛逼啊!接私活必备的 400 多个开源项目!赶快收藏吧(附源码合集)!
用雪花 id 和 uuid 做 MySQL 主键,被领导怼了
项目从 MySQL 切换 PostgreSQL,踩了太多的坑!!!
嘿,你在看吗?