实习记录(二)Java常用工具库

一.Lombok

1.背景概述

        Lombok是一个非常高效的专用于Java的自动构建插件库,其简化了 JavaBean 的编写,避免了冗余和样板式代码的出现,让编写的类更加简洁明了,可以帮助大家节省很多重复低效的代码编写。比如重复性的Setter、Getter、ToString、构造函数、日志等等,只需要一行注解就可以帮我们自动完成。

  • Lombok官方文档

2.原理分析

        Lombok 使用的是编译期增强技术。目前Java语言比较通用的编译期增强技术有两种方案:1.使用特殊的编译器,如 AspectJ 的编译器 AJC;2.使用 JSR 269 提供的相关接口,如 Lombok 的实现方案。Aspectj 的 AJC 我们在下面会进行讨论,所以这里我们先讨论一下 Lombok 的实现方案 JSR 269。

        JSR 269的具体内容是 Pluggable Annotation Processing API,翻译过来就是插件化注解处理应用程序接口,这是Java在编译期提供出来的一个扩展点,用户可以在代码编译成字节码阶段对类的内容做调整,整体工作流程如下图所示:

         上图展示了一个一般 Javac 的编译过程,Java 文件首先通过源码解析构建出一个AST(Abstract Syntax Tree 抽象语法树),然后执行 JSR 269 的插件扩展进行注解处理,最后经过分析优化将最终的 Java 文件生成二进制的 .class 文件。

        Lombok 就是利用了 JSR 269 提供的能力,在我们进行代码编译的阶段完成了我们非常诟病的 getter、setter 等的重复工作,但是由于是在进行代码编译阶段时类的 getter、setter 等方法才会生成,所以当我们使用IDE工具的时候如果没有进行特殊的功能支持(没有安装 Lomobk 插件)的话,我们是无法使用IDE的代码提示功能的,并且还会代码报红。

3.安装配置

(1)引入maven依赖

<dependencies>
	<dependency>
		<groupId>org.projectlombok</groupId>
		<artifactId>lombok</artifactId>
		<version>1.18.28</version>
		<scope>provided</scope>
	</dependency>
</dependencies>

(2)安装IDEA插件

  • Go to File > Settings > Plugins

  • Click on Browse repositories...

  • Search for Lombok Plugin

  • Click on Install plugin

  • Restart IntelliJ IDEA

        注意:lombok的引入,在.java文件编译之后的.class文件中会包含get、set方法,但源码找不到方法的定义,IDEA会认为这是错误,所以需要安装一个lombok插件

4.常用注解

(1)@Getter / @Setter 注解

        使用@Getter 和/或@Setter 注释任何字段属性或整个类,让lombok 自动生成默认的getter/setter方法,默认为public。其使用方式如下:

  • 编译前:
@Getter
@Setter
public class LombokAnnotationTest {
    private Integer id;
    private String name;
}
  • 编译后:
public class LombokAnnotationTest {
    private Integer id;
    private String name;

    public LombokAnnotationTest() {
    }

    public Integer getId() {
        return this.id;
    }

    public String getName() {
        return this.name;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public void setName(String name) {
        this.name = name;
    }
}

(2)@ToString/@EqualsAndHashCode

        这两个注解也比较好理解,就是生成toString,equals和hashcode方法,同时后者还会生成一个canEqual方法,用于判断某个对象是否是当前类的实例。生成方法时默认只会作用于类中的非静态成员变量字段。

@ToString
@EqualsAndHashCode
public class Demo {

    private String name;
    private int age;
}

//@EqualsAndHashCode也有类似的下面的属性,
@ToString(
        includeFieldNames = true, //是否使用字段名
        exclude = {"name"}, //排除某些字段
        of = {"age"}, //只使用某些字段
        callSuper = true //是否让父类字段也参与 默认false
)
//@EqualsAndHashCode也有类似的下面的属性,
@ToString(
        includeFieldNames = true, //是否使用字段名
        exclude = {"name"}, //排除某些字段
        of = {"age"}, //只使用某些字段
        callSuper = true //是否让父类字段也参与 默认false
)

(3)@NoArgsConstructor/@RequiredArgsConstructor/@AllArgsConstructor

        这 3 个注释都用于生成构造函数,该构造函数将接受某些字段的 1 个参数,并简单地将此参数分配给该字段。其中:

  • @NoArgsConstructor:用于生成无参构造函数

  • @RequiredArgsConstructor:生成所有未初始化的 final 字段以及标记为 @NonNull 且未在声明处初始化的字段的特殊字段构造函数。 对于那些标有@NonNull 的字段,还会生成显式空检查。

  • @AllArgsConstructor:用于生成所有属性字段的有参构造函数,标有@NonNull 的字段会导致对这些参数进行空检查。

(4)@Data

        @Data 是一个复合注解,它将@ToString、@EqualsAndHashCode、@Getter / @Setter 和@RequiredArgsConstructor 的特性捆绑在一起。@Data 生成所有字段的 getter,所有非 final 字段的 setter,以及涉及类字段的适当的 toString、equals 和 hashCode 实现,以及初始化所有 final 字段以及所有非 final 字段的构造函数 没有标有@NonNull 的初始化器,以确保该字段永远不会为空。

