简介
说明
本文介绍Spring的BeanUtils工具类的用法。
我们经常需要将不同的两个对象实例进行属性复制,比如将DO对象进行属性复制到DTO,这种转换最原始的方式就是手动编写大量的 get/set代码,很繁琐。为了解决这一痛点,就诞生了一些方便的类库,常用的有 Apache的 BeanUtils,Spring的 BeanUtils, Dozer,Orika等拷贝工具。
由于Apache的BeanUtils的性能很差,强烈不建议使用。阿里巴巴Java开发规约插件上也明确指出:
“Ali-Check | 避免用Apache Beanutils进行属性的copy。”
Spring的BeanUtils方法
方法 | 说明 |
---|---|
BeanUtils.copyProperties(source, target); | source对应的对象成员赋值给target对应的对象成员 |
BeanUtils.copyProperties(source, target, “id”, “time”); | 忽略拷贝某些字段。本处是忽略:id与time |
Spring的BeanUtils方法注意事项
1.BeanUtils是浅拷贝。
2.泛型只在编译期起作用,不能依靠泛型来做运行期的限制;
浅拷贝和深拷贝
1.浅拷贝:对基本数据类型进行值传递,对引用数据类型,使用其引用地址,不拷贝其内容,此为浅拷贝
2.深拷贝:对基本数据类型进行值传递,对引用数据类型,创建一个新的对象,并复制其内容,此为深拷贝。
Spring的BeanUtils与Apache的BeanUtils区别
项 | Spring的BeanUtils | Apache的BeanUtils |
---|---|---|
性能 | 好。原因:对两个对象中相同名字的属性进行简单的get/set,仅检查属性的可访问性 | 差。原因:有很多检验:类型的转换、对象所属类的可访问性等,注意:本处类型转换是类似的类,多个String转为List是不行的。 |
使用方面 | 第一个参数是源,第二个参数是目标。无需捕获异常 | 第一个参数是目标,第二个参数是源。必须捕获异常 |
深/浅拷贝 | 浅拷贝 | 浅拷贝 |
Apache的BeanUtils方法。(依赖:maven里直接搜“commons-beanutils”)
方法 | 说明 |
---|---|
BeanUtils.copyProperties(Object target, Object source); | source对应的对象成员赋值给target对应的对象成员 |
BeanUtils.copyProperties(Object target, String name, Object source); | 只拷贝某些字段。 |
Apache的BeanUtils支持的类型转换:
1.java.lang.BigDecimal
2.java.lang.BigInteger
3.boolean and java.lang.Boolean
4.byte and java.lang.Byte
5.char and java.lang.Character
6.java.lang.Class
7.double and java.lang.Double
8.float and java.lang.Float
http://9.int and java.lang.Integer
10.long and java.lang.Long
11.short and java.lang.Short
12.java.lang.String
13.java.sql.Date
14.java.sql.Time
15.java.sql.Timestamp
java.util.Date是不被支持的,而它的子类java.sql.Date是被支持的。因此如果对象包含时间类型的属性,且希望被转换的时候,一定要使用java.sql.Date类型。否则在转换时会提示argument mistype异常。
实例
entity
package com.example.entity;
import lombok.Data;
@Data
public class Question {
private Integer id;
private Integer studentId;
private String content;
private Integer value;
}
package com.example.entity;
import lombok.Data;
@Data
public class Student {
private Integer id;
private String name;
private Integer points;
}
vo
package com.example.vo;
import lombok.Data;
import java.io.Serializable;
import java.util.List;
@Data
public class QuestionStudentVO implements Serializable {
private Integer id;
private String content;
private Integer value;
private Integer studentId;
private List<String> name;
private Integer points;
}
测试类
package com.example;
import com.example.entity.Question;
import com.example.entity.Student;
import com.example.vo.QuestionStudentVO;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.BeanUtils;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
@RunWith(SpringRunner.class)
@SpringBootTest
public class DemoApplicationTests {
@Test
public void beanUtilTest(){
Question question = new Question();
question.setId(2);
// question.setStudentId(3);
question.setContent("This is content");
question.setValue(100);
Student student = new Student();
student.setId(3);
student.setName("Tony Stark");
student.setPoints(201);
QuestionStudentVO questionStudentVO = new QuestionStudentVO();
BeanUtils.copyProperties(question, questionStudentVO);
BeanUtils.copyProperties(student, questionStudentVO);
System.out.println(questionStudentVO);
}
}
执行结果
QuestionStudentVO(id=3, content=Thi
1、什么是MapStruct
1.1 JavaBean 的困扰
对于代码中 JavaBean之间的转换, 一直是困扰我很久的事情。在开发的时候我看到业务代码之间有很多的 JavaBean 之间的相互转化, 非常的影响观感,却又不得不存在。我后来想的一个办法就是通过反射,或者自己写很多的转换器。
第一种通过反射的方法确实比较方便,但是现在无论是 BeanUtils, BeanCopier 等在使用反射的时候都会影响到性能。虽然我们可以进行反射信息的缓存来提高性能。但是像这种的话,需要类型和名称都一样才会进行映射,有很多时候,由于不同的团队之间使用的名词不一样,还是需要很多的手动 set/get 等功能。
第二种的话就是会很浪费时间,而且在添加新的字段的时候也要进行方法的修改。不过,由于不需要进行反射,其性能是很高的。
1.2 MapStruct 带来的改变
MapSturct 是一个生成类型安全,高性能且无依赖的 JavaBean 映射代码的注解处理器(annotation processor)。
- 注解处理器
- 可以生成 JavaBean 之间那的映射代码
- 类型安全,高性能,无依赖性
2、MapStruct 入门
2.1 添加依赖
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.20</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-jdk8</artifactId>
<version>${org.mapstruct.version}</version>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${org.mapstruct.version}</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.1.0</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
2.2 po类
@Data
public class User {
private Integer id;
private String name;
private String address;
private Date birth;
}
2.3 dto类
@Data
public class UserDto implements Serializable {
private Integer id;
private String name;
private String address;
private Date birth;
}
2.4 创建转换接口
//可以使用abstract class代替接口
@Mapper
public interface UserMapper {
UserDto userToUserDto(User user);
//集合
List<UserDto> userToUserDto(List<User> users);
}
2.5 测试方法
@Test
public void userPoToUserDto() {
User user =new User();
user.setId(1);
user.setName("myx");
user.setAddress("河北沧州");
user.setBirth(new Date());
UserMapper mapper = Mappers.getMapper(UserMapper.class);
UserDto userDto = mapper.userToUserDto(user);
System.out.println(userDto);
}
2.6 运行效果
![图片](data:image/svg+xml,%3C%3Fxml version=‘1.0’ encoding=‘UTF-8’%3F%3E%3Csvg width=‘1px’ height=‘1px’ viewBox=‘0 0 1 1’ version=‘1.1’ xmlns=‘http://www.w3.org/2000/svg’ xmlns:xlink=‘http://www.w3.org/1999/xlink’%3E%3Ctitle%3E%3C/title%3E%3Cg stroke=‘none’ stroke-width=‘1’ fill=‘none’ fill-rule=‘evenodd’ fill-opacity=‘0’%3E%3Cg transform=‘translate(-249.000000, -126.000000)’ fill=‘%23FFFFFF’%3E%3Crect x=‘249’ y=‘126’ width=‘1’ height=‘1’%3E%3C/rect%3E%3C/g%3E%3C/g%3E%3C/svg%3E)
2.7 查看编译的class
底层通过自动取值赋值操作完成
![图片](data:image/svg+xml,%3C%3Fxml version=‘1.0’ encoding=‘UTF-8’%3F%3E%3Csvg width=‘1px’ height=‘1px’ viewBox=‘0 0 1 1’ version=‘1.1’ xmlns=‘http://www.w3.org/2000/svg’ xmlns:xlink=‘http://www.w3.org/1999/xlink’%3E%3Ctitle%3E%3C/title%3E%3Cg stroke=‘none’ stroke-width=‘1’ fill=‘none’ fill-rule=‘evenodd’ fill-opacity=‘0’%3E%3Cg transform=‘translate(-249.000000, -126.000000)’ fill=‘%23FFFFFF’%3E%3Crect x=‘249’ y=‘126’ width=‘1’ height=‘1’%3E%3C/rect%3E%3C/g%3E%3C/g%3E%3C/svg%3E)
3、MapStruct优点分析
3.1 性能高
这是相对反射来说的,反射需要去读取字节码的内容,花销会比较大。而通过 MapStruct 来生成的代码,其类似于人手写。速度上可以得到保证。
3.2 使用简单
如果是完全映射的,使用起来肯定没有反射简单。用类似 BeanUtils 这些工具一条语句就搞定了。但是,如果需要进行特殊的匹配(特殊类型转换,多对一转换等),其相对来说也是比较简单的。
基本上,使用的时候,我们只需要声明一个接口,接口下写对应的方法,就可以使用了。当然,如果有特殊情况,是需要额外处理的。
3.3 代码独立
生成的代码是对立的,没有运行时的依赖。
3.4 易于 debug
在我们生成的代码中,我们可以轻易的进行 debug。
4、MapStruct使用案例
4.1 属性名称相同
在实现类的时候,如果属性名称相同,则会进行对应的转化。通过此种方式,我们可以快速的编写出转换的方法。(入门案例)
4.2 属性名不相同
属性名不相同,在需要进行互相转化的时候,则我们可以通过@Mapping
注解来进行转化。
@Data
public class UserDto implements Serializable {
private Integer id;
private String name;
private String address;
private Date birth;
private String password;
}
@Data
public class User {
private Integer id;
private String name;
private String address;
private Date birth;
private String pwd;
}
@Mapper
public interface UserMapper {
//单个属性
//@Mapping(source = "pwd",target = "password")
//多个属性
@Mappings({
@Mapping(source = "pwd",target = "password")
})
UserDto userToUserDto(User user);
}
- source 需要转换的对接,通常是入参
- target 转换的对接,通常是出参
- ignore 忽略,默认false不忽略,需要忽略设置为true
- defaultValue 默认值
- expressions 可以通过表达式来构造一些简单的转化关系。虽然设计的时候想兼容很多语言,不过目前只能写Java代码。
@Mappings({
@Mapping(source = "birthdate", target = "birth"),//属性名不一致映射
@Mapping(target = "birthformat", expression = "java(org.apache.commons.lang3.time.DateFormatUtils.format(person.getBirthdate(),\"yyyy-MM-dd HH:mm:ss\"))"),//自定义属性通过java代码映射
})
public PersonVo PersonToPersonVo(Person person);
这里用到演示了如何使用TimeAndFormat对time和format操作,这里必须要指定需要使用的Java类的完整包名,不然编译的时候不知道你使用哪个Java类,会报错。
@Test
public void userPoToUserDto() {
User user =new User();
user.setId(1);
user.setName("myx");
user.setAddress("河北沧州");
user.setBirth(new Date());
user.setPwd("123456");
UserMapper mapper = Mappers.getMapper(UserMapper.class);
UserDto userDto = mapper.userToUserDto(user);
System.out.println(userDto);
}
![图片](data:image/svg+xml,%3C%3Fxml version=‘1.0’ encoding=‘UTF-8’%3F%3E%3Csvg width=‘1px’ height=‘1px’ viewBox=‘0 0 1 1’ version=‘1.1’ xmlns=‘http://www.w3.org/2000/svg’ xmlns:xlink=‘http://www.w3.org/1999/xlink’%3E%3Ctitle%3E%3C/title%3E%3Cg stroke=‘none’ stroke-width=‘1’ fill=‘none’ fill-rule=‘evenodd’ fill-opacity=‘0’%3E%3Cg transform=‘translate(-249.000000, -126.000000)’ fill=‘%23FFFFFF’%3E%3Crect x=‘249’ y=‘126’ width=‘1’ height=‘1’%3E%3C/rect%3E%3C/g%3E%3C/g%3E%3C/svg%3E)
4.3 转换非基础类型属性
如果subUser与subUserDto字段名称相同直接配置即可完成(对象类型,包括list)
@Data
public class UserDto implements Serializable {
private Integer id;
private String name;
private String address;
private Date birth;
private String password;
private List<SubUserDto> subUserDto;
}
@Data
public class User {
private Integer id;
private String name;
private String address;
private Date birth;
private String pwd;
private List<SubUser> subUser;
}
@Mappings({
@Mapping(source = "pwd",target = "password"),
@Mapping(source = "subUser", target = "subUserDto")
})
UserDto userToUserDto(User user);
4.4 Mapper 中使用自定义的转换
有时候,对于某些类型,无法通过代码生成器的形式来进行处理。那么, 就需要自定义的方法来进行转换。这时候,我们可以在接口(同一个接口,后续还有调用别的 Mapper 的方法)中定义默认方法(Java8及之后)。
@Data
public class UserDto implements Serializable {
private Integer id;
private String name;
private String address;
private Date birth;
private String password;
private SubUserDto subUserDto;
}
@Data
public class SubUserDto {
private Boolean result;
private String name;
}
@Data
public class User {
private Integer id;
private String name;
private String address;
private Date birth;
private String pwd;
private SubUser subUser;
}
@Data
public class SubUser {
private Integer deleted;
private String name;
}
@Mapper
public interface UserMapper {
@Mappings({
@Mapping(source = "pwd",target = "password"),
@Mapping(source = "subUser", target = "subUserDto")
})
UserDto userToUserDto(User user);
default SubUserDto subSource2subTarget(SubUser subUser) {
if (subUser == null) {
return null;
}
SubUserDto subUserDto = new SubUserDto();
subUserDto.setResult(!subUser.getDeleted().equals(0));
subUserDto.setName(subUser.getName()==null?"":subUser.getName());
return subUserDto;
}
}
只能存在一个default修饰的方法
@Test
public void userPoToUserDto() {
User user =new User();
user.setId(1);
user.setName("myx");
user.setAddress("河北沧州");
user.setBirth(new Date());
user.setPwd("123456");
SubUser subUser =new SubUser();
subUser.setDeleted(0);
subUser.setName("rkw");
user.setSubUser(subUser);
UserMapper mapper = Mappers.getMapper(UserMapper.class);
UserDto userDto = mapper.userToUserDto(user);
System.out.println(userDto);
}
![图片](data:image/svg+xml,%3C%3Fxml version=‘1.0’ encoding=‘UTF-8’%3F%3E%3Csvg width=‘1px’ height=‘1px’ viewBox=‘0 0 1 1’ version=‘1.1’ xmlns=‘http://www.w3.org/2000/svg’ xmlns:xlink=‘http://www.w3.org/1999/xlink’%3E%3Ctitle%3E%3C/title%3E%3Cg stroke=‘none’ stroke-width=‘1’ fill=‘none’ fill-rule=‘evenodd’ fill-opacity=‘0’%3E%3Cg transform=‘translate(-249.000000, -126.000000)’ fill=‘%23FFFFFF’%3E%3Crect x=‘249’ y=‘126’ width=‘1’ height=‘1’%3E%3C/rect%3E%3C/g%3E%3C/g%3E%3C/svg%3E)
4.5 多转一
我们在实际的业务中少不了将多个对象转换成一个的场景。MapStruct 当然也支持多转一的操作。
![5](https://gitee.com/chenjiabing666/BlogImage/raw/master/%E6%80%A7%E8%83%BD%E9%AB%98%E3%80%81%E4%B8%8A%E6%89%8B%E5%BF%AB%EF%BC%8C%E5%AE%9E%E4%BD%93%E7%B1%BB%E8%BD%AC%E6%8D%A2%E5%B7%A5%E5%85%B7%20MapStruct%20%E5%88%B0%E5%BA%95%E6%9C%89%E5%A4%9A%E5%BC%BA%E5%A4%A7%EF%BC%81/5.png)@Data
public class SubUser {
private Integer deleted;
private String name;
}
@Data
public class User {
private Integer id;
private String name;
private String address;
private Date birth;
private String pwd;
}
@Mapper
public interface UserMapper {
@Mappings({
@Mapping(source = "user.pwd",target = "password"),
@Mapping(source = "subUser.name", target = "name")
})
NewUserDto userToUserDto(User user,SubUser subUser);
}
@Test
public void userPoToUserDto() {
User user =new User();
user.setId(1);
user.setName("myx");
user.setAddress("河北沧州");
user.setBirth(new Date());
user.setPwd("123456");
SubUser subUser =new SubUser();
subUser.setDeleted(0);
subUser.setName("rkw");
UserMapper mapper = Mappers.getMapper(UserMapper.class);
NewUserDto userDto = mapper.userToUserDto(user,subUser);
System.out.println(userDto);
}
![图片](data:image/svg+xml,%3C%3Fxml version=‘1.0’ encoding=‘UTF-8’%3F%3E%3Csvg width=‘1px’ height=‘1px’ viewBox=‘0 0 1 1’ version=‘1.1’ xmlns=‘http://www.w3.org/2000/svg’ xmlns:xlink=‘http://www.w3.org/1999/xlink’%3E%3Ctitle%3E%3C/title%3E%3Cg stroke=‘none’ stroke-width=‘1’ fill=‘none’ fill-rule=‘evenodd’ fill-opacity=‘0’%3E%3Cg transform=‘translate(-249.000000, -126.000000)’ fill=‘%23FFFFFF’%3E%3Crect x=‘249’ y=‘126’ width=‘1’ height=‘1’%3E%3C/rect%3E%3C/g%3E%3C/g%3E%3C/svg%3E)
4.5.1 遵循原则
- 当多个对象中, 有其中一个为 null, 则会直接返回 null
- 如一对一转换一样, 属性通过名字来自动匹配。因此, 名称和类型相同的不需要进行特殊处理
- 当多个原对象中,有相同名字的属性时,需要通过 @Mapping 注解来具体的指定, 以免出现歧义(不指定会报错)。如上面的 name
属性也可以直接从传入的参数来赋值
@Mapping(source = "person.description", target = "description")
@Mapping(source = "name", target = "name")
DeliveryAddress personAndAddressToDeliveryAddressDto(Person person, String name);
4.6 更新 Bean 对象
有时候,我们不是想返回一个新的 Bean 对象,而是希望更新传入对象的一些属性。这个在实际的时候也会经常使用到。
@Mapper
public interface UserMapper {
NewUserDto userToNewUserDto(User user);
/**
* 更新, 注意注解 @MappingTarget
* 注解 @MappingTarget后面跟的对象会被更新。
*/
void updateDeliveryAddressFromAddress(SubUser subUser,@MappingTarget NewUserDto newUserDto);
}
@Test
public void userPoToUserDto() {
User user =new User();
user.setId(1);
user.setName("myx");
user.setAddress("河北沧州");
user.setBirth(new Date());
SubUser subUser =new SubUser();
subUser.setDeleted(0);
subUser.setName("rkw");
UserMapper mapper = Mappers.getMapper(UserMapper.class);
NewUserDto userDto = mapper.userToNewUserDto(user);
mapper.updateDeliveryAddressFromAddress(subUser,userDto);
System.out.println(userDto);
}
4.7 map映射
@MapMapping(valueDateFormat ="yyyy-MM-dd HH:mm:ss")
public Map<String ,String> DateMapToStringMap(Map<String,Date> sourceMap);
@Test
public void mapMappingTest(){
Map<String,Date> map=new HashMap<>();
map.put("key1",new Date());
map.put("key2",new Date(new Date().getTime()+9800000));
Map<String, String> stringObjectMap = TestMapper.MAPPER.DateMapToStringMap(map);
}
4.8 多级嵌套
只需要在mapper接口中定义相关的类型转换方法即可,list类型也适用
4.8.1 方式1
@Data
public class User {
private Integer id;
private String name;
private String address;
private Date birth;
private Boolean isDisable;
private List<SubUser> user;
}
@Data
public class SubUser {
private Integer deleted;
private String name;
private List<SubSubUser> subUser;
}
@Data
public class SubSubUser {
private String aaa;
private String ccc;
}
@Data
public class UserDto implements Serializable {
private Integer id;
private String name;
private String address;
private Date birth;
private String isDisable;
private List<SubUserDto> user;
}
@Data
public class SubUserDto {
private Integer deleted;
private String name;
private List<SubSubUserDto> subUser;
}
@Data
public class SubSubUserDto {
private String aaa;
private String bbb;
}
@Mapper
public interface UserMapper {
UserDto userToNewUserDto(User user);
//子集字段相同方法不用编写会自动生成
//孙子集字段不相同(list会自动读取此方法生成list)
@Mapping(source = "ccc",target = "bbb")
SubSubUserDto bbb(SubSubUser subSubUser);
}
4.8.2 方式2
通过uses配置类型转换
@Mapper(uses = {TestMapper.class})
public interface UserMapper {
UserDto userToNewUserDto(User user);
}
@Mapper
public interface TestMapper {
@Mapping(source = "ccc",target = "bbb")
SubSubUserDto bbb(SubSubUser subSubUser);
}
5、获取 mapper
5.1 通过 Mapper 工厂获取
我们都是通过 Mappers.getMapper(xxx.class)
的方式来进行对应 Mapper 的获取。此种方法为通过 Mapper 工厂获取。
如果是此种方法,约定俗成的是在接口内定义一个接口本身的实例 INSTANCE, 以方便获取对应的实例。
@Mapper
public interface SourceMapper {
SourceMapper INSTANCE = Mappers.getMapper(SourceMapper.class);
// ......
}
这样在调用的时候,我们就不需要在重复的去实例化对象了。类似下面
Target target = SourceMapper.INSTANCE.source2target(source);
5.2 使用依赖注入
对于 Web 开发,依赖注入应该很熟悉。MapSturct 也支持使用依赖注入,同时也推荐使用依赖注入。
@Mapper(componentModel = "spring")
5.3 依赖注入策略
可以选择是通过构造方法或者属性注入,默认是属性注入。
public enum InjectionStrategy {
/** Annotations are written on the field **/
FIELD,
/** Annotations are written on the constructor **/
CONSTRUCTOR
}
类似如此使用
@Mapper(componentModel = "cdi" injectionStrategy = InjectionStrategy.CONSTRUCTOR)
5.4 自定义类型转换
有时候,在对象转换的时候可能会出现这样一个问题,就是源对象中的类型是Boolean类型,而目标对象类型是String类型,这种情况可以通过@Mapper
的uses属性来实现:
@Data
public class User {
private Integer id;
private String name;
private String address;
private Date birth;
private Boolean isDisable;
}
@Data
public class UserDto implements Serializable {
private Integer id;
private String name;
private String address;
private Date birth;
private String isDisable;
}
@Mapper(uses = {BooleanStrFormat.class})
public interface UserMapper {
UserDto userToNewUserDto(User user);
}
public class BooleanStrFormat {
public String toStr(Boolean isDisable) {
if (isDisable) {
return "Y";
} else {
return "N";
}
}
public Boolean toBoolean(String str) {
if (str.equals("Y")) {
return true;
} else {
return false;
}
}
}
要注意的是,如果使用了例如像spring这样的环境,Mapper引入uses类实例的方式将是自动注入,那么这个类也应该纳入Spring容器
@Test
public void userPoToUserDto() {
User user =new User();
user.setId(1);
user.setName("myx");
user.setAddress("河北沧州");
user.setBirth(new Date());
user.setIsDisable(true);
SubUser subUser =new SubUser();
subUser.setDeleted(0);
subUser.setName("rkw");
UserMapper mapper = Mappers.getMapper(UserMapper.class);
UserDto userDto = mapper.userToNewUserDto(user);
System.out.println(userDto);
}
```## 简介
## 说明
本文介绍Spring的BeanUtils工具类的用法。
我们经常需要将不同的两个对象实例进行属性复制,比如将DO对象进行属性复制到DTO,这种转换最原始的方式就是手动编写大量的 get/set代码,很繁琐。为了解决这一痛点,就诞生了一些方便的类库,常用的有 Apache的 BeanUtils,Spring的 BeanUtils, Dozer,Orika等拷贝工具。
由于Apache的BeanUtils的性能很差,强烈不建议使用。阿里巴巴Java开发规约插件上也明确指出:
“Ali-Check | 避免用Apache Beanutils进行属性的copy。”
## Spring的BeanUtils方法
| 方法 | 说明 |
| ------------------------------------------------------- | ---------------------------------------------- |
| BeanUtils.copyProperties(source, target); | source对应的对象成员赋值给target对应的对象成员 |
| BeanUtils.copyProperties(source, target, "id", "time"); | 忽略拷贝某些字段。本处是忽略:id与time |
### Spring的BeanUtils方法注意事项
1.BeanUtils是浅拷贝。
2.**泛型只在编译期起作用**,不能依靠泛型来做运行期的限制;
### 浅拷贝和深拷贝
1.浅拷贝:对基本数据类型进行值传递,对引用数据类型,使用其引用地址,不拷贝其内容,此为浅拷贝
2.深拷贝:对基本数据类型进行值传递,对引用数据类型,创建一个新的对象,并复制其内容,此为深拷贝。
## Spring的BeanUtils与Apache的BeanUtils区别
| 项 | Spring的BeanUtils | Apache的BeanUtils |
| --------- | ------------------------------------------------------------ | ------------------------------------------------------------ |
| 性能 | 好。原因:对两个对象中相同名字的属性进行简单的get/set,仅检查属性的可访问性 | 差。原因:有很多检验:类型的转换、对象所属类的可访问性等,注意:本处类型转换是类似的类,多个String转为List<String>是不行的。 |
| 使用方面 | 第一个参数是源,第二个参数是目标。无需捕获异常 | 第一个参数是目标,第二个参数是源。必须捕获异常 |
| 深/浅拷贝 | 浅拷贝 | 浅拷贝 |
### Apache的BeanUtils方法。(依赖:maven里直接搜“commons-beanutils”)
| 方法 | 说明 |
| ------------------------------------------------------------ | ---------------------------------------------- |
| BeanUtils.copyProperties(Object target, Object source); | source对应的对象成员赋值给target对应的对象成员 |
| BeanUtils.copyProperties(Object target, String name, Object source); | 只拷贝某些字段。 |
Apache的BeanUtils支持的类型转换:
1.java.lang.BigDecimal
2.java.lang.BigInteger
3.boolean and java.lang.Boolean
4.byte and java.lang.Byte
5.char and java.lang.Character
6.java.lang.Class
7.double and java.lang.Double
8.float and java.lang.Float
[http://9.int](https://link.zhihu.com/?target=http%3A//9.int) and java.lang.Integer
10.long and java.lang.Long
11.short and java.lang.Short
12.java.lang.String
13.java.sql.Date
14.java.sql.Time
15.java.sql.Timestamp
java.util.Date是不被支持的,而它的子类java.sql.Date是被支持的。因此如果对象包含时间类型的属性,且希望被转换的时候,一定要使用java.sql.Date类型。否则在转换时会提示argument mistype异常。
## 实例
entity
```java
package com.example.entity;
import lombok.Data;
@Data
public class Question {
private Integer id;
private Integer studentId;
private String content;
private Integer value;
}
package com.example.entity;
import lombok.Data;
@Data
public class Student {
private Integer id;
private String name;
private Integer points;
}
vo
package com.example.vo;
import lombok.Data;
import java.io.Serializable;
import java.util.List;
@Data
public class QuestionStudentVO implements Serializable {
private Integer id;
private String content;
private Integer value;
private Integer studentId;
private List<String> name;
private Integer points;
}
测试类
package com.example;
import com.example.entity.Question;
import com.example.entity.Student;
import com.example.vo.QuestionStudentVO;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.BeanUtils;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
@RunWith(SpringRunner.class)
@SpringBootTest
public class DemoApplicationTests {
@Test
public void beanUtilTest(){
Question question = new Question();
question.setId(2);
// question.setStudentId(3);
question.setContent("This is content");
question.setValue(100);
Student student = new Student();
student.setId(3);
student.setName("Tony Stark");
student.setPoints(201);
QuestionStudentVO questionStudentVO = new QuestionStudentVO();
BeanUtils.copyProperties(question, questionStudentVO);
BeanUtils.copyProperties(student, questionStudentVO);
System.out.println(questionStudentVO);
}
}
执行结果
QuestionStudentVO(id=3, content=Thi
1、什么是MapStruct
1.1 JavaBean 的困扰
对于代码中 JavaBean之间的转换, 一直是困扰我很久的事情。在开发的时候我看到业务代码之间有很多的 JavaBean 之间的相互转化, 非常的影响观感,却又不得不存在。我后来想的一个办法就是通过反射,或者自己写很多的转换器。
第一种通过反射的方法确实比较方便,但是现在无论是 BeanUtils, BeanCopier 等在使用反射的时候都会影响到性能。虽然我们可以进行反射信息的缓存来提高性能。但是像这种的话,需要类型和名称都一样才会进行映射,有很多时候,由于不同的团队之间使用的名词不一样,还是需要很多的手动 set/get 等功能。
第二种的话就是会很浪费时间,而且在添加新的字段的时候也要进行方法的修改。不过,由于不需要进行反射,其性能是很高的。
1.2 MapStruct 带来的改变
MapSturct 是一个生成类型安全,高性能且无依赖的 JavaBean 映射代码的注解处理器(annotation processor)。
- 注解处理器
- 可以生成 JavaBean 之间那的映射代码
- 类型安全,高性能,无依赖性
2、MapStruct 入门
2.1 添加依赖
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.20</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-jdk8</artifactId>
<version>${org.mapstruct.version}</version>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${org.mapstruct.version}</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.1.0</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
2.2 po类
@Data
public class User {
private Integer id;
private String name;
private String address;
private Date birth;
}
2.3 dto类
@Data
public class UserDto implements Serializable {
private Integer id;
private String name;
private String address;
private Date birth;
}
2.4 创建转换接口
//可以使用abstract class代替接口
@Mapper
public interface UserMapper {
UserDto userToUserDto(User user);
//集合
List<UserDto> userToUserDto(List<User> users);
}
2.5 测试方法
@Test
public void userPoToUserDto() {
User user =new User();
user.setId(1);
user.setName("myx");
user.setAddress("河北沧州");
user.setBirth(new Date());
UserMapper mapper = Mappers.getMapper(UserMapper.class);
UserDto userDto = mapper.userToUserDto(user);
System.out.println(userDto);
}
2.6 运行效果
![图片](data:image/svg+xml,%3C%3Fxml version=‘1.0’ encoding=‘UTF-8’%3F%3E%3Csvg width=‘1px’ height=‘1px’ viewBox=‘0 0 1 1’ version=‘1.1’ xmlns=‘http://www.w3.org/2000/svg’ xmlns:xlink=‘http://www.w3.org/1999/xlink’%3E%3Ctitle%3E%3C/title%3E%3Cg stroke=‘none’ stroke-width=‘1’ fill=‘none’ fill-rule=‘evenodd’ fill-opacity=‘0’%3E%3Cg transform=‘translate(-249.000000, -126.000000)’ fill=‘%23FFFFFF’%3E%3Crect x=‘249’ y=‘126’ width=‘1’ height=‘1’%3E%3C/rect%3E%3C/g%3E%3C/g%3E%3C/svg%3E)
2.7 查看编译的class
底层通过自动取值赋值操作完成
![图片](data:image/svg+xml,%3C%3Fxml version=‘1.0’ encoding=‘UTF-8’%3F%3E%3Csvg width=‘1px’ height=‘1px’ viewBox=‘0 0 1 1’ version=‘1.1’ xmlns=‘http://www.w3.org/2000/svg’ xmlns:xlink=‘http://www.w3.org/1999/xlink’%3E%3Ctitle%3E%3C/title%3E%3Cg stroke=‘none’ stroke-width=‘1’ fill=‘none’ fill-rule=‘evenodd’ fill-opacity=‘0’%3E%3Cg transform=‘translate(-249.000000, -126.000000)’ fill=‘%23FFFFFF’%3E%3Crect x=‘249’ y=‘126’ width=‘1’ height=‘1’%3E%3C/rect%3E%3C/g%3E%3C/g%3E%3C/svg%3E)
3、MapStruct优点分析
3.1 性能高
这是相对反射来说的,反射需要去读取字节码的内容,花销会比较大。而通过 MapStruct 来生成的代码,其类似于人手写。速度上可以得到保证。
3.2 使用简单
如果是完全映射的,使用起来肯定没有反射简单。用类似 BeanUtils 这些工具一条语句就搞定了。但是,如果需要进行特殊的匹配(特殊类型转换,多对一转换等),其相对来说也是比较简单的。
基本上,使用的时候,我们只需要声明一个接口,接口下写对应的方法,就可以使用了。当然,如果有特殊情况,是需要额外处理的。
3.3 代码独立
生成的代码是对立的,没有运行时的依赖。
3.4 易于 debug
在我们生成的代码中,我们可以轻易的进行 debug。
4、MapStruct使用案例
4.1 属性名称相同
在实现类的时候,如果属性名称相同,则会进行对应的转化。通过此种方式,我们可以快速的编写出转换的方法。(入门案例)
4.2 属性名不相同
属性名不相同,在需要进行互相转化的时候,则我们可以通过@Mapping
注解来进行转化。
@Data
public class UserDto implements Serializable {
private Integer id;
private String name;
private String address;
private Date birth;
private String password;
}
@Data
public class User {
private Integer id;
private String name;
private String address;
private Date birth;
private String pwd;
}
@Mapper
public interface UserMapper {
//单个属性
//@Mapping(source = "pwd",target = "password")
//多个属性
@Mappings({
@Mapping(source = "pwd",target = "password")
})
UserDto userToUserDto(User user);
}
- source 需要转换的对接,通常是入参
- target 转换的对接,通常是出参
- ignore 忽略,默认false不忽略,需要忽略设置为true
- defaultValue 默认值
- expressions 可以通过表达式来构造一些简单的转化关系。虽然设计的时候想兼容很多语言,不过目前只能写Java代码。
@Mappings({
@Mapping(source = "birthdate", target = "birth"),//属性名不一致映射
@Mapping(target = "birthformat", expression = "java(org.apache.commons.lang3.time.DateFormatUtils.format(person.getBirthdate(),\"yyyy-MM-dd HH:mm:ss\"))"),//自定义属性通过java代码映射
})
public PersonVo PersonToPersonVo(Person person);
这里用到演示了如何使用TimeAndFormat对time和format操作,这里必须要指定需要使用的Java类的完整包名,不然编译的时候不知道你使用哪个Java类,会报错。
@Test
public void userPoToUserDto() {
User user =new User();
user.setId(1);
user.setName("myx");
user.setAddress("河北沧州");
user.setBirth(new Date());
user.setPwd("123456");
UserMapper mapper = Mappers.getMapper(UserMapper.class);
UserDto userDto = mapper.userToUserDto(user);
System.out.println(userDto);
}
![图片](data:image/svg+xml,%3C%3Fxml version=‘1.0’ encoding=‘UTF-8’%3F%3E%3Csvg width=‘1px’ height=‘1px’ viewBox=‘0 0 1 1’ version=‘1.1’ xmlns=‘http://www.w3.org/2000/svg’ xmlns:xlink=‘http://www.w3.org/1999/xlink’%3E%3Ctitle%3E%3C/title%3E%3Cg stroke=‘none’ stroke-width=‘1’ fill=‘none’ fill-rule=‘evenodd’ fill-opacity=‘0’%3E%3Cg transform=‘translate(-249.000000, -126.000000)’ fill=‘%23FFFFFF’%3E%3Crect x=‘249’ y=‘126’ width=‘1’ height=‘1’%3E%3C/rect%3E%3C/g%3E%3C/g%3E%3C/svg%3E)
4.3 转换非基础类型属性
如果subUser与subUserDto字段名称相同直接配置即可完成(对象类型,包括list)
@Data
public class UserDto implements Serializable {
private Integer id;
private String name;
private String address;
private Date birth;
private String password;
private List<SubUserDto> subUserDto;
}
@Data
public class User {
private Integer id;
private String name;
private String address;
private Date birth;
private String pwd;
private List<SubUser> subUser;
}
@Mappings({
@Mapping(source = "pwd",target = "password"),
@Mapping(source = "subUser", target = "subUserDto")
})
UserDto userToUserDto(User user);
4.4 Mapper 中使用自定义的转换
有时候,对于某些类型,无法通过代码生成器的形式来进行处理。那么, 就需要自定义的方法来进行转换。这时候,我们可以在接口(同一个接口,后续还有调用别的 Mapper 的方法)中定义默认方法(Java8及之后)。
@Data
public class UserDto implements Serializable {
private Integer id;
private String name;
private String address;
private Date birth;
private String password;
private SubUserDto subUserDto;
}
@Data
public class SubUserDto {
private Boolean result;
private String name;
}
@Data
public class User {
private Integer id;
private String name;
private String address;
private Date birth;
private String pwd;
private SubUser subUser;
}
@Data
public class SubUser {
private Integer deleted;
private String name;
}
@Mapper
public interface UserMapper {
@Mappings({
@Mapping(source = "pwd",target = "password"),
@Mapping(source = "subUser", target = "subUserDto")
})
UserDto userToUserDto(User user);
default SubUserDto subSource2subTarget(SubUser subUser) {
if (subUser == null) {
return null;
}
SubUserDto subUserDto = new SubUserDto();
subUserDto.setResult(!subUser.getDeleted().equals(0));
subUserDto.setName(subUser.getName()==null?"":subUser.getName());
return subUserDto;
}
}
只能存在一个default修饰的方法
@Test
public void userPoToUserDto() {
User user =new User();
user.setId(1);
user.setName("myx");
user.setAddress("河北沧州");
user.setBirth(new Date());
user.setPwd("123456");
SubUser subUser =new SubUser();
subUser.setDeleted(0);
subUser.setName("rkw");
user.setSubUser(subUser);
UserMapper mapper = Mappers.getMapper(UserMapper.class);
UserDto userDto = mapper.userToUserDto(user);
System.out.println(userDto);
}
![图片](data:image/svg+xml,%3C%3Fxml version=‘1.0’ encoding=‘UTF-8’%3F%3E%3Csvg width=‘1px’ height=‘1px’ viewBox=‘0 0 1 1’ version=‘1.1’ xmlns=‘http://www.w3.org/2000/svg’ xmlns:xlink=‘http://www.w3.org/1999/xlink’%3E%3Ctitle%3E%3C/title%3E%3Cg stroke=‘none’ stroke-width=‘1’ fill=‘none’ fill-rule=‘evenodd’ fill-opacity=‘0’%3E%3Cg transform=‘translate(-249.000000, -126.000000)’ fill=‘%23FFFFFF’%3E%3Crect x=‘249’ y=‘126’ width=‘1’ height=‘1’%3E%3C/rect%3E%3C/g%3E%3C/g%3E%3C/svg%3E)
4.5 多转一
我们在实际的业务中少不了将多个对象转换成一个的场景。MapStruct 当然也支持多转一的操作。
![5](https://gitee.com/chenjiabing666/BlogImage/raw/master/%E6%80%A7%E8%83%BD%E9%AB%98%E3%80%81%E4%B8%8A%E6%89%8B%E5%BF%AB%EF%BC%8C%E5%AE%9E%E4%BD%93%E7%B1%BB%E8%BD%AC%E6%8D%A2%E5%B7%A5%E5%85%B7%20MapStruct%20%E5%88%B0%E5%BA%95%E6%9C%89%E5%A4%9A%E5%BC%BA%E5%A4%A7%EF%BC%81/5.png)@Data
public class SubUser {
private Integer deleted;
private String name;
}
@Data
public class User {
private Integer id;
private String name;
private String address;
private Date birth;
private String pwd;
}
@Mapper
public interface UserMapper {
@Mappings({
@Mapping(source = "user.pwd",target = "password"),
@Mapping(source = "subUser.name", target = "name")
})
NewUserDto userToUserDto(User user,SubUser subUser);
}
@Test
public void userPoToUserDto() {
User user =new User();
user.setId(1);
user.setName("myx");
user.setAddress("河北沧州");
user.setBirth(new Date());
user.setPwd("123456");
SubUser subUser =new SubUser();
subUser.setDeleted(0);
subUser.setName("rkw");
UserMapper mapper = Mappers.getMapper(UserMapper.class);
NewUserDto userDto = mapper.userToUserDto(user,subUser);
System.out.println(userDto);
}
![图片](data:image/svg+xml,%3C%3Fxml version=‘1.0’ encoding=‘UTF-8’%3F%3E%3Csvg width=‘1px’ height=‘1px’ viewBox=‘0 0 1 1’ version=‘1.1’ xmlns=‘http://www.w3.org/2000/svg’ xmlns:xlink=‘http://www.w3.org/1999/xlink’%3E%3Ctitle%3E%3C/title%3E%3Cg stroke=‘none’ stroke-width=‘1’ fill=‘none’ fill-rule=‘evenodd’ fill-opacity=‘0’%3E%3Cg transform=‘translate(-249.000000, -126.000000)’ fill=‘%23FFFFFF’%3E%3Crect x=‘249’ y=‘126’ width=‘1’ height=‘1’%3E%3C/rect%3E%3C/g%3E%3C/g%3E%3C/svg%3E)
4.5.1 遵循原则
- 当多个对象中, 有其中一个为 null, 则会直接返回 null
- 如一对一转换一样, 属性通过名字来自动匹配。因此, 名称和类型相同的不需要进行特殊处理
- 当多个原对象中,有相同名字的属性时,需要通过 @Mapping 注解来具体的指定, 以免出现歧义(不指定会报错)。如上面的 name
属性也可以直接从传入的参数来赋值
@Mapping(source = "person.description", target = "description")
@Mapping(source = "name", target = "name")
DeliveryAddress personAndAddressToDeliveryAddressDto(Person person, String name);
4.6 更新 Bean 对象
有时候,我们不是想返回一个新的 Bean 对象,而是希望更新传入对象的一些属性。这个在实际的时候也会经常使用到。
@Mapper
public interface UserMapper {
NewUserDto userToNewUserDto(User user);
/**
* 更新, 注意注解 @MappingTarget
* 注解 @MappingTarget后面跟的对象会被更新。
*/
void updateDeliveryAddressFromAddress(SubUser subUser,@MappingTarget NewUserDto newUserDto);
}
@Test
public void userPoToUserDto() {
User user =new User();
user.setId(1);
user.setName("myx");
user.setAddress("河北沧州");
user.setBirth(new Date());
SubUser subUser =new SubUser();
subUser.setDeleted(0);
subUser.setName("rkw");
UserMapper mapper = Mappers.getMapper(UserMapper.class);
NewUserDto userDto = mapper.userToNewUserDto(user);
mapper.updateDeliveryAddressFromAddress(subUser,userDto);
System.out.println(userDto);
}
4.7 map映射
@MapMapping(valueDateFormat ="yyyy-MM-dd HH:mm:ss")
public Map<String ,String> DateMapToStringMap(Map<String,Date> sourceMap);
@Test
public void mapMappingTest(){
Map<String,Date> map=new HashMap<>();
map.put("key1",new Date());
map.put("key2",new Date(new Date().getTime()+9800000));
Map<String, String> stringObjectMap = TestMapper.MAPPER.DateMapToStringMap(map);
}
4.8 多级嵌套
只需要在mapper接口中定义相关的类型转换方法即可,list类型也适用
4.8.1 方式1
@Data
public class User {
private Integer id;
private String name;
private String address;
private Date birth;
private Boolean isDisable;
private List<SubUser> user;
}
@Data
public class SubUser {
private Integer deleted;
private String name;
private List<SubSubUser> subUser;
}
@Data
public class SubSubUser {
private String aaa;
private String ccc;
}
@Data
public class UserDto implements Serializable {
private Integer id;
private String name;
private String address;
private Date birth;
private String isDisable;
private List<SubUserDto> user;
}
@Data
public class SubUserDto {
private Integer deleted;
private String name;
private List<SubSubUserDto> subUser;
}
@Data
public class SubSubUserDto {
private String aaa;
private String bbb;
}
@Mapper
public interface UserMapper {
UserDto userToNewUserDto(User user);
//子集字段相同方法不用编写会自动生成
//孙子集字段不相同(list会自动读取此方法生成list)
@Mapping(source = "ccc",target = "bbb")
SubSubUserDto bbb(SubSubUser subSubUser);
}
4.8.2 方式2
通过uses配置类型转换
@Mapper(uses = {TestMapper.class})
public interface UserMapper {
UserDto userToNewUserDto(User user);
}
@Mapper
public interface TestMapper {
@Mapping(source = "ccc",target = "bbb")
SubSubUserDto bbb(SubSubUser subSubUser);
}
5、获取 mapper
5.1 通过 Mapper 工厂获取
我们都是通过 Mappers.getMapper(xxx.class)
的方式来进行对应 Mapper 的获取。此种方法为通过 Mapper 工厂获取。
如果是此种方法,约定俗成的是在接口内定义一个接口本身的实例 INSTANCE, 以方便获取对应的实例。
@Mapper
public interface SourceMapper {
SourceMapper INSTANCE = Mappers.getMapper(SourceMapper.class);
// ......
}
这样在调用的时候,我们就不需要在重复的去实例化对象了。类似下面
Target target = SourceMapper.INSTANCE.source2target(source);
5.2 使用依赖注入
对于 Web 开发,依赖注入应该很熟悉。MapSturct 也支持使用依赖注入,同时也推荐使用依赖注入。
@Mapper(componentModel = "spring")
5.3 依赖注入策略
可以选择是通过构造方法或者属性注入,默认是属性注入。
public enum InjectionStrategy {
/** Annotations are written on the field **/
FIELD,
/** Annotations are written on the constructor **/
CONSTRUCTOR
}
类似如此使用
@Mapper(componentModel = "cdi" injectionStrategy = InjectionStrategy.CONSTRUCTOR)
5.4 自定义类型转换
有时候,在对象转换的时候可能会出现这样一个问题,就是源对象中的类型是Boolean类型,而目标对象类型是String类型,这种情况可以通过@Mapper
的uses属性来实现:
@Data
public class User {
private Integer id;
private String name;
private String address;
private Date birth;
private Boolean isDisable;
}
@Data
public class UserDto implements Serializable {
private Integer id;
private String name;
private String address;
private Date birth;
private String isDisable;
}
@Mapper(uses = {BooleanStrFormat.class})
public interface UserMapper {
UserDto userToNewUserDto(User user);
}
public class BooleanStrFormat {
public String toStr(Boolean isDisable) {
if (isDisable) {
return "Y";
} else {
return "N";
}
}
public Boolean toBoolean(String str) {
if (str.equals("Y")) {
return true;
} else {
return false;
}
}
}
要注意的是,如果使用了例如像spring这样的环境,Mapper引入uses类实例的方式将是自动注入,那么这个类也应该纳入Spring容器
@Test
public void userPoToUserDto() {
User user =new User();
user.setId(1);
user.setName("myx");
user.setAddress("河北沧州");
user.setBirth(new Date());
user.setIsDisable(true);
SubUser subUser =new SubUser();
subUser.setDeleted(0);
subUser.setName("rkw");
UserMapper mapper = Mappers.getMapper(UserMapper.class);
UserDto userDto = mapper.userToNewUserDto(user);
System.out.println(userDto);
}