MapStruct是一个用于生成类型安全,高性能和无依赖的bean映射代码的注释处理器。简而言之就是一个可以在我们做vo->dto->do转换时通过注解在编译时生成类型转换使用的代码,免去了我们每次都要使用BeanCopy或者遍历去将每个字段的值设置到另一个对象里的过程。

前言

在规范的项目中是需要有VO、DTO、DO、BO等领域模型来划分不同层级使用的Java类,这样做的好处就是不会将一个类中冗杂过多的无用属性,而且负责某一层开发的人员如果需要做改动可以最小化影响其他层级的开发人员。

常用的类型转换方式

手写set属性

DemoDO demoDo = new DemoDO();
DemoDTO dto = new DemoDTO();
dto.setName(demoDo.getName());
// ...

BeanCopy

DemoDO demoDo = new DemoDO();
DemoDTO dto = new DemoDTO();
// org.springframework.beans.BeanUtils
// 使用BeanCopy前提是两个类的属性完全一样(属性名、类型)
BeanUtils.copyProperties(demoDo, dto);

不可否认对象copy的方式确实相对第一种会简单很多,也能节省很多代码,但是它依旧有局限性,比如遇到两个属性的类型不同、需要将源对象里的某个对象属性中的一个属性设置到目标对象里的某个属性上时就不能这么简单处理了。

@Setter
@Getter
public class DemoDO {
	private String name;
	private Integer age;
	private InnerProperty innerProperty;
	private Date 
}
------
@Setter
@Getter
public class InnerProperty {
	private String testProperty;
}
------
@Setter
@Getter
public class DemoDTO {
	private String name;
	private Integer age;
	private String testProperty;
}
------

DemoDO demoDo = new DemoDO();
DemoDTO dto = new DemoDTO();
BeanUtils.copyProperties(demoDo, dto);
InnerProperty inner demoDO.getInnerProperty();
dto.setTestProperty(inner.getTestProperty());

相信你也看到了,如果这样的属性再多几个,再多些类型不一样的,那么工作量就多了起来。

使用Mapstruct做类型转换

依赖安装

  • maven

<properties>
    <org.mapstruct.version>1.4.1.Final</org.mapstruct.version>
</properties>

<dependencies>
    <dependency>
        <groupId>org.mapstruct</groupId>
        <artifactId>mapstruct</artifactId>
        <version>${org.mapstruct.version}</version>
    </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>
                    <path>
                        <groupId>org.mapstruct</groupId>
                        <artifactId>mapstruct-processor</artifactId>
                        <version>${org.mapstruct.version}</version>
                    </path>
                </annotationProcessorPaths>
            </configuration>
        </plugin>
    </plugins>
</build>
  • gradle-3.4及以下

plugins {
    id 'net.ltgt.apt' version '0.20'
}

// You can integrate with your IDEs.
// See more details: https://github.com/tbroyer/gradle-apt-plugin#usage-with-ides
apply plugin: 'net.ltgt.apt-idea'
apply plugin: 'net.ltgt.apt-eclipse'

dependencies {
    implementation "org.mapstruct:mapstruct:${mapstructVersion}"
    annotationProcessor "org.mapstruct:mapstruct-processor:${mapstructVersion}"

    // If you are using mapstruct in test code
    testAnnotationProcessor "org.mapstruct:mapstruct-processor:${mapstructVersion}"
}
  • gradle-3.3及以下
plugins {
    id 'net.ltgt.apt' version '0.20'
}

// You can integrate with your IDEs.
// See more details: https://github.com/tbroyer/gradle-apt-plugin#usage-with-ides
apply plugin: 'net.ltgt.apt-idea'
apply plugin: 'net.ltgt.apt-eclipse'

dependencies {
    compile "org.mapstruct:mapstruct:${mapstructVersion}"
    annotationProcessor "org.mapstruct:mapstruct-processor:${mapstructVersion}"

    // If you are using mapstruct in test code
    testAnnotationProcessor "org.mapstruct:mapstruct-processor:${mapstructVersion}"
}

简单使用

然后需要创建一个用来做类型转换的类,使用该类调用方法做转换

@Mapper// org.mapstruct.Mapper
public interface DemoConvertor {
	// 使用@Mapper的默认模式需要提供一个instance,才能调用接口里方法而不用自己实现
	public static final INSTANCE = Mappers.getMaper(DemoConvertor.class);

	// org.mapstruct.Mappings
	@Mappings({
		@Mapping(source="innerProperty.testProperty", target="testProperty")
	})
	DemoDTO toDemoDTO(DemoDO demoDO);
	
	// 使用list转换的前提是定义了对应的一个对象的转换方法,比如上面的toDemoDTO
	List<DemoDTO> toDemoDTOList(List<DemoDO> doList);
}

使用时

public class MapStructTest {

	public void toDemoDTO() {
		DemoDO demoDO = new DemoDO();
		// 设置测试数据
		demoDO.setName("test");
		DemoDTO dto = DemoConvertor.INSTANCE.toDemoDTO(demoDO);
		Assert.assertEquals(demoDO.getName(), dto.getName());
		Assert.assertEquals(demoDO.getInnerProperty().getTestProperty(), dto.getTestProperty())
		// ...
	}
}

结合Spring等使用