        注意:@RequiredArgsConstructor注解当类中没有 final 和 @NonNull 注解的成员变量时会生成一个无参构造方法(因为没有符合要求的参数),而很多人认为 @Data 会生成无参构造方法就是此导致的。

(5)@Slf4j

        日志类注解用在类上,可以省去从日志工厂生成日志对象这一步,直接进行日志记录。具体注解根据日志工具的不同而不同,此处以Slf4j为例。日志门面(Simple Logging Facade For Java) , Slf4j主要是为了给Java日志访问提供一套标准、规范的API框架,其主要意义在于提供接口,具体的实现可以交由其他日志框架,例如log4j和logback等。

  • 编译前:

@Slf4j
public class LombokAnnotationTest {
    private Integer id;
    private String name;
}
  • 编译后:

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

public class LombokAnnotationTest {
    private static final Logger log = LoggerFactory.getLogger(LombokAnnotationTest.class);
    private Integer id;
    private String name;

    public LombokAnnotationTest() {
    }
}

二.MapStruct

1.MapStruct概述

(1)项目架构分层背景

        常见的项目架构开发过程中,都会对软件项目进行分层设计,层次设计表格与架构设计图大致如下:

层次

模块名

方法名称

缩写

表现层

web

controller

VO(view object):视图对象,用于展示层,它的作用是把某个指定页面(或组件)的所有数据封装起来。

领域层

service

service

DTO(Data Transfer Object):数据传输对象,展示层与服务层之间的数据传输对象。

应用层

biz

domain/application

DO(Domain Object)领域对象,就是从现实世界中抽象出来的有形或无形的业务实体。

持久层

dal

dao

PO(Persistent Object)持久化对象,它跟持久层(通常是关系型数据库)的数据结构形成一一对应的映射关系,如果持久层是关系型数据库,那么,数据表中的每个字段(或若干个)就对应PO的一个(或若干个)属性。

 请求流程如下:

  1. 用户发出请求(可能是填写表单),表单的数据在展示层被匹配为VO。

  2. 展示层把VO转换为服务层对应方法所要求的DTO,传送给服务层。

  3. 服务层首先根据DTO的数据构造(或重建)一个DO,调用DO的业务方法完成具体业务。

  4. 服务层把DO转换为持久层对应的PO,调用持久层的持久化方法,把PO传递给它,完成持久化操作。

  5. 对于一个逆向操作,如读取数据,也是用类似的方式转换和传递。

(2)编码问题

        分层过程代带来一个编码问题:应用与应用之间,还有单独的应用细分模块之后,DO 一般不会让外部依赖,这时候需要在提供对外接口的模块里放 DTO 用于对象传输,也即是 DO 对象对内,DTO对象对外,DTO 可以根据业务需要变更,并不需要映射 DO 的全部属性。这种 对象与对象之间的互相转换,就需要有一个专门用来解决转换问题的工具,毕竟每一个字段都 get/set 会很麻烦。因此,对象转换框架的出现,解决了上面的编码痛点。

        没有出现对象装换工具之前,通常我们用get/set等方式逐一进行对象字段之间的映射操作,非常繁杂且硬编码;后来出现了很多开源的对象转换工具包,比如常见的BeanUtils.copyProperties,但是限制较多且不够灵活,因此便有了MapStruct。

        MapStruct 是一个基于 Java 注释的映射处理器,用于生成类型安全的 bean 映射类。您只需定义一个 mapper接口,该接口声明任何必需的映射方法。在编译期间,MapStruct 将自动生成此接口的实现。此实现使用普通的 Java 方法调用在源对象和目标对象之间进行映射,在使用过程中需要只需要配置完成后运行 mvn compile就会发现 target文件夹中生成了一个mapper接口的实现类。

  • MapStruct官方文档

2.MapStruct使用指南

2.1 配置引入

...
<!-- 版本定义 -->
<properties>
    <org.mapstruct.version>1.5.5.Final</org.mapstruct.version>
</properties>
...
<!-- mapstruct核心依赖 -->
<dependencies>
    <dependency>
        <groupId>org.mapstruct</groupId>
        <artifactId>mapstruct</artifactId>
        <version>${org.mapstruct.version}</version>
    </dependency>
</dependencies>
...
<!-- mapstruct代码构建和生成插件 -->
<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>3.8.1</version>
            <configuration>
                <source>1.8</source> <!-- depending on your project -->
                <target>1.8</target> <!-- depending on your project -->
                <annotationProcessorPaths>
                    <path>
                        <groupId>org.mapstruct</groupId>
                        <artifactId>mapstruct-processor</artifactId>
                        <version>${org.mapstruct.version}</version>
                    </path>
                    <!-- other annotation processors -->
                </annotationProcessorPaths>
            </configuration>
        </plugin>
    </plugins>
</build>

2.2 映射器定义

        使用 mapstruct 首先要创建映射转换器,将核心注解 @Mapper 标记在接口或抽象类上就定义了一个映射器,作为访问入口。当我们构建/编译应用程序时,MapStruct注解处理器插件会识别出对应接口并为其生成一个实现类,具体的转换/映射方法会在接口中声明。

AnnotationDescriptionOptional Elements
@MapperMarks an interface or abstract class as a mapper and activates the generation of a implementation of that type via MapStruct.
  • injectionStrategy:转换字段的生成注入策略,包括构造函数注入、字段注入,默认为 InjectionStrategy.FIELD
  • componentModel:生成映射器的组件模型(定义使用方式),包括default(getMapper方式)、spring(spring注入方式)等,默认为 default

(1)工厂单例注入

//接口定义
@Mapper
public interface CarMapper {
    CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);
    //convert method
    //...
}
//单例使用
CarDto carDto = CarMapper.INSTANCE.carToCarDto(car);

(2)Spring依赖注入

//接口定义
@Mapper(componentModel = "spring")
public interface CarMapper {
    //convert method
    //...
}
//注入
@Autowired
private CarMapper carMapper;
//使用
CarDto carDto = carMapper.carToCarDto(car);

2.3 映射

2.3.1 基本映射

        最简单使用方式就是在映射器中直接声明转换方法(不用配置任何注解),转换方法需要源对象作为参数并需要目标对象作为返回值,该方法的方法名可以自由选择,映射器只需要关注入参类型和返回类型就可以确定生成转换代码,公共的可读属性都会被复制到目标中的相应属性中(包括超类上声明的属性),所有名称与类型一样的字段都会被隐式复制。基本映射包括以下几种情况,并在代码示例中说明:

  • 一致映射:转换之间属性字段名、字段类型、和字段数量完全一致并一一对应,此时会完全等价转换
  • 字段不一致:转换之间属性字段名称不完全一致,不一致的属性名不会自动映射,采用默认值或null
  • 数量不一致:转换之间属性字段数量不完全一致,包括多转少、少转多,缺失或多余的属性采用默认值或null
  • 类型不一致:转换之间属性字段类型不完全一致,有些同名字段之间会做隐式转换,包括基本类型与包装类型、基本类型与String、String与枚举类型等

        项目构建编译以后就会生成对应的实现类,相关文件位于项目中的 target/generated-sources/annotations/...

@Mapper
public interface ConvertMapper{
    ConvertMapper INSTANCE = Mappers.getMapper(ConvertMapper.class);

    /**
     * 单对象转换: Source to Target
     */
    public Target singleConvert(Source source);

    /**
     * 集合List转换:List<Source> to List<Target>
     */
    public List<Target> listConvert(List<Source> sourceList);

    /**
     * 集合Set转换:Set<Integer> to Set<String>
     */
    public Set<String> setConvert(Set<Integer>  integerSet);
}

        其中集合类型的映射(ListSet等等)以对象映射相同的方式映射元素类型,通过循环调用定义的对应单对象转换方法完成集合映射,若没有在接口事先声明对应的单对象映射方法则会隐式生成代码。注意不允许使用可迭代源和不可迭代目标声明映射方法,反之亦然。其生成的实现方法源码如下:

@Generated(
    value = "org.mapstruct.ap.MappingProcessor",
    date = "2023-06-14T14:05:44+0800",
    comments = "version: 1.5.5.Final, compiler: javac, environment: Java 11.0.19 (Oracle Corporation)"
)
public class ConvertMapperImpl implements ConvertMapper {

    /**
     * 单对象转换: Source to Target
     */
    @Override
    public Target singleConvert(Source source){
        if ( source == null ) {
            return null;
        }

        Target target = new Target();

        target.setId( source.getId() );
        target.setName( source.getName() );
        target.setXXX...

        return target;
    }


    /**
     * 集合List转换:List<Source> to List<Target>
     */
    @Override
    public List<Target> listConvert(List<Source> sourceList){
        if ( sourceList == null ) {
            return null;
        }
        //- 若元素映射方法事先声明:mapstruct则会自动查找对应的源和目标方法,并进行隐式调用
        //- 若元素映射方法未声明:mapstruct则自动会隐式生成singleConvert转换方法
        List<Target> list = new ArrayList<Target>( sourceList.size() );
        for ( Source source : sourceList ) {
            list.add( singleConvert( source ) );
        }

        return list;
    }

    /**
     * 集合Set转换:Set<Integer> to Set<String>
     */
    @Override
    public Set<String> integerSetToStringSet(Set<Integer> integerSet) {
        if ( integerSet == null ) {
            return null;
        }

        Set<String> set = new LinkedHashSet<String>();
        //自动调用元素类型的隐式转换
        for ( Integer integer : integerSet ) {
            set.add( String.valueOf( integer ) );
        }

        return set;
    }

}

2.3.2 不一致映射

        在实际开发中,通常不同模型之间字段名不会完全相同,名称可能会有轻微的变化。对于不一致的映射我们这里主要考虑名称不一致、多源映射的情况,对于数据不一致我们单独开一节去讲。不一致映射主要通过 @Mapping 和 @Mappings 注解转换:

AnnotationDescription
@Mapping Configures the mapping of one bean attribute or enum constant.
  • target:要映射的目标对象的字段名,同一目标属性不得多次映射。

  • source:数据源对象的字段名。注意此属性不能与constant()或expression()一起使用。

  • expression:计算表达式必须以 Java 表达式的形式给出,格式如下: java( <expression> )。表达式中引用的任何类型都必须通过它们的完全限定名称给出。或者,可以通过Mapper.imports()导入类型。注意此属性不能与source() 、 defaultValue() 、 defaultExpression() 、 qualifiedBy() 、 qualifiedByName()或constant()一起使用。

  • ignore:如果设置为true,则指定的字段不会做转换,默认false。

  • dateFormat:如果属性从String映射到Date或反之亦然,则可以由SimpleDateFormat处理的格式字符串。对于所有其他属性类型和映射枚举常量时,将被忽略。例:dateFormat = "yyyy-MM-dd HH:mm:ss"

  • numberFormat:如果带注释的方法从Number映射到String ,则可以由DecimalFormat处理的格式字符串,反之亦然。对于所有其他元素类型将被忽略。例:numberFormat = "$#.00"

  • defaultValue:默认值。如果 source 属性为null ,则会使用此处的默认值。如果target字段不是String类型,会尝试找到可以匹配的转换方法,否则会报错。注意此属性不能与constant() 、 expression()或defaultExpression()一起使用。

  • constant:不管原属性值,直接将目标属性设置为指定的常量。如果target字段不是String类型,会尝试找到可以匹配的转换方法,否则会报错。注意此属性不能与source() 、 defaultValue() 、 defaultExpression()或expression()一起使用。

  • qualifiedBy:选择映射器对target字段赋值。这在多个映射方法(手写或生成)符合条件并因此导致“发现模糊映射方法”错误的情况下很有用。

AnnotationDescriptionOptional Elements
@Mappings可以包装配置一组多个 @Mapping 转换,当使用 Java 8 或更高版本时,可以省略 @Mappings 包装器注释并直接在一个方法上指定多个 @Mapping 注释。
  • Mapping [ ]:配置 @Mapping 集合

(1)字段名称不一致

        此处字段名称不一致主要指的是类型一致、但字段名称不一致的情况,类型不一致我们放到数据转换章节分析。

// We need map Source.sourceName to Target.targetName
@Mapper
public interface ConvertMapper{
    ConvertMapper INSTANCE = Mappers.getMapper(ConvertMapper.class);

    @Mapping(source="sourceName", target="targetName")
    @Mapping(source="sourceId", target="targetId")
    public Target singleConvert(Source source);
}

// generates:
@Override
public Target singleConvert(Source source) {
    target.setTargetName( source.getSourceName() );
    target.setTargetId( source.getSourceId() );
    // ...
}

(2)多源映射

        多源映射是指映射方法具有多个源参数,将多个源参数实体组合成一个返回目标对象。因为有时单个类不足以构建目标,我们可能希望将多个类中的值聚合为一个返回目标。多源映射与单参数映射方法一样,公共属性按名称对应自动映射。但如果多个源对象定义具有相同名称的公共属性,则必须使用@Mapping注解指定从中检索属性的源参数是哪个。

@Mapper
public interface ConvertMapper{
    ConvertMapper INSTANCE = Mappers.getMapper(ConvertMapper.class);

    @Mapping(source="source_a.Id", target="Id")
    @Mapping(source="source_b.sourceName", target="Name")
    public Target singleConvert(SourceA source_a,SourceB source_b);
}

2.3.3 更新现有实例

        在某些情况下,我们并不需要映射一个新的对象出来(以上方式的实现都是new object and return),而是需要对已有对象实例的某些映射属性值进行更新。可以通过为目标对象参数添加 @MappingTarget 注解来实现:

AnnotationDescriptionOptional Elements
@MappingTargetDeclares a parameter of a mapping method to be the target of the mapping.

最多只能将一个方法参数声明为 MappingTarget。注意:作为映射目标传递的参数不能为空(null)。

//1. Update exist bean without return value - 无返回值
@Mapper
public interface ConvertMapper {
    void updateHuman(HumanDto humanDto, @MappingTarget Human human);
}
// generates
@Override
public void updateHuman(HumanDto humanDto, Human human) {
    human.setName( humanDto.getName() );
    // ...
}
 
//2. Update exist bean and return it - 更新并返回
@Mapper
public interface ConvertMapper {
    Human updateHuman(HumanDto humanDto, @MappingTarget Human human);
}
// generates:
@Override
public Human updateHuman(HumanDto humanDto, Human human) {
    // ...
    human.setName( humanDto.getName() );
    return human;
}

注意:更新现有实例也可以同步添加@Mapping注解来映射不一致字段,二者并不冲突 ,非映射项则不会更新。

2.3.4 自定义映射

