Java中的DO,BO,DTO,VO,POJO 及 mapstruct.Mapper注解

Java项目中的DO,DTO,VO,POJO

作为后端最常用的编程语言之一,Java 已经有很多年的历史了,在阿里内部,Java 也是使用最广泛的一门语言。在阿里实习的这段时间,规范一词是我感受最深的。没有规矩不成方圆,今天来说一下 Java 中的各种 O(bject)。

为什么会出现这些 O?

我们知道,这些 O 不管叫什么名字,其本质都还是对象(Object),既然本质都一样,为什么非要给他们套上各种马甲?个人认为原因有三:第一,随着编程工业化的发展,需要有一套合理的体系出现。中国人喜欢造神,外国人喜欢造概念,于是 MVC、MVP、MVVM 等编程模型就出现了,为了搭配这些编程模型的使用,需要对 Object 的功能进行划分,于是我们便看到了这些层出不穷的 Object。当然这里并没有批评这些概念的意思。其二,我认为在团队协作编码中,一个好的命名方式是可以节约很多时间成本的。就比如 getItemById 一眼看去就知道是通过 id 获取一个 item 对象, ItemVO 一眼看去就知道是前端透出的 json 对应的对象。其三,如此划分,可以让项目结构更加清楚,不至于出现东一块西一块,对象乱扔的局面。尽可能避免了在多人协作时对象混乱的情况。总的来说,这一切都是为了让软件编程更加合理、更加规范、更加高效。

有哪些 O?

这些 O 有很多衍生出的命名,比如 VO、DO、BO,这里我们把常见的 O 列举出来,然后一一解释。

以下内容参考阿里巴巴 Java 开发手册,如果有需要可以在微信公众号「01 二进制」后台回复「Java 开发手册」获得。

  • DO( Data Object):与数据库表结构一一对应,通过 DAO 层向上传输数据源对象。

  • PO(Persistant Object):持久对象,一个 PO 的数据结构对应着库中表的结构,表中的一条记录就是一个 PO 对象

  • DTO( Data Transfer Object):数据传输对象,Service 或 Manager 向外传输的对象。

  • BO( Business Object):业务对象。由 Service 层输出的封装业务逻辑的对象。

  • AO( Application Object):应用对象。在 Web 层与 Service 层之间抽象的复用对象模型,极为贴近展示层,复用度不高。

  • VO( View Object):显示层对象,通常是 Web 向模板渲染引擎层传输的对象。

  • POJO( Plain Ordinary Java Object):POJO 专指只有 setter/getter/toString 的简单类,包括 DO/DTO/BO/VO 等。

  • DAO(Data Access Objects):数据访问对象,和上面那些 O 不同的是,其功能是用于进行数据操作的。通常不会用于描述数据实体。

    一下子给出 8 个常见的 O,光看解释大家可能会有些迷糊,接下来我们从下面这张图入手,带大家直观的感受下,这些 O 的用处。

数据的流向

 

 

我们知道,一般情况下,前端是不会凭空造出数据的,因此最后前端展示的数据一定是从数据库中来的,数据的流向通常也是从数据库流向页面。我将其分成三个部分:数据访问、业务处理和业务解释。

\1. 数据访问:这一部分是用于从数据库中读取数据,将数据记录转换成数据实体也就是 Java 对象,便于操作。 2. 业务处理:这一部分是数据流的核心,几乎所有数据的操作都是在这一部分完成的。 3. 业务解释:这一部分是用于展示给前端的数据,解释业务体现在某些字段 / 值是需要经过处理的才会呈现的。

关键点

说了这么多,我们整理出以下关键点。

• DAO,是用于 操作数据 而不是描述数据的。 • PO/DO/Entity,其数据结构对应数据表中的一条记录,因此是同一类别的。 • BO,可以理解为 PO 的组合,举个简单的例子,假设 PO 是一条交易记录,BO 就可以是一个人全部的交易记录集合对象。 • DTO,用于传输数据,可能传递给前端,也有可能传递给其他系统。用于 承载数据 。 • VO,这个最好理解,前端最后需要的数据长什么样,对应的对象就是 VO。

如何使用这些 O?

说了这么多,在实际的项目中,我们应该如何去使用这些 O?

教条主义?

首先,这几个概念很完整,但是我们在用的时候是必须按这个来做吗?答案当然不是的,规矩是死的,人是活的。文章开头我们就说了,之所以引入这些概念,很大程度上是为了提升编程体验,而且系统和系统的复杂度不同,协作水平不同,完全没有必要教条主义,适合自己的才是最好的。

省略方案

\1. 不管你是叫 PO 还是 DO 还是 Entity,用于描述数据库记录的对象一定要存在,不可省略。 2. DTO 和 BO 在一般情况下,如果业务系统不是非常复杂,可以考虑省略。 3. VO 和 DTO,DTO 可以用于将数据传递给前端,如果你不需要删减字段的话,VO 可以考虑省略。

注意事项

领域模型命名规约:

