Java中MapStruct轉(zhuǎn)換實(shí)體的步驟實(shí)現(xiàn)
前幾天在代碼中看到有人用UnitChangeConvert.INSTANCE.dtoToBO(processVo),感覺(jué)非常帥。這只是一個(gè)接口,并沒(méi)有給出實(shí)現(xiàn)類(lèi),就給實(shí)體轉(zhuǎn)換了,省去了我們自己寫(xiě)轉(zhuǎn)換類(lèi)的時(shí)間,今天摸魚(yú)中了解了下:MapStruct 是一個(gè)專(zhuān)注于「Java Bean 之間屬性映射」的代碼生成器,與 Spring 原生轉(zhuǎn)換、MyBatis-Plus 類(lèi)型轉(zhuǎn)換器是完全不同的工具,解決的是「對(duì)象與對(duì)象之間的屬性拷貝」問(wèn)題(如 DTO ↔ POJO、VO ↔ Entity),而非參數(shù) / 數(shù)據(jù)庫(kù)字段的類(lèi)型轉(zhuǎn)換。
下面從 核心定位、與其他轉(zhuǎn)換工具的區(qū)別、使用場(chǎng)景、快速上手、高級(jí)特性 五個(gè)維度,幫你理清 MapStruct 的核心價(jià)值和用法:
一、核心定位:Java Bean 映射的 “代碼生成神器”
日常開(kāi)發(fā)中,我們經(jīng)常需要在不同層級(jí)的 Java Bean 之間拷貝屬性(如:
- Controller 接收前端
UserDTO→ 服務(wù)層轉(zhuǎn)換為UserEntity存入數(shù)據(jù)庫(kù); - 數(shù)據(jù)庫(kù)查詢(xún)出
UserEntity→ 轉(zhuǎn)換為UserVO返回給前端)。
傳統(tǒng)做法是手動(dòng)寫(xiě) setter 拷貝(繁瑣易出錯(cuò)),或用 BeanUtils.copyProperties(反射效率低、字段名不一致需手動(dòng)處理、類(lèi)型轉(zhuǎn)換不靈活)。
MapStruct 的核心解決思路:
- 基于「注解 + 接口」定義映射規(guī)則;
- 編譯期自動(dòng)生成 類(lèi)型安全、無(wú)反射、高效 的映射實(shí)現(xiàn)類(lèi)(純 Java 代碼,不是運(yùn)行時(shí)動(dòng)態(tài)處理);
- 支持字段名映射、類(lèi)型自動(dòng)轉(zhuǎn)換、自定義轉(zhuǎn)換邏輯、依賴(lài)注入等。
二、與其他 “轉(zhuǎn)換工具” 的核心區(qū)別
你之前接觸的工具和 MapStruct 適用場(chǎng)景完全不同,用表格清晰對(duì)比:
| 工具 / 框架 | 核心用途 | 適用場(chǎng)景 | 底層實(shí)現(xiàn) |
|---|---|---|---|
| MapStruct | Java Bean ↔ Java Bean(屬性拷貝) | DTO↔Entity、VO↔POJO(業(yè)務(wù)層) | 編譯期生成 Java 代碼(無(wú)反射) |
| Spring 原生 Converter | 基礎(chǔ)類(lèi)型 / 簡(jiǎn)單對(duì)象轉(zhuǎn)換(如 String→Integer) | Web 參數(shù)轉(zhuǎn)換、配置注入(Web / 配置層) | 接口實(shí)現(xiàn) + Spring 容器管理 |
| MyBatis-Plus 轉(zhuǎn)換器 | 數(shù)據(jù)庫(kù)字段 ↔ Java 實(shí)體(ORM 層) | 數(shù)據(jù)庫(kù)存儲(chǔ)類(lèi)型→Java 類(lèi)型 | MyBatis 類(lèi)型處理器接口 |
| BeanUtils(Spring/Commons) | 快速屬性拷貝 | 簡(jiǎn)單場(chǎng)景(字段名完全一致) | 運(yùn)行時(shí)反射(效率低) |
一句話區(qū)分:
- 要拷貝兩個(gè) Java Bean 的屬性 → 用 MapStruct;
- 要把 String 轉(zhuǎn) Integer / 枚舉(非 Bean 拷貝) → 用 Spring
Converter; - 要把數(shù)據(jù)庫(kù)字段轉(zhuǎn) Java 實(shí)體 → 用 MyBatis-Plus 轉(zhuǎn)換器。
三、MapStruct 快速上手(Spring Boot 環(huán)境)
步驟 1:引入依賴(lài)(Maven)
<!-- MapStruct 核心依賴(lài) -->
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>1.5.5.Final</version> <!-- 推薦穩(wěn)定版 -->
</dependency>
?
<!-- 編譯期代碼生成依賴(lài)(必須,否則不會(huì)生成實(shí)現(xiàn)類(lèi)) -->
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>1.5.5.Final</version>
<scope>provided</scope>
</dependency>
?
<!-- 若用 Lombok,需確保 MapStruct 能識(shí)別 Lombok 生成的 getter/setter -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
<scope>provided</scope>
</dependency>
?
<!-- Lombok 與 MapStruct 兼容依賴(lài)(Java 11+ 可能需要) -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok-mapstruct-binding</artifactId>
<version>0.2.0</version>
<scope>provided</scope>
</dependency>
步驟 2:定義待映射的 Java Bean
假設(shè)存在「用戶(hù)實(shí)體」和「用戶(hù) DTO」(字段名部分不一致,類(lèi)型不同):
// 數(shù)據(jù)庫(kù)實(shí)體(POJO)
@Data
@NoArgsConstructor
@AllArgsConstructor
public class UserEntity {
private Long id; // 主鍵
private String userName; // 用戶(hù)名(字段名:userName)
private Integer age; // 年齡(int 類(lèi)型)
private LocalDate birth; // 生日(LocalDate 類(lèi)型)
private String phone; // 手機(jī)號(hào)
}
?
// 前端傳輸對(duì)象(DTO)
@Data
@NoArgsConstructor
@AllArgsConstructor
public class UserDTO {
private Long userId; // 主鍵(字段名:userId,與實(shí)體 id 對(duì)應(yīng))
private String name; // 用戶(hù)名(字段名:name,與實(shí)體 userName 對(duì)應(yīng))
private String age; // 年齡(String 類(lèi)型,與實(shí)體 int 對(duì)應(yīng))
private String birth; // 生日(String 類(lèi)型,格式:yyyy-MM-dd)
private String phoneNum; // 手機(jī)號(hào)(字段名:phoneNum,與實(shí)體 phone 對(duì)應(yīng))
}
步驟 3:定義 MapStruct 映射接口
用 @Mapper 注解(MapStruct 的注解,非 MyBatis 的 @Mapper?。┒x映射規(guī)則,componentModel = "spring" 表示讓 Spring 管理映射器實(shí)例(支持依賴(lài)注入):
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.Mappings;
import org.mapstruct.factory.Mappers;
?
// 關(guān)鍵注解:componentModel="spring" → Spring 管理該映射器
@Mapper(componentModel = "spring", imports = {LocalDate.class, DateTimeFormatter.class})
public interface UserConvert {
?
// 方式 1:Spring 依賴(lài)注入用(無(wú)需手動(dòng)創(chuàng)建實(shí)例)
// 方式 2:非 Spring 環(huán)境用(手動(dòng)獲取實(shí)例)
// UserConvert INSTANCE = Mappers.getMapper(UserConvert.class);
?
/**
* DTO → 實(shí)體(核心映射方法)
* @Mapping:指定字段映射規(guī)則(字段名不一致、類(lèi)型轉(zhuǎn)換、格式轉(zhuǎn)換)
*/
@Mappings({
// DTO 的 userId → 實(shí)體的 id(字段名不一致)
@Mapping(source = "userId", target = "id"),
// DTO 的 name → 實(shí)體的 userName(字段名不一致)
@Mapping(source = "name", target = "userName"),
// DTO 的 age(String)→ 實(shí)體的 age(int):自動(dòng)類(lèi)型轉(zhuǎn)換(MapStruct 內(nèi)置支持)
@Mapping(source = "age", target = "age"),
// DTO 的 birth(String)→ 實(shí)體的 birth(LocalDate):自定義格式轉(zhuǎn)換
@Mapping(
source = "birth",
target = "birth",
dateFormat = "yyyy-MM-dd" // 指定日期格式
),
// DTO 的 phoneNum → 實(shí)體的 phone(字段名不一致)
@Mapping(source = "phoneNum", target = "phone")
})
UserEntity dtoToEntity(UserDTO userDTO);
?
/**
* 實(shí)體 → DTO(反向映射,字段名對(duì)應(yīng)規(guī)則與正向一致)
* 可復(fù)用正向映射規(guī)則,用 @InheritInverseConfiguration 簡(jiǎn)化
*/
@InheritInverseConfiguration(name = "dtoToEntity")
@Mapping(
source = "birth",
target = "birth",
dateFormat = "yyyy-MM-dd" // 反向轉(zhuǎn)換也需指定日期格式
)
UserDTO entityToDto(UserEntity userEntity);
?
/**
* 自定義轉(zhuǎn)換邏輯(如特殊格式處理、復(fù)雜計(jì)算)
* 若 MapStruct 內(nèi)置轉(zhuǎn)換不滿(mǎn)足,可定義默認(rèn)方法(default)
*/
default Integer stringToAge(String ageStr) {
if (ageStr == null || ageStr.trim().isEmpty()) {
return 0; // 空值默認(rèn) 0
}
try {
return Integer.parseInt(ageStr.trim());
} catch (NumberFormatException e) {
return 0; // 格式錯(cuò)誤默認(rèn) 0
}
}
?
// 反向轉(zhuǎn)換:Integer → String
default String ageToString(Integer age) {
return age == null ? "" : String.valueOf(age);
}
}
步驟 4:編譯項(xiàng)目,查看生成的實(shí)現(xiàn)類(lèi)
MapStruct 會(huì)在 編譯期 生成 UserConvertImpl 實(shí)現(xiàn)類(lèi)(無(wú)需手動(dòng)寫(xiě)),路徑在 target/generated-sources/annotations/ 下,核心邏輯如下(自動(dòng)生成的純 Java 代碼):
// 自動(dòng)生成的實(shí)現(xiàn)類(lèi)(Spring 管理)
@Component
public class UserConvertImpl implements UserConvert {
?
@Override
public UserEntity dtoToEntity(UserDTO userDTO) {
if (userDTO == null) {
return null;
}
UserEntity userEntity = new UserEntity();
userEntity.setId(userDTO.getUserId());
userEntity.setUserName(userDTO.getName());
// 調(diào)用自定義的 stringToAge 方法轉(zhuǎn)換類(lèi)型
userEntity.setAge(stringToAge(userDTO.getAge()));
// 自動(dòng)按指定格式轉(zhuǎn)換 String → LocalDate
if (userDTO.getBirth() != null) {
userEntity.setBirth(LocalDate.parse(userDTO.getBirth(), DateTimeFormatter.ofPattern("yyyy-MM-dd")));
}
userEntity.setPhone(userDTO.getPhoneNum());
return userEntity;
}
?
@Override
public UserDTO entityToDto(UserEntity userEntity) {
// 類(lèi)似正向轉(zhuǎn)換邏輯,自動(dòng)處理字段映射和類(lèi)型轉(zhuǎn)換
}
?
// 自動(dòng)生成自定義方法的調(diào)用邏輯...
}
步驟 5:在 Spring 中使用映射器
通過(guò) @Autowired 注入 UserConvert,直接調(diào)用映射方法:
@Service
public class UserService {
?
@Autowired
private UserConvert userConvert; // 注入 MapStruct 映射器
?
public void addUser(UserDTO userDTO) {
// 1. DTO → 實(shí)體(自動(dòng)完成字段映射和類(lèi)型轉(zhuǎn)換)
UserEntity userEntity = userConvert.dtoToEntity(userDTO);
// 2. 存入數(shù)據(jù)庫(kù)(假設(shè)用 MyBatis 操作)
// userMapper.insert(userEntity);
System.out.println("轉(zhuǎn)換后的實(shí)體:" + userEntity);
}
?
public UserDTO getUserById(Long id) {
// 1. 從數(shù)據(jù)庫(kù)查詢(xún)實(shí)體
UserEntity userEntity = new UserEntity(id, "張三", 25, LocalDate.of(2000, 1, 1), "13800138000");
// 2. 實(shí)體 → DTO(反向轉(zhuǎn)換)
UserDTO userDTO = userConvert.entityToDto(userEntity);
return userDTO;
}
}
測(cè)試效果:
// 測(cè)試 DTO → 實(shí)體 UserDTO dto = new UserDTO(1L, "張三", "25", "2000-01-01", "13800138000"); UserEntity entity = userConvert.dtoToEntity(dto); // 輸出:UserEntity(id=1, userName=張三, age=25, birth=2000-01-01, phone=13800138000) ? // 測(cè)試實(shí)體 → DTO UserEntity entity = new UserEntity(1L, "張三", 25, LocalDate.of(2000, 1, 1), "13800138000"); UserDTO dto = userConvert.entityToDto(entity); // 輸出:UserDTO(userId=1, name=張三, age=25, birth=2000-01-01, phoneNum=13800138000)
四、MapStruct 核心注解與高級(jí)特性
1. 核心注解(常用)
| 注解 | 作用 | 示例 |
|---|---|---|
| @Mapper | 標(biāo)識(shí)映射接口,指定組件模型(如 Spring) | @Mapper(componentModel = "spring") |
| @Mapping | 單個(gè)字段映射規(guī)則 | @Mapping(source = "name", target = "userName") |
| @Mappings | 多個(gè) @Mapping 的集合 | 包裹多個(gè) @Mapping 注解 |
| @InheritInverseConfiguration | 繼承反向映射規(guī)則(避免重復(fù)寫(xiě)注解) | @InheritInverseConfiguration(name = "dtoToEntity") |
| @MappingTarget | 映射到已存在的對(duì)象(更新屬性) | void updateEntity(UserDTO dto, @MappingTarget UserEntity entity) |
| @Named | 命名自定義轉(zhuǎn)換方法(用于復(fù)雜映射) | @Named("stringToAge") default Integer stringToAge(String s) {} |
2. 高級(jí)特性
(1)集合映射(自動(dòng)支持 List/Set/Array)
MapStruct 會(huì)自動(dòng)為集合生成映射方法,無(wú)需手動(dòng)定義:
@Mapper(componentModel = "spring")
public interface UserConvert {
// 單個(gè)對(duì)象映射(已定義)
UserEntity dtoToEntity(UserDTO dto);
UserDTO entityToDto(UserEntity entity);
?
// 集合映射(自動(dòng)生成,無(wú)需手動(dòng)寫(xiě)實(shí)現(xiàn))
List<UserEntity> dtoListToEntityList(List<UserDTO> dtoList);
Set<UserDTO> entitySetToDtoSet(Set<UserEntity> entitySet);
}
(2)自定義轉(zhuǎn)換邏輯(復(fù)雜場(chǎng)景)
若內(nèi)置轉(zhuǎn)換不滿(mǎn)足(如枚舉轉(zhuǎn)換、對(duì)象嵌套),可定義 default 方法或單獨(dú)的轉(zhuǎn)換類(lèi):
// 示例:枚舉轉(zhuǎn)換
public enum GenderEnum {
MALE(1, "男"), FEMALE(2, "女");
// getter/setter/靜態(tài)方法...
}
?
// DTO 中 gender 是 String 類(lèi)型("男"/"女"),實(shí)體中是 GenderEnum
@Mapping(
source = "gender",
target = "gender",
qualifiedByName = "genderStrToEnum" // 指定自定義轉(zhuǎn)換方法
)
UserEntity dtoToEntity(UserDTO dto);
?
// 自定義枚舉轉(zhuǎn)換方法(用 @Named 標(biāo)識(shí))
@Named("genderStrToEnum")
default GenderEnum genderStrToEnum(String genderStr) {
if ("男".equals(genderStr)) return GenderEnum.MALE;
if ("女".equals(genderStr)) return GenderEnum.FEMALE;
return null;
}
(3)依賴(lài)注入其他服務(wù)
因 componentModel = "spring",映射器可注入 Spring Bean(如業(yè)務(wù)服務(wù)、工具類(lèi)):
@Mapper(componentModel = "spring")
public interface UserConvert {
?
@Autowired
UserService userService; // 注入 Spring 服務(wù)
?
@Mapping(
source = "userId",
target = "deptId",
qualifiedByName = "getDeptIdByUserId" // 調(diào)用注入的服務(wù)
)
UserEntity dtoToEntity(UserDTO dto);
?
@Named("getDeptIdByUserId")
default Long getDeptIdByUserId(Long userId) {
// 調(diào)用業(yè)務(wù)服務(wù)獲取部門(mén)ID(復(fù)雜邏輯)
return userService.getDeptIdByUserId(userId);
}
}
(4)日期 / 數(shù)字格式轉(zhuǎn)換
通過(guò) dateFormat/numberFormat 指定格式:
// 日期格式 @Mapping(source = "birth", target = "birth", dateFormat = "yyyy-MM-dd HH:mm:ss") // 數(shù)字格式(如保留2位小數(shù)) @Mapping(source = "amount", target = "amount", numberFormat = "#.00") UserEntity dtoToEntity(UserDTO dto);
五、注意事項(xiàng)
編譯期生成代碼:
- 必須引入
mapstruct-processor依賴(lài),否則不會(huì)生成實(shí)現(xiàn)類(lèi),運(yùn)行時(shí)會(huì)報(bào)「找不到實(shí)現(xiàn)類(lèi)」錯(cuò)誤; - 若用 IDEA,需開(kāi)啟「Annotation Processing」(Settings → Build → Compiler → Annotation Processors),否則 IDEA 可能不識(shí)別生成的類(lèi)。
- 必須引入
與 Lombok 兼容:
- 確保 Lombok 版本 ≥ 1.18.16,MapStruct 版本 ≥ 1.4.2;
- 若字段無(wú) getter/setter(如用
@Data生成),MapStruct 無(wú)法識(shí)別字段,需確保 Lombok 正確生成訪問(wèn)器。
字段名匹配規(guī)則:
- 默認(rèn)按「字段名相同」映射(忽略大小寫(xiě)?不,嚴(yán)格匹配);
- 若字段名是
userName(實(shí)體)和user_name(DTO),可開(kāi)啟unmappedTargetPolicy = ReportingPolicy.IGNORE忽略未映射字段,或手動(dòng)指定@Mapping。
總結(jié)
MapStruct 是 Java Bean 映射的最優(yōu)解,核心優(yōu)勢(shì):
- 類(lèi)型安全:編譯期檢查字段名、類(lèi)型是否匹配,避免運(yùn)行時(shí)錯(cuò)誤;
- 效率高:無(wú)反射,編譯期生成原生 Java 代碼,性能遠(yuǎn)超
BeanUtils; - 靈活:支持字段映射、類(lèi)型轉(zhuǎn)換、自定義邏輯、依賴(lài)注入;
- 簡(jiǎn)化代碼:無(wú)需手動(dòng)寫(xiě)
setter拷貝,注解驅(qū)動(dòng)開(kāi)發(fā)。
到此這篇關(guān)于Java中MapStruct轉(zhuǎn)換實(shí)體的步驟實(shí)現(xiàn)的文章就介紹到這了,更多相關(guān)Java MapStruct轉(zhuǎn)換實(shí)體內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
springboot從application.properties中注入list,?map方式
這篇文章主要介紹了springboot從application.properties中注入list,map方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-11-11
Druid數(shù)據(jù)庫(kù)連接池監(jiān)控使用及說(shuō)明
Druid是Java數(shù)據(jù)庫(kù)連接池,具有監(jiān)控和擴(kuò)展功能,性能好,自帶監(jiān)控頁(yè)面,支持密碼加密、SQL執(zhí)行日志等,Druid已經(jīng)在阿里巴巴部署了超過(guò)600個(gè)應(yīng)用2025-12-12
SpringBoot通過(guò)RedisTemplate執(zhí)行Lua腳本的方法步驟
這篇文章主要介紹了SpringBoot通過(guò)RedisTemplate執(zhí)行Lua腳本的方法步驟,本文給大家介紹的非常詳細(xì),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-02-02
Java程序數(shù)據(jù)庫(kù)連接滿(mǎn)問(wèn)題的排查指南
這篇文章主要介紹了Java應(yīng)用數(shù)據(jù)庫(kù)連接滿(mǎn)問(wèn)題的識(shí)別、診斷、原因排查及解決方案,提出通過(guò)監(jiān)控、代碼優(yōu)化、配置調(diào)整和預(yù)防措施,結(jié)合Arthas等工具,有效應(yīng)對(duì)和預(yù)防該問(wèn)題,需要的朋友可以參考下2025-07-07
詳解spring boot實(shí)現(xiàn)多數(shù)據(jù)源代碼實(shí)戰(zhàn)
本篇文章主要介紹了詳解spring boot實(shí)現(xiàn)多數(shù)據(jù)源代碼實(shí)戰(zhàn),小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-07-07
JAVA不可變類(lèi)(immutable)機(jī)制與String的不可變性(推薦)
這篇文章主要介紹了JAVA不可變類(lèi)(immutable)機(jī)制與String的不可變性(推薦)的相關(guān)資料,非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友可以參考下2016-08-08
SpringBoot結(jié)合kaptcha生成圖片驗(yàn)證碼詳解
這篇文章主要為大家詳細(xì)介紹了SpringBoot如何結(jié)合kaptcha實(shí)現(xiàn)圖片驗(yàn)證碼功能,文中的示例代碼講解詳細(xì),有需要的小伙伴可以參考一下2024-01-01
DoytoQuery中關(guān)于N+1查詢(xún)問(wèn)題解決方案詳解
這篇文章主要為大家介紹了DoytoQuery中關(guān)于N+1查詢(xún)問(wèn)題解決方案詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-12-12

