前言
已存在的值拷贝工具中,mapstruct流行很实用流行很广 ,但是源码非常不好改,无法扩展,只能等待更新,因此在它所有拥有的功能基础上,实现大部分功能的同时扩展思路和常用方法,更加快捷
XToolBean是一个Java注释处理器,用于生成类型安全的bean映射类。
您要做的就是定义一个映射器接口,该接口声明任何必需的映射方法。在编译期间,XToolBean将生成此接口相关Bean的Class文件。
与动态映射框架如Spring的beanUtil或是json序列化生成对象相比,具有更快的速度,以下优点:
- 通过使用普通方法调用(settter/getter)而不是反射来快速执行
- 编译时类型安全性:只能映射相互映射的对象和属性,不能将order实体意外映射到customer DTO等。
- 如果有如下问题,编译时会抛出异常
- 类字段(包括父类)必须实现Getter/Setter方法(结合lombok @Data即可)
- 类深度拷贝必须实现序列化(Serializable)
- 异常处理:generated-sources 会生成java文件,由此查看编译失败的原因
相较于mapstruct优点:
1 只有一个依赖
2 只有一个工具 一行代码
3 有timezone时区
4 json字符串和对象映射
5 代码功能容易扩展
6 …其它的功能详见下文
缺点:不能关联第三个类的拷贝,不能通过子字段对象的子字段赋值,还有一些不常用的功能未开发
性能比较(预编译相同,性能类似于mapstruct)
工具 | 10次 | 1000次 | 1W次 | 10W次 |
---|---|---|---|---|
mapstruct | 2ms | 5ms | 7ms | 31ms |
hutools的BeanUtil.copyProperties | 61ms | 809ms | 6802ms | |
hutools的BeanUtil.copyToList() | 125082ms | |||
spring的BeanUtils) | 5ms | 42ms | 235ms | 1934ms |
apache的BeanUtils | 39ms | 209ms | 1602ms | 16174ms |
1. 设置
1.1 Maven
对于基于Maven的项目,将以下内容添加到您的POM文件中以使用MapStruct:
<!--xtool-bean依赖 高性能对象映射-->
<dependency>
<groupId>io.github.myswordsky</groupId>
<artifactId>xtool-bean</artifactId>
<version>0.0.1</version>
</dependency>
Lombok依赖:(版本最好在1.16.16以上,否则会出现问题)通常是和lombok一起使用
的
<dependency>
<groupid>org.projectlombok</groupid>
<artifactid>lombok</artifactid>
<version>1.18.12</version>
</dependency>
1.2 预编译自动映射器
除了基础包装类型转换,还可以实现以下转换
类型(包含包装类型) | boolean/Boolean | byte | short | int | long | float | double | String | Date | BigDecimal | 对象 |
---|---|---|---|---|---|---|---|---|---|---|---|
boolean/Boolean | √ | √ 0/1 | √ false/true | ||||||||
byte/Byte | √ | √ | √ | ||||||||
short/Short | √ | √ | √ | ||||||||
int/Integer | √ true/false | √ | √ | √ | √ | √ | √ | √ | √ 时间戳秒 | ||
long/Long | √ | √ | √ | √ | √ 时间戳毫秒 | ||||||
float/Float | √ | √ | √ | √ | √ | √ | |||||
double/Double | √ | √ | √ | √ | √ | √ | |||||
String | √ | √ | √ | √ | √ | √ | √ | √ | √ | √ 需要开启jsonMapping | |
Date | √ 时间戳秒 | √ 时间戳毫秒 | √ 默认yyyy-MM-dd HH:mm:ss | √ | |||||||
BigDecimal | √ | √ | √ | √ | √ | √ | |||||
对象 | √ 需要开启jsonMapping | √ |
2. 定义一个预编译映射器
2.1 基本映射 @XToolMapping字段注解
要创建映射器,只需使用所需的映射方法定义一个Java接口,并用注释对其进行org.mapstruct.Mapper注释:
该@Mapper注释将使得MapStruct代码生成器创建的执行PersonMapper 过程中生成时的界面。
在生成的方法实现中,源类型(例如Person)的所有可读属性都将被复制到目标类型(例如PersonDTO)的相应属性中:
-
当一个属性与其目标实体对应的名称相同时,它将被隐式映射。
-
当属性在目标实体中具有不同的名称时,可以通过@Mapping注释指定其名称。
如果不指定@XToolMapping,默认映射name相同的field
如果映射的对象field name不一样,通过 @Mapping 指定。
忽略字段加@XToolMapping#ignore() = true
@Data
public class Person implements Serializable{
String describe;
private String id;
private String name;
private int age;
private BigDecimal source;
private double height;
private Date createTime;
}
@Data
public class PersonDTO implements Serializable{
String describe;
private Long id;
private String personName;
private String age;
private String source;
private String height;
private String createTime;
}
@XToolBean()//必须注解
public interface PersonMapper {
@XToolMapping(target = "personName", source = "name") //默认相互映射, 如果不存在则不映射
@XToolMapping(target = "id", ignore = true) // 忽略id,不进行映射
void copy(StringGen first, StringGenDTO second);
//不需要任何@XToolMapping更改属性,可直接使用默认配置转化
// void copy(StringGen first, StringGenDTO second);
}
生成的实现类:
public class PersonAndPersonDTO
implements BaseMapping<Person,PersonDTO> {
public Person toEntity(PersonDTO source) {
if (source == null) {
return null;
}
Person target = new Person();
target.setName(source.getPersonName());
target.setDescribe(source.getDescribe());
if (!Objects.equals(source.getAge(), null)) {
target.setAge(Integer.parseInt(source.getAge()));
}
if (!Objects.equals(source.getHeight(), null)) {
target.setHeight(Double.parseDouble(source.getHeight()));
}
return target;
}
public PersonDTO toDto(Person source) {
if (source == null) {
return null;
}
PersonDTO target = new PersonDTO();
target.setDescribe(source.getDescribe());
if (Objects.equals(source.getSource(), null)) {
target.setSource(null);
} else {
target.setSource(String.valueOf(source.getSource()));
}
target.setAge(String.valueOf(source.getAge()));
target.setHeight(String.valueOf(source.getHeight()));
return target;
}
public Person thisEntity(Person source) {
if (source == null) {
return null;
}
Person target = new Person();
if (Objects.equals(source.getCreateTime(), null)) {
target.setCreateTime(null);
} else {
target.setCreateTime(new Date(source.getCreateTime().getTime()));
}
target.setDescribe(source.getDescribe());
if (Objects.equals(source.getSource(), null)) {
target.setSource(null);
} else {
target.setSource(BigDecimal.valueOf(source.getSource().doubleValue()));
}
target.setAge(source.getAge());
target.setHeight(source.getHeight());
return target;
}
public PersonDTO thisDto(PersonDTO source) {
if (source == null) {
return null;
}
PersonDTO target = new PersonDTO();
target.setPersonName(source.getPersonName());
target.setDescribe(source.getDescribe());
target.setSource(source.getSource());
target.setAge(source.getAge());
target.setHeight(source.getHeight());
return target;
}
}
测试:
@Test
public void test(){
Person person = new Person();
person.setDescribe("测试");
person.setAge(18);
person.setName("张三");
person.setHeight(170.5);
person.setSource(new BigDecimal("100"));
PersonDTO dto = BeanMapping.copy(person, PersonDTO.class);
System.out.println(dto);
// PersonDTO(describe=测试, id=null, personName=张三, age=18, source=100, height=170.5)
Person entity = BeanMapping.copy(dto, Person.class);
System.out.println(entity);
// Person(describe=测试, id=null, name=null, age=18, source=100, height=170.5)
}
2.2 拷贝指向 index()
@XToolMapping(target = "personName", source = "name", index = MappingIndexEnums.First)//不加参数默认是相互映射
void copy(PersonDTO first, Person second)
MappingIndexEnums.First ~= PersonDTO copy(Person second)
MappingIndexEnums.Second ~= Person copy(PersonDTO first)
默认Default 即双向映射类似于mapstruct的映射和@InheritInverseConfiguration结合的双向映射
MappingIndexEnums.First指向第一个参数,则target对应的目标是PersonDTO,相当于 PersonDTO copy(Person second)
同理MappingIndexEnums.Second对应的target对应的目标是Person
2.3 指定值(targetValue()直接忽略来源值)、默认值(defaultValue():来源值为空才处理)(自定义java代码、使用表达式)
target() 必须添加,source()可以不添加,则直接使用defaultValue,defaultValue()实际是动态的java代码
设置目标字段默认值(支持表达式) 字段不同类型需要指定index(如果同类型可以无需指定)
设置目标字段默认值(支持表达式) 字段不同类型需要指定index(如果同类型可以无需指定)
例:defaultValue = "1"
支持{@link FieldType}的基础类型和包装类型
其它类型(或新对象) 需要XToolMapping.index()配置
1 数字:defaultValue = "25"
2 字符串:defaultValue = "\"25\""
3 使用拷贝来源变量值(固定为source.getXX)
defaultValue = "source.getName().toString"
4 对象需要携带全名(可以结合@XToolBean#imports()来导入新的类,这样就不用加全名了):
defaultValue = "new java.util.Date()"
defaultValue = "new java.math.BigDecimal(1.23)"
defaultValue = "new java.util.ArrayList<io.github.xtools.bean.entity.config.ClassInfo>(){{
add(new io.github.xtools.bean.entity.config.ClassInfo());
}};"
defaultValue = "new new io.github.xtools.bean.entity.config.User(\"name\", 25)"
5 调用接口方法
...
@XToolMapping(target = "describe", defaultValue = "\"默认值\"")//字符串类型需要加\"
@XToolMapping(target = "age", defaultValue = "25")
//复制非基础类型需要完整包名(或者提前导入类 @see 下文XToolBean的imports())
//这里必须要指定MappingIndexEnums.First,因为PersonDTO和Person的createTime是不同类型
@XToolMapping(target = "createTime", defaultValue = "null", index = MappingIndexEnums.First)
@XToolMapping(target = "createTime", defaultValue = "new java.util.Date()", index = MappingIndexEnums.Second)
void copy(PersonDTO first, Person second);
生成的impl:这里功能不全
...
if (person.getDescribe() != null) {
personDTO.setDescribe(person.getDescribe());
} else {
personDTO.setDescribe("默认值");
}
...
测试:
@Test
public void test(){
Person person = new Person();
//person.setDescribe("测试");
person.setAge(18);
person.setName("张三");
person.setHeight(170.5);
person.setSource(new BigDecimal("100"));
PersonDTO dto = BeanMapping.copy(person, PersonDTO.class);
System.out.println(dto);
// PersonDTO(describe=默认值, id=null, name=张三, age=18, source=100, height=170.5, createTime=null)
}
2.4 日期处理 dateFormat() 和 timeZone()
如果属性从字符串映射到日期,则该格式字符串可由SimpleDateFormat处理,反之亦然。
mapstruct是没有timeZone时区的,这里
....
//如果不加dateFormat ,则默认映射数据类型为yyyy-MM-dd HH:mm:ss
@XToolMapping(target = "createTime" ,source = "createTime", dateFormat = "yyyy-MM-dd")
void conver2(Person person, PersonDTO dto);
@XToolMapping(target = "createTime" ,source = "createTime", dateFormat = "yyyy-MM-dd", timeZone = "GMT+8")
void conver2(Person person, PersonDTO dto);
impl:
try {
if (person.getCreateTime() != null) {
personDTO.setCreateTime((new SimpleDateFormat("yyyy-MM-dd")).parse(person.getCreateTime()));
}
} catch (ParseException var4) {
throw new RuntimeException(var4);
}
try {
if (person.getCreateTime() != null) {
SimpleDateFormat sf = new SimpleDateFormat("yyyy-MM-dd")
sf.setTimeZone(TimeZone.getTimeZone("GMT+8"));
personDTO.setCreateTime(sf.parse(person.getCreateTime()));
}
} catch (ParseException var4) {
throw new RuntimeException(var4);
}
2.5 组合映射 当前版本暂不支持该功能
2.6 嵌套映射 暂不支持
2.7 字符串格式化 numberFormat()
目前支持格式:Double->String Float->String BigDecimal->String
如果带注释的方法从数字映射到字符串,则使用DecimalFormat将格式字符串作为可处理的格式。反之亦然。对于所有其他元素类型,将被忽略。
从基本2.1 基本映射可以看出,number类型与字符串直接的转换是通过valueOf(),如果字符串格式不正确会抛出java.lang.NumberFormatException
异常,例如:Integer.valueOf(“10.2”)
使用numberFormat()之后DecimalFormat格式转换,还是会抛出NFE
异常
// mapper
....
@XToolMapping(target = "age",source = "age", numberFormat = "#0.00")
void conver2(Person person, PersonDTO dto);
...
// imppl
personDTO.setAge((new DecimalFormat("#0.00")).format((long)person.getAge()));
2.8 逆映射 自带 (实体到DTO以及从DTO到实体)
2.9.1 忽略映射 igonre()
@XToolMapping#igonre() = true 不对目标类的target指定字段进行复制
@XToolMapping(target = "age", ignore = true)
void conver(Person person, PersonDTO dto);
2.9.2 不同类型是否自动映射 copyType() 和@MappingCopyType
映射详细参考1.2表格
字段级 @XToolMapping#copyType()
方法级:@MappingCopyType
@XToolMapping(target = "age", source = "age", copyType= false)//该方法字段类型如果不对则不复制
void conver(Person person, PersonDTO dto);
@MappingCopyType(false)//该方法字段类型如果不对则不复制
void conver(Person person, PersonDTO dto);
2.9.3 json转对象或对象转json(需要fastjson的支持) jsonMapping()
Person class{
String nameJson;
}
PersonDTO class{
User nameJson;
}
@XToolMapping(source = "nameJson", target = "nameJson", jsonMapping = true)
void conver(Person person, PersonDTO dto);
2.9.4 字段方法执行器 resultType() 和resultTypeIsInner()
字段方法执行器 (使用此功能defaultValue失效、且需要配合resultTypeIsInner使用) 必须指定index First or Second且类型一致
user方法(目标字段名) 执行映射时, 可以将返回值转换为 User 类型。
//source不为空则为默认字段 找不到则不处理
@XToolMapping(source = "name", target = "name", index = MappingIndexEnums.First, resultType = Name.class)
void copy(Source source, Target target);
//public外部类 非public内部类需要加上resultTypeIsInner = false
package entity;
public class Name {
//User:返回字段类型 user:返回字段名称(String:来源字段类型 user:随意变量值(取决于source))
public User user(String user) {
String[] parts = user.split(",");
return new User(parts[0], parts[1]);
}
public String name(String name) {
return "new User(parts[0], parts[1])";
}
}
package entity;
@AllArgsConstructor
@Data
public class User{
String s1;
String s2;
}
impl
Source class{
String user;
}
Target class{
User user;
}
target.setUser((entity.User)ClassUtils.executeMethod("entity.Name", targetFieldName, source.getUser()));
3.@XToolBean类注解
3.1 isSpring()是否是Spring类
会在生成类上加Component 注解
@XToolBean(isSpring = true)
public class DateFormtUtil {
void copy(Source source, Target target);
}
可以结合Spring List注入进行统一管理
3.2 使用 copyType()
同2.92 但此注解作用在类上 copyType =false则该类所有方法不执行类型转换
@XToolBean(copyType = false)
public class DateFormtUtil {
void copy(Person source, PersonDTO target);
}
3.3 生成类额外导入的类 imports()
使用 生成类额外导入的类(添加import),可结合defaultValue或targetValue执行
import entity.User;
public class PersonAndPersonDTO implements BaseMapping<Person,PersonDTO> {
@XToolBean(imports= {User.class})
public class DateFormtUtil {
@XToolMapping(target = "user", defaultValue = "new User()", index = MappingIndexEnums.First)
void copy(Person source, PersonDTO target);
}
3.3 生成类的继承 extendsClazz()
import entity.User;
public class PersonAndPersonDTO extend User implements BaseMapping<Person,PersonDTO> {
@XToolBean(extendsClazz= User.class)
public class DateFormtUtil {
void copy(Source source, Target target);
}
可以统一管理
3.4 克隆模式 mappingControl() 和方法级@MappingDeepClone
@XToolBean#mappingControl 将作用于该类下所有的方法
仅作用于某个方法 详见{@link MappingDeepClone}
DeepClone 深度克隆(需要实现Serializable
接口)
默认:基础类型(还有String)不影响,对象类型浅克隆
@XToolBean(mappingControl = MappingDeepClone.class)
public class DateFormtUtil {
void copy(Source source, Target target);
@MappingDeepClone
void copy2(Source source, Target target);
}
4 Guava自动映射器(不建议使用)
1 运行时动态拷贝(完全和预编译独立), 需要开启是否使用Guava,因为使用代码直接加载,一个显著的缺点是:每个第一次拷贝的耗时500毫秒左右,因此可使用线程池在项目启动时异步加载
@Component
public class StartRunning implements ApplicationRunner {
@Override
public void run(ApplicationArguments args) throws Exception {
String init = BeanMapping.initAuto(true);
//如果运行时速度很慢,可异步加载
List<Consumer<?>> list = new ArrayList<Consumer<?>>() {{
add((e) -> BeanMapping.copy(new Person(), PersonDTO.class));
//...
}};
ScheduledExecutorService executorService = Executors.newScheduledThreadPool(4);
list.forEach(e -> executorService.execute(() -> e.accept(null)));
}
}