反射实现DTO速转VO

技术22-08-2023

前言

在开发的过程中,我们要常常要实现DTO(数据传输对象)转VO(视图对象)。
可能你会问,什么是DTO,什么是VO?

DTO(Data Transfer Object)数据传输对象
1、在服务间的调用中,传输的数据对象
2、个人理解,DTO是可以存在于各层服务中(接口、服务、数据库等等)服务间的交互使用DTO来解耦

VO (view object/value object)表示层对象
1、前端展示的数据,在接口数据返回给前端的时候需要转成VO
2、个人理解使用场景,接口层服务中,将DTO转成VO,返回给前台

那么,高效的实现DTO(数据传输对象)转VO(视图对象)非常重要。

业务场景举例

我们先来看一个业务场景。
我们在请求登录成功的时候,我们毫无疑问要从数据库获取信息,并且将部分信息返回到前端。
我们从数据库获取信息时的数据传输对象(dto),假设是这样的:

@Data
@TableName("db_account")
@AllArgsConstructor
public class Account implements BaseData {
    @TableId(type = IdType.AUTO)
    Integer id;
    String username;
    String password;
    String email;
    String role;
    @TableField("registerTime")
    Date registerTime;
}

而我们返回给前端的时候,不能返回过多的数据,比如密码这些我们肯定不能返回,我们通常返回部分数据回去。

假设我们使用jwt的校验方案,那么毫无疑问得返回token回去。

下面是我们要返回的表示层对象(vo)

@Data
public class AuthorizeVO {
    String username;
    String role;
    String token;
    Date expire;
}

那么从dto传输值到vo。我们有很多方案。

方案一:直接手动设置

如下,我们可以这样以get和set的方式,进行手动设置。
但是问题也明显,代码冗余,可维护性差、可读性差、增加错误风险!
后期的开发,我们会有很多的dto和vo对象,如果都是这样手动设置,显然非常低效。

vo.setUsername(account.getUsername());
vo.setToken(token);
vo.setRole(account.getRole());
vo.setExpire(utils.expireTime());

方案二:使用官方提供工具库

我们可以使用BeanUtils.copyProperties() 去除重复代码,BeanUtils.copyProperties()底层是使用了反射实现。
但是这样我觉得依然不是很优雅,因为他是对相同的值进行设置,我们还要在后面去设置没有相同字段的值。

BeanUtils.copyProperties(account,vo);

方案三:自己实现dto转vo接口

我们知道,BeanUtils是通过反射机制实现,那我们也可以通过反射实现一个属于自己的dto转vo。
我的代码如下:

通过反射机制实现了将DTO对象转换为VO对象的功能。它可以通过让DTO类实现BaseData接口并调用接口中的方法来实现转换。转换过程中,会将DTO对象中的字段值复制到VO对象中,从而实现数据的传输和转换。

package com.example.entity;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.function.Consumer;

/**
 * 用于DTO快速转换VO实现,只需将DTO类继承此类即可使用
 */
public interface BaseData {

    /**
     * 创建指定的VO类并将当前DTO对象中的所有成员变量值直接复制到VO对象中
     * @param clazz 指定VO类型
     * @param consumer 返回VO对象之前可以使用Lambda进行额外处理
     * @return 指定VO对象
     * @param <V> 指定VO类型
     */
    default <V> V asViewObject(Class<V> clazz, Consumer<V> consumer) {
        V v = this.asViewObject(clazz);
        consumer.accept(v);
        return v;
    }


    /**
     * 创建指定的VO类并将当前DTO对象中的所有成员变量值直接复制到VO对象中
     * @param clazz 指定VO类型
     * @return 指定VO对象
     * @param <V> 指定VO类型
     */
    default <V> V asViewObject(Class<V> clazz) {
        try {
            Field[] fields = clazz.getDeclaredFields();
            Constructor<V> constructor = clazz.getConstructor();
            V v = constructor.newInstance();
            Arrays.asList(fields).forEach(field -> convert(field,v));
            return v;
        } catch (ReflectiveOperationException exception) {
            Logger logger = LoggerFactory.getLogger(BaseData.class);
            logger.error("在VO与DTO转换时出现了一些错误");
            throw new RuntimeException(exception.getMessage());
        }
    }

    /**
     * 内部使用,快速将当前类中目标对象字段同名字段的值复制到目标对象字段上
     * @param field 目标对象字段
     * @param target 目标对象
     */
    private void convert(Field field, Object target) {
        try {
            Field source = this.getClass().getDeclaredField(field.getName());
            field.setAccessible(true);
            source.setAccessible(true);
            field.set(target,source.get(this));
        } catch (IllegalAccessException | NoSuchFieldException ignored) {}
    }
}

使用方法:

AuthorizeVO vo = account.asViewObject(AuthorizeVO.class, v-> {
    v.setExpire(utils.expireTime());
    v.setToken(token);
});

这样,我们不仅可以实现类型BeanUtils的功能,还能利用Lambda,一步到位的实现,减少冗余代码。

Author's photo

HuanXin-Chen

A tech enthusiast and avid sharer, this dream chaser firmly believes that great things will happen!

See other articles: