mapstruct

1、概述

  • 需求驱动
    随着一个工程的越来越成熟,模块划分会越来越细,其中实体类一般存于 domain 之中,但 domain 工程最好不要被其他工程依赖,所以其他工程想获取实体类数据时就需要在各自工程写 model,自定义 model 可以根据自身业务需要映射相应的实体属性。这样一来,这个映射工程貌似并不简单了

  • 解决方法之mapstruct
    mapstruct 的插件,它就是专门用来处理 domin 实体类与 model 类的属性映射的,我们只需定义 mapper 接口,mapstruct 在编译的时候就会自动的帮我们实现这个映射接口,避免了麻烦复杂的映射实现。

  • 同类工具 BeanUtils
    为啥不用 BeanUtils 的 copyProperties 方法呢?BeanUtils 只能同属性映射

2、相关文档

  1. Github地址:https://github.com/mapstruct/mapstruct/
  2. 使用例子:https://github.com/mapstruct/mapstruct-examples
  3. MapStruct官网地址: http://mapstruct.org/
  4. http://mapstruct.org/documentation/stable/reference/html/

3、如何使用

3.1 引入依赖

	<properties>
		<java.version>1.8</java.version>
		<org.mapstruct.version>1.4.2.Final</org.mapstruct.version>
	</properties>
	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
			<version>1.18.12</version>
		</dependency>
		<dependency>
			<groupId>org.mapstruct</groupId>
			<artifactId>mapstruct</artifactId>
			<version>${org.mapstruct.version}</version>
		</dependency>
		<!--单元测试-->
		<dependency>
			<groupId>junit</groupId>
			<artifactId>junit</artifactId>
			<version>4.12</version>
		</dependency>
		<!--包含时间,字符串等基本的工具类依赖-->
		<dependency>
			<groupId>org.apache.commons</groupId>
			<artifactId>commons-lang3</artifactId>
		</dependency>
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-compiler-plugin</artifactId>
				<version>3.8.1</version>
				<configuration>
					<source>1.8</source>
					<target>1.8</target>
					<annotationProcessorPaths>
<!--mapstruct 对应自动生成实现类的路径和lombok版本(mapstruct和lombok是有对应版本的,版本不匹配会报错)-->
						<path>
							<groupId>org.projectlombok</groupId>
							<artifactId>lombok</artifactId>
							<version>1.18.12</version>
						</path>
						<path>
							<groupId>org.mapstruct</groupId>
							<artifactId>mapstruct-processor</artifactId>
							<version>${org.mapstruct.version}</version>
						</path>
					</annotationProcessorPaths>
				</configuration>
			</plugin>
		</plugins>
	</build>

3.2 定义三个对象

这里定义两个 DO 对象 Person 和 User,其中 user 是 Person 的一个属性 ,一个 DTO 对象 PersonDTO

package com.st.valid.entity;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.Date;

/**
 * @创建人: 放生
 * @创建时间: 2022/4/2
 * @描述:
 */
@NoArgsConstructor
@AllArgsConstructor
@Data
public class Person {

    private Long id;
    private String name;
    private String email;
    private Date birthday;
    private User user;

}

package com.st.valid.entity;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * @创建人: 放生
 * @创建时间: 2022/4/2
 * @描述:
 */
@NoArgsConstructor
@AllArgsConstructor
@Data
public class User {

    private Integer age;

}

package com.st.valid.entity;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.Date;

/**
 * @创建人: 放生
 * @创建时间: 2022/4/2
 * @描述:
 */
@NoArgsConstructor
@AllArgsConstructor
@Data
public class PersonDTO {

    private Long id;
    private String name;
    /**
     * 对应 Person.user.age
     */
    private Integer age;
    private String email;
    /**
     * 与 DO 里面的字段名称(birthDay)不一致
     */
    private Date birth;
    /**
     * 对 DO 里面的字段(birthDay)进行拓展,dateFormat 的形式
     */
    private String birthDateFormat;
    /**
     * 对 DO 里面的字段(birthDay)进行拓展,expression 的形式
     */
    private String birthExpressionFormat;


}

