SpringBoot與MapStruct整合,解決復雜對象轉換中的類型安全漏洞問題
我們在寫多層架構時,數(shù)據(jù)傳輸對象(DTO)、實體類和其他業(yè)務對象之間的轉換是不可避免的。手動編寫這些映射邏輯不僅耗時而且容易出錯。為了提高開發(fā)效率和代碼質量,我們建議引入 MapStruct 作為我們的對象映射工具。
MapStruct 的優(yōu)勢
1. 編譯時檢查
- 類型安全: 編譯器會在編譯時檢查映射邏輯,確保源對象和目標對象的字段匹配。
- 錯誤報告: 如果映射出現(xiàn)問題,編譯器會立即報錯,便于調試和修復。
2. 高性能
- 無反射: 生成的映射代碼不依賴反射,執(zhí)行速度快。
- 零運行時開銷: 不需要額外的庫或運行時組件。
3. 清晰的配置
- 注解驅動: 使用簡潔的注解來定義映射規(guī)則,易于理解和維護。
- 默認行為: 提供合理的默認行為,減少樣板代碼。
4. 強大的功能支持
- 嵌套對象: 自動處理嵌套對象的映射。
- 集合映射: 支持列表、數(shù)組等多種集合類型的映射。
- 自定義方法: 可以通過自定義方法實現(xiàn)復雜的映射邏輯。
- 枚舉映射: 支持枚舉類型的映射。
代碼實操
<dependencies>
<!-- Spring Boot Starter Data JPA -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- MapStruct -->
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>1.5.3.Final</version>
</dependency>
<!-- MapStruct Processor -->
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>1.5.3.Final</version>
<scope>provided</scope>
</dependency>
<!-- Lombok (Optional, for reducing boilerplate code) -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>1.5.3.Final</version>
</path>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok-mapstruct-binding</artifactId>
<version>0.2.0</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
實體類 Address
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
@Entity
publicclass Address {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String street;
private String city;
// Getters and Setters (or use Lombok)
}
實體類 User
import javax.persistence.*;
import java.util.List;
@Entity
publicclass User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private Integer age;
private Boolean isActive;
@OneToOne(cascade = CascadeType.ALL)
private Address address;
@OneToMany(cascade = CascadeType.ALL)
private List<Address> addresses;
// Getters and Setters (or use Lombok)
}
DTO 類 AddressDto
public class AddressDto {
private Long id;
private String street;
private String city;
// Getters and Setters (or use Lombok)
}
枚舉類 Status
public enum Status {
ACTIVE,
INACTIVE
}
DTO 類 UserDto
import java.util.List;
publicclass UserDto {
private Long id;
private String name;
private Integer age;
private Status status;
private AddressDto address;
private List<AddressDto> addresses;
// Getters and Setters (or use Lombok)
}
創(chuàng)建兩個 Mapper 接口來分別處理
Address
到AddressDto
的映射和User
到UserDto
的映射。
AddressMapper 接口
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
@Mapper(componentModel = "spring")
public interface AddressMapper {
AddressMapper INSTANCE = Mappers.getMapper(AddressMapper.class);
AddressDto addressToAddressDto(Address address);
Address addressDtoToAddress(AddressDto addressDto);
}
UserMapper 接口
import org.mapstruct.*;
import org.mapstruct.factory.Mappers;
import java.util.List;
@Mapper(componentModel = "spring", uses = {AddressMapper.class})
public interface UserMapper {
UserMapper INSTANCE = Mappers.getMapper(UserMapper.class);
@Mapping(source = "address", target = "address")
@Mapping(source = "addresses", target = "addresses")
@Mapping(target = "status", source = "isActive", qualifiedByName = "activeToStatus")
@Mapping(target = "age", defaultValue = "18") // Default value if age is null
UserDto userToUserDto(User user);
@Mapping(source = "address", target = "address")
@Mapping(source = "addresses", target = "addresses")
@Mapping(target = "isActive", source = "status", qualifiedByName = "statusToActive")
User userDtoToUser(UserDto userDto);
@Named("activeToStatus")
default Status activeToStatus(Boolean isActive) {
return isActive != null && isActive ? Status.ACTIVE : Status.INACTIVE;
}
@Named("statusToActive")
default Boolean statusToActive(Status status) {
return status == Status.ACTIVE;
}
}
代碼解讀
- 默認值 (
defaultValue
):
- 在
UserMapper
中,@Mapping(target = "age", defaultValue = "18")
指定了如果User
對象中的age
屬性為null
,則默認值為18
。
- 條件映射 (
qualifiedByName
):
- 使用
@Named
注解定義了兩個方法activeToStatus
和statusToActive
,用于在User
和UserDto
之間進行狀態(tài)轉換。 - 這些方法通過
@Mapping(target = ..., source = ..., qualifiedByName = ...)
被引用。
- 自定義方法:
- 自定義方法
activeToStatus
和statusToActive
直接在UserMapper
接口中實現(xiàn),提供了靈活的數(shù)據(jù)轉換邏輯。
- 枚舉映射:
- 通過自定義方法將布爾類型的
isActive
字段映射到枚舉類型的Status
字段,并反之。
常見問題與解決方案
MapStruct 是否支持循環(huán)引用?
MapStruct 默認不支持循環(huán)引用,但可以通過自定義方法或使用 @AfterMapping
注解來處理循環(huán)引用的情況。
如何處理不同命名的屬性?
使用 @Mapping
注解中的 source
和 target
屬性來指定不同的屬性名稱:
@Mapping(source = "entityPropertyName", target = "dtoPropertyName")
如何處理日期格式轉換?
使用 @Mapping
注解中的 dateFormat
屬性:
@Mapping(source = "dateField", dateFormat = "yyyy-MM-dd")
如何處理集合過濾?
使用 @IterableMapping
注解結合 qualifiedByName
或 qualifiedBy
來過濾集合:
@IterableMapping(qualifiedByName = "filterNulls")
List<String> filterStrings(List<String> strings);
@Named("filterNulls")
default String filterNulls(String string) {
return string != null ? string : "";
}