(1)使用表达式

        有时我们的目标属性并不只是进行简单的映射,MapStruct允许在@Mapping注解中定义Java表达式来进行简单的逻辑映射。该注解参数包括 defaultExpression source 取值为 null时生效),或者 expression(固定执行,不能与 source 、defaultExpression 一起使用),整个源对象都可以在表达式中使用,但表达式中引用的任何类型都必须通过其完全限定名称给出, 或者通过 Mapper.imports() 导入类型。

//1. 全限定类名引用
@Mapper
public interface SourceTargetMapper {

    SourceTargetMapper INSTANCE = Mappers.getMapper( SourceTargetMapper.class );

    @Mapping(target="id", source="sourceId", defaultExpression = "java( java.util.UUID.randomUUID().toString() )")
    @Mapping(target = "timeAndFormat",expression = "java( new org.sample.TimeAndFormat( s.getTime(), s.getFormat() ) )")
    @Mapping(target = "targetTime", expression = "java(date2Long(s.getSourceTime()))")
    Target sourceToTarget(Source s);

    default Long date2Long(Date date) {
        return date != null ? date.getTime() : null;
    }
}

//2. imports 导入引用
imports org.sample.TimeAndFormat;
imports java.util.UUID;

@Mapper( imports = {TimeAndFormat.class, UUID.class})
public interface SourceTargetMapper {

    SourceTargetMapper INSTANCE = Mappers.getMapper( SourceTargetMapper.class );

    @Mapping(target="id", source="sourceId", defaultExpression = "java( UUID.randomUUID().toString() )")
    @Mapping(target = "timeAndFormat",
         expression = "java( new TimeAndFormat( s.getTime(), s.getFormat() ) )")
    Target sourceToTarget(Source s);

    //...
}

注意:MapStruct在任何方法中数据类型不匹配需要转换时(比如date2Long),会根据入参和返回值类型,自动匹配定义的方法(即使不指明),即隐式调用。但是如果你的Mapper接口比较复杂了,里面定义了出参和返回值相同的两个方法,则必须使用@Mapping指定使用哪个方法(或者使用@Named标记方法防止隐式调用),否则在编译时MapStruct会因为不知道用哪个方法而报错。当然你可以不用想这么多,先编译再说,如果报错了再去处理即可,这也是MapStruct的一个好处:在编译期就可以发现对象转换的错误,而不是到运行时。

(2)自定义方法

        大部分情况下我们通过mapstruct 注解自动生成的映射代码就可以进行各种属性转换,但有时我们也需要实现一些复杂、自定义的映射逻辑,这种情况下mapstruct允许我们在映射器中添加自定义的映射方法,并且像其他自动映射方法一样访问:

  • 接口 interface:通过接口方法的默认实现 default
  • 抽象类 abstract:通过类方法的实现,可以在类中声明其他字段
//1. 接口方式
@Mapper
public interface ConvertMapper {

    @Mapping(...)
    ...
    CarDto carToCarDto(Car car);

    default PersonDto personToPersonDto(Person person) {
        //hand-written mapping logic
    }
}

//2. 抽象类方式
@Mapper
public abstract class CarMapper {

    @Mapping(...)
    ...
    public abstract CarDto carToCarDto(Car car);

    public PersonDto personToPersonDto(Person person) {
        //hand-written mapping logic
    }
}

注意:如果参数和返回类型匹配,mapstruct自动生成的代码将会在嵌套映射或集合映射需要时调用自定义方法实现做元素转换。 

2.4 数据转换

2.4.1 隐式类型转换

        对于映射数据类型不一致的情况,MapStruct支持sourcetarget属性之间的大部分常见数据类型的自动转换,或称为隐式转换。自动类型转换适用于:

  • 基本类型及其对应的包装类之间。比如, intIntegerfloatFloatlongLongbooleanBoolean 等。
  • 任意基本类型与任意包装类之间。如 intlongbyteInteger 等。
  • 所有基本数据类型及包装类与String之间。如 booleanStringIntegerStringfloatString 等。
  • 枚举类和String之间。
  • Java大数类型(java.math.BigIntegerjava.math.BigDecimal) 和Java基本类型(包括其包装类)与String之间。
  • Java日期类型 DateTimeLocalDateTimeLocalDateLocalTime和 String 之间等,可以通过SimpleDateFormat的dateFormat选项指定格式字符串。
  • Java String 和 StringBuilder、UUID String 之间等。
  • 其它情况详见MapStruct官方文档。

        因此,在生成映射器代码的过程中,如果源字段和目标字段之间属于上述任何一种情况,则MapStrcut会自行处理类型转换即隐式转换。

@Mapper
public interface CarMapper {
    //int to String
    @Mapping(source = "price", numberFormat = "$#.00")
    CarDto carToCarDto(Car car);
    //Date to String
    @Mapping(source = "manufacturingDate", dateFormat = "dd.MM.yyyy")
    CarDto carToCarDto(Car car);
}

2.4.2 嵌套对象转换

(1)引用映射与隐式调用

        通常情况下,对象的属性并不止包括基本数据类型,还有对其它对象的引用或更深的嵌套。比如 Car 类可以包含一个名为 driver 的 Person 对象(代表汽车驾驶员)的引用属性,该对象应映射到 CarDto 类引用的 PersonDto 对象。