3.3 编写映射接口PersonConverter

  • 写一个 Mapper 接口 PersonConverter,其中两个方法,一个是单实体映射,另一个是List映射

  • 若源对象属性与目标对象属性名字一致,会自动映射对应属性,不一样的需要指定,也可以用 format 转成自己想要的类型,也支持表达式的方式,可以看到像 id、name、email这些名词一致的我并没有指定 source-target,而birthday-birth指定了,转换格式的 birthDateFormat 加了dateFormat 或者 birthExpressionFormat 加了 expression,如果某个属性你不想映射,可以加个 ignore=true

package com.st.valid.service;
import com.st.valid.entity.Person;
import com.st.valid.entity.PersonDTO;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.Mappings;
import org.mapstruct.factory.Mappers;
import java.util.List;
/**
 * @创建人: 放生
 * @创建时间: 2022/4/2
 * @描述:
 */
@Mapper
public interface PersonConverter {

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

    @Mappings({
            @Mapping(source = "birthday", target = "birth"),
            @Mapping(source = "birthday", target = "birthDateFormat", dateFormat = "yyyy-MM-dd HH:mm:ss"),
            @Mapping(target = "birthExpressionFormat", expression = "java(org.apache.commons.lang3.time.DateFormatUtils.format(person.getBirthday(),\"yyyy-MM-dd HH:mm:ss\"))"),
            @Mapping(source = "user.age", target = "age"),
            @Mapping(target = "email", ignore = true)
    })
    PersonDTO domain2dto(Person person);

    List<PersonDTO> domain2dto(List<Person> people);


}

编译MapStruct之后,手工编译或者启动 IDE 的时候 IDE 也会帮我们编译, 会自动在 target/classes 下生成对应的实现类

手工编译命令
mvn compile

注意!!!下面这个 PersonConverterImpl 是自动生成的,不是自己写的!

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package com.st.valid.service;

import com.st.valid.entity.Person;
import com.st.valid.entity.PersonDTO;
import com.st.valid.entity.User;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import org.apache.commons.lang3.time.DateFormatUtils;

public class PersonConverterImpl implements PersonConverter {
    public PersonConverterImpl() {
    }

