MapStruct:Java Bean映射的简单之道

什么是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,不妨试试看,相信你会喜欢上它的。

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
MapStruct是一个Java Bean映射框架,它可以帮助开发人员轻松地处理Java Bean之间的转换。使用MapStruct,您可以定义一个映射接口,该接口将自动实现Bean之间的转换。在本文中,我将向您展示如何使用MapStruct处理烦人的Bean转换。 1. 添加MapStruct依赖 首先,您需要将MapStruct添加到您的项目中。您可以在Maven中添加以下依赖项: ```xml <dependency> <groupId>org.mapstruct</groupId> <artifactId>mapstruct</artifactId> <version>1.4.2.Final</version> </dependency> ``` 2. 创建映射接口 接下来,您需要定义一个映射接口来描述Bean之间的映射。这个接口应该只包含一些抽象方法,这些方法定义了如何从源Bean映射到目标Bean。 例如,假设您有两个Bean:`SourceBean`和`TargetBean`。您希望将`SourceBean`的属性映射到`TargetBean`的属性。那么你可以创建一个映射接口,如下所示: ```java @Mapper public interface SourceTargetMapper { TargetBean mapToTargetBean(SourceBean source); } ``` `@Mapper`注解告诉MapStruct这是一个映射接口。`mapToTargetBean`方法将源Bean映射到目标Bean。请注意,这个方法是抽象的,所以您需要提供一个实现。 3. 生成映射器 现在,您需要让MapStruct生成映射器实现。为此,您可以使用`MapperFactory`类。以下是一个示例: ```java MapperFactory mapperFactory = new DefaultMapperFactory.Builder().build(); SourceTargetMapper mapper = mapperFactory.getMapper(SourceTargetMapper.class); ``` 这将创建一个新的`SourceTargetMapper`实例,它将使用由MapStruct生成的映射器实现。 4. 执行映射 现在,您可以使用映射器将源Bean映射到目标Bean。以下是一个示例: ```java SourceBean source = new SourceBean(); source.setName("John"); source.setAge(30); TargetBean target = mapper.mapToTargetBean(source); System.out.println(target.getName()); // Output: John System.out.println(target.getAge()); // Output: 30 ``` 这将创建一个新的`SourceBean`实例,并将其映射到一个`TargetBean`实例。`TargetBean`实例将具有与`SourceBean`实例相同的属性值。 总结 MapStruct是一个非常有用的Java Bean映射框架,它可以帮助您轻松处理Bean之间的转换。通过定义一个映射接口,然后让MapStruct生成一个映射器实现,您可以快速、轻松地实现Bean之间的转换。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值