官网地址:MapStruct

官方示例:mapstruct-examples

关联:

个人实践:SpringBoot-Labs-Junw 中的 jLab-2-MapStruct-jdk17 jLab-2-MapStruct-jdk8

其他参考:【1】【2】

安装/依赖

	<properties>
		<java.version>17</java.version>
		<mapstruct.version>1.5.5.Final</mapstruct.version>
		<lombok.version>1.18.32</lombok.version>
		<lombok-mapstruct-binding.version>0.2.0</lombok-mapstruct-binding.version>
	</properties>

	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter</artifactId>
		</dependency>

		<dependency>
			<groupId>org.mapstruct</groupId>
			<artifactId>mapstruct</artifactId>
			<version>${mapstruct.version}</version>
		</dependency>
		<dependency>
			<groupId>org.mapstruct</groupId>
			<artifactId>mapstruct-processor</artifactId>
			<version>${mapstruct.version}</version>
		</dependency>

		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
			<version>${lombok.version}</version>
		</dependency>
		
		<dependency>
		    <groupId>io.springfox</groupId>
		    <artifactId>springfox-swagger2</artifactId>
		    <version>${swagger2.version}</version>
		    <scope>compile</scope>
		    <exclusions>
		        <exclusion>
					<groupId>org.mapstruct</groupId>
					<artifactId>mapstruct</artifactId>
		        </exclusion>
		    </exclusions>
		</dependency>	
	</dependencies>

	<build>
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-compiler-plugin</artifactId>
				<version>${maven-compiler-plugin.version}</version>
				<configuration>
					<source>${java.version}</source>
					<target>${java.version}</target>
					<release>>${java.version}</release>
					<annotationProcessorPaths>
						<path>
							<groupId>org.mapstruct</groupId>
							<artifactId>mapstruct-processor</artifactId>
							<version>${mapstruct.version}</version>
						</path>
						<path>
							<groupId>org.projectlombok</groupId>
							<artifactId>lombok</artifactId>
							<version>${lombok.version}</version>
						</path>
						
						<path>
							<groupId>org.projectlombok</groupId>
							<!--兼容插件-->
							<artifactId>lombok-mapstruct-binding</artifactId>
							<version>${lombok-mapstruct-binding.version}</version>
						</path>
					</annotationProcessorPaths>
					<!--在编译时打印相关转换信息-->
	 				<showWarnings>true</showWarnings>
			        <compilerArgs>

						<compilerArg>
							<!--在编译时打印相关转换信息-->
			    			-Amapstruct.verbose=true
						</compilerArg>
			            <compilerArg>
							<!--   以spring注入的方式访问mapper-->
			                -Amapstruct.defaultComponentModel=spring
			            </compilerArg>
						<compilerArg>
							<!--   注入方式 默认是字段注入 设置为constructor是构造器注入-->
			                -Amapstruct.defaultInjectionStrategy=field
			            </compilerArg>
			        </compilerArgs>
				</configuration>
			</plugin>
		</plugins>
	</build>

@Mapper注解上的componentModel(1)和injectionStrategy(2)会覆盖默认配置中的设置。

  1. componentModel 基于spring通常这里不需要覆盖默认配置

  2. InjectionStrategy.CONSTRUCTOR可以设置基于构造器注入,覆盖默认配置的字段注入

1. 属性名相同

1.1 实体

	@Data
	public class SourceVO {
	    private String test;
	}
	

1.2 接口

	@Mapper
	public interface SourceToSource {
	    SourceToSource INSTANCE = Mappers.getMapper( SourceToSource.class );
	    SourceVO toSource(SourceVO s );
	}

1.3 测试用例

	@Test
    public void sourceToSourceTest() {
        SourceVO s = new SourceVO();
        s.setTest( "5" );

        SourceVO newS = SourceToSource.INSTANCE.toSource( s );
        assertEquals( "5", newS.getTest() );
    }

2. 属性名不同

2.1 实体

	@Data
	public class SourceVO {
	    private String test;
	}
	
	@Data
	public class TargetVO {
	    private Long testing;
	}

2.2 接口

	@Mapper
	public interface SourceToTarget {
	
	    SourceToTarget INSTANCE = Mappers.getMapper( SourceToTarget.class );
	
	    @Mapping( source = "test", target = "testing" )
	    TargetVO toTarget(SourceVO s );
	}

