1. 什么是對象轉換和數據翻譯?
對象轉換
對象轉換是指將一種類型的對象(如數據庫實體 UserDO)轉換為另一種類型的對象(如前端響應對象 UserVO 或服務層 DTO)。例如,一個 UserDO 包含用戶 ID、姓名和部門 ID,我們需要將其轉換為 UserVO,包含 ID、姓名和部門名稱。這種轉換在分層架構(如 Controller、Service、DAO)中非常常見。
數據翻譯
數據翻譯是指將某個字段的值“翻譯”為另一個值。例如,UserVO 的 deptId 字段需要讀取數據庫中 DeptDO 的 name 字段,設置為 UserVO 的 deptName 字段。這種操作通常涉及關聯查詢或手動拼接。
為什么需要這些操作?
- 分層隔離:DO(Data Object)用于數據庫操作,VO(Value Object)用于前端響應,DTO(Data Transfer Object)用于服務層傳遞,各自職責不同。
- 數據格式化:前端需要友好的數據格式(如部門名稱而非 ID)。
- 性能優化:通過翻譯減少復雜 SQL 聯表查詢。
2. 對象轉換:MapStruct vs BeanUtils
項目中提供了兩種對象轉換工具:MapStruct 和 BeanUtils。我們先來對比它們的優缺點,然后分別展示使用方法。
2.1 MapStruct
MapStruct 是一個編譯時生成映射代碼的框架,通過注解(如 @Mapper 和 @Mapping)定義映射規則,生成高效的 getter/setter 調用代碼。
- 優點:
- 高性能:生成純 Java 代碼,避免反射開銷。
- 類型安全:編譯時檢查映射規則,減少運行時錯誤。
- 靈活性:支持復雜映射、自定義邏輯。
- 缺點:
- 需要編寫注解,配置稍復雜。
- 學習成本稍高,需了解 MapStruct 語法。
- 適用場景:高性能要求、復雜映射場景。
引入依賴
<dependencies><dependency><groupId>org.mapstruct</groupId><artifactId>mapstruct</artifactId><version>${mapstruct.version}</version></dependency></dependencies>
public class UserDO {private Long id;private String name;private Long deptId;public UserDO() {}public UserDO(Long id, String name, Long deptId) {this.id = id;this.name = name;this.deptId = deptId;}public Long getId() { return id; }public void setId(Long id) { this.id = id; }public String getName() { return name; }public void setName(String name) { this.name = name; }public Long getDeptId() { return deptId; }public void setDeptId(Long deptId) { this.deptId = deptId; }
}public class DeptDO {private Long id;private String name;public DeptDO() {}public DeptDO(Long id, String name) {this.id = id;this.name = name;}public Long getId() { return id; }public void setId(Long id) { this.id = id; }public String getName() { return name; }public void setName(String name) { this.name = name; }
}public class UserVO {private Long id;private String name;private String deptName;public UserVO() {}public UserVO(Long id, String name, String deptName) {this.id = id;this.name = name;this.deptName = deptName;}public Long getId() { return id; }public void setId(Long id) { this.id = id; }public String getName() { return name; }public void setName(String name) { this.name = name; }public String getDeptName() { return deptName; }public void setDeptName(String deptName) { this.deptName = deptName; }
}@Mapper
public interface UserConvert {UserConvert INSTANCE = Mappers.getMapper(UserConvert.class);@Mapping(source = "user.id", target = "id")@Mapping(source = "user.name", target = "name")@Mapping(source = "dept.name", target = "deptName")UserVO convert(UserDO user, DeptDO dept);default List<UserVO> convertList(List<UserDO> users, Map<Long, DeptDO> deptMap) {return users.stream().map(user -> convert(user, deptMap.get(user.getDeptId()))).toList();}
}
- UserDO:數據訪問層的用戶對象,包含用戶 ID、姓名和部門 ID
- DeptDO:數據訪問層的部門對象,包含部門 ID 和部門名稱
- UserVO:視圖層的用戶對象,包含用戶 ID、姓名和部門名稱(直接包含部門名稱而非部門 ID)
單個對象轉換(convert 方法)
- @Mapper 注解:告訴 MapStruct 這是一個轉換器接口,會自動生成實現類
- INSTANCE 常量:獲取 MapStruct 生成的實現類實例
- @Mapping 注解:定義字段映射規則
source = "user.id", target = "id"
:將 user 對象的 id 字段映射到 VO 的 id 字段source = "user.name", target = "name"
:將 user 對象的 name 字段映射到 VO 的 name 字段source = "dept.name", target = "deptName"
:將 dept 對象的 name 字段映射到 VO 的 deptName 字段
列表轉換(convertList 方法)
- 這是一個默認方法(Java 8 特性),提供了批量轉換的實現
- 使用 Java Stream API 遍歷 UserDO 列表
- 對于每個 UserDO,通過 deptId 從部門映射表中獲取對應的 DeptDO
- 調用單對象轉換方法 convert () 完成轉換
- 將轉換后的 UserVO 收集到新列表中返回
2.2 BeanUtils
BeanUtils(項目基于 Hutool 的 BeanUtil 封裝)通過反射復制字段,適合簡單場景。
- 優點:
- 簡單易用:字段名一致時無需額外配置。
- 封裝增強:支持 List、Page 轉換,允許 Consumer 自定義邏輯。
- 易替換:封裝一層便于切換實現(如換成 Spring 的 BeanUtils)。
- 缺點:
- 反射導致性能略低于 MapStruct。
- 不適合復雜字段映射。
- 適用場景:簡單映射、快速開發。
public class UserConvertBeanUtils {/*** 將用戶數據對象(UserDO)和部門數據對象(DeptDO)轉換為用戶視圖對象(UserVO)* <p>* 轉換邏輯:* 1. 先通過 BeanUtil 將 UserDO 自動轉換為 UserVO(自動映射 id、name 字段)* 2. 手動將 DeptDO 的 name 字段賦值給 UserVO 的 deptName 字段** @param user 用戶數據對象(包含 id、name、deptId)* @param dept 部門數據對象(包含 id、name,可為 null)* @return 轉換后的用戶視圖對象(UserVO),若 dept 為 null 則 deptName 為 null*/public static UserVO convert(UserDO user, DeptDO dept) {// 1. 自動轉換 UserDO -> UserVO(映射 id、name 字段)UserVO userVO = BeanUtil.toBean(user, UserVO.class);// 2. 手動填充部門名稱(若部門對象不為 null)if (dept != null) {userVO.setDeptName(dept.getName()); // 將 DeptDO 的 name 賦值給 UserVO 的 deptName}return userVO;}/*** 擴展轉換方法:支持在轉換后對 UserVO 進行自定義處理* <p>* 轉換邏輯:* 1. 繼承 convert() 方法的邏輯(自動映射 + 手動填充部門名稱)* 2. 通過 Consumer 對轉換后的 UserVO 進行額外處理(如屬性校驗、補充數據等)** @param user 用戶數據對象* @param dept 部門數據對象(可為 null)* @param consumer 自定義處理器,用于對 UserVO 進行后處理(如 set 其他屬性)* @return 處理后的用戶視圖對象*/public static UserVO convertWithConsumer(UserDO user, DeptDO dept, Consumer<UserVO> consumer) {// 先執行基礎轉換(自動映射 + 部門名稱填充)UserVO userVO = convert(user, dept);// 調用自定義處理器(若 consumer 不為 null)if (consumer != null) {consumer.accept(userVO); // 將 UserVO 傳入處理器進行額外處理}return userVO;}/*** 將用戶數據對象列表轉換為用戶視圖對象列表* <p>* 轉換邏輯:* 1. 遍歷用戶列表,逐個調用 convert() 方法進行轉換* 2. 通過部門 ID(deptId)從部門映射表(deptMap)中獲取對應的 DeptDO 對象** @param users 用戶數據對象列表(不可為 null)* @param deptMap 部門映射表(key=部門 ID,value=DeptDO 對象)* @return 轉換后的用戶視圖對象列表*/public static List<UserVO> convertList(List<UserDO> users, Map<Long, DeptDO> deptMap) {return users.stream() // 將列表轉換為流.map(user -> // 對每個用戶對象,根據 deptId 從 deptMap 中獲取部門對象,再調用 convert() 轉換convert(user, deptMap.get(user.getDeptId())) ).collect(Collectors.toList()); // 收集結果為列表}
}
輸入:
UserDO user = new UserDO(1L, "Alice", 10L);
DeptDO dept = new DeptDO(10L, "工程部");
Map<Long, DeptDO> deptMap = new HashMap<>();
deptMap.put(10L, new DeptDO(10L, "工程部"));
List<UserDO> users = Arrays.asList(user);
代碼:
// 簡單場景
UserVO userVO = UserConvertBeanUtils.convert(user, dept);
// 復雜場景(添加額外字段)
UserVO userVOWithConsumer = UserConvertBeanUtils.convertWithConsumer(user, dept, vo -> vo.setDeptName("自定義-" + vo.getDeptName()));
// 列表轉換
List<UserVO> userVOs = UserConvertBeanUtils.convertList(users, deptMap);
輸出:
// userVO: {id=1, name="Alice", deptName="工程部"}
// userVOWithConsumer: {id=1, name="Alice", deptName="自定義-工程部"}
// userVOs: [{id=1, name="Alice", deptName="工程部"}]
- BeanUtil.toBean 使用反射復制字段,適合字段名一致的場景。
- Consumer 提供靈活性,允許自定義字段處理。
- convertList 通過 Stream API 實現列表轉換。
性能對比:MapStruct 性能優于 BeanUtils,但相比數據庫操作的耗時,差距可忽略。因此,簡單場景推薦 BeanUtils,復雜場景推薦 MapStruct。
3. 數據翻譯:SQL 聯表 vs Java 拼接 vs easy-trans
數據翻譯是將一個字段(如 deptId)轉換為另一個字段(如 deptName)。項目提供三種方案:
- SQL 聯表查詢:通過 MyBatis 的關聯查詢直接獲取目標字段(如 DeptDO.name)。
- Java 拼接:多次單表查詢(如先查 UserDO,再查 DeptDO),在 Java 代碼中拼接。
- easy-trans 框架:通過注解(如 @Trans)自動翻譯字段。
推薦:優先使用 Java 拼接(方案二)或 easy-trans(方案三),因為:
- 減少數據庫壓力:避免復雜的 SQL 聯表查詢。
- 易維護:Java 代碼邏輯清晰,SQL 改動成本高。
- 靈活性:easy-trans 提供注解式翻譯,簡化開發。
3.1easy-trans
public class OperateLogRespVO implements VO {private Long id; // 操作日志IDprivate Long userId; // 用戶ID,關聯AdminUserDO的id字段// @Trans注解:聲明該字段需要從其他對象轉換而來@Trans(type = "SIMPLE", target = AdminUserDO.class, fields = "nickname", ref = "userNickname")private String userNickname; // 存儲從AdminUserDO映射過來的nickname字段// getter和setter方法public Long getId() { return id; }public void setId(Long id) { this.id = id; }public Long getUserId() { return userId; }public void setUserId(Long userId) { this.userId = userId; }public String getUserNickname() { return userNickname; }public void setUserNickname(String userNickname) { this.userNickname = userNickname; }
}public class AdminUserDO {private Long id; // 用戶IDprivate String nickname; // 用戶昵稱// 無參構造函數public AdminUserDO() {}// 全參構造函數public AdminUserDO(Long id, String nickname) {this.id = id;this.nickname = nickname;}// getter和setter方法public Long getId() { return id; }public void setId(Long id) { this.id = id; }public String getNickname() { return nickname; }public void setNickname(String nickname) { this.nickname = nickname; }
}
輸入:
OperateLogRespVO logVO = new OperateLogRespVO();
logVO.setId(1L);
logVO.setUserId(1L);
// 假設數據庫中 AdminUserDO(1L, "Alice") 存在
代碼:
// Spring MVC 自動翻譯(easy-trans 全局配置)
return logVO;
輸出:
// logVO: {id=1, userId=1, userNickname="Alice"}
實現原理
- OperateLogRespVO 實現 VO 接口,啟用 easy-trans 翻譯。
- @Trans(type = "SIMPLE", target = AdminUserDO.class, fields = "nickname", ref = "userNickname"):
- type = "SIMPLE":使用 MyBatis Plus 查詢 AdminUserDO。
- target:指定目標實體類。
- fields:讀取 nickname 字段。
- ref:設置到 userNickname 字段。
- easy-trans 自動根據 userId 查詢 AdminUserDO,填充 userNickname。
3.2 場景二:跨模塊翻譯(easy-trans)
場景:在 yudao-module-crm 模塊的 CrmProductRespVO 中,將 ownerUserId 翻譯為 AdminUserDO 的 nickname。
/*** CRM產品響應視圖對象* 用于封裝產品信息并返回給前端,包含產品基本信息和關聯的所有者用戶昵稱*/
public class CrmProductRespVO implements VO {private Long id; // 產品ID,唯一標識一個產品private Long ownerUserId; // 產品所有者的用戶ID,關聯到AdminUserDO的id字段/*** 產品所有者的昵稱,通過@Trans注解自動映射* 映射規則:* - type="SIMPLE":簡單類型映射* - targetClassName:指定目標數據對象類的全限定名* - fields="nickname":從AdminUserDO中獲取nickname字段的值* - ref="ownerNickname":將獲取的值映射到當前類的ownerNickname字段*/@Trans(type = "SIMPLE", targetClassName = "com.example.model.AdminUserDO", fields = "nickname", ref = "ownerNickname")private String ownerNickname; // 存儲從AdminUserDO映射過來的nickname字段值// 以下是各字段的Getter和Setter方法public Long getId() { return id; }public void setId(Long id) { this.id = id; }public Long getOwnerUserId() { return ownerUserId; }public void setOwnerUserId(Long ownerUserId) { this.ownerUserId = ownerUserId; }public String getOwnerNickname() { return ownerNickname; }public void setOwnerNickname(String ownerNickname) { this.ownerNickname = ownerNickname; }
}
輸入:
OperateLogRespVO logVO = new OperateLogRespVO();
logVO.setId(1L);
logVO.setUserId(1L);
// 假設數據庫中 AdminUserDO(1L, "Alice") 存在
代碼:
// Spring MVC 自動翻譯(easy-trans 全局配置)
return logVO;
輸出:
// logVO: {id=1, userId=1, userNickname="Alice"}
3.3 場景三:Excel 導出翻譯(easy-trans)
場景:導出 UserVO 列表到 Excel,翻譯 deptId 為 deptName。
/*** 用戶數據導出工具類* 負責生成用戶數據并進行數據轉換,用于Excel導出*/
public class UserExcelExport {/*** 導出用戶列表數據* * 1. 創建用戶數據* 2. 調用TranslateUtils進行數據轉換(將部門ID轉換為部門名稱)* 3. 返回轉換后的用戶列表,用于Excel導出* * @return 轉換后的用戶視圖對象列表*/public List<UserVO> exportUsers() {// 創建單個用戶數據(實際場景可能從數據庫查詢)UserVO user = new UserVO();user.setId(1L); // 設置用戶IDuser.setName("Alice"); // 設置用戶名user.setDeptId(10L); // 設置部門ID(關聯DeptDO的ID)// 構建用戶列表List<UserVO> users = Arrays.asList(user);// 調用工具類進行數據轉換// 此方法會根據@Trans注解,將deptId轉換為對應的部門名稱deptNameTranslateUtils.translate(users);// 返回轉換后的用戶列表,此時列表中的deptName已被填充return users;}
}
/####################################################################//*** 用戶視圖對象* 用于前端展示或數據導出,包含部門名稱(通過@Trans注解自動映射)*/
public class UserVO implements VO {private Long id; // 用戶IDprivate String name; // 用戶名稱private Long deptId; // 部門ID(關聯DeptDO的ID)/*** 部門名稱(通過@Trans注解自動映射)* * type="SIMPLE": 簡單類型轉換* target=DeptDO.class: 目標數據對象類* fields="name": 從DeptDO中獲取name字段* ref="deptName": 將值映射到當前類的deptName字段* * TranslateUtils會根據此注解,* 通過deptId查找對應的DeptDO對象,* 并將其name字段值賦給當前對象的deptName字段*/@Trans(type = "SIMPLE", target = DeptDO.class, fields = "name", ref = "deptName")private String deptName; // 部門名稱(通過@Trans自動映射)// 以下是各字段的Getter和Setter方法public Long getId() { return id; }public void setId(Long id) { this.id = id; }public String getName() { return name; }public void setName(String name) { this.name = name; }public Long getDeptId() { return deptId; }public void setDeptId(Long deptId) { this.deptId = deptId; }public String getDeptName() { return deptName; }public void setDeptName(String deptName) { this.deptName = deptName; }
}
/*** VO 數據翻譯 Utils*/
public class TranslateUtils {private static TransService transService;public static void init(TransService transService) {TranslateUtils.transService = transService;}/*** 數據翻譯** 使用場景:無法使用 @TransMethodResult 注解的場景,只能通過手動觸發翻譯** @param data 數據* @return 翻譯結果*/public static <T extends VO> List<T> translate(List<T> data) {if (CollUtil.isNotEmpty((data))) {transService.transBatch(data);}return data;}}
4. 擴展:實用技巧與注意事項
4.1 優化性能
- MapStruct:優先用于高并發場景,生成代碼無反射開銷。
- BeanUtils:適合快速開發,但避免在高頻接口中使用。
- easy-trans:全局翻譯(easy-trans.is-enable-global=true)方便但可能影響性能,建議在數據量大或樹形結構時使用 @IgnoreTrans 注解:
@IgnoreTrans public List<UserVO> getLargeData() {// 手動翻譯或避免翻譯return users; }
4.2 復雜邏輯處理
- MapStruct 自定義邏輯:使用 @AfterMapping 或 default 方法處理復雜映射:
@Mapper public interface UserConvert {@Mapping(target = "deptName", ignore = true)UserVO convert(UserDO user, DeptDO dept);@AfterMappingdefault void afterConvert(@MappingTarget UserVO userVO, DeptDO dept) {if (dept != null) {userVO.setDeptName("自定義-" + dept.getName());}} }
-
BeanUtils Consumer:通過 Consumer 添加動態邏輯:
-
UserVO userVO = UserConvertBeanUtils.convertWithConsumer(user, dept, vo -> {vo.setDeptName(vo.getDeptName() + "-增強"); });
5.3 跨模塊翻譯優化
- 緩存:跨模塊查詢(如 AdminUserDO)可能涉及多次數據庫訪問,建議緩存 DeptDO 或 AdminUserDO:
Map<Long, DeptDO> deptMap = deptService.getDeptMap(); List<UserVO> userVOs = users.stream().map(user -> UserConvertBeanUtils.convert(user, deptMap.get(user.getDeptId()))).collect(Collectors.toList());
5.4 Excel 導出優化
- 批量翻譯:TranslateUtils.translate 支持批量處理,但大數據量時建議分批:
List<List<UserVO>> batches = ListUtils.partition(users, 1000); batches.forEach(TranslateUtils::translate);
5. 總結
- 對象轉換:
- MapStruct:高性能,適合復雜映射,需配置 @Mapping。
- BeanUtils:簡單易用,適合字段名一致的場景,支持 Consumer 擴展。
- 數據翻譯:
- SQL 聯表:適合簡單場景,但可能增加數據庫壓力。
- Java 拼接:靈活,推薦多次單表查詢后拼接。
- easy-trans:注解式翻譯,模塊內用 target,跨模塊用 targetClassName,Excel 導出用 TranslateUtils。
- 注意事項:
- 性能敏感場景用 MapStruct 或禁用全局翻譯。
- 復雜邏輯通過 default 方法或 Consumer 處理。
- 跨模塊翻譯和 Excel 導出可結合緩存優化性能。