相信大家对于VO、DTO、PO等都不陌生,其实都是各执其责,在不同的作用域中有着属于自己单独的作用。
先简单介绍一下它们吧
这里我贴出一张图来让大家直观的看出来它们各自的作用域
DTO(Data Transfer Object)数据传输对象
这个传输通常指的前后端之间的传输 DTO是一个比较特殊的对象,他有两种存在形式: 一种是前端和后端交互所使用的对象,另一种是微服务之间的一种传输对象,我们一般也是用DTO来进行传输,但是如果是一个单体项目只是划分为不同的模块的话,模块间的传输就不是用DTO了,这个时候可能就是图中的BO,也可能是我们各自项目中的
entity
或者domain
包中的实体,其实这些也只是看大家的个人见解来认识并去理解。
VO(Value Object)值对象
VO就是展示用的数据,不管展示方式是网页,还是客户端,还是APP,只要是这个东西是让人看到的,这就叫VO,这个大家都很理解,反正就是我们的接口返回给前端的对象都是用VO来返回,跟DTO不一样的是,VO是我们返回给前端,DTO是我们从前端接收的时候用的,即一个是入参,一个是返回结果
PO(Persistant Object)持久对象
PO比较好理解,简单说PO就是数据库中的记录,一个PO的数据结构对应着库中表的结构,表中的一条记录就是一个PO对象,通常PO里面除了get,set之外没有别的方法,对于PO来说,数量是相对固定的,一定不会超过数据库表的数量,等同于BO,这俩概念是一致的
那么在使用这些对象的过程中我们就避免不了去进行对象的转换,最开始的阶段我们可能就是如下操作进行一个属性一个属性的进行set()
,将同含义的字段的值由DTO set到BO,再由BO set到PO,最终存入数据库中,但是当这种操作变得频繁的时候我们就会很厌烦这种编写,很浪费我们的时间,拉低我们的开发效率,减少我们的摸鱼时间,当然有的公司可能是通过看代码量来评绩效,这个时候我们还是这种方式来写比较合适哈哈哈哈。
我记得我刚工作的时候,我看见了别人通过BeanUtils来进行属性拷贝,仿佛发现了新大陆,这个真的好用,一两行代码就完事,直接把相同名称及类型的字段给赋值到新对象中,真的是太爱了。所以期间用到对象拷贝我也基本都是用的这个工具类,但是后面个人在一些项目中发现了另一个方法,当时我也不知道是干嘛的,后面再看的时候发现也是属性拷贝的作用,这个时候我可能就想了想这个mapstruct
和BeanUtils
有什么区别,下面我来贴一下源码图吧,大家能直观的看出来二者的一个很大的区别:
BeanUtils 源码
我们可以看见,BeanUtils的源码中是利用了反射然后通过遍历再加上不断的判断来进行属性赋值,不得不说,这个工具类的开发者真牛逼,相信他在开发这个工具类的过程中也很痛苦吧,解决了很多bug吧哈哈哈
MapStruct 编译后生成的代码
我们不难看出,编译后的代码其实就是我们经常写的set()
方法,不同属性或者类型会自动进行判断赋值。
由此可见二者最明显的区别就是运行效率上的区别,反射较于我们自己set的话会花费更多的时间,何况遍历里面又套了层层判断,但是MapStruct就是直接通过最简单的关系映射逐个 set 进去,效率上会快的多。
这里我贴出一个基于BeanUtils上封装的一个对象转换工具类,方便我们进行流式使用,比原生的BeanUtils更好用。
java
import org.springframework.beans.BeanUtils;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Supplier;
/**
* 转换对象工具
*/
public class BeanConvertUtils extends BeanUtils {
public static <S, T> T convertTo(S source, Supplier<T> targetSupplier) {
return convertTo(source, targetSupplier, null);
}
/**
* 转换对象
*
* @param source 源对象
* @param targetSupplier 目标对象供应方
* @param callBack 回调方法
* @param <S> 源对象类型
* @param <T> 目标对象类型
* @return 目标对象
*/
public static <S, T> T convertTo(S source, Supplier<T> targetSupplier, ConvertCallBack<S, T> callBack) {
if (null == source || null == targetSupplier) {
return null;
}
T target = targetSupplier.get();
copyProperties(source, target);
if (callBack != null) {
callBack.callBack(source, target);
}
return target;
}
public static <S, T> List<T> convertListTo(List<S> sources, Supplier<T> targetSupplier) {
return convertListTo(sources, targetSupplier, null);
}
/**
* 转换对象
*
* @param sources 源对象list
* @param targetSupplier 目标对象供应方
* @param callBack 回调方法
* @param <S> 源对象类型
* @param <T> 目标对象类型
* @return 目标对象list
*/
public static <S, T> List<T> convertListTo(List<S> sources, Supplier<T> targetSupplier, ConvertCallBack<S, T> callBack) {
if (null == sources || null == targetSupplier) {
return null;
}
List<T> list = new ArrayList<>(sources.size());
for (S source : sources) {
T target = targetSupplier.get();
copyProperties(source, target);
if (callBack != null) {
callBack.callBack(source, target);
}
list.add(target);
}
return list;
}
/**
* 回调接口
*
* @param <S> 源对象类型
* @param <T> 目标对象类型
*/
@FunctionalInterface
public interface ConvertCallBack<S, T> {
void callBack(S s, T t);
}
}
下面是测试类
java
@Test
public void test04(){
//==================== copy Object =====================
User user = new User().setAge("20").setName("xxw");
// user = null; //当user对象为空的时候,copy方法不会报错,也只会返回null
System.out.println("复制前的数据:"+user);
//普通写法
UserDTO userDTO = BeanConvertUtils.convertTo(user, UserDTO::new);
System.out.println("复制后的数据:"+userDTO);
//lambda写法,可以对不同类型的字段进行手动赋值
UserDTO dto = BeanConvertUtils.convertTo(user, UserDTO::new, (u, d) -> d.setAge(Integer.valueOf(u.getAge())));
System.out.println("复制后的数据:"+dto);
System.out.println("=================================================");
//==================== copy List<Object> =====================
User user1 = new User().setAge("22").setName("xw");
ArrayList<User> users = Lists.newArrayList(user1);
System.out.println("复制前的数据:"+users);
//普通写法
List<UserDTO> dtos = BeanConvertUtils.convertListTo(users, UserDTO::new);
System.out.println("复制后的数据:"+dtos);
//lambda写法,可以对不同类型的字段进行手动赋值
List<UserDTO> dtoList = BeanConvertUtils.convertListTo(users, UserDTO::new, (u, d) -> d.setAge(Integer.valueOf(u.getAge())));
System.out.println("复制后的数据:"+dtoList);
}
输出结果
java
复制前的数据:User(userId=null, name=xxw, age=20)
复制后的数据:UserDTO(name=xxw, age=null)
复制后的数据:UserDTO(name=xxw, age=20)
=================================================
复制前的数据:[User(userId=null, name=xw, age=22)]
复制后的数据:[UserDTO(name=xw, age=null)]
复制后的数据:[UserDTO(name=xw, age=22)]
使用MapStruct 首先有两个需要换转的对象
java
@Data
@Accessors(chain = true)
public class BeanDto {
private String name;
private Integer age;
private String time;
private List<Integer> list;
private Set<String> set;
}
java
@Data
@Accessors(chain = true)
public class BeanPo {
private String name;
private String age;
private String newTime;
private List<String> list1;
private Set<String> set1;
}
第一步先引入依赖
xml
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>1.4.2.Final</version>
</dependency>
基于对象创建一个转换接口
java
import com.google.common.collect.Lists;
import com.itself.mapstruct.bean.BeanDto;
import com.itself.mapstruct.bean.BeanPo;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.Mappings;
import org.mapstruct.Named;
import org.mapstruct.factory.Mappers;
import org.mockito.internal.util.collections.Sets;
import java.util.List;
import java.util.stream.Collectors;
/**
* @Author: duJi
* @Date: 2024-01-19
**/
@Mapper
public interface BeanConvert{
BeanConvert INSTANCE = Mappers.getMapper( BeanConvert.class );
BeanDto poToDto(BeanPo po);
@Mappings({
@Mapping(source = "time", target = "newTime"),//不同的字段名称可以通过这种方式来进行自动映射
@Mapping(source = "set", target = "set1"),//不同的字段名称可以通过这种方式来进行自动映射
@Mapping(target = "age",qualifiedByName = "intToString"),//利用自定义方法来对不同类型的字段进行转换映射
@Mapping(source = "list", target = "list1",qualifiedByName = "intToStringOnList")//利用自定义方法来对不同类型的字段集合进行转换映射
})
BeanPo dtoToPo(BeanDto dto);
@Named("intToString")
static String intToString(Integer num){
return String.valueOf(num);
}
@Named("intToStringOnList")
static List<String> intToString(List<Integer> nums){
return nums.stream().map(String::valueOf).collect(Collectors.toList());
}
static void main(String[] args) {
BeanDto dto = new BeanDto().setName("dto").setAge(1).setTime("2022").setList(Lists.newArrayList(1,2)).setSet(Sets.newSet("2"));
BeanPo beanPo = BeanConvert.INSTANCE.dtoToPo(dto);
System.out.println(beanPo);
}
}
当我们两个对象中相同的字段类型但是不同的名称的时候可以利用@Mapping
注解进行映射,如果是不同的字段类型的话我们可以利用自定义方法去对字段类型进行转换,利用@Named
注解进行调用自定义方法来转换并赋值,我上面的demo也基本上覆盖了可能会出现的场景,大家可自行下去体验。
总的来说,如果对性能要求不高的话BeanUtils
用着挺方便,不用引入依赖单独建转换接口及方法,对性能有要求的话就手动 set 或者使用MapStruct
来进行对象转换,这些方法使用的过程中唯一需要注意的就是我们要仔细检查映射字段的名称及类型是否一致,不一致的话就容易报错或者没有成功赋值。