乖乖,咱不用BeanUtil.copy了,咱试试这款神级工具(超详细)

引言

在现代Java应用程序开发中,处理对象之间的映射是一个常见而且必不可少的任务。随着项目规模的增长,手动编写繁琐的映射代码不仅耗时且容易出错,因此开发者们一直在寻找更高效的解决方案。比如基于Dozer封装的或者Spring自带的BeanUtil.copyProperties对应对象之间的属性拷贝。但是Dozer采用运行时映射的方式,通过反射在运行时动态生成映射代码。这意味着在每次映射时都需要进行反射操作,Dozer在处理复杂映射时可能需要额外的配置和自定义转换器,可能导致一定的性能开销,尤其在大型项目中可能表现不佳。另外在处理处理复杂映射(例如字段名称不一致,某些字段不需要映射)时可能需要额外的配置和自定义转换器,使用起来并不是那么的便捷。那么此时MapStruct变应用而生,成为简化Java Bean映射的利器。

MapStruct是一款基于注解和编译时代码生成的工具,旨在简化Java Bean之间的映射过程。通过在编译时生成高效的映射代码 ,避免了运行时的性能开销,使得映射过程更加高效。MapStruct不仅消除了手写映射代码的痛苦,还提供了性能优势。它支持在Java Bean之间进行映射,并通过使用注解标记映射方法和类,提供了一种声明性的方式定义映射规则,简化了映射代码的编写。使得开发者能够专注于业务逻辑而不必过多关注对象之间的转换。并且它还支持自定义转换器和表达式,适用于处理各种复杂的映射场景。

下面我们就开始介绍如何使用MapStruct来高效的完成对象之间的映射。

如何MapStruct使用

使用MapStruct进行Java Bean映射通常包括几个基本步骤,包括项目配置、注解标记、自定义转换器等。以下是详细的使用步骤:

1、依赖

xml 复制代码
<dependency>
    <groupId>org.mapstruct</groupId>
    <artifactId>mapstruct</artifactId>
    <version>1.5.5.Final</version>
</dependency>

同时在 pom.xml 需要正确配置MapStruct的依赖和注解处理器插件。例如:

xml 复制代码
<build>  
    <plugins>        
        <plugin>            
            <groupId>org.apache.maven.plugins</groupId>  
            <artifactId>maven-compiler-plugin</artifactId>  
            <configuration>                 
                <annotationProcessorPaths>                   
                    <path>                        
                        <groupId>org.mapstruct</groupId>  
                        <artifactId>mapstruct-processor</artifactId>  
                        <version>1.5.5.Final</version>  
                    </path>                    
                    <path>                        
                        <groupId>org.projectlombok</groupId>  
                        <artifactId>lombok</artifactId>  
                        <version>1.18.22</version>  
                    </path>                    
                    <path>                        
                        <groupId>org.projectlombok</groupId>  
                        <artifactId>lombok-mapstruct-binding</artifactId>  
                        <version>0.2.0</version>  
                    </path>                
                </annotationProcessorPaths>            
            </configuration>        
        </plugin>    
    </plugins>
</build>

当然如果你同时使用了lombok,也需要同时配置lombok编译生成代码的插件。

2、创建映射接口

创建一个Java接口,并使用@Mapper注解标记它。例如:

java 复制代码
@Mapper
public interface MyMapper {
    MyMapper INSTANCE = Mappers.getMapper(MyMapper.class);

    TargetObject sourceToTarget(SourceObject source);
    // 定义其他映射方法
}

上述代码定义了一个映射接口MyMapper,其中有一个映射方法sourceToTarget用于将SourceObject映射为TargetObjectINSTANCE字段用于获取映射器的实例。

此时我们编译项目之后,可以看见生成的MyMapper实现类中的代码:

ini 复制代码
@Override  
public TargetObject sourceToTarget(SourceObject source) {  
    if ( source == null ) {  
        return null;  
    }  

    TargetObject targetObject = new TargetObject();  

    targetObject.setUserName( source.getUserName() );  
    targetObject.setUserId( source.getUserId() );  
    targetObject.setSex( source.getSex() );  

    return targetObject;  
}

这样就省去了我们自己手写两个对象之间的字段映射,避免了大量的重复工作,大大增加了开发效率,其次也是最重要的一点就是我们可以很直观的看见两个对象之间的字段映射关系,不像Dozer那样每次基于反射区实现映射,我们无法看见两边的字段的映射,出现问题后不方便排查,功能上不可控。

很重要的一点提示:我们要养成在写完一个映射方法后,要养成一定一定提前编译看一下生成的实现类方法是否正确,同时也看看是否存在字段映射关系设置错误导致编译不通过。

3、映射接口使用

在业务代码或者其他代码方法中,我们可以直接使用MyConverter.INSTANCE.sourceToTarget(source)进行sourcetarget之间的转换。

bash 复制代码
TargetObject handleObject(SourceObject source){  
    return MyConverter.INSTANCE.sourceToTarget(source);  
}

怎么样?是不是很简单。接下来让我们继续介绍MapStruct的详细功能,揭开它神秘的面纱。。。。。

MapStruct常用注解

了解MapStruct的注解及其属性是非常重要的,因为它们定义了映射规则和行为。以下是MapStruct中常用的注解及其属性:

1.@Mapper

用于标记一个接口或抽象类,用于定义对象之间的映射规则。它有多个属性可以配置映射器的功能。以下是 @Mapper 注解的一些常用属性:

1.1 componentModel