@Mapper(componentModel = "spring")// 默认使用的是componentModel = "default"
// 支持的值有 "cdi"、"spring"、"jsr330"、"default"
public interface DemoConvertor {

	@Mappings({
		@Mapping(source="innerProperty.testProperty", target="testProperty")
	})
	DemoDTO toDemoDTO(DemoDO demoDO);
}

进阶使用

@Mapper
public interface CarMapper {

    @Mapping(source = "price", numberFormat = "$#.00")// 数字转字符串,format格式可以使用java.text.DecimalFormat能识别的格式
    @Mapping(source = "manufacturingDate", dateFormat = "dd.MM.yyyy")// 使用java.text.SimpleDateFormat可识别的格式
    @Mapping(target = "timeAndFormat",
         expression = "java( new org.sample.TimeAndFormat( s.getTime(), s.getFormat() ) )")// 使用别的包里的方法格式化时间,这个属性可以让我们可以做到一些骚操作。
    @Mapping(source="car", target="name", qualifiedByName="joinName")// 这个可以让我们实现对某一属性特殊处理,比如两个属性值合并到一个属性上
    CarDto carToCarDto(Car car);

    @Named("joinName")
    default String joinName(Car car) {
	String firstName = car.getFirstName() == null ? "" : car.getFirstName();
	String lastName = car.getLastName() == null ? "" : car.getLastName(); 
	return String.join(" ", firstName, lastName);
    }
}

甚至如果业务复杂还可以将某个方法覆写掉。

使用一个抽象类实现Convertor的接口,同时Convertor类也要加一个注解@DecoratedWith(Decorator.class)

@Mapper(componentModel = "spring")
@DecoratedWith(DemoConvertorDecorator.class)
public interface DemoConvertor {

	@Mappings({
		@Mapping(source="innerProperty.testProperty", target="testProperty")
	})
	DemoDTO toDemoDTO(DemoDO demoDO);
}

  • 使用spring模式
public abstract class DemoConvertorDecorator implements DemoConvertor {

    @Autowired
    @Qualifier("delegate")
    private DemoConvertor delegate;

    @Override
    public DemoDTO toDemoDTO(DemoDO demoDO) {
        DemoDTO dto = delegate.toDemoDTO(demoDO);
	// 做业务处理
        return dto;
    }
}
  • 使用default模式

使用默认模式需要一个属性名为delegate的单参构造函数。

public abstract class DemoConvertorDecorator implements DemoConvertor {

    private final DemoConvertor delegate;

    public DemoConvertorDecorator(DemoConvertor delegate) {
	this.delegate = delegate;
    }

    @Override
    public DemoDTO toDemoDTO(DemoDO demoDO) {
        DemoDTO dto = delegate.toDemoDTO(demoDO);
	// 做业务处理
        return dto;
    }
}

这里需要注意的是如果有其他方法需要用到toDemoDTO这个被覆写的方法(比如toDemoDTOList),那么这个方法也要在DemoConvertorDecorator这个抽象类里覆写,并调用覆写的toDemoDTO方法。这也是我使用时踩到的一个坑,看了生成的代码才明白,生成的toDemoDTOList方法调用的依旧是生成方法里的toDemoDTO,而这个方法是最先调用的,我们覆写的方法是之后才会调用的。

我们看下生成的代码就会明白了,它这里采用的是装饰器的设计模式。

生成的代码

// Generate Code
@Component
@Qualifier("delegate")
public class DemoConvertorImpl_ implements DemoConvertor {
    
    @Override
    public DemoDTO toDemoDTO(DemoDO demoDO) {
        if ( demoDO == null ) {
            return null;
        }

        DemoDTO dto = new DemoDTO();

        dto.setName( demoDO.getName() );
        dto.setAge( demoDO.getAge() );
        if(demoDO.getInnerProperty() != null) {
		        dto.setTestProperty( demoDO.getInnerProperty().getTestProperty() );        		
        } 

        return dto;
    }

    @Override
    public List<DemoDTO> toDemoDTOList(List<DemoDO> doList) {
        if ( doList == null ) {
            return null;
        }

        List<DemoDTO> list = new ArrayList<DemoDTO>( doList.size() );
        for ( DemoDO demoDO : dtoList ) {
            list.add( toDemoDTO( demoDO ) );
        }

        return list;
    }
}
// Generate Code
@Component
@Primary
public class DemoConvertorImpl extends DemoConvertorDecorator implements DemoConvertor {

    @Autowired
    @Qualifier("delegate")
    private MappConvertor delegate;

    @Override
    public List<DemoDTO> toDemoDTOList(List<DemoDO> doList) {
	return return delegate.toDemoDTOList( doList );
    }
}

可以看到在DemoConvertorImpl有一个@Primary注解,也就是我们调用方法时使用的是这个实现类的方法,而这个类的的toDemoDTOList中调用的是DemoConvertorImpl_里的toDemoDTOList方法,而这个方法又调用的是本类里的toDemoDTO方法。事实上DemoConvertorImpl_这个类由于是最基础的实现类,它并不会知道它实现的方法所调用的方法会被覆写,因此我们如果需要toDemoDTOList达到与toDemoDTO一样的输出结果就需要将toDemoDTOList也覆写,让它调用覆写的toDemoDTO方法。

附录

附上MapStruct的官方文档,了解更多的用法,本文仅提供了一个对象属性映射的方式,如果需要深入了解,请参考官方文档。