什么是MapStruct?
MapStruct是一个代码生成器,它可以根据约定优于配置的原则,大大简化Java Bean类型之间的映射实现。生成的映射代码使用普通的方法调用,因此速度快、类型安全、易于理解。
为什么要用MapStruct?
多层次的应用程序通常需要在不同的对象模型之间进行映射(例如实体和DTO)。编写这样的映射代码是一项繁琐和容易出错的任务。MapStruct旨在通过尽可能多地自动化这项工作来简化这项工作。与其他映射框架不同,MapStruct在编译时生成Bean映射,这可以确保高性能、快速的开发者反馈和彻底的错误检查。
MapStruct有哪些特点?
MapStruct除了提供基本的属性映射功能外,还有许多其他特点,例如:
- 支持嵌套属性、集合、数组、枚举、日期和时间等类型的映射
- 支持自定义转换器、装饰器、表达式和默认值等扩展机制
- 支持与Spring、CDI、JSR 330等依赖注入框架集成
- 支持与Lombok、Immutables等值对象框架集成
- 支持增量更新和构建器模式等高级功能
MapStruct有哪些优势?
MapStruct最大的优势就是性能。由于它在编译时生成映射代码,所以避免了运行时反射或其他开销。有人做过性能测试,结果显示MapStruct的性能远远高于BeanUtils等其他映射框架,这应该是大佬使用MapStruct的主要原因。
MapStruct还有其他优势,例如:
- 易于使用。只需要定义一个接口,就可以让MapStruct生成映射代码,无需编写复杂的XML配置或注解。
- 易于理解。生成的代码是普通的Java代码,可以直接阅读和调试,无需猜测框架的内部逻辑。
- 易于维护。生成的代码是类型安全的,可以在编译时检查错误,避免运行时异常。如果源或目标类型发生变化,只需要重新编译即可更新映射代码。
如何使用MapStruct?
MapStruct是一个注解处理器,它可以插入到Java编译器中,并且可以在命令行构建(Maven、Gradle等)以及从你喜欢的IDE中使用。MapStruct使用合理的默认值,但在需要配置或实现特殊行为时会让你自己决定。下面是一个简单的例子,展示了如何使用MapStruct定义一个Mapper接口。
@Mapper
public interface CarMapper {
CarMapper INSTANCE = Mappers.getMapper( CarMapper.class );
@Mapping(source = "numberOfSeats", target = "seatCount")
CarDto carToCarDto(Car car);
}
这个接口定义了一个从Car到CarDto的映射方法,并且指定了一个属性映射规则。MapStruct会在编译时生成这个接口的实现类,如下所示:
public class CarMapperImpl implements CarMapper {
@Override
public CarDto carToCarDto(Car car) {
if ( car == null ) {
return null;
}
CarDto carDto = new CarDto();
carDto.setSeatCount( car.getNumberOfSeats() );
carDto.setMake( car.getMake() );
return carDto;
}
}
可以看到,生成的代码是简单明了的,没有任何反射或其他魔法。我们可以直接调用这个实现类来执行映射操作:
Car car = new Car("Morris", 5, CarType.SEDAN);
CarDto carDto = CarMapper.INSTANCE.carToCarDto(car);
这种方式很方便,但也有一些限制。首先,它要求我们的Mapper接口必须有一个无参构造器,否则Mappers.getMapper会抛出异常。其次,它不能与依赖注入框架(如Spring)集成,因为它不会考虑到其他Bean的注入。最后,它可能会导致内存泄漏,因为它会缓存所有创建过的Mapper实例,而不会释放它们。
因此,如果我们想要更灵活和安全地使用MapStruct,我们可以考虑使用其他方式来获取或注入Mapper实例。例如:
- 使用@ComponentModel注解来指定生成的实现类为Spring组件,并且使用@Autowired来注入它们
- 使用@Named或@Qualifier注解来指定生成的实现类的名称,并且使用@Qualifier来注入它们
- 使用@InheritConfiguration或@InheritInverseConfiguration注解来复用其他Mapper接口中定义的映射配置,并且使用@Uses来注入它们
MapStruct中的自动类型转换
MapStruct在很多情况下可以自动处理类型转换。例如,如果源Bean中的一个属性是int类型,而目标Bean中的对应属性是String类型,那么生成的代码会透明地执行转换,分别调用String#valueOf(int)和Integer#parseInt(String)方法。
MapStruct支持以下一些常见的自动类型转换:
- 基本类型和对应的包装类之间的转换
- 基本类型和String之间的转换
- 枚举和String之间的转换
- 日期和时间相关的类型之间的转换(如Date、Calendar、LocalDate等)
- 集合和数组之间的转换
MapStruct在进行自动类型转换时,也会考虑到空值的情况。例如,如果源Bean中的一个属性是null,那么目标Bean中的对应属性也会被设置为null,而不会抛出空指针异常。
如果MapStruct不能自动处理某种类型转换,那么它会在编译时报错,并提示需要提供一个自定义的转换方法或表达式。我们可以使用@Mapping注解来指定如何进行自定义的类型转换,例如:
@Mapper
public interface CarMapper {
CarMapper INSTANCE = Mappers.getMapper( CarMapper.class );
@Mapping(source = "numberOfSeats", target = "seatCount")
@Mapping(source = "manufacturingDate", target = "manufacturingYear", dateFormat = "yyyy")
CarDto carToCarDto(Car car);
}
这里我们使用dateFormat属性来指定如何将Date类型的manufacturingDate属性转换为String类型的manufacturingYear属性。MapStruct会根据这个属性生成相应的格式化代码。
MapStruct框架相关注解的示例
为了更好地理解MapStruct框架相关注解的用法,我们可以参考一些示例代码。下面是一些常用的注解的示例:
- @Mapper:定义一个Mapper接口,指定生成的实现类为Spring组件,并且使用其他Mapper接口作为依赖。
@Mapper(componentModel = "spring", uses = {DateMapper.class, AddressMapper.class})
public interface PersonMapper {
PersonDto personToPersonDto(Person person);
Person personDtoToPerson(PersonDto personDto);
}
- @Mapping:定义一个映射方法,指定源对象和目标对象的属性映射规则,并且使用自定义的表达式来进行映射。
@Mapper
public interface CarMapper {
@Mapping(source = "numberOfSeats", target = "seatCount")
@Mapping(source = "manufacturingDate", target = "manufacturingYear", dateFormat = "yyyy")
@Mapping(target = "color", expression = "java(car.getColor().toUpperCase())")
CarDto carToCarDto(Car car);
}
- @Mappings:定义一个映射方法,指定多个属性映射规则。
@Mapper
public interface OrderMapper {
@Mappings({
@Mapping(source = "customer.name", target = "customerName"),
@Mapping(source = "customer.billingAddress", target = "billingAddress"),
@Mapping(source = "customer.shippingAddress", target = "shippingAddress"),
@Mapping(source = "items", target = "orderItems")
})
OrderDto orderToOrderDto(Order order);
}
- @InheritConfiguration:定义一个映射方法,继承另一个映射方法中定义的映射配置。
@Mapper
public interface CarMapper {
@Mapping(source = "numberOfSeats", target = "seatCount")
CarDto carToCarDto(Car car);
// inherit the configuration from carToCarDto()
@InheritConfiguration
void updateCarFromCarDto(CarDto carDto, @MappingTarget Car car);
}
- @InheritInverseConfiguration:定义一个映射方法,继承并反转另一个映射方法中定义的映射配置。
@Mapper
public interface CarMapper {
@Mapping(source = "numberOfSeats", target = "seatCount")
CarDto carToCarDto(Car car);
// inherit and reverse the configuration from carToCarDto()
@InheritInverseConfiguration
Car carDtoToCar(CarDto carDto);
}
- @IterableMapping:定义一个集合类型之间的映射方法或参数,指定目标集合元素类型,并且使用自定义的转换器方法名称。
@Mapper
public interface BookMapper {
// specify the element type and the converter method name for the list mapping
@IterableMapping(elementTargetType = BookDto.class, qualifiedByName = "bookToBookDto")
List<BookDto> booksToBookDtos(List<Book> books);
// specify the element type and the converter method name for the set mapping
@IterableMapping(elementTargetType = Book.class, qualifiedByName = "bookDtoToBook")
Set<Book> bookDtosToBooks(Set<BookDto> bookDtos);
// define the converter methods with custom names
@Named("bookToBookDto")
BookDto bookToBookDto(Book book);
@Named("bookDtoToBook")
Book bookDtoToBook(BookDto bookDto);
}
- @MapMapping:定义一个Map类型之间的映射方法或参数,指定目标Map键值类型,并且使用自定义的转换器方法名称。
@Mapper
public interface DictionaryMapper {
// specify the key and value types and the converter method names for the map mapping
@MapMapping(keyTargetType = String.class, valueTargetType = String.class, keyQualifiedByName = "wordToWordDto", valueQualifiedByName = "definitionToDefinitionDto")
Map<String, String> dictionaryToDictionaryDto(Map<Word, Definition> dictionary);
// specify the key and value types and the converter method names for the map mapping
@MapMapping(keyTargetType = Word.class, valueTargetType = Definition.class, keyQualifiedByName = "wordDtoToWord", valueQualifiedByName = "definitionDtoToDefinition")
Map<Word, Definition> dictionaryDtoToDictionary(Map<String, String> dictionaryDto);
// define the converter methods with custom names
@Named("wordToWordDto")
String wordToWordDto(Word word);
@Named("wordDtoToWord")
Word wordDtoToWord(String wordDto);
@Named("definitionToDefinitionDto")
String definitionToDefinitionDto(Definition definition);
@Named("definitionDtoToDefinition")
Definition definitionDtoToDefinition(String definitionDto);
}
- @BeanMapping:定义一个Bean类型之间的映射方法,指定忽略所有未匹配的属性,并且指定忽略源对象中未匹配的属性列表。
@Mapper
public interface UserMapper {
// ignore all unmapped properties and ignore the password property from the source object
@BeanMapping(ignoreByDefault = true, ignoreUnmappedSourceProperties = {"password"})
@Mapping(source = "username", target = "name")
UserDto userToUserDto(User user);
}
使用嵌套属性、集合、数组等复杂类型的映射示例
为了更好地理解如何使用MapStruct进行嵌套属性、集合、数组等复杂类型的映射,我们可以参考一个示例代码。假设我们有以下几个类:
// 源类
public class Customer {
private String name;
private Address address;
private List<Order> orders;
private String[] hobbies;
// getters and setters
}
public class Address {
private String street;
private String city;
private String zipCode;
// getters and setters
}
public class Order {
private String id;
private BigDecimal amount;
// getters and setters
}
// 目标类
public class CustomerDto {
private String name;
private AddressDto addressDto;
private List<OrderDto> orderDtos;
private List<String> hobbies;
// getters and setters
}
public class AddressDto {
private String street;
private String city;
private String zipCode;
// getters and setters
}
public class OrderDto {
private String id;
private BigDecimal amount;
// getters and setters
}
我们可以看到,源类和目标类之间有以下几种复杂类型的映射:
- 嵌套属性:Customer的address属性需要映射到CustomerDto的addressDto属性,而不是直接复制。
- 集合:Customer的orders属性需要映射到CustomerDto的orderDtos属性,而不是直接复制。
- 数组:Customer的hobbies属性需要映射到CustomerDto的hobbies属性,而不是直接复制。
为了实现这些复杂类型的映射,我们需要定义以下几个Mapper接口:
@Mapper
public interface CustomerMapper {
CustomerDto customerToCustomerDto(Customer customer);
}
@Mapper
public interface AddressMapper {
AddressDto addressToAddressDto(Address address);
}
@Mapper
public interface OrderMapper {
OrderDto orderToOrderDto(Order order);
}
然后,我们需要在CustomerMapper接口上使用@Mapper注解的uses属性来指定使用其他Mapper接口作为依赖:
@Mapper(uses = {AddressMapper.class, OrderMapper.class})
public interface CustomerMapper {
CustomerDto customerToCustomerDto(Customer customer);
}
这样,MapStruct就会在生成CustomerMapperImpl类时,自动注入AddressMapper和OrderMapper的实例,并且调用它们的方法来进行嵌套属性和集合类型的映射。对于数组类型的映射,MapStruct会自动将数组转换为List,并且使用Arrays.asList和List.toArray方法来进行转换。
最后,我们可以看一下MapStruct生成的CustomerMapperImpl类:
public class CustomerMapperImpl implements CustomerMapper {
private final AddressMapper addressMapper = Mappers.getMapper(AddressMapper.class);
private final OrderMapper orderMapper = Mappers.getMapper(OrderMapper.class);
@Override
public CustomerDto customerToCustomerDto(Customer customer) {
if (customer == null) {
return null;
}
CustomerDto customerDto = new CustomerDto();
customerDto.setName(customer.getName());
customerDto.setAddressDto(addressMapper.addressToAddressDto(customer.getAddress()));
customerDto.setOrderDtos(orderListToOrderDtoList(customer.getOrders()));
customerDto.setHobbies(arrayListToStringList(customer.getHobbies()));
return customerDto;
}
protected List<OrderDto> orderListToOrderDtoList(List<Order> list) {
if (list == null) {
return null;
}
List<OrderDto> list1 = new ArrayList<OrderDto>(list.size());
for (Order order : list) {
list1.add(orderMapper.orderToOrderDto(order));
}
return list1;
}
protected List<String> arrayListToStringList(String[] array) {
if (array == null) {
return null;
}
List<String> list = new ArrayList<String>(array.length);
for (String string : array) {
list.add(string);
}
return list;
}
}
可以看到,MapStruct已经为我们生成了复杂类型的映射代码,而我们只需要定义简单的Mapper接口即可。
MapStruct与BeanUtils的比较
BeanUtils是Apache Commons提供的一个工具类,它可以用来复制Java Bean之间的属性。它的使用方法很简单,只需要调用BeanUtils.copyProperties方法,传入源对象和目标对象即可。例如:
Car car = new Car("Morris", 5, CarType.SEDAN);
CarDto carDto = new CarDto();
BeanUtils.copyProperties(car, carDto);
这种方式看起来很方便,但是它也有一些缺点:
- BeanUtils使用反射来复制属性,这会导致性能低下,尤其是当属性数量较多时。
- BeanUtils不能自动处理不同类型之间的转换,例如int和String之间的转换,需要我们手动进行转换或提供一个转换器。
- BeanUtils不能自动处理嵌套属性、集合、数组等复杂类型的映射,需要我们手动进行映射或提供一个映射器。
MapStruct是一个代码生成器,它可以根据我们定义的Mapper接口,自动生成映射代码。它的使用方法也很简单,只需要调用Mapper接口中定义的映射方法即可。例如:
@Mapper
public interface CarMapper {
CarMapper INSTANCE = Mappers.getMapper( CarMapper.class );
@Mapping(source = "numberOfSeats", target = "seatCount")
CarDto carToCarDto(Car car);
}
Car car = new Car("Morris", 5, CarType.SEDAN);
CarDto carDto = CarMapper.INSTANCE.carToCarDto(car);
这种方式相比BeanUtils有很多优点:
- MapStruct在编译时生成映射代码,使用普通的方法调用来复制属性,因此性能高效,无论属性数量多少。
- MapStruct可以自动处理不同类型之间的转换,例如int和String之间的转换,无需我们手动进行转换或提供一个转换器。
- MapStruct可以自动处理嵌套属性、集合、数组等复杂类型的映射,无需我们手动进行映射或提供一个映射器。
有人做过性能测试,结果显示MapStruct的性能远远高于BeanUtils,这应该是大佬使用MapStruct的主要原因。
可以看出随着属性个数的增加,BeanUtils的耗时也在增加,并且BeanUtils的耗时跟属性个数成正比,而MapStruct的耗时几乎不变。
总结
MapStruct是一个优秀的Java Bean映射框架,它可以帮助我们简化映射代码的编写和维护,提高性能和可读性。如果你还没有使用过MapStruct,不妨试试看,相信你会喜欢上它的。