指定生成的映射器实例的组件模型,以便与应用框架集成。他有"default"(默认值)、"cdi"、"spring"等可选值(具体参考MappingConstants.ComponentModel)。我们着重介绍一下default以及spring:

  • default:MapStruct的默认组件模型
    在默认模式下,MapStruct 会生成一个无参数的构造函数的映射器实例。映射器实例的创建和管理由 MapStruct自动处理。实例通常通过 Mappers.getMapper(Class)获取。适用于简单的映射场景,无需额外的依赖注入或容器管理。
  • spring:使用Spring Framework的组件模型
    在 Spring 模式下,MapStruct 会生成一个使用 @Component 注解标记的映射器实例,从而允许通过 Spring 的 IoC 容器进行管理和依赖注入。适用于 Spring 框架中的应用,可以利用 Spring 的依赖注入功能。稍后我们会介绍这种模型的使用,也是我们日常使用SpringBoot开发时用的比较多的模型。比如上例中,我们使用spring的模型,则生成的代码:
java 复制代码
@Component  
public class MySpringMapperImpl implements MySpringMapper {  

    @Override  
    public TargetObject sourceToTarget(SourceObject source) {  
        if ( source == null ) {  
            return null;  
        }  

        TargetObject targetObject = new TargetObject();  

        targetObject.setUserName( source.getUserName() );  
        targetObject.setUserId( source.getUserId() );  
        targetObject.setSex( source.getSex() );  

        return targetObject;  
    }  
}

可以看见实现类中自动加上了@Component,注入到Spring的容器中管理。

  • cdi:使用 Contexts and Dependency Injection (CDI) 的组件模型。
    在 CDI 模式下,MapStruct 会生成一个使用 @Dependent 注解标记的映射器实例,允许通过 CDI 容器进行管理和依赖注入。适用于Java EEJakarta EE中使用 CDI 的应用,可以利用 CDI 容器进行管理。

其余的大家感兴趣的可以去阅读源码,平时使用不多,这里就不过多介绍了。

1.2 uses

指定映射器使用的自定义转换器。自定义转换器是在映射过程中调用的方法,用于处理特定类型之间的自定义映射逻辑。如果我们两个对象之间有一个字段的属性值需要特殊处理之后在进行映射,即需要加上一些转换逻辑,我们就可以自定义一个转换器,然后在映射器中使用转换器中的方法。例如:SoureObject中的有一个枚举值,但是转换到TargetObject中时需要转换为具体的说明,那么此时我们就可以使用自定义转换器。

我们自定义一个转换器,并且定义一个转换方法:

kotlin 复制代码
public class MyConverter {  

    @Named("convertSexDesc")  
    public String convertSexDesc(Integer sex){  
        return SexEnum.descOfCode(sex);  
    }  
}

然后再映射器MyMapper中使用uses指定转换器,同时使用@Mapping注解指定两个字段的映射规则:

ini 复制代码
@Mapper(uses = {MyConverter.class})  
public interface MyMapper {  

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

    @Mapping(target = "sex", source = "sex", qualifiedByName = "convertSexDesc")  
    TargetObject sourceToTarget(SourceObject source);  
}

编译后可以看见实现类中生成的代码:

java 复制代码
public class MyMapperImpl implements MyMapper {  

    private final MyConverter myConverter = new MyConverter();  

    @Override  
    public TargetObject sourceToTarget(SourceObject source) {  
        if ( source == null ) {  
            return null;  
        }  

        TargetObject targetObject = new TargetObject();  

        targetObject.setSex( myConverter.convertSexDesc( source.getSex() ) );  
        targetObject.setUserName( source.getUserName() );  
        targetObject.setUserId( source.getUserId() );  

        return targetObject;  
    }  
}

当然假如你的转换器或者转换方法,是你这个映射器独有,其他映射器不会使用这个转换方法,那么你可以直接在MyMapper中定义一个default的转换方法,就不必使用uses引入转换器:

kotlin 复制代码
@Mapper  
public interface MyMapper {  

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

    @Mapping(target = "sex", source = "sex", qualifiedByName = "convertSexDesc")  
    TargetObject sourceToTarget(SourceObject source);  


    @Named("convertSexDesc")  
    default String convertSexDesc(Integer sex){  
        return SexEnum.descOfCode(sex);  
    }  
}

编译后生成的实现类中,直接可以调用到这个方法:

java 复制代码
public class MyMapperImpl implements MyMapper {  

    @Override  
    public TargetObject sourceToTarget(SourceObject source) {  
        if ( source == null ) {  
            return null;  
        }  

        TargetObject targetObject = new TargetObject();  

        targetObject.setSex( convertSexDesc( source.getSex() ) );  
        targetObject.setUserName( source.getUserName() );  
        targetObject.setUserId( source.getUserId() );  

        return targetObject;  
    }  
}

在Java中,接口可以包含默认方法(Default Methods)。默认方法是在接口中提供一个默认的实现,这样在接口的实现类中就不需要强制性地实现该方法了。默认方法使用关键字 default 进行声明。

1.3 imports

导入其他类的全限定名,使其在生成的映射器接口中可见。比如我们可以导入其他的工具类去处理我们的字段,例如:StringUtils, CollectionUtilsMapUtils,或者一些枚举类等。同常运用@Mapping中的expression上。

java 复制代码
@Mapper(imports = {StringUtils.class, SexEnum.class})  
public interface MyMapper {  

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

    @Mapping(target = "sex",  expression = "java(SexEnum.descOfCode(source.getSex()))")  
    TargetObject sourceToTarget(SourceObject source);
}    

编译后生成的实现类中直接importimports中定义的类:

java 复制代码
import com.springboot.code.mapstruct.SexEnum;
import org.springframework.util.StringUtils;

public class MyMapperImpl implements MyMapper {  