2.3 测试用例


    @Test
    public void sourceToTargetTest() {
        SourceVO s = new SourceVO();
        s.setTest( "5" );

        TargetVO t = SourceToTarget.INSTANCE.toTarget( s );
        assertEquals( 5, (long) t.getTesting() );
    }

3. 多参数源

3.1 实体

	@Data
	@AllArgsConstructor
	public class ManVO {
	    private String name;
	    private Integer age;
	}
	
	@Data
	@AllArgsConstructor
	public class WomanVO {
	    private String name;
	    private Integer age;
	}
	
	@Data
	public class HomeVO {
	    private String manName;
	    private String womanName;
	    private Integer manAge;
	    private Integer womanAge;
	}

3.2 接口

	@Mapper
	public interface ManWomanToHome {
	
	    ManWomanToHome INSTANCE = Mappers.getMapper( ManWomanToHome.class );
	
	    @Mappings({
	        @Mapping( source = "man.name", target = "manName" ),
	        @Mapping( source = "man.age", target = "manAge" ),
	        @Mapping( source = "woman.name", target = "womanName" ),
	        @Mapping( source = "woman.age", target = "womanAge" )
	    })
	    HomeVO toHome(ManVO man, WomanVO woman);
	}

3.3 测试用例


    @Test
    public void multiParamsTest() {
        ManVO man = new ManVO("小帅",24);
        WomanVO woman = new WomanVO("小美",21);

        HomeVO home = ManWomanToHome.INSTANCE.toHome( man, woman);
        assertEquals( "HomeVO(manName=小帅, womanName=小美, manAge=24, womanAge=21)", home.toString() );
    }

4. 多参数类型

4.1 实体

@Data
@AllArgsConstructor
public class ChildEO {
    private String bname;
    private int bAge;
}

@Data
@AllArgsConstructor
public class ChildVO {
    private String bname;
    private int bAge;
}

@Data
public class SourceEntity {
    private int intValue;
    private Integer integerValue;
    private BigDecimal bigDecimalValue;
    private LocalDateTime localDateTimeValue;
    private Date dateValue;
    private MyEnum enumValue;
    private java.sql.Timestamp timestampValue;
    private String stringValue;
    private String ignore;
    private ChildVO baby;
    private String sourceId;
}

@Data
public class TargetEntity {
    private String intValueStr;
    private int intValue;
    private String formattedBigDecimal;
    private String localDateTimeValueStr;
    private String formattedDate;
    private String enumValueAsString;
    private Date timestampAsDate;
    private String stringValue;
    private String constantStr;
    private String expression_str;
    private String expression_date;
    private String expression_method;
    private String ignoreStr;
    private String expression_util;
    private ChildEO baby;
    private String id;
}

public enum MyEnum {

    VALUE_1(0, "正常"),
    VALUE_2(1, "已删除");

    private int value;
    private String label;

    private MyEnum(int value, String label) {
        this.value = value;
        this.label = label;
    }

    public int getValue() {
        return this.value;
    }

    public String getLabel() {
        return this.label;
    }
}

4.2 接口

@Mapper(imports = {StringBOUtils.class})
public interface EntityMapper {

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

    // 定义映射方法,指定映射规则
    @Mappings({
            // 基本类型 --> String
            @Mapping(source = "intValue", target = "intValueStr"),
            // 基本类型包装类 --> 基本类型
            @Mapping(source = "integerValue", target = "intValue"),
            // LocalDateTime --> String
            @Mapping(source = "localDateTimeValue", target = "localDateTimeValueStr"),
            // BigDecimal --> String  保留两位小数
            @Mapping(source = "bigDecimalValue", target = "formattedBigDecimal", numberFormat = "#.00"),
            // Date --> String  指定时间格式 yyyy-MM-dd HH:mm:ss
            @Mapping(source = "dateValue", target = "formattedDate", dateFormat = "yyyy-MM-dd HH:mm:ss"),
            // 枚举类 --> String
            @Mapping(source = "enumValue", target = "enumValueAsString"),
            // Timestamp --> Date
            @Mapping(source = "timestampValue", target = "timestampAsDate"),
            // defaultValue 设置默认值,来源为null时,目标为默认值。
            @Mapping(source = "stringValue", target = "stringValue",defaultValue = "Hello"),
            // 目标值指定为常量
            @Mapping(target = "constantStr",constant = "我是常量"),
            // ignore = true 忽略属性,不拷贝
            @Mapping(target = "ignoreStr",ignore = true),
            // expression 表达式:常量
            @Mapping(target = "expression_str",expression = "java(\"Jaws\")"),
            // expression 表达式:一段代码
            @Mapping(target = "expression_date",expression = "java(\"表达式 --> 时间:\" + new java.util.Date())"),
            // expression 表达式:方法调用-当前类
            @Mapping(target = "expression_method",expression = "java(EntityMapper.getLastRunTime(sourceEntity))"),
            // expression 表达式:方法调用-外部工具类
            @Mapping(target = "expression_util", expression = "java(StringBOUtils.toBOString(sourceEntity.getIgnore()))"),
            // 嵌套-子对象属性完全一致,可以用.
            @Mapping(target = ".", source = "baby"),
            // 默认表达式,仅source值为null时生效
            @Mapping(target="id", source="sourceId", defaultExpression = "java(\"UUID\"  )")
    })
    TargetEntity mapToTarget(SourceEntity sourceEntity);