    public PersonDTO domain2dto(Person person) {
        if (person == null) {
            return null;
        } else {
            PersonDTO personDTO = new PersonDTO();
            personDTO.setBirth(person.getBirthday());
            if (person.getBirthday() != null) {
                personDTO.setBirthDateFormat((new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")).format(person.getBirthday()));
            }

            personDTO.setAge(this.personUserAge(person));
            personDTO.setId(person.getId());
            personDTO.setName(person.getName());
            personDTO.setBirthExpressionFormat(DateFormatUtils.format(person.getBirthday(), "yyyy-MM-dd HH:mm:ss"));
            return personDTO;
        }
    }

    public List<PersonDTO> domain2dto(List<Person> people) {
        if (people == null) {
            return null;
        } else {
            List<PersonDTO> list = new ArrayList(people.size());
            Iterator var3 = people.iterator();

            while(var3.hasNext()) {
                Person person = (Person)var3.next();
                list.add(this.domain2dto(person));
            }

            return list;
        }
    }

    private Integer personUserAge(Person person) {
        if (person == null) {
            return null;
        } else {
            User user = person.getUser();
            if (user == null) {
                return null;
            } else {
                Integer age = user.getAge();
                return age == null ? null : age;
            }
        }
    }
}

在这里插入图片描述

3.4 测试

写一个单元测试类 PersonConverterTest 测试一下,看看效果

   @Test
    public  void test01() {

        Person person = new Person(1L,"fangSheng","zhige.me@gmail.com",new Date(),new User(1));
		PersonDTO personDTO = PersonConverter.INSTANCE.domain2dto(person);
        log.info(personDTO.toString());
        log.info(personDTO.getId()+":::"+person.getId());
        log.info(personDTO.getName()+":::"+person.getName());
        log.info(personDTO.getBirth()+":::"+ person.getBirthday());


		String format = DateFormatUtils.format(personDTO.getBirth(), "yyyy-MM-dd HH:mm:ss");
        log.info(personDTO.getBirthDateFormat()+":::"+format);
        log.info(personDTO.getBirthExpressionFormat()+":::"+format);


		List<Person> people = new ArrayList<>();
		people.add(person);
		List<PersonDTO> personDTOs = PersonConverter.INSTANCE.domain2dto(people);
		



    }

结果:

18:43:54.812 [main] INFO com.st.valid.mapstruct.test.TT - PersonDTO(id=1, name=fangSheng, age=1, email=null, birth=Sun Apr 03 18:43:54 CST 2022, birthDateFormat=2022-04-03 18:43:54, birthExpressionFormat=2022-04-03 18:43:54)
18:43:54.814 [main] INFO com.st.valid.mapstruct.test.TT - 1:::1
18:43:54.814 [main] INFO com.st.valid.mapstruct.test.TT - fangSheng:::fangSheng
18:43:54.814 [main] INFO com.st.valid.mapstruct.test.TT - Sun Apr 03 18:43:54 CST 2022:::Sun Apr 03 18:43:54 CST 2022
18:43:54.815 [main] INFO com.st.valid.mapstruct.test.TT - 2022-04-03 18:43:54:::2022-04-03 18:43:54
18:43:54.815 [main] INFO com.st.valid.mapstruct.test.TT - 2022-04-03 18:43:54:::2022-04-03 18:43:54

4、多对一映射

  • MapStruct 可以将几种类型的对象映射为另外一种类型,比如将多个 DO 对象转换为 DTO

例子

4.1 创建对象

两个 DO 对象 Item 和 Sku,一个 DTO 对象 SkuDTO

package com.st.valid.entity;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * @创建人: 放生
 * @创建时间: 2022/4/2
 * @描述:
 */
@NoArgsConstructor
@AllArgsConstructor
@Data
public class Item {
    private Long id;
    private String title;
}

package com.st.valid.entity;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * @创建人: 放生
 * @创建时间: 2022/4/2
 * @描述:
 */
@NoArgsConstructor
@AllArgsConstructor
@Data
public class Sku {
    private Long id;
    private String code;
    private Integer price;
}

package com.st.valid.entity;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * @创建人: 放生
 * @创建时间: 2022/4/2
 * @描述:
 */
@NoArgsConstructor
@AllArgsConstructor
@Data
public class SkuDTO {
    private Long skuId;
    private String skuCode;
    private Integer skuPrice;
    private Long itemId;
    private String itemName;
}

4.2 创建ItemConverter

创建 ItemConverter(映射)接口,MapStruct 就会自动实现该接口

package com.st.valid.service;

import com.st.valid.entity.Item;
import com.st.valid.entity.Sku;
import com.st.valid.entity.SkuDTO;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.Mappings;
import org.mapstruct.factory.Mappers;

/**
 * @创建人: 放生
 * @创建时间: 2022/4/2
 * @描述:
 */
@Mapper
public interface ItemConverter {
    ItemConverter INSTANCE = Mappers.getMapper(ItemConverter.class);

    @Mappings({
            @Mapping(source = "sku.id",target = "skuId"),
            @Mapping(source = "sku.code",target = "skuCode"),
            @Mapping(source = "sku.price",target = "skuPrice"),
            @Mapping(source = "item.id",target = "itemId"),
            @Mapping(source = "item.title",target = "itemName")
    })
    SkuDTO domain2dto(Item item, Sku sku);

}

4.3 测试

 @Test
    public void test02(){

        Item item = new Item(1L, "iPhone X");
        Sku sku = new Sku(2L, "phone12345", 1000000);
        SkuDTO skuDTO = ItemConverter.INSTANCE.domain2dto(item, sku);
        log.info(skuDTO.toString());
        log.info(skuDTO.getSkuId()+":::"+ sku.getId());
        log.info(skuDTO.getSkuCode()+":::"+ sku.getCode());
        log.info(skuDTO.getSkuPrice()+":::"+ sku.getPrice());
        log.info(skuDTO.getItemId()+":::"+ item.getId());
        log.info(skuDTO.getItemName()+":::"+ item.getTitle());

    }

结果

18:50:26.105 [main] INFO com.st.valid.mapstruct.test.TT - SkuDTO(skuId=2, skuCode=phone12345, skuPrice=1000000, itemId=1, itemName=iPhone X)
18:50:26.106 [main] INFO com.st.valid.mapstruct.test.TT - 2:::2
18:50:26.106 [main] INFO com.st.valid.mapstruct.test.TT - phone12345:::phone12345
18:50:26.107 [main] INFO com.st.valid.mapstruct.test.TT - 1000000:::1000000
18:50:26.107 [main] INFO com.st.valid.mapstruct.test.TT - 1:::1
18:50:26.107 [main] INFO com.st.valid.mapstruct.test.TT - iPhone X:::iPhone X

5 、添加自定义方法

方法如下

 // 形式如下
    default PersonDTO personToPersonDTO(Person person) {
        //hand-written mapping logic
    }

5.1 在PersonConverter 添加如下方法

 // 比如在 PersonConverter 里面加入如下
    default Boolean convert2Bool(Integer value) {
        if (value == null || value < 1) {
            return Boolean.FALSE;
        } else {
            return Boolean.TRUE;
        }
    }

    default Integer convert2Int(Boolean value) {
        if (value == null) {
            return null;
        }
        if (Boolean.TRUE.equals(value)) {
            return 1;
        }
        return 0;
    }

5.2 测试

@Test
    public void test03(){
        // 测试类 PersonConverterTest 加入
        log.info(PersonConverter.INSTANCE.convert2Bool(1)+"");
        log.info((int)PersonConverter.INSTANCE.convert2Int(true)+"");


    }

6 、更新目标对象

如果已经有了接收对象,更新目标对象

// 比如在 PersonConverter 里面加入如下,@InheritConfiguration 用于继承刚才的配置
@InheritConfiguration(name = "domain2dto")
void update(Person person, @MappingTarget PersonDTO personDTO);

// 测试类 PersonConverterTest 加入如下
Person person = new Person(1L,"fangsheng","fangsheng@163.com",new Date(),new User(1));
PersonDTO personDTO = PersonConverter.INSTANCE.domain2dto(person);
assertEquals("fangsheng", personDTO.getName());
person.setName("fangsheng");
PersonConverter.INSTANCE.update(person, personDTO);
assertEquals("fangsheng", personDTO.getName());

7、Spring 注入的方式

// 刚才一直写的例子是默认的方式
PersonConverter INSTANCE = Mappers.getMapper(PersonConverter.class);

还有一种常用的方式,是和常用的框架 Spring 结合,在 @Mapper 后面加入 componentModel="spring"

@Mapper(componentModel="spring")
public interface PersonConverter {
    @Mappings({
        @Mapping(source = "birthday", target = "birth"),
        @Mapping(source = "birthday", target = "birthDateFormat", dateFormat = "yyyy-MM-dd HH:mm:ss"),
        @Mapping(target = "birthExpressionFormat", expression = "java(org.apache.commons.lang3.time.DateFormatUtils.format(person.getBirthday(),\"yyyy-MM-dd HH:mm:ss\"))"),
        @Mapping(source = "user.age", target = "age"),
        @Mapping(target = "email", ignore = true)
    })
    PersonDTO domain2dto(Person person);
}

这时候测试类改一下,我用的 spring boot 的形式

@RunWith(SpringRunner.class)
@SpringBootTest(classes = BaseTestConfiguration.class)
public class PersonConverterTest {
    //这里把转换器装配进来
    @Autowired
    private PersonConverter personConverter;
    @Test
    public void test() {
        Person person = new Person(1L,"fangsheng","fangsheng@163	.com",new Date(),new User(1));
        PersonDTO personDTO = personConverter.domain2dto(person);

        assertNotNull(personDTO);
        assertEquals(personDTO.getId(), person.getId());
        assertEquals(personDTO.getName(), person.getName());
        assertEquals(personDTO.getBirth(), person.getBirthday());
        String format = DateFormatUtils.format(personDTO.getBirth(), "yyyy-MM-dd HH:mm:ss");
        assertEquals(personDTO.getBirthDateFormat(),format);
        assertEquals(personDTO.getBirthExpressionFormat(),format);

    }
}

8、MapStruct 注解的关键词

@Mapper 只有在接口加上这个注解, MapStruct 才会去实现该接口
    @Mapper 里有个 componentModel 属性,主要是指定实现类的类型,一般用到两个
    default:默认,可以通过 Mappers.getMapper(Class) 方式获取实例对象
    spring:在接口的实现类上自动添加注解 @Component,可通过 @Autowired 方式注入
@Mapping:属性映射,若源对象属性与目标对象名字一致,会自动映射对应属性
    source:源属性
    target:目标属性
    dateFormat:StringDate 日期之间相互转换,通过 SimpleDateFormat,该值为 SimpleDateFormat              的日期格式
    ignore: 忽略这个字段
@Mappings:配置多个@Mapping
@MappingTarget 用于更新已有对象
@InheritConfiguration 用于继承配置
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值