• 数据对象:xxxDO,xxx 即为数据表名。 • 数据传输对象:xxxDTO,xxx 为业务领域相关的名称。 • 展示对象:xxxVO,xxx 一般为网页名称。 • POJO 是 DO/DTO/BO/VO 的统称,禁止命名成 xxxPOJO。

 

 

MapStruct.Mapper 使用

  • maven依赖

    <dependency>
       <groupId>org.mapstruct</groupId>
       <artifactId>mapstruct-jdk8</artifactId>
       <version>1.3.0.Final</version>
    </dependency>
    <dependency>
       <groupId>org.mapstruct</groupId>
       <artifactId>mapstruct-processor</artifactId>
       <version>1.3.0.Final</version>
    </dependency>

    先定义两个entity

    @Data
    public class Source {
    
      private String id;
    
      private Integer num;
    
      private Integer count;
    }
    @Data
    public class Target {
    
      private String id;
    
      private Integer num;
    
      private Integer count;
    }

    Source 为转换类,Target 为待转换类,接下来定义转换器

    @Mapper
    public interface SourceMapper {
    
      SourceMapper INSTANCE = Mappers.getMapper(SourceMapper.class);
    
      Target source2target(Source source);
    
    }

    定义一个 INSTANCE 是为了方便调用,方法名没有限制,mapstruct会帮我们生成一个接口的实现类,

    @Generated(
        value = "org.mapstruct.ap.MappingProcessor",
        date = "2020-08-01T19:56:53+0800",
        comments = "version: 1.3.0.Final, compiler: javac, environment: Java 1.8.0_181 (Oracle Corporation)"
    )
    public class SourceMapperImpl implements SourceMapper {
    
        @Override
        public Target source2target(Source source) {
            if ( source == null ) {
                return null;
            }
    
            Target target = new Target();
    
            target.setId( source.getId() );
            target.setNum( source.getNum() );
            target.setCount( source.getCount() );
    
            return target;
        }
    }

    调用转换器

    public class Client {
      public static void main(String[] args) {
        Source source = new Source();
        source.setId("1");
        source.setNum(2);
        source.setCount(3);
    
        Target target = SourceMapper.INSTANCE.source2target(source);
        System.out.println(source);
        System.out.println(target);
      }
    }

    输出结果为

    Source(id=1, num=2, count=3)
    Target(id=1, num=2, count=3)

    属性名不同的转换

    如果属性名不同的话,可以通过 Mapping 注解来转换

    @Data
    public class Source {
    
      private String sourceId;
    
      private Integer sourceNum;
    
      private Integer sourceCount;
    }
    @Data
    public class Target {
    
      private String targetId;
    
      private Integer targetNum;
    
      private Integer targetCount;
    }
    @Mapper
    public interface SourceMapper {
    
      SourceMapper INSTANCE = Mappers.getMapper(SourceMapper.class);
    
      @Mapping(source = "sourceId", target = "targetId")
      @Mapping(source = "sourceNum", target = "targetNum")
      @Mapping(source = "sourceCount", target = "targetCount")
      Target source2target(Source source);
    
    }

    Mapping 注解是一个可重复注解,通过 Mapping 注解指定源属性名和目标属性名就可以了。

    public class Client {
      public static void main(String[] args) {
    
        Source source = new Source();
        source.setSourceId("1");
        source.setSourceNum(2);
        source.setSourceCount(3);
    
        Target target = SourceMapper.INSTANCE.source2target(source);
        System.out.println(source);
        System.out.println(target);
      }
    }

    结果符合预期。

    自定义转换

    有时候,某些类型的转换不能通过 mapstruct 来实现,我们可以定义自己的转换逻辑。

    @Data
    public class Source {
    
      private String sourceId;
    
      private Integer sourceNum;
    
      private Integer sourceCount;
    
      private SubSource subSource;
    
    }
    @Data
    public class SubSource {
    
      private String deleted;
    
    }
    @Data
    public class Target {
    
      private String targetId;
    
      private Integer targetNum;
    
      private Integer targetCount;
    
      private SubTarget subTarget;
    
    }
    @Data
    public class SubTarget {
    
      private Boolean deleted;
    
    }

    定义 SubSource 转换器

    @Mapper
    public class SubSourceMapper {
    
      SubTarget subSource2subTarget(SubSource subSource) {
        if (subSource == null) {
          return null;
        }
        SubTarget subTarget = new SubTarget();
    // 特殊的转换逻辑
        subTarget.setDeleted(subSource.getDeleted().equals("T"));
        return subTarget;
      }
    }

    让 SourceMapper 使用自定义的转换器

    @Mapper(uses = SubSourceMapper.class)
    public interface SourceMapper {
    
      SourceMapper INSTANCE = Mappers.getMapper(SourceMapper.class);
    
      @Mapping(source = "sourceCount", target = "targetCount")
      @Mapping(source = "sourceNum", target = "targetNum")
      @Mapping(source = "sourceId", target = "targetId")
      @Mapping(source = "subSource", target = "subTarget")
      Target source2target(Source source);
    
    }

    Mapper注解的uses属性表示使用的其他转换器,既可以是我们自定义的,也可以是
    mapstruct 生成的。java8之后我们也可以通过默认方法的方式来实现自定义转换。

    @Mapper
    public interface SourceMapper {
    
      SourceMapper INSTANCE = Mappers.getMapper(SourceMapper.class);
    
      @Mapping(source = "sourceCount", target = "targetCount")
      @Mapping(source = "sourceNum", target = "targetNum")
      @Mapping(source = "sourceId", target = "targetId")
      @Mapping(source = "subSource", target = "subTarget")
      Target source2target(Source source);
    
      default SubTarget subSource2subTarget(SubSource subSource) {
        if (subSource == null) {
          return null;
        }
        SubTarget subTarget = new SubTarget();
        subTarget.setDeleted(subSource.getDeleted().equals("T"));
        return subTarget;
      }
    }

    多对一转换

    将多个对象转换成一个

    @Data
    public class Person {
    
      private String firstName;
      private String lastName;
      private int height;
      private String description;
    
    }
    @Data
    public class Address {
    
      private String street;
      private int zipCode;
      private int houseNo;
      private String description;
    
    }
    @Data
    public class DeliveryAddress {
    
      private String firstName;
      private String lastName;
      private int height;
      private String street;
      private int zipCode;
      private int houseNumber;
      private String description;
    }
    @Mapper
    public interface AddressMapper {
    
      AddressMapper INSTANCE = Mappers.getMapper(AddressMapper.class);
    
      @Mapping(source = "person.description", target = "description")
      @Mapping(source = "address.houseNo", target = "houseNumber")
      DeliveryAddress personAndAddress2DeliveryAddress(Person person, Address address);
    }

    两个输入源都有description,必须指定一个输入源。

    装饰器

    装饰器可以让我们在转换前后添加一些额外的逻辑,以上一个程序为例,重新设置DeliveryAddress的description。

    public abstract class AddressMapperDecorate implements AddressMapper {
    
      private final AddressMapper delegate;
    
      protected AddressMapperDecorate(AddressMapper addressMapper) {
        this.delegate = addressMapper;
      }
    
    // 装饰器逻辑 重新设置description
      @Override
      public DeliveryAddress personAndAddress2DeliveryAddress(Person person, Address address) {
        DeliveryAddress deliveryAddress = delegate.personAndAddress2DeliveryAddress(person, address);
        deliveryAddress.setDescription(person.getDescription() + ":" + address.getDescription());
        return deliveryAddress;
      }
    }

    定义一个装饰器,必须实现转换接口并添加一个接口的构造器,定义为抽象类可以让我们只装饰指定的方法。

    使用DecoratedWith注解来表明所使用的装饰器

    @Mapper
    @DecoratedWith(AddressMapperDecorate.class)
    public interface AddressMapper {
    
      AddressMapper INSTANCE = Mappers.getMapper(AddressMapper.class);
    
      @Mapping(source = "person.description", target = "description")
      @Mapping(source = "address.houseNo", target = "houseNumber")
      DeliveryAddress personAndAddress2DeliveryAddress(Person person, Address address);
    }

    前置后置处理器

    我们可以在转换方法调用前后做一些操作

    @Mapper
    public interface AddressMapper {
    
      AddressMapper INSTANCE = Mappers.getMapper(AddressMapper.class);
    
      @Mapping(source = "person.description", target = "description")
      @Mapping(source = "address.houseNo", target = "houseNumber")
      DeliveryAddress personAndAddress2DeliveryAddress(Person person,
                                                       Address address,
                                                       @Context Locale locale);
    
      @BeforeMapping
      default void beforeMapping(Person person,
                                 Address address,
                                 @MappingTarget DeliveryAddress deliveryAddress,
                                 @TargetType Class<DeliveryAddress> deliveryAddressClass,
                                 @Context Locale locale) {
        System.out.println("before mapping start...");
        System.out.println(person);
        System.out.println(address);
        System.out.println(deliveryAddress);
        System.out.println(deliveryAddressClass);
        System.out.println(locale);
        System.out.println("before mapping end...");
      }
    
      @AfterMapping
      default void afterMapping(Person person,
                                Address address,
                                @MappingTarget DeliveryAddress deliveryAddress) {
        deliveryAddress.setDescription(person.getDescription() + "," + address.getDescription());
      }
    }

    BeforeMapping 注解表示前置处理器,AfterMapping 注解表示后置处理器,MappingTarget 注解表示此参数为target实例,TargetType 注解表示参数为target类型,Context 注解表示参数为上下文参数,对应转换方法中的上下文,其余的参数为source。

    依赖注入

    我们也可以将转换器定义为spring的bean

    @Mapper(componentModel = "spring")
    public interface AddressMapper {
    
      AddressMapper INSTANCE = Mappers.getMapper(AddressMapper.class);
    
      @Mapping(source = "person.description", target = "description")
      @Mapping(source = "address.houseNo", target = "houseNumber")
      DeliveryAddress personAndAddress2DeliveryAddress(Person person, Address address);
    }

    接口实现类上会加上 Component 注解。

 

posted @ 2022-08-16 20:01  韩增  阅读(725)  评论(0)    收藏  举报