@Mapper
public interface CarMapper {

    CarDto carToCarDto(Car car);

    PersonDto personToPersonDto(Person person);
}

        生成的carToCarDto()方法代码将自动调用personToPersonDto()用于映射driver属性的方法,而生成的personToPersonDto()实现用于执行person对象的映射。 理论上这样就可以映射任意深度对象图,这种自动映射也被称为隐式调用,常出现在以下场景:

  • 如果source和target属性具有相同的类型,则该值将简单地从源复制到目标(浅拷贝)。如果属性是集合(例如 List),则集合的副本将被设置到目标属性中。

  • 如果源属性和目标属性类型不同,则会自动检查是否存在一种已经声明的映射方法(该方法将源属性的类型作为参数类型,并将目标属性的类型作为返回类型),如果存在这样的方法,则会在生成的映射实现中自动调用它来转换。

  • 如果不存在此类方法,MapStruct将查判断否存在属性的源类型和目标类型的内置转换(隐式类型转换)。如果是这种情况,生成的映射代码将应用此转换。

  • 如果不存在这样的方法,MapStruct 将尝试应用复杂的转换

    • target = method1( method2( source ) )

    • target = method( conversion( source ) )

    • target = conversion( method( source ) )

  • 如果没有找到这样的方法,MapStruct将尝试生成一个自动子映射方法,该方法将执行源和目标属性之间的简单映射。

  • 如果MapStruct无法创建基于名称的映射方法,则会在构建编译时引发错误,指示不可映射的属性及其路径。

        注意:可以使用 @Mapper( disableSubMappingMethodsGeneration = true ) 阻止MapStruct 生成自动子映射方法,也可以通过元注释完全控制映射比如 @DeepClone

(2)嵌套映射

        在一些复杂情况下,嵌套对象属性的引用可能包含多个层级并且很多情况下名称也存在不匹配的差异,这时就需要人为的在自动映射的基础上进行映射控制。现有source和target如下:

//1. spurce FishTank
public class Fish {

    private String type;
}

public class Interior {

    private String designer;
    private Ornament ornament;
}

public class FishTank {

    private Fish fish;
    private String name;
    private MaterialType material;
    private Interior interior;
    private WaterQuality quality;
}

//2. target FishTankDto 
public class FishDto {

    private String kind;

    // make sure that mapping on name does not happen based on name mapping
    private String name;
}

public class MaterialDto {

    private String manufacturer;
    private MaterialTypeDto materialType;// same to MaterialType
}

public class FishTankDto {

    private FishDto fish;
    private String name;
    private MaterialDto material;
    private OrnamentDto ornament; //same to Interior.ornament
    private WaterQualityDto quality;
}

        在简单的场景中,我们只需要对嵌套级别上的属性需要更正(如fish);可以使用相同的构造来忽略嵌套级别的某些属性(如ignore);当源和目标不共享相同的嵌套级别(相同深度的属性)时,可以进行层级选择(如material);当映射首先共享一个共同映射基础时,可以在基础上进行映射补全(如quality和report),其映射器如下:

@Mapper
public interface FishTankMapper {

    @Mapping(target = "fish.kind", source = "fish.type")
    @Mapping(target = "fish.name", ignore = true)
    @Mapping(target = "ornament", source = "interior.ornament")
    @Mapping(target = "material.materialType", source = "material")
    @Mapping(target = "quality.report.organisation.name", source = "quality.report.organisationName")
    FishTankDto map( FishTank source );
}

        甚至包括另一种情况的两种写法:

//第一种写法:逐一指定各个资源的映射关系
//优点:方便精细控制需要转换的字段
@Mapping(source = "userInfo.idCard", target = "idCard")
@Mapping(source = "userInfo.avatar", target = "avatar")
UserVO convert(UserDO person);

//第二种写法:利用隐式转换对所有同名字段做转换
//优点:书写简单
@Mapping(source = "userInfo", target = ".")
UserVO convert(UserDO person);

2.5 高级映射

2.5.1 自定义切面

        为了进一步控制和定制化,MapStruct 还提供了两个自定义切面注解 @BeforeMapping, @AfterMapping 用来实现在对应类型映射方法前后进行方法增强和统一的逻辑控制。注解注释的方法必须有对应的实现体,在接口interface中以default方法实现,在抽象类abstract中以非抽象方法实现。

If the @BeforeMapping / @AfterMapping method has parameters, the method invocation is only generated if the return type of the method (if non-void) is assignable to the return type of the mapping method and all parameters can be assigned by the source or target parameters of the mapping method

  • 两个注解没有任何参数
  • 两个注解可以标记多个方法为切面,同一类型调用顺序按照定义顺序