    @Override  
    public TargetObject sourceToTarget(SourceObject source) {  
        if ( source == null ) {  
            return null;  
        }  

        TargetObject targetObject = new TargetObject();  

        targetObject.setUserName( source.getUserName() );  
        targetObject.setUserId( source.getUserId() );  

        targetObject.setSex( SexEnum.descOfCode(source.getSex()) );  

        return targetObject;  
    }  
}

当然我们也可以不使用imports去导入其他的类,那我们在使用这些类的方法时,必须写上他们的全路径:

java 复制代码
@Mapper  
public interface MyMapper {  

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

    @Mapping(target = "sex",  expression = "java(com.springboot.code.mapstruct.SexEnum.descOfCode(source.getSex()))")  
    TargetObject sourceToTarget(SourceObject source);
}    

编译后生成的实现类中,就不会import类了:

java 复制代码
public class MyMapperImpl implements MyMapper {  

    @Override  
    public TargetObject sourceToTarget(SourceObject source) {  
        if ( source == null ) {  
            return null;  
        }  

        TargetObject targetObject = new TargetObject();  

        targetObject.setUserName( source.getUserName() );  
        targetObject.setUserId( source.getUserId() );  

        targetObject.setSex( com.springboot.code.mapstruct.SexEnum.descOfCode(source.getSex()) );  

        return targetObject;  
    }  
}
1.4 config

config 属性允许你指定一个映射器配置类,该配置类用于提供全局的配置选项。通过配置类,你可以定义一些全局行为,例如处理 null 值的策略、映射器名称、映射器组件模型等。

我们使用@MapperConfig定义一个映射器配置类 MyMapperConfig

ini 复制代码
@MapperConfig(  
        nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS,  
        componentModel = "default",  
        uses = MyConverter.class,  
        unmappedTargetPolicy = org.mapstruct.ReportingPolicy.WARN  
)  
public interface MyMapperConfig {  
}

然后再MyMapper中指定config:

ini 复制代码
@Mapper(config = MyMapperConfig.class)  
public interface MyMapper {  

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

    @Mapping(target = "sex", source = "sex", qualifiedByName = "convertSexDesc")  
    TargetObject sourceToTarget(SourceObject source);
 }   

我们可以集中管理映射器的一些全局行为,而不需要在每个映射器中重复配置。

在实际应用中,你可以根据项目需求定义不同的映射器配置类,用于管理不同的全局配置选项。这有助于提高代码的组织性和可维护性。

1.5 nullValueCheckStrategy

用于指定映射器对源对象字段的null值进行检查的策略。检查策略枚举类NullValueCheckStrategy值如下:

  • ALWAYS:始终对源值进行NULL检查。
    生成的实现类中,都是源值进行判NULL
java 复制代码
@Override  
public TargetObject sourceToTarget(SourceObject source) {  
    if ( source == null ) {  
        return null;  
    }  

    TargetObject targetObject = new TargetObject();  

    if ( source.getSex() != null ) {  
        targetObject.setSex( myConverter.convertSexDesc( source.getSex() ) );  
    }  
    if ( source.getUserName() != null ) {  
        targetObject.setUserName( source.getUserName() );  
    }  
    if ( source.getUserId() != null ) {  
        targetObject.setUserId( source.getUserId() );  
    }  

    return targetObject;  
}
  • ON_IMPLICIT_CONVERSION:不检查NULL值,直接将源值赋值给目标值

除了上述的属性值之外,还有一些其他的属性值,例如:

  • unmappedSourcePolicy: 未映射源对象字段的处理策略。
  • unmappedTargetPolicy: 未映射目标对象字段的处理策略。
    可选值:ReportingPolicy.IGNORE(忽略未映射字段,默认)、ReportingPolicy.WARN(警告)、ReportingPolicy.ERROR(抛出错误)。

以及其他的一些属性值,如果需要用到的同学,可以看一下源码中的介绍,这里就不过多叙述了。

2.@MapperConfig

注解用于定义映射器配置类,它允许在一个单独的配置类中集中管理映射器的全局配置选项。可以将一些全局的配置选项集中在一个配置类中,使得映射器的配置更为清晰和可维护。在实际应用中,可以根据需要定义不同的映射器配置类,以便在不同的场景中使用。配置类可以在映射器中通过@Mapperconfig属性引入。它大部分的属性值跟@Mapper一致。

ini 复制代码
@MapperConfig(  
        nullValueCheckStrategy = NullValueCheckStrategy.ON_IMPLICIT_CONVERSION,  
        componentModel = "default",  
        uses = MyConverter.class,  
        unmappedTargetPolicy = org.mapstruct.ReportingPolicy.WARN  
)  
public interface MyMapperConfig {  
}

然后再MyMapper中指定config:

ini 复制代码
@Mapper(config = MyMapperConfig.class)  
public interface MyMapper {  

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

    @Mapping(target = "sex", source = "sex", qualifiedByName = "convertSexDesc")  
    TargetObject sourceToTarget(SourceObject source);
 } 

3.@Mapping

用于自定义映射器方法中的映射规则。它允许你指定源对象和目标对象之间字段的映射关系。

3.1 source`和 target
  • source 含义: 源对象字段的名称或表达式。
  • target 含义: 目标对象字段的名称。
ini 复制代码
@Mapping(target = "sourceField", source = "sourceField")  
TargetObject sourceToTarget(SourceObject source);

或者使用表达式的方式:

ini 复制代码
@Mapping(expression = "java(source.getSourceField())", target = "targetField")
TargetObject sourceToTarget(SourceObject source);
3.2 qualifiedByName 和 qualifiedBy
  • qualifiedByName: 指定使用自定义转换器方法进行映射。

定义一个转换器MyNameConverter:

typescript 复制代码
public class MyNameConverter {  

    @Named("convertUserName")  
    public String convertUserName(String userName){  
        return Optional.ofNullable(userName).map(String::toUpperCase).orElse(userName);  
    }  
}

使用自定义转换器的方法:

ini 复制代码
@Mapper( uses = {MyNameConverter.class}, nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS)  
public interface MyMapper {  

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

    @Mapping(target = "userName", source = "userName", qualifiedByName = "convertUserName")  
    TargetObject sourceToTarget(SourceObject source);
  • qualifiedBy: 指定使用基于@qualifier注解的转换方法

先定义一个基于@qualifier(mapstruct包下)的作用于转换器类上的注解@StrConverter:

less 复制代码
@Qualifier  
@Target(ElementType.TYPE)  
@Retention(RetentionPolicy.CLASS)  
public @interface StrConverter {  
}

再定义一个基于@qualifier(mapstruct包下)的作用于转换器方法上的注解@NameUpper:

less 复制代码
@Qualifier  
@Target(ElementType.METHOD)  
@Retention(RetentionPolicy.CLASS)  
public @interface NameUpper {  
}

最后定义一个自定义转换器MyNameConverter:

typescript 复制代码
@StrConverter  
public class MyNameConverter {  


    @NameUpper  
    public String convertUserName(String userName){  
        return Optional.ofNullable(userName).map(String::toUpperCase).orElse(userName);  
    }  
}

然后我们在@Mappinbg中通过使用:

kotlin 复制代码
@Mapper(uses = {MyNameConverter.class} ,nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS)  
public interface MyMapper {
    @Mapping(target = "userName", source = "userName", qualifiedBy = NameUpper.class) 
    TargetObject sourceToTarget(SourceObject source);
}

最终两种方式编译后的结果是一致的:

java 复制代码
public class MyMapperImpl implements MyMapper {  

    private final MyNameConverter myNameConverter = new MyNameConverter();  

    @Override  
    public TargetObject sourceToTarget(SourceObject source) {  
        if ( source == null ) {  
            return null;  
        }  

        TargetObject targetObject = new TargetObject();  

        if ( source.getUserName() != null ) {  
            targetObject.setUserName( myNameConverter.convertUserName( source.getUserName() ) );  
        }  
        if ( source.getUserId() != null ) {  
            targetObject.setUserId( source.getUserId() );  
        }  

        return targetObject;  
    }  
}

以上基于qualifiedBy的使用示例参考自@Qualifier源码文档。

3.3 ignore

是否忽略某字段的映射。为true时忽略。

ini 复制代码
@Mapping(target = "sex", source = "sex", ignore = true)
TargetObject sourceToTarget(SourceObject source);

编译后实现类中不会对这个字段进行赋值:

java 复制代码
@Override  
public TargetObject sourceToTarget(SourceObject source) {  
    if ( source == null ) {  
        return null;  
    }  

    TargetObject targetObject = new TargetObject();  

    if ( source.getUserName() != null ) {  
        targetObject.setUserName( source.getUserName() );  
    }  
    if ( source.getUserId() != null ) {  
        targetObject.setUserId( source.getUserId() );  
    }  

    return targetObject;  
}
3.4 defaultExpression

指定默认表达式,当源对象字段为 null 时使用。

ini 复制代码
@Mapping(target = "sex", source = "sex", defaultExpression = "java(SexEnum.MAN.desc)")
TargetObject sourceToTarget(SourceObject source);

编译后实现类:

javascript 复制代码
 if ( source.getSex() != null ) {  
        targetObject.setSex( String.valueOf( source.getSex() ) );  
  }  
  else {  
    targetObject.setSex( SexEnum.MAN.desc );  
  } 

defaultExpression不能与expression,defaultValue,constant一起使用。

3.5 defaultValue

指定默认值,当源对象字段为 null 时使用。

ini 复制代码
@Mapping(target = "sex", source = "sex", defaultValue = "男人")  
TargetObject sourceToTarget(SourceObject source);

编译后:

javascript 复制代码
if ( source.getSex() != null ) {  
    targetObject.setSex( String.valueOf( source.getSex() ) );  
}  
else {  
    targetObject.setSex( "男人" );  
}

defaultValue不能与expression,defaultExpression,constant一起使用。

3.6 constant

将目标对象的字段设置为该常量。不从源对象中映射值。

ini 复制代码
@Mapping(target = "source", constant = "API")  
TargetObject sourceToTarget(SourceObject source);

编译后:

arduino 复制代码
targetObject.setSource( "API" );

constant不能与defaultExpression,expression,defaultValue,constant, source一起使用。

3.7 expression

通过表达式完成映射。要基于该字符串设置指定的目标属性。目前,Java 是唯一受支持的"表达式语言",表达式必须使用以下格式以 Java 表达式的形式给出:java(<EXPRESSION>)

java 复制代码
@Mapping(target = "sex", expression = "java(SexEnum.descOfCode(source.getSex()))")  
TargetObject sourceToTarget(SourceObject source);

编译后:

less 复制代码
targetObject.setSex( SexEnum.descOfCode(source.getSex()) );

expression不能与source, defaultValue, defaultExpression, qualifiedBy, qualifiedByName 以及constant 一起使用

3.8 dateFormat

指定日期格式化模式,仅适用于日期类型的字段。可以实现String类型时间和Date相互转换,基于SimpleDateFormat实现。

kotlin 复制代码
@Data  
public class TargetObject {
    private String createTime;  

    private Date loginDate;
}

@Data  
public class SourceObject {
    private Date createTime;  

    private String loginDate;
}


@Mapping(target = "createTime", source = "createTime", dateFormat = "yyyy-MM-dd HH:mm:ss")  
@Mapping(target = "loginDate", source = "loginDate", dateFormat = "yyyy-MM-dd")  
TargetObject sourceToTarget(SourceObject source);

编译后:

vbscript 复制代码
if ( source.getCreateTime() != null ) {  
    targetObject.setCreateTime( new SimpleDateFormat( "yyyy-MM-dd HH:mm:ss" ).format( source.getCreateTime() ) );  
}  
try {  
    if ( source.getLoginDate() != null ) {  
        targetObject.setLoginDate( new SimpleDateFormat( "yyyy-MM-dd" ).parse( source.getLoginDate() ) );  
    }  
}  
catch ( ParseException e ) {  
    throw new RuntimeException( e );  
}
3.9 numberFormat

指定数值格式化格式,仅适用Number类型的字段。可以实现String类型数值与Number相互转换,基于DecimalFormat实现。

kotlin 复制代码
@Data  
public class TargetObject {
    private double amountDouble;  

    private String amountStr;
}

@Data  
public class SourceObject {
    private String amountStr;  

    private double amountDouble;
}

@Mapping(target = "amountDouble", source = "amountStr", numberFormat = "#,###.00")  
@Mapping(target = "amountStr", source = "amountDouble", numberFormat = "#,###.00")  
TargetObject sourceToTarget(SourceObject source);

编译后:

less 复制代码
try {  
    if ( source.getAmountStr() != null ) {  
        targetObject.setAmountDouble( new DecimalFormat( "#,###.00" ).parse( source.getAmountStr() ).doubleValue() );  
    }  
}  
catch ( ParseException e ) {  
    throw new RuntimeException( e );  
}  
targetObject.setAmountStr( new DecimalFormat( "#,###.00" ).format( source.getAmountDouble() ) );

还有其他的属性,这里就不过多叙述了,有兴趣或者需要的可以阅读源码。

4.@Mappings

包含多个@Mapping注解,将多个字段映射规则组合在一起,使代码更清晰。

less 复制代码
@Mappings({  
        @Mapping(target = "source", constant = "API"),  
        @Mapping(target = "sex", expression = "java(SexEnum.descOfCode(source.getSex()))"),  
        @Mapping(target = "createTime", source = "createTime", dateFormat = "yyyy-MM-dd HH:mm:ss"),  
        @Mapping(target = "loginDate", source = "loginDate", dateFormat = "yyyy-MM-dd"),  
        @Mapping(target = "amountDouble", source = "amountStr", numberFormat = "#,###.00"),  
        @Mapping(target = "amountStr", source = "amountDouble", numberFormat = "#,###.00")  
})  
TargetObject sourceToTarget(SourceObject source);

5.@Named

用于标记自定义转换器或者映射器中的某个方法的名称。一般配合qualifiedByName 使用:

arduino 复制代码
/**
* 标记映射方法名称
*/
@Named("sourceToTarget")  
TargetObject sourceToTarget(SourceObject source);  

/**
* 标记转换器方法名称
*/
@Named("convertSexDesc")  
default String convertSexDesc(Integer sex){  
    return SexEnum.descOfCode(sex);  
}

我们在定义自己的转换器方法时,最好把方法都加上@Named的注解标记你的方法名称,否则如果后续代码中再写一个同类型的不同方法名的转换方法时编译报错:不明确的映射方法。

image.png

image.png

6. @IterableMapping

go 复制代码
用于集合映射,定义集合元素的映射规则。其中一些属性例如:`qualifiedByName`,`qualifiedBy`以及`dateFormat`,`numberFormat`参考`@Mapping`中的用法。
less 复制代码
@Named("sourceToTarget")  
TargetObject sourceToTarget(SourceObject source);  

@IterableMapping(qualifiedByName = "sourceToTarget")  
List<TargetObject> sourceToTargetList(List<SourceObject> sourceObjectList);

编译后的实现类代码:

typescript 复制代码
@Override  
public List<TargetObject> sourceToTargetList(List<SourceObject> sourceObjectList) {  
    if ( sourceObjectList == null ) {  
        return null;  
    }  

    List<TargetObject> list = new ArrayList<TargetObject>( sourceObjectList.size() );  
    for ( SourceObject sourceObject : sourceObjectList ) {  
        list.add( sourceToTarget( sourceObject ) );  
    }  

    return list;  
}

可看出它内部循环调用sourceToTarget的方法完成list的转换。

需要特别注意,在写集合类型的转换时一定要配合IterableMappingqualifiedByNameNamed使用,如果不使用@IterableMapping中显示声明循环使用的方法时,它的内部会重新生成一个映射方法去使用。这样会在开发过程中出现一些莫名其妙的忽然就不好使的错误。。。。。

less 复制代码
    @Named("sourceToTarget")  
    TargetObject sourceToTarget(SourceObject source);  

    @Named("sourceToTarget2")  
    TargetObject sourceToTarget2(SourceObject source);  

//    @IterableMapping(qualifiedByName = "sourceToTarget")  
    List<TargetObject> sourceToTargetList(List<SourceObject> sourceObjectList);

编译后,实现类中代码可以看出并没有使用以上两个方法,而是重新生成的:

image.png

image.png

image.png

image.png

7.@MappingTarget

标记在映射方法的目标对象参数上,允许在映射方法中修改目标对象的属性。当目标对象已经创建了,此时可以将目标对象也当做参数传递到映射器方法中。

less 复制代码
@Mapping(target = "source", constant = "API")  
@Mapping(target = "sex", expression = "java(SexEnum.descOfCode(source.getSex()))")  
@Named("sourceToTarget3")
void sourceToTarget3(@MappingTarget TargetObject targetObject, SourceObject source);

编译后实现类代码:

typescript 复制代码
@Override  
public void sourceToTarget3(TargetObject targetObject, SourceObject source) {  
    if ( source == null ) {  
        return;  
    }  

    targetObject.setUserName( source.getUserName() );  
    targetObject.setUserId( source.getUserId() );  

    targetObject.setSource( "API" );  
    targetObject.setSex( SexEnum.descOfCode(source.getSex()) );  
}

8.@InheritConfiguration

它用于在映射接口中引用另一个映射方法的配置。主要用于减少代码重复,提高映射方法的可维护性。

less 复制代码
    @Mappings({  
            @Mapping(target = "source", constant = "API"),  
            @Mapping(target = "sex", expression = "java(SexEnum.descOfCode(source.getSex()))")
    })  
    @Named("sourceToTarget")  
    TargetObject sourceToTarget(SourceObject source);

    @InheritConfiguration(name = "sourceToTarget")  
    @Named("sourceToTarget2")  
    TargetObject sourceToTarget2(SourceObject source);

    @InheritConfiguration(name = "sourceToTarget")  
    void sourceToTarget4(@MappingTarget TargetObject targetObject, SourceObject source);

sourceToTarget2sourceToTarget4就可以直接继承使用sourceToTarget的规则了。避免了再次定义一份相同的规则。

9. @BeanMapping

用于配置映射方法级别的注解,它允许在单个映射方法上指定一些特定的配置。例如忽略某些属性、配置映射条件等(开始我们在@Mapper中定义)。它提供了一种在方法级别自定义映射行为的方式。

less 复制代码
@BeanMapping(nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS)  
@Named("sourceToTarget2")  
TargetObject sourceToTarget2(SourceObject source);

编译后实现的代码:

typescript 复制代码
@Override  
public TargetObject sourceToTarget2(SourceObject source) {  
    if ( source == null ) {  
        return null;  
    }  

    TargetObject targetObject = new TargetObject();  

    if ( source.getUserName() != null ) {  
        targetObject.setUserName( source.getUserName() );  
    }  
    if ( source.getUserId() != null ) {  
        targetObject.setUserId( source.getUserId() );  
    }  
    if ( source.getSex() != null ) {  
        targetObject.setSex( String.valueOf( source.getSex() ) );  
    }  

    return targetObject;  
}

校验了源对象值的null

10.@ValueMapping

用于自定义枚举类型或其他可映射类型的值映射。该注解允许在枚举类型映射时,定义自定义的值映射规则,使得在映射中可以转换不同的枚举值。他只有两个属性值:

  • source:只能取值:枚举值名称,MappingConstants.NULLMappingConstants.ANY_REMAININGMappingConstants.ANY_UNMAPPED
  • target: 只能取值:枚举值名称MappingConstants.NULLMappingConstants.ANY_UNMAPPED
less 复制代码
  public enum OrderType { RETAIL, B2B, C2C, EXTRA, STANDARD, NORMAL }

  public enum ExternalOrderType { RETAIL, B2B, SPECIAL, DEFAULT }

  @ValueMappings({  
        @ValueMapping(target = "SPECIAL", source = "EXTRA"),  
        @ValueMapping(target = "DEFAULT", source = "STANDARD"),  
        @ValueMapping(target = "DEFAULT", source = "NORMAL"),  
        @ValueMapping(target = MappingConstants.THROW_EXCEPTION, source = "C2C" )  
})  
ExternalOrderTypeEnum mapOrderType(OrderTypeEnum orderType);    

编译后实现类代码:

ini 复制代码
@Override  
public ExternalOrderTypeEnum mapOrderType(OrderTypeEnum orderType) {  
    if ( orderType == null ) {  
        return null;  
    }  

    ExternalOrderTypeEnum externalOrderTypeEnum;  

    switch ( orderType ) {  
        case EXTRA: externalOrderTypeEnum = ExternalOrderTypeEnum.SPECIAL;  
        break;  
        case STANDARD: externalOrderTypeEnum = ExternalOrderTypeEnum.DEFAULT;  
        break;  
        case NORMAL: externalOrderTypeEnum = ExternalOrderTypeEnum.DEFAULT;  
        break;  
        case C2C: throw new IllegalArgumentException( "Unexpected enum constant: " + orderType );  
        case RETAIL: externalOrderTypeEnum = ExternalOrderTypeEnum.RETAIL;  
        break;  
        case B2B: externalOrderTypeEnum = ExternalOrderTypeEnum.B2B;  
        break;  
        default: throw new IllegalArgumentException( "Unexpected enum constant: " + orderType );  
    }  

    return externalOrderTypeEnum;  
}

11.@Context

@Context注解在MapStruct框架中用于标记映射方法的参数,使得这些参数作为映射上下文来处理。被标注为@Context的参数会在适用的情况下传递给其他映射方法、@ObjectFactory方法或者@BeforeMapping@AfterMapping方法,从而可以在自定义代码中使用它们。

具体作用如下:

  • 传递上下文信息 : 当MapStruct执行映射操作时,它会将带有@Context注解的参数值向下传递到关联的方法中。这意味着你可以在不同的映射阶段(包括属性映射、对象工厂方法调用以及映射前后的处理方法)共享和利用这些上下文数据。
  • 调用相关方法 : MapStruct还会检查带有@Context注解的参数类型上是否声明了@BeforeMapping@AfterMapping方法,并在适用时对提供的上下文参数值调用这些方法。
  • 空值处理 : 注意,MapStruct不会在调用与@Context注解参数相关的映射前后方法或对象工厂方法之前进行空值检查。调用者需要确保在这种情况下不传递null值。
  • 生成代码的要求 : 为了使生成的代码能够正确调用带有@Context参数的方法,正在生成的映射方法声明必须至少包含那些相同类型(或可赋值类型)的@Context参数。MapStruct不会为缺失的@Context参数创建新实例,也不会以null代替它们传递。

因此,@Context注解提供了一种机制,允许开发者在映射过程中携带并传播额外的状态或配置信息,增强了映射逻辑的灵活性和定制能力。

一个简单的用法示例:

less 复制代码
    @Named("sourceToTarget5")  
    @Mapping(target = "createTime", constant = "createTime", qualifiedByName = "formatDate")  
    TargetObject sourceToTarget5(SourceObject source, @Context ContextObject contextObject);  

    @Named("formatDate")  
    default String formatDate(LocalDateTime createTime, @Context ContextObject contextObject){  
    DateTimeFormatter dateTimeFormatter = contextObject.getDateTimeFormatter();  
    return dateTimeFormatter.format(createTime);  
}

生成的实现类代码:

typescript 复制代码
@Override  
public TargetObject sourceToTarget5(SourceObject source, ContextObject contextObject) {  
    if ( source == null ) {  
        return null;  
    }  

    TargetObject targetObject = new TargetObject();  

    targetObject.setUserName( source.getUserName() );  
    targetObject.setUserId( source.getUserId() );  
    if ( source.getSex() != null ) {  
        targetObject.setSex( String.valueOf( source.getSex() ) );  
    }  

    targetObject.setCreateTime( formatDate( LocalDateTime.parse( "createTime" ), contextObject ) );  

    return targetObject;  
}

12.@BeforeMapping

这个注解可以标注在一个没有返回值的方法上,该方法会在执行实际映射操作前被调用。在此方法中可以通过@Context注入上下文对象,并根据需要对源对象或上下文进行修改或预处理。

less 复制代码
    @Named("sourceToTarget5")  
    @Mapping(target = "createTime", constant = "createTime", qualifiedByName = "formatDate")  
    TargetObject sourceToTarget5(SourceObject source, @Context ContextObject contextObject);  

    @Named("formatDate")  
    default String formatDate(LocalDateTime createTime, @Context ContextObject contextObject){  
        DateTimeFormatter dateTimeFormatter = contextObject.getDateTimeFormatter();  
        return dateTimeFormatter.format(createTime);  
    }  

    @BeforeMapping  
    default void beforeFormatDate(@Context ContextObject context) {  
        // 在映射之前初始化或更新上下文中的信息  
        context.setDateTimeFormatter(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));  
}

编译后生成的实现类代码中,会发现在sourceToTarget5的方法第一行会调用beforeFormatDate这个方法:

scss 复制代码
@Override  
public TargetObject sourceToTarget5(SourceObject source, ContextObject contextObject) {  
    // 第一行调用@BeforeMapping的方法
    beforeFormatDate( contextObject );  

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

    TargetObject targetObject = new TargetObject();  

    targetObject.setUserName( source.getUserName() );  
    targetObject.setUserId( source.getUserId() );  
    if ( source.getSex() != null ) {  
        targetObject.setSex( String.valueOf( source.getSex() ) );  
    }  

    targetObject.setCreateTime( formatDate( LocalDateTime.parse( "createTime" ), contextObject ) );  

    return targetObject;  
}

13.@AfterMapping

这个注解同样可以标注在一个没有返回值的方法上,但它会在完成所有属性映射后被调用。你可以在这里执行一些额外的转换逻辑或者基于映射结果和上下文进行后期处理。

less 复制代码
@Named("sourceToTarget5")  
@Mapping(target = "createTime", constant = "createTime", qualifiedByName = "formatDate")  
TargetObject sourceToTarget5(SourceObject source, @Context ContextObject contextObject);  

@Named("formatDate")  
default String formatDate(LocalDateTime createTime, @Context ContextObject contextObject){  
    DateTimeFormatter dateTimeFormatter = contextObject.getDateTimeFormatter();  
    return dateTimeFormatter.format(createTime);  
}  

@BeforeMapping  
default void beforeFormatDate(@Context ContextObject context) {  
// 在映射之前初始化或更新上下文中的信息  
    context.setDateTimeFormatter(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));  
}  

@AfterMapping  
default void afterHandler(SourceObject source, @MappingTarget TargetObject targetObject, @Context ContextObject contextObject){  
    targetObject.setContext(contextObject.getContext());  
}

编译后,可以发现在sourceTarget5的实现方法中的最后会调用afterHandler方法:

scss 复制代码
@Override  
public TargetObject sourceToTarget5(SourceObject source, ContextObject contextObject) {  
    beforeFormatDate( contextObject );  

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

    TargetObject targetObject = new TargetObject();  

    targetObject.setUserName( source.getUserName() );  
    targetObject.setUserId( source.getUserId() );  
    if ( source.getSex() != null ) {  
        targetObject.setSex( String.valueOf( source.getSex() ) );  
    }  

    targetObject.setCreateTime( formatDate( LocalDateTime.parse( "createTime" ), contextObject ) );  

    afterHandler( source, targetObject, contextObject );  

    return targetObject;  
}

@BeforeMapping@AfterMapping 注解的方法默认会作用于在同一接口内使用了相同参数类型的映射方法上。如果想要在一个地方定义一个通用的前置或后置处理逻辑,并让它应用于多个映射方法,可以编写一个不带具体映射源和目标参数的方法,并在需要应用这些逻辑的所有映射方法上保持相同的@Context参数类型。

14.@ObjectFactory

此注解用于声明一个工厂方法,该方法在目标对象实例化阶段被调用。这里也可以通过@Context获取到上下文信息,以便在创建目标对象时就考虑到某些上下文依赖。

less 复制代码
@Named("sourceToTarget5")  
@Mapping(target = "createTime", constant = "createTime", qualifiedByName = "formatDate")  
TargetObject sourceToTarget5(SourceObject source, @Context ContextObject contextObject);  

@Named("formatDate")  
default String formatDate(LocalDateTime createTime, @Context ContextObject contextObject){  
    DateTimeFormatter dateTimeFormatter = contextObject.getDateTimeFormatter();  
    return dateTimeFormatter.format(createTime);  
}  

@BeforeMapping  
default void beforeFormatDate(@Context ContextObject context) {  
    // 在映射之前初始化或更新上下文中的信息  
    context.setDateTimeFormatter(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));  
}  

@AfterMapping  
default void afterHandler(SourceObject source, @MappingTarget TargetObject targetObject, @Context ContextObject contextObject){  
    targetObject.setContext(contextObject.getContext());  
}  

@ObjectFactory  
default TargetObject createTargetObject(@Context ContextObject contextObject){  
    TargetObject targetObject = new TargetObject();  
    // 根据上下文初始化dto的一些属性  
    targetObject.setContext(contextObject.getContext());  
    return targetObject;  
}

编译后生成的实现类中,会看见TargetObject会通过createTargetObject方法创建:

scss 复制代码
@Override  
public TargetObject sourceToTarget5(SourceObject source, ContextObject contextObject) {  
    beforeFormatDate( contextObject );  

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

    TargetObject targetObject = createTargetObject( contextObject );  

    targetObject.setUserName( source.getUserName() );  
    targetObject.setUserId( source.getUserId() );  
    if ( source.getSex() != null ) {  
        targetObject.setSex( String.valueOf( source.getSex() ) );  
    }  

    targetObject.setCreateTime( formatDate( LocalDateTime.parse( "createTime" ), contextObject ) );  

    afterHandler( source, targetObject, contextObject );  

    return targetObject;  
}

@ObjectFactory 标记的方法则更具有针对性,它通常用于为特定的目标对象创建实例。如果你定义了一个@ObjectFactory方法且没有指定具体映射方法,则这个工厂方法会作为默认的实例化方式,在所有未明确提供实例化方法的映射目标对象时被调用。

SpringBoot集成

上面我们说到了@Mapper注解以及他的属性componentModel,将该值设置为Spring也就是MappingConstants.ComponentModel.SPRING值时,这个映射器生成的实现类就可以被Spring容器管理,这样就可以在使用时就可以注入到其他组件中了。

less 复制代码
@Mapper(uses = {MyNameConverter.class}, imports = {SexEnum.class}, componentModel = MappingConstants.ComponentModel.SPRING)  
public interface MyMapper {
    @Mappings({  
    @Mapping(target = "source", constant = "API"),  
    @Mapping(target = "sex", expression = "java(SexEnum.descOfCode(source.getSex()))"),  
    })  
    @Named("sourceToTarget")  
    TargetObject sourceToTarget(SourceObject source);
}    

生成的实现类自动加上@Component注解,并将其注册为Spring Bean,:

java 复制代码
@Component  
public class MyMapperImpl implements MyMapper {
    @Override  
    public TargetObject sourceToTarget(SourceObject source) {  
        if ( source == null ) {  
            return null;  
        }  

        TargetObject targetObject = new TargetObject();  

        targetObject.setUserName( source.getUserName() );  
        targetObject.setUserId( source.getUserId() );  
        if ( source.getCreateTime() != null ) {  
            targetObject.setCreateTime( DateTimeFormatter.ISO_LOCAL_DATE_TIME.format( source.getCreateTime() ) );  
        }  

        targetObject.setSource( "API" );  
        targetObject.setSex( SexEnum.descOfCode(source.getSex()) );  

        return targetObject;  
    }
}

这样就可以在其他组件中注入MyMapper

typescript 复制代码
@SpringBootTest  
public class SpringbootCodeApplicationTests {
    private MyMapper mapper;

    @Test  
    void testMapper(){  
        TargetObject targetObject = mapper.sourceToTarget(new SourceObject());  
        System.out.println(targetObject.getSex());  
    }

    @Autowired  
    public void setMapper(MyMapper mapper) {  
    this.mapper = mapper;  
}

总结

MapStruct是一个利用注解和编译时代码生成技术的Java Bean映射工具,通过在接口上定义映射规则并自动创建实现类,极大地简化了对象转换过程。相比于手动编写映射代码及运行时反射工具如Dozer,MapStruct提供了更高的性能、更好的可读性和易于维护性。它支持灵活的字段映射配置、自定义转换逻辑,并可通过组件模型适应不同框架,是提升开发效率与降低维护成本的理想对象映射解决方案。

写在最后:可能大家觉得要防御性编程,但是咱可以把编译后实现类的代码CV到你的代码里面就可以了,这样免去了自己手写getset方法映射,这样不出错,还可以节省时间摸鱼。。。。

本文已收录于我的个人博客:码农Academy的技术博客,专注分享Java技术干货,包括Java基础、Spring Boot、Spring Cloud、Mysql、Redis、Elasticsearch、中间件、架构设计、面试题、程序员攻略等

相关推荐
程序员鱼皮36 分钟前
我代表编程导航,向大家道歉!
前端·后端·程序员
间彧1 小时前
Spring Boot项目中如何实现Redis分布式锁
java
zjjuejin1 小时前
Maven 生命周期与插件机制
后端·maven
掘金安东尼1 小时前
AI 应用落地谈起 ,免费试用 Amazon Bedrock 的最佳时机
java·架构
阿杆1 小时前
为什么我建议你把自建 Redis 迁移到云上进行托管
redis·后端
杨杨杨大侠1 小时前
案例03-附件E-部署运维
java·docker·github
Java水解1 小时前
go语言教程(全网最全,持续更新补全)
后端·go
杨杨杨大侠1 小时前
案例03-附件B-映射器实现
java·开源·github
杨杨杨大侠1 小时前
案例03-附件A-订单实体设计
java·开源·github
杨杨杨大侠1 小时前
案例03-附件C-性能优化
java·开源·github