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

news2024/11/27 4:32:19

一.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;
    }
}

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【c语言】五道经典练习题④

目录 ①、年月日经过n天后的日期 ②、坐标排序 ③、统计文件中出现某个单词的次数 ④、输出含for的行 ⑤、比较两个文本是否相等 ①、年月日经过n天后的日期 题述&#xff1a;定义包含年月日表示的日期的结构体&#xff0c;写程序实现计算某年某月某日过n天后的日期是哪年…

肠道细菌阻碍阿卡波糖的降血糖作用

我们知道&#xff0c;口服抗糖尿病药是治疗糖尿病的有效方式之一。然而&#xff0c;患者对抗糖尿病药的反应程度各不相同&#xff0c;例如&#xff0c;有些患者在长期使用阿卡波糖后会产生耐药性。 阿卡波糖通常在饭前口服。它抑制人α-葡萄糖苷酶达到降血糖作用&#xff0c;包…

GWO-VMD-近似熵-极限学习机的轴承故障诊断软件,以西储大学轴承数据为例,采用MATLABAPP开发

采用灰狼算法优化VMD两个参数&#xff0c;以包络熵为最小适应度值&#xff0c;在最佳参数下提取采用近似熵指标提取西储大学轴承数据的特征向量&#xff0c;最后选用极限学习机ELM进行故障诊断。将以上程序集成在MATLABAPP进行开发。 首先是这个软件的各个界面展示。 软件启动…

云安全技术(四)之云计算安全的设计原则

计算安全的设计原则 Understand Design Principles of Secure Cloud Computing 1.1 云安全数据生命周期 Cloud secure data lifecycle 数据始终是安全保护的首要问题。必须深刻了解数据生命周期&#xff0c;以便正确制定和遵守安全策略&#xff0c;把握正确的步骤顺序&#xf…

万博智云与品高股份完成产品兼容性互认证,持续助力国产化生态建设

近日&#xff0c;万博智云的HyperBDR云容灾软件与广州市品高股份有限公司&#xff08;简称&#xff1a;品高股份&#xff09;旗下产品品高基础架构云资源管理软件V9.0完成了产品兼容性认证。 经万博智云和品高云双方人员的共同测试&#xff0c;得出结论&#xff1a; HyperBDR…

【C/C++数据结构与算法】C语言链表

目录 一、单链表 二、双向循环链表 三、判断链表是否带环 四、链表的回文结构判断 五、复制带随机指针的链表 一、单链表 优点&#xff1a;头部增删效率高&#xff0c;动态存储无空间浪费 缺点&#xff1a;尾部增删、遍历效率低&#xff0c;不支持随机访问节点 头结点&…

【夜深人静学习数据结构与算法 | 第六篇】贪心算法

目录 前言&#xff1a; 引入: 贪心算法&#xff1a; 455. 分发饼干 - 力扣&#xff08;LeetCode&#xff09; 376. 摆动序列 - 力扣&#xff08;LeetCode&#xff09; 53. 最大子数组和 - 力扣&#xff08;LeetCode&#xff09; 122. 买卖股票的最佳时机 II - 力扣&a…

【Python 随练】统计字符类型个数

题目&#xff1a; 输入一行字符&#xff0c;分别统计出其中英文字母、空格、数字和其它字符的个数。 简介&#xff1a; 在本篇博客中&#xff0c;我们将解决一个字符统计问题&#xff1a;输入一行字符&#xff0c;统计其中英文字母、空格、数字和其他字符的个数。我们将提供…

学习python爬虫需要掌握哪些库?

Python爬虫是指使用Python编写的程序&#xff0c;用来自动化地获取互联网上的数据。通过爬取网站的HTML内容&#xff0c;并解析和提取所需的数据&#xff0c;可以实现自动化地收集、分析和处理大量的在线数据。 学习Python爬虫需要掌握以下几个核心库&#xff1a; Requests&am…

【ARM AMBA AXI 入门 9 - AXI 总线 AxPROT 与安全之间的关系 】

文章目录 介绍ARM Trustzone的安全扩展简介 1.1 AXI AxPROT 介绍1.1.1 AXI 对 Trustzone的支持 介绍 ARMv8 架构中的AXI&#xff08;Advanced eXtensible Interface&#xff09;总线与NS&#xff08;Non-Secure&#xff09;位密切相关。NS位是指在ARM TrustZone安全扩展中定义…