    static String getLastRunTime(SourceEntity sourceEntity){
        return "lastRunTime --> " + sourceEntity.getDateValue();
    }
}

4.3 其他类

public class StringBOUtils {
    public static String toBOString(String poString) {
        return poString + "BO";
    }
}

4.4 测试用例


public class MultiParamTypeTest {
    /**
     * 多参数源
     */
    @Test
    public void multiParamTypeTest() {
        // 创建源实体对象
        SourceEntity sourceEntity = new SourceEntity();
        sourceEntity.setIntValue(10);
        sourceEntity.setIntegerValue(12);
        sourceEntity.setLocalDateTimeValue(LocalDateTime.now());
        sourceEntity.setBigDecimalValue(new BigDecimal("123.456"));
        sourceEntity.setDateValue(new Date());
        sourceEntity.setEnumValue(MyEnum.VALUE_1);
        sourceEntity.setTimestampValue(new Timestamp(System.currentTimeMillis()));
        // sourceEntity.setStringValue("Hello");
        sourceEntity.setIgnore("ignore");
        ChildVO babyVO = new ChildVO("baby",1);
        sourceEntity.setBaby(babyVO);

        // 调用映射方法,将源实体映射为目标实体
        TargetEntity targetEntity = EntityMapper.INSTANCE.mapToTarget(sourceEntity);

        System.out.println(targetEntity.toString());
        // 验证映射结果
        assert "10".equals(targetEntity.getIntValueStr());
        assert targetEntity.getIntValue() == 12;
        assert targetEntity.getStringValue().equals("Hello");
        assert targetEntity.getFormattedBigDecimal().equals("123.46"); // 根据规则,保留两位小数
        // 验证日期格式化
        // 在此为了简化,这里不验证具体格式,只验证是否为非空字符串
        assert targetEntity.getFormattedDate() != null && !targetEntity.getFormattedDate().isEmpty();
        assert targetEntity.getEnumValueAsString().equals(MyEnum.VALUE_1.toString());
        assert targetEntity.getTimestampAsDate() != null;
    }
}

5. 注解应用示例

5.1 实体

@Data
@AllArgsConstructor
@NoArgsConstructor
public class UserDTO {
    private Long id;
    private String fullName;
    private String email;
}

@Data
public class UserEntity {
    private Long id;
    private String firstName;
    private String lastName;
    private String email;
    private String address;
}

5.2 接口

@Mapper(componentModel = "spring")
public interface UserMapper {

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

    // 基本映射
    @Mapping(source = "firstName", target = "fullName", qualifiedByName = "fullNameMapping")
    @Mapping(source = "email", target = "email")
    @BeanMapping(ignoreByDefault = true) // 使用 @BeanMapping 忽略未映射的属性
    UserDTO toDTO(UserEntity userEntity, @Context MappingContext context);

    // 逆向映射
    @InheritInverseConfiguration
    UserEntity toEntity(UserDTO userDTO, @Context MappingContext context);

    // 更新目标对象
    @BeanMapping(ignoreByDefault = true) // 使用 @BeanMapping 忽略未映射的属性
    @Mapping(target = "firstName", expression = "java(userDTO.getFullName().split(\" \")[0])")
    @Mapping(target = "lastName", ignore = true)
    void updateEntityFromDTO(UserDTO userDTO, @MappingTarget UserEntity userEntity, @Context MappingContext context);