@Mapper
public abstract class HumanConvertor {
    //前置通知类型1:无参类型
    @BeforeMapping
    public void calledWithoutArgsBefore() {
         // ...
    }
    //前置通知类型2:参数含有对应Source类型
    @BeforeMapping
    protected void calledWithHuman(Human human) {
        // ...
    }
    //前置通知类型3:参数含有对应Source类型和对应Target类型@MappingTarget(性别判断需求)
    @BeforeMapping
    protected void calledWithSourceAndTargetBefore(Human human, @MappingTarget HumanDto humanDto) {
        if (human instanceof Man) {
            humanDto.setGenderType(GenderType.MAN);
        } else if (human instanceof Woman) {
            humanDto.setGenderType(GenderType.WOMAN);
        }
    }


    //后置通知类型1:无参类型
    @AfterMapping
    public void calledWithoutArgsAfter() {
         // ...
    }
    //后置通知类型2:参数含有对应Target类型(@MappingTarget)
    @AfterMapping
    protected void calledWithDto(@MappingTarget HumanDto humanDto) {
        humanDto.setName(String.format("【%s】", humanDto.getName()));
    }
    //后置通知类型3:参数含有对应Source类型和对应Target类型@MappingTarget
    @AfterMapping
    public void calledWithSourceAndTargetAfter(Human human, @MappingTarget HumanDto humanDto) {
         // ...
    }

    public abstract HumanDto toHumanDto(Human human);
}

        生成的代码调用顺序如下:

 // generates:
public class HumanConvertorImpl extends HumanConvertor {

    @Override
    public HumanDto toHumanDto(Human human) {
        //前置通知1
        calledWithoutArgsBefore();
        //前置通知2
        calledWithHuman(human);

        if (human == null) {
            return null;
        }

        HumanDto humanDto = new HumanDto();

        //前置通知3
        calledWithSourceAndTargetBefore( human, humanDto );

        ...

        //后置通知1
        calledWithoutArgsAfter();
        //后置通知2
        calledWithDto( humanDto );
        //后置通知3
        calledWithSourceAndTargetAfter( human, humanDto );

        return humanDto;
    }
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/30241.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

日志是什么?耗时2个月搞懂Linux日志

这里写目录标题 日志基本介绍日志管理服务日志轮替 日志基本介绍 日志是用来记录重大事件的工具。 日志文件是重要的系统信息文件&#xff0c;其中记录了很多重要的系统事件。包括用户的登录信息&#xff0c;系统的启动信息&#xff0c;系统的安全信息&#xff0c;邮件相关信息…

ChatGPT:数字时代革新与展望

ChatGPT&#xff1a;数字时代革新与展望 AGI 未来的愿景&#xff1a;建安全有益的 AGI OpenAI团队对AGI的展望&#xff1a; 我们希望 AGI 能够赋予人类在宇宙中最大程度地繁荣发展的能力。我们不期望未来是一个不合格的乌托邦&#xff0c;但我们希望将好的最大化&#xff0c;将…

【云计算 | Azure】微软 Azure 基础解析(九)Azure 标识、身份管理、Azure AD 的功能与用途

本系列博文还在更新中&#xff0c;收录在专栏&#xff1a;「Azure探秘&#xff1a;构建云计算世界」 专栏中。 本系列文章列表如下&#xff1a; 【Azure】微软 Azure 基础解析&#xff08;三&#xff09;云计算运营中的 CapEx 与 OpEx&#xff0c;如何区分 CapEx 与 OpEx 【A…

国产MCU-CW32F030开发学习--按键检测

国产MCU-CW32F030开发学习–按键检测 bsp_key 按键驱动程序用于扫描独立按键&#xff0c;具有软件滤波机制&#xff0c;采用 FIFO 机制保存键值。可以检测 如下事件&#xff1a; 按键按下。 按键弹起。 长按键。 长按时自动连发。 我们将按键驱动分为两个部分来介绍&#xff…

Spark大数据处理学习笔记1.5 掌握Scala内建控制结构

文章目录 一、学习目标二、条件表达式&#xff08;一&#xff09;语法格式&#xff08;二&#xff09;执行情况&#xff08;三&#xff09;案例演示任务1、根据输入值的不同进行判断任务2、编写Scala程序&#xff0c;判断奇偶性 三、块表达式&#xff08;一&#xff09;语法格式…

电弧打火机方案,点烟器单片机开发方案

市面上的打火机除了明火之外&#xff0c;还有电热丝、电弧两种类型的点烟器。电热丝在使用过程中会变细并且烧断。宇凡微推出的电弧打火机方案&#xff0c;该点烟器芯片为宇凡微单片机YF系列&#xff0c;电弧点烟器IC性价比高。 一、电弧打火机方案的原理 电弧打火机使用的是电…

【git切换分支/tag】git stash保存暂不提交的更改

目录 问题git stash使用方法git stash pop 还原修改 git stash使用、修改指定tag的代码 其他git指令 问题 情景&#xff1a;分支1上开发新功能&#xff0c;临时切换到其他分支或tag上修改bug。 1、直接切换&#xff1a;如果没有冲突&#xff0c;分支1的修改会带到要切换的分支…

拉普拉斯方程解决有介质导体球壳问题

一个内径和外径分别为和的导体球壳&#xff0c;带电荷&#xff0c;同心地包围着一个的导体球&#xff0c;使这个导体球接地&#xff0c;求空间各点的电势和这个导体球的感应电荷 我们不难发现&#xff0c;球对称性非常强&#xff0c;电势只和半径有关系 所以我们可以假设电势为…

【GitLab】-HTTP Basic: Access denied.remote:You must use a personal access token

写在前面 本文简要说明GitLab配置accessToken以及双因子认证&#xff08;Two-factor authentication&#xff09;。 目录 写在前面一、场景描述二、具体步骤1.环境说明2.配置accessToken3.克隆项目4.双因子认证 三、参考资料写在后面 一、场景描述 在使用账号和密码的方式拉取公…

Alex-Net 与 VGG-16

Alex-Net 由加拿大多伦多大学的 Alex Krizhevsky、Ilya Sutskever(G. E. Hinton 的两位博士生)和 Geoffrey E. Hinton 提出&#xff0c;网络名“Alex-Net”即 取自第一作者名。 下图所示是 Alex-Net 的网络结构&#xff0c;共含五层卷积层和三层全连接层。其中&#xff0c;Ale…

【Vue3 生态】Vue Router 路由知识概览

前言 在 Web 前端开发中&#xff0c;路由是非常重要的一环&#xff0c;但是路由到底是什么呢&#xff1f; 从路由的用途上讲 路由是指随着浏览器地址栏的变化&#xff0c;展示给用户不同的页面。 从路由的实现原理上讲 路由是URL到函数的映射。它将 URL 和应用程序的不同部分…

01 React入门、虚拟DOM

总结 一、React 入门 1.1 特点 高性能、声明式、组件化、单向响应的数据流、JSX扩展、灵活 1.2 React初体验 <head><meta charset"UTF-8"><meta http-equiv"X-UA-Compatible" content"IEedge"><meta name"viewpo…

软件 安全,处理威胁STRIDE模型

微软威胁分析工具&#xff1a; https://www.microsoft.com/en-us/securityengineering/sdl/threatmodeling?azure-portaltrue STRIDE 是微软定义的6中威胁 身份假冒&#xff08;Spoofing&#xff09; 身份假冒&#xff0c;即伪装成某对象或某人。例如&#xff0c;我们通过伪…

1-简单回归问题

一.梯度下降&#xff08;gradient descent&#xff09; 1.预测函数 这里有一组样本点&#xff0c;横纵坐标分别代表一组有因果关系的变量 我们的任务是设计一个算法&#xff0c;让机器能够拟合这些数据&#xff0c;帮助我们算出参数w 我们可以先随机选一条过原点的直线&#xf…

无人机电池能不能上高铁以及什么型号的电池可以上高铁

无人机电池能不能上高铁以及什么型号的电池可以上高铁 高铁关于电池的限制电池参数介绍可上高铁的无人机锂电池6S电池3S电池 高铁关于电池的限制 2022年7月1日起施行的《铁路旅客禁止、限制携带和托运物品目录》规定,充电宝、锂电池单块额定能量不超过100Wh 电池参数介绍 明…

HarmonyOS学习路之开发篇—Java UI框架(DependentLayout)

DependentLayout DependentLayout是Java UI框架里的一种常见布局。与DirectionalLayout相比&#xff0c;拥有更多的排布方式&#xff0c;每个组件可以指定相对于其他同级元素的位置&#xff0c;或者指定相对于父组件的位置。 支持的XML属性DependentLayout的共有XML属性继承自…

SpringBoot不在使用@Validated 做参数校验但是不想在Controller层怎么办?

目录 场景再现&#xff1a; 怎么做&#xff1f; 遇到了什么问题&#xff1f; 怎么实现&#xff1f; 场景再现&#xff1a; 某API接口接受加密的json字符串&#xff0c;接受字符串之后先进行解密处理&#xff0c;解密完成之后还要进行参数校验处理&#xff0c;如果参数不合规…

使用 TensorFlow.js 将机器学习引入您的 Web 应用程序

如何使用 TensorFlow.js 在您的 Web 应用程序中实施机器学习 原文作者: Abhay Singh Rathore 机器学习 (ML) 不再是一个崇高、遥不可及的概念。借助 TensorFlow.js 等库&#xff0c;开发人员现在可以将 ML 整合到他们的 Web 应用程序中。例如&#xff0c;您可以创建一个系统&am…

Springcloud之Feign、Hystrix、Ribbon如何设置超时时间

一&#xff0c;概述 我们在微服务调用服务的时候&#xff0c;会使用hystrix、feign和ribbon&#xff0c;比如有一个实例发生了故障而该情况还没有被服务治理机制及时的发现和摘除&#xff0c;这时候客户端访问该节点的时候自然会失败。 所以&#xff0c;为了构建更为健壮的应…

美味度配方

8 种配料每种配料可以放 1 到 5 克&#xff0c;美味度为配料质量之和&#xff0c;给定一个美味度 n&#xff0c;求解 8 种配料的所有搭配方案及方案数量 。 (本笔记适合学了 Python 循环&#xff0c;正在熟炼的 coder 翻阅) 【学习的细节是欢悦的历程】 Python 官网&#xff1a…