LeetCode 1254. Number of Closed Islands【DFS,BFS,并查集】中等

本文属于「征服LeetCode」系列文章之一&#xff0c;这一系列正式开始于2021/08/12。由于LeetCode上部分题目有锁&#xff0c;本系列将至少持续到刷完所有无锁题之日为止&#xff1b;由于LeetCode还在不断地创建新题&#xff0c;本系列的终止日期可能是永远。在这一系列刷题文章…

单片机MCU如何实现让部分代码运行在RAM中

随着单片机硬件的发展&#xff0c;其中的RAM和flash越做越大。MCU在实际的使用中&#xff0c;通常程序都是运行在flash上的&#xff0c;RAM的高速空间并没有得到充分的利用&#xff0c;如果我们的程序需要运行的更快&#xff0c;系统有更好的实时性&#xff0c;我们可以考虑将这…

CSS查缺补漏之《常用长度单位(px、em、rem、%、vw/vh、vmin/vmax)》

此文内容较少&#xff0c;轻轻松松掌握&#xff0c;莫要有压力~ 正如现实生活中长度具有mm、dm、cm、m等&#xff0c;在css中&#xff0c;也具备多种长度单位&#xff0c;本文对常用的几种单位进行详细举例介绍~ px&#xff1a;像素单位 初学css时&#xff0c;px单位经常被使用…

【Leetcode60天带刷】day08字符串——344.反转字符串, 541. 反转字符串II,剑指Offer 05.替换空格,151.翻转字符串里的单词

题目&#xff1a; 344. 反转字符串 编写一个函数&#xff0c;其作用是将输入的字符串反转过来。输入字符串以字符数组 s 的形式给出。 不要给另外的数组分配额外的空间&#xff0c;你必须原地修改输入数组、使用 O(1) 的额外空间解决这一问题。 示例 1&#xff1a; 输入&…

基于SpringBoot+Vue的“漫画之家”系统设计与实现

博主介绍&#xff1a; 大家好&#xff0c;我是一名在Java圈混迹十余年的程序员&#xff0c;精通Java编程语言&#xff0c;同时也熟练掌握微信小程序、Python和Android等技术&#xff0c;能够为大家提供全方位的技术支持和交流。 我擅长在JavaWeb、SSH、SSM、SpringBoot等框架下…

新电脑机环境安装笔记

「Navicat_15.0.25_64bit_Setup.exe」 下载https://www.aliyundrive.com/s/b9xUw2JpuJb Navicat Keygen Patch v5.6.0 下载 https://www.aliyundrive.com/s/YYyE5BQMMuN 全程断网操作 patch 将安装目录选中 提示 check 64 mysql安装&#xff1a; https://baijiahao.baidu…

因子分析——SPSS实例分析

【续上篇主成分分析】 因子分析常用于通过可观测变量推断出其背后的公共因子&#xff08;也称为隐变量&#xff09;&#xff0c;样本在公共因子上的取值变化影响其在可观测变量上的取值&#xff0c;因为一般公共因子的个数小于可观测变量的数目&#xff0c;所以因子分析也可以…

渠道归因(一)传统渠道归因

渠道归因&#xff08;一&#xff09;传统渠道归因 小P&#xff1a;小H&#xff0c;我又来了。。。最近在做ROI数据&#xff0c;但是有个问题。。。 小H&#xff1a;什么问题&#xff0c;不就是收入/成本吗&#xff1f; 小P&#xff1a;是的&#xff0c;每个渠道的成本很容易计算…

基于html+css的图展示134

准备项目 项目开发工具 Visual Studio Code 1.44.2 版本: 1.44.2 提交: ff915844119ce9485abfe8aa9076ec76b5300ddd 日期: 2020-04-16T16:36:23.138Z Electron: 7.1.11 Chrome: 78.0.3904.130 Node.js: 12.8.1 V8: 7.8.279.23-electron.0 OS: Windows_NT x64 10.0.19044 项目…

如何打造创意百变的虚拟直播场景?

场景对于直播来说是直接呈现给观众的&#xff0c;也是直播带货的“直接”的视觉冲击的价值核心&#xff0c;所以场景的设计十分重要。今天&#xff0c;我们就一起来看看如何低成本搭建一个网红同款直播间吧&#xff01; 直播间类型 直播间大体可以分为三种类型&#xff1a;虚拟…