    // 映射前的操作
    @BeforeMapping
    default void beforeMapping(@MappingTarget UserEntity userEntity, @Context MappingContext context) {
        // 在映射前执行一些操作
        System.out.println("BeforeMapping -->" + context.pContext());
    }

    // 映射后的操作
    @AfterMapping
    default void afterMapping(@MappingTarget UserDTO userDTO, @Context MappingContext context) {
        // 在映射后执行一些操作
        System.out.println("AfterMapping -->" + context.pContext());
    }

    // 使用自定义方法进行映射
    @Named("fullNameMapping")
    default String fullNameMapping(String firstName) {
        return firstName + " Smith";
    }
}

5.3 其他

public class MappingContext {
    // 上下文类中可以包含一些共享数据或方法
    public String pContext(){
        return "965";
    }
}

5.4 测试用例

    /**
     * @BeanMapping:在 toDTO 和 updateEntityFromDTO 方法中使用,结合 ignoreByDefault = true 参数,用于忽略未显式映射的属性。这确保了只有明确映射的属性会被映射,从而避免了不必要的属性映射。
     *
     * @MappingTarget:在 updateEntityFromDTO 方法中使用,用于指示目标对象是现有对象,应在其基础上更新字段。
     *
     * @BeforeMapping 和 @AfterMapping:在映射前后执行额外操作。
     *
     * @Context:用于传递上下文信息,便于在映射过程中使用共享数据或方法。
     *
     * @InheritConfiguration 和 @InheritInverseConfiguration:用于继承其他映射方法的配置,以简化映射定义。
     *
     * @Named:定义自定义的映射方法。
     */
public class AnnotatinsTest {

    @Test
    public void testMapping() {
        // 创建实体对象
        UserEntity userEntity = new UserEntity();
        userEntity.setId(1L);
        userEntity.setFirstName("John");
        userEntity.setLastName("Doe");
        userEntity.setEmail("john.doe@example.com");
        userEntity.setAddress("123 Main St");

        // 创建上下文
        MappingContext context = new MappingContext();

        // 执行映射
        UserDTO userDTO = UserMapper.INSTANCE.toDTO(userEntity, context);

        System.out.println(userDTO.toString());
        // 验证映射结果
        assertNull(userDTO.getId());
        assertEquals("John Smith", userDTO.getFullName());
        assertEquals(userEntity.getEmail(), userDTO.getEmail());
        // 确认忽略了 address 属性
    }

    @Test
    public void testInverseMapping() {
        // 创建 DTO 对象
        UserDTO userDTO = new UserDTO();
        userDTO.setId(1L);
        userDTO.setFullName("John Smith");
        userDTO.setEmail("john.smith@example.com");

        // 创建上下文
        MappingContext context = new MappingContext();

        // 执行逆向映射
        UserEntity userEntity = UserMapper.INSTANCE.toEntity(userDTO, context);
        System.out.println(userEntity.toString());
        // 验证逆向映射结果
        assertNull(userEntity.getId());
        assertEquals("John Smith Smith", userEntity.getFirstName());
        assertNull(userEntity.getLastName());
        assertEquals(userDTO.getEmail(), userEntity.getEmail());
    }

    @Test
    public void testUpdateEntityFromDTO() {
        // 创建初始实体对象
        UserEntity userEntity = new UserEntity();
        userEntity.setId(1L);
        userEntity.setFirstName("John");
        userEntity.setLastName("Doe");
        userEntity.setEmail("john.doe@example.com");
        userEntity.setAddress("123 Main St");

        // 创建 DTO 对象
        UserDTO userDTO = new UserDTO();
        userDTO.setId(1L);
        userDTO.setFullName("Sun Smith");
        userDTO.setEmail("john.smith@example.com");

        // 创建上下文
        MappingContext context = new MappingContext();

        // 执行更新映射
        UserMapper.INSTANCE.updateEntityFromDTO(userDTO, userEntity, context);
        System.out.println(userEntity.toString());
        // 验证更新映射结果
        assertEquals(userDTO.getId(), userEntity.getId());
        assertEquals("Sun", userEntity.getFirstName());
        assertEquals("Doe", userEntity.getLastName());
        assertNotEquals(userDTO.getEmail(), userEntity.getEmail());
        // 确认忽略了 address 属性
    }
}