欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 科技 > 名人名企 > Aop + 注解实现数据字典类型转换 EasyExcel导出

Aop + 注解实现数据字典类型转换 EasyExcel导出

2025/6/6 14:45:17 来源:https://blog.csdn.net/qq_45525848/article/details/148372494  浏览:    关键词:Aop + 注解实现数据字典类型转换 EasyExcel导出

Aop +注解 实现数据字典类型转换

文章目录

  • Aop +注解 实现数据字典类型转换
  • 一、基础方式
    • ✅字典转换简介
    • 👉实现步骤
      • ✅ 1. 定义自定义注解`@Dict `
      • ✅ 2. 定义查询字典项的两个方法
      • ✅ 3. 定义Aop拦截我们查询的方法
      • ✅ 4. VO映射类
      • ✅ 5. Controller层
      • ✅ 6. serviceImpl
      • ✅ 7. 对象转换工具
        • Mapper
        • BeanCopierUtil
  • 二、优化改成符合多种类型转换
    • ✅ 步骤二:封装一个通用字典转换工具类改进方案结构图:
    • ✅ 步骤二:封装一个通用字典转换工具类步骤一:注解保留不变
    • ✅ 步骤二:封装一个查询字典工具类
      • Mapper.xml
      • Mapper 接口
      • ServiceImpl
    • ✅ 步骤三:AOP 对返回数据进行增强处理
      • 👉 环绕通知处理 Controller 的返回数据
    • ✅ 步骤四:VO 示例
    • ✅ 示例 Controller
    • ✅ 示例返回 JSON:
    • ✅ 总结要点
  • 三、通用的递归遍历版本(最终版 v1)
    • ✅它可以处理以下情况:
    • ✅核心思路是:
    • 1. `@Dict` 注解
    • 2. 示例 VO(你的“复杂数据”场景)
    • 3. `DictService` 接口及实现(从数据库查询字典值)
    • 4. 核心:通用的 `DictAspect`(AOP 切面)
      • ✅讲解
    • 5. 最终在 Controller 中的使用示例
    • 6. ✅ 总结
  • 四、只拦截特定 Controller 类(最终版 v2)
    • ✅ 1. 自定义注解标记 Controller
    • ✅ 2. 自定义字段注解 `@Dict`
    • ✅ 3. 在 Controller 上使用标记注解
    • ✅ 4. 字典查询接口(模拟数据库)
    • ✅ 5. 通用字典转换工具类(支持递归、List、Page、Map 等)
    • ✅ 6. AOP 拦截特定 Controller 并自动转换
    • ✅ 7. 测试返回结构
    • ✅ 效果示例
      • 🔍原始:
      • ✅ 完整代码
  • 五、只拦截特定 Controller 类中指定的方法(最终版 v3)
    • ✅ 该方法适应以下场景:
    • 1. `pom.xml`
    • 2. `src/main/resources/application.properties`
    • 3. `src/main/resources/schema.sql`
    • 4. `src/main/resources/data.sql`
    • ✅5. 目录结构
      • 5.1 `DemoApplication.java`
      • 5.2 注解:`Dict.java`
      • 5.3 注解:`EnableDictConvert.java`
      • 5.4 切面:`DictAspect.java`
      • 5.5 工具类:`DictValueConverter.java`
      • 5.6 服务层接口:`DictService.java`
      • 5.7 服务层实现:`DictServiceImpl.java`
      • 5.8 通用响应体:`R.java`
      • 5.9 域对象:`UserVO.java`
      • 5.10 域对象:`StudentVO.java`
      • 5.11 域对象:`PostVO.java`
      • 5.12 控制器:`UserController.java`
    • 6. 启动与验证
    • 7. ✅ 说明与可扩展点
  • 六、EasyExcel 导出嵌套数据并动态映射字典
    • 1. 在导出前通过自定义注解和 AOP 机制对字段进行数据字典转换。
      • ✅主要功能如下:
    • 2. 项目结构
    • 3. 数据库与字典表
    • 4. 实体/VO 定义
    • 5. 自定义注解 @Dict
    • 6. 字典解析服务(DictResolverService)
    • 7. AOP 切面(可选)
    • 8. 导出接口 Controller
    • 9. 前端页面(Thymeleaf)

一、基础方式

✅字典转换简介

以前我们从数据字典里面取值,拿到的都是一堆状态码,我们需要在前台进行判断,然后转义成中文,这样是十分麻烦的,这又是每个字典字段不可少的一个地方,所以我就想到了利用AOP+切面 来帮我们实现中文的转义。

首先直接看下最后实现的效果吧,一般我们的数据字典接口就之后返回我们的状态码,在aop处理过之后,它把我们的一些性别、岗位等状态码转成了中文多加了几个字段一并返回给我们,这样我们在前台绑定的时候就能直接通过 XXText的字段进行绑定了,前端在映射的时候直接取XXText字段,我们的不需要进行转义了

{"code": 0,"msg": null,"data": [{"id": null,"userNickname": "郭德纲","userPhone": "13800138001","userEmail": "guodegang@xxx.com","userGender": 1,"userBirth": "1973-01-18T00:00:00","userScore": 21,"userReward": 100.21,"sex": "0","sexText": "女","postType": "3","postTypeText": "运维"},{"id": null,"userNickname": "于谦","userPhone": "13800138002","userEmail": "yuqian@xxx.com","userGender": 2,"userBirth": "1969-01-24T00:00:00","userScore": 24,"userReward": 100.24,"sex": "2","sexText": "未知","postType": "2","postTypeText": "管理"},{"id": null,"userNickname": "栾云平","userPhone": "13800138003","userEmail": "luanyunping@xxx.com","userGender": 0,"userBirth": "1984-05-20T00:00:00","userScore": 35,"userReward": 100.35,"sex": "1","sexText": "男","postType": "1","postTypeText": "维修"},{"id": null,"userNickname": "岳云鹏","userPhone": "13800138004","userEmail": "yueyunpeng@xxx.com","userGender": 1,"userBirth": "1985-04-15T00:00:00","userScore": 20,"userReward": 100.20,"sex": "0","sexText": "女","postType": "4","postTypeText": "开发"},{"id": null,"userNickname": "孙越","userPhone": "13800138005","userEmail": "sunyue@xxx.com","userGender": 2,"userBirth": "1979-10-13T00:00:00","userScore": 22,"userReward": 100.22,"sex": "2","sexText": "未知","postType": "4","postTypeText": "开发"},{"id": null,"userNickname": "郭麒麟","userPhone": "13800138006","userEmail": "guoqilin@xxx.com","userGender": 0,"userBirth": "1996-02-08T00:00:00","userScore": 24,"userReward": 100.24,"sex": "0","sexText": "女","postType": "3","postTypeText": "运维"},{"id": null,"userNickname": "阎鹤祥","userPhone": "13800138007","userEmail": "yanhexiang@xxx.com","userGender": 1,"userBirth": "1981-09-14T00:00:00","userScore": 25,"userReward": 100.25,"sex": "1","sexText": "男","postType": "1","postTypeText": "维修"},{"id": null,"userNickname": "张云雷","userPhone": "13800138008","userEmail": "zhangyunlei@xxx.com","userGender": 2,"userBirth": "1992-01-11T00:00:00","userScore": 29,"userReward": 100.29,"sex": "0","sexText": "女","postType": "2","postTypeText": "管理"},{"id": null,"userNickname": "杨九郎","userPhone": "13800138009","userEmail": "yangjiulang@xxx.com","userGender": 0,"userBirth": "1989-07-17T00:00:00","userScore": 35,"userReward": 100.35,"sex": "1","sexText": "男","postType": "3","postTypeText": "运维"},{"id": null,"userNickname": "孟鹤堂","userPhone": "13800138010","userEmail": "mengetang@xxx.com","userGender": 1,"userBirth": "1988-04-26T00:00:00","userScore": 30,"userReward": 100.30,"sex": "0","sexText": "女","postType": "2","postTypeText": "管理"},{"id": null,"userNickname": "周九良","userPhone": "13800138011","userEmail": "zhoujiuliang@xxx.com","userGender": 2,"userBirth": "1994-09-14T00:00:00","userScore": 26,"userReward": 100.26,"sex": "2","sexText": "未知","postType": "4","postTypeText": "开发"}]
}

##✅ 实现思路:

aop 后置通知(handleDictConversion)拦截我们的查询数据的方法,我们遍历查询出的code,然后通过code去调用方法查询出它对应的text文本值,添加了注解@Dict(dicDataSource = “stu_sex”) 会在字典服务立马查出来对应的text 然后在请求list的时候将这个字典text,已字段名称加 Text形式返回到前端(例如:我的性别字段是sex,然后我们通过拿到这个字段的值2去查询出它对应的未知,然后把这个字段赋值给我们的sexText 然后一并添加到方法中返回),然后前端就可以直接取这个字段进行赋值

👉实现步骤

✅ 1. 定义自定义注解@Dict

package com.js.archive.annotation;import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Dict {/*** 方法描述:  数据dataSource* @return 返回类型: String*/String dicDataSource();/*** 方法描述:  这是返回后Put到josn中的文本 key 值* @return 返回类型: String*/String dicText() default "";
}

✅ 2. 定义查询字典项的两个方法

/*** dto条件查询获取字典项List** @author Jshuai* @date 2025-06-01* @param searchDto 查询实体参数* @return List<SysDictDataVO>*/@Overridepublic List<SysDictDataVO> getList(SysDictDataSearchDTO searchDto){return Mapper.map(getLambdaWrapper(searchDto, null).list(), SysDictDataVO.class);}/*** 根据字典类型查询单个字典项* @param type 字典类型 字典值* @return*/public  SysDictDataVO getQueryOne(String type ,String value){return Mapper.map(lambdaQuery().eq(StringTools.isNotBlank(type), SysDictData::getDictType, type).eq(StringTools.isNotBlank(value), SysDictData::getDictValue, value).one(), SysDictDataVO.class);}

✅ 3. 定义Aop拦截我们查询的方法

核心代码,利用aop对我们的查询人员方法进行拦截,并且查询出我们转义的中文发送到前端

package com.js.archive.aspect;import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.js.archive.annotation.Dict;
import com.js.archive.service.SysDictDataService;
import com.js.archive.util.ObjConvertUtils;
import com.js.core.domain.R;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;import java.lang.reflect.Field;
import java.text.SimpleDateFormat;
import java.util.*;@Aspect
@Component
@Slf4j
public class DictAspect {private static final String DICT_TEXT_SUFFIX = "Text";@Autowiredprivate SysDictDataService sysDictDataService;// 定义切点:拦截controller包下所有方法@Pointcut("execution(* com.js.archive.controller..*.*(..))")public void controllerPointcut() {log.debug("拦截Controller方法: {}");}// 后置通知:处理响应结果@AfterReturning(pointcut = "controllerPointcut()", returning = "result")public void handleDictConversion(JoinPoint joinPoint, Object result) {try {// 非R类型响应直接跳过if (!(result instanceof R)) {log.debug("响应类型非R,跳过处理");return;}// 使用通配符泛型,兼容所有R<T>R r = (R) result;Object data = r.getData();// 空值判断(包含null、空字符串、"null"字符串)if (ObjConvertUtils.isEmpty(data)) {log.debug("响应数据为空,跳过处理");return;}// 处理数据并更新到R对象中if (data instanceof Page<?>) {// 分页数据,只处理记录列表部分List records = ((Page) data).getRecords();Object o = processData(records);((Page) data).setRecords((List) o);return;}else {r.setData(processData(data));}log.debug("字典转换完成,方法: {},数据类型: {}",joinPoint.getSignature().toShortString(),data.getClass().getName());} catch (Exception e) {log.error("字典转换异常: 方法={}, 原因={}",joinPoint.getSignature().toShortString(),e.getMessage(), e);}}// 数据处理入口private Object processData(Object data) {// 处理JSON对象if (data instanceof JSONObject) {return processJSONObject((JSONObject) data);}// 处理JSON数组else if (data instanceof JSONArray) {return processJSONArray((JSONArray) data);}// 处理List集合(可能包含JSON对象或实体类)else if (data instanceof List) {return processList((List<?>) data);}// 处理Map集合else if (data instanceof Map) {return processMap((Map<?, ?>) data);}// 处理普通实体类(转为JSON后处理)else {return processBean(data);}}// 处理JSONObjectprivate JSONObject processJSONObject(JSONObject json) {JSONObject result = new JSONObject(true); // 保留字段顺序for (Map.Entry<String, Object> entry : json.entrySet()) {String key = entry.getKey();Object value = entry.getValue();result.put(key, processData(value)); // 递归处理嵌套值}// 处理当前对象的字段注解return processAnnotatedFields(result, null);}// 处理JSONArrayprivate JSONArray processJSONArray(JSONArray array) {JSONArray result = new JSONArray();for (int i = 0; i < array.size(); i++) {result.add(processData(array.get(i))); // 递归处理每个元素}return result;}// 处理List集合private List<Object> processList(List<?> list) {List<Object> result = new ArrayList<>();for (Object item : list) {result.add(processData(item));}return result;}// 处理Map集合private Map<Object, Object> processMap(Map<?, ?> map) {Map<Object, Object> result = new LinkedHashMap<>(); // 保留插入顺序for (Map.Entry<?, ?> entry : map.entrySet()) {result.put(entry.getKey(), processData(entry.getValue()));}return result;}// 处理普通实体类(转为JSON后处理)private Object processBean(Object bean) {if (ObjConvertUtils.isEmpty(bean)) {return null;}String jsonStr = null;try {// 将实体类转为JSON字符串jsonStr = JSON.toJSONString(bean);JSONObject json = JSON.parseObject(jsonStr);// 处理字段注解processAnnotatedFields(json, bean);// 尝试反序列化为原类型(保持泛型类型一致性)return JSON.toJavaObject(json, bean.getClass());} catch (Exception e) {log.error("实体类处理失败: {},转为JSON: {}",bean.getClass().getName(), jsonStr, e);return bean; // 转换失败返回原始对象}}// 处理字段注解(字典和日期)private JSONObject processAnnotatedFields(JSONObject json, Object originalObject) {try {// 获取所有字段(适用于实体类)Field[] fields = originalObject != null ?ObjConvertUtils.getAllFields(originalObject) :new Field[0];for (Field field : fields) {processField(json, field, originalObject);}// 处理JSON原生字段(无实体类场景,通过字段名判断)if (originalObject == null) {for (String fieldName : json.keySet()) {handleDateFormatForJsonField(json, fieldName);}}} catch (Exception e) {log.error("字段注解处理失败: {}", e.getMessage(), e);}return json;}// 处理单个字段(适用于实体类)private void processField(JSONObject json, Field field, Object originalObject) throws IllegalAccessException {String fieldName = field.getName();if (!json.containsKey(fieldName)) {return;}field.setAccessible(true);Object fieldValue = json.get(fieldName);// 处理字典注解handleDictAnnotation(json, field, fieldName, fieldValue);// 处理日期格式化handleDateFormat(json, field, fieldName, fieldValue);}// 处理字典注解private void handleDictAnnotation(JSONObject json, Field field, String fieldName, Object fieldValue) {Dict dict = field.getAnnotation(Dict.class);if (dict == null || ObjConvertUtils.isEmpty(fieldValue)) {return;}String dictType = dict.dicDataSource();String dictText = dict.dicText();String key = String.valueOf(fieldValue);String translatedValue = translateDictValue(dictType, key);if (!ObjConvertUtils.isEmpty(translatedValue)) {String targetField = StringUtils.hasText(dictText) ? dictText : fieldName + DICT_TEXT_SUFFIX;json.put(targetField, translatedValue);}}// 处理日期格式化private void handleDateFormat(JSONObject json, Field field, String fieldName, Object fieldValue) {if (field.getType() == Date.class &&field.getAnnotation(JsonFormat.class) == null &&!ObjConvertUtils.isEmpty(fieldValue)) {try {long timestamp;if (fieldValue instanceof Long) {timestamp = (Long) fieldValue;} else if (fieldValue instanceof String) {timestamp = Long.parseLong((String) fieldValue);} else {return; // 不支持的时间类型}SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");json.put(fieldName, sdf.format(new Date(timestamp)));} catch (Exception e) {log.debug("日期格式化失败: {},值: {}", fieldName, fieldValue);}}}// 处理JSON原生字段的日期格式化(无实体类场景)private void handleDateFormatForJsonField(JSONObject json, String fieldName) {Object value = json.get(fieldName);if (value instanceof Long || (value instanceof String && value.toString().matches("\\d+"))) {try {long timestamp = Long.parseLong(value.toString());SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");json.put(fieldName, sdf.format(new Date(timestamp)));} catch (Exception e) {log.debug("非时间戳字段: {}", fieldName);}}}// 翻译字典值private String translateDictValue(String dictType, String key) {if (ObjConvertUtils.isEmpty(dictType) || ObjConvertUtils.isEmpty(key)) {return null;}StringBuilder result = new StringBuilder();String[] keys = key.split(",");for (String k : keys) {String trimmedKey = k.trim();if (ObjConvertUtils.isEmpty(trimmedKey)) {continue;}try {String label = sysDictDataService.getQueryOne(dictType, trimmedKey).getDictLabel();if (!ObjConvertUtils.isEmpty(label)) {if (result.length() > 0) {result.append(",");}result.append(label);}} catch (Exception e) {log.warn("字典查询失败: type={}, key={}, 原因: {}", dictType, trimmedKey, e.getMessage());}}return result.length() > 0 ? result.toString() : null;}
}

✅ 4. VO映射类

package com.js.archive.domain.vo;import java.math.BigDecimal;
import java.time.LocalDateTime;import com.js.archive.annotation.Dict;
import com.js.core.domain.vo.BaseVO;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;/*** <p>*  VO* </p>** @author JiangShuai* @date 2025-06-01* @since JDK 1.8*/@ApiModel(value="TbUserInfo视图对象", description="")
@EqualsAndHashCode(callSuper = true)
@Data
public class TbUserInfoVO extends BaseVO {private static final long serialVersionUID = 1L;@ApiModelProperty(value = "用户昵称")private String userNickname;@ApiModelProperty(value = "用户手机")private String userPhone;@ApiModelProperty(value = "用户邮箱")private String userEmail;@ApiModelProperty(value = "用户性别")private Integer userGender;@ApiModelProperty(value = "用户生日")private LocalDateTime userBirth;@ApiModelProperty(value = "用户积分")private Integer userScore;@ApiModelProperty(value = "用户佣金")private BigDecimal userReward;@ApiModelProperty(value = "性别")@Dict(dicDataSource = "sex", dicText = "sexText")private String sex;@ApiModelProperty(value = "性别文本")private String sexText;@ApiModelProperty(value = "岗位类型")@Dict(dicDataSource = "post_type", dicText = "postTypeText")private String postType;@ApiModelProperty(value = "岗位文本")private String postTypeText;}

✅ 5. Controller层

package com.js.archive.controller;import com.js.archive.domain.vo.SysDictTypeVO;
import com.js.archive.domain.dto.SysDictTypeAddDTO;
import com.js.archive.domain.dto.SysDictTypeUpdateDTO;
import com.js.archive.domain.dto.SysDictTypeSearchDTO;
import com.js.archive.domain.dto.SysDictTypePageSearchDTO;
import com.js.archive.service.SysDictTypeService;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;import com.js.core.domain.R;
import io.swagger.annotations.*;
import org.springframework.web.bind.annotation.*;
import lombok.AllArgsConstructor;import javax.validation.constraints.NotNull;
import java.io.IOException;
import java.util.List;/*** <p>* TODO* </p>** @author JiangShuai* @date 2025-06-01* @since JDK 1.8*/@AllArgsConstructor
@Api(value = "", tags = "字典类型管理")
@RestController
@RequestMapping("/sysDictType")
public class SysDictTypeController {private SysDictTypeService sysDictTypeService;@ApiOperation("id获取单条字典类型")@GetMapping("/one/{id}")public R<SysDictTypeVO> getSysDictType(@NotNull @PathVariable("id") Long id ) {return R.ok(sysDictTypeService.getOne(id));}@ApiOperation("dto条件查询获取字典类型List")@GetMapping("/list")public R<List<SysDictTypeVO>> get(SysDictTypeSearchDTO searchDto) {return R.ok(sysDictTypeService.getList(searchDto));}@ApiOperation("dto条件查询获取字典类型Page")@GetMapping("/page")public R<Page<SysDictTypeVO>> get(SysDictTypePageSearchDTO pageSearchDto) {return R.ok(sysDictTypeService.getPage(pageSearchDto));}}

✅ 6. serviceImpl

package com.js.archive.service.impl;import com.js.archive.domain.entity.TbUserInfo;
import com.js.archive.domain.vo.TbUserInfoVO;
import com.js.archive.domain.dto.TbUserInfoAddDTO;
import com.js.archive.domain.dto.TbUserInfoUpdateDTO;
import com.js.archive.domain.dto.TbUserInfoSearchDTO;
import com.js.archive.domain.dto.TbUserInfoPageSearchDTO;
import com.js.archive.mapper.TbUserInfoMapper;
import com.js.archive.service.TbUserInfoService;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.js.core.service.impl.BaseServiceImpl;
import com.js.core.toolkit.StringPool;
import com.js.core.toolkit.Assert;
import com.js.core.toolkit.Mapper;
import org.springframework.stereotype.Service;import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;/*** <p>*  服务实现类* </p>** @author JiangShuai* @date 2025-06-01* @since JDK 1.8*/@Service
public class TbUserInfoServiceImpl extends BaseServiceImpl<TbUserInfoMapper, TbUserInfo> implements TbUserInfoService {/*** 添加单条人员信息** @author Jshuai* @date 2025-06-01* @param addDto 添加实体参数* @return TbUserInfoVO*/@Overridepublic TbUserInfoVO addOne(TbUserInfoAddDTO addDto){TbUserInfo entity = Mapper.map(addDto, TbUserInfo.class);save(entity);return Mapper.map(entity, TbUserInfoVO.class);}/*** id删除单条人员信息** @author Jshuai* @date 2025-06-01* @param id 主键id* @return Boolean*/@Overridepublic Boolean deleteOne(Long id){return removeById(id);}/*** ids删除多条人员信息** @author Jshuai* @date 2025-06-01* @param ids 主键id字符长串,逗号分隔* @return Boolean*/@Overridepublic Boolean deleteBatch(String ids){Assert.notEmpty(ids, "ids参数不能为空!");List<Long> idList = Arrays.stream(ids.split(StringPool.COMMA)).mapToLong(Long::parseLong).boxed().collect(Collectors.toList());return removeByIds(idList);}/*** id修改单条人员信息** @author Jshuai* @date 2025-06-01* @param updateDto 修改实体参数* @return TbUserInfoVO*/@Overridepublic TbUserInfoVO updateOne(TbUserInfoUpdateDTO updateDto){TbUserInfo entity = Mapper.map(updateDto, TbUserInfo.class);updateById(entity);return Mapper.map(entity, TbUserInfoVO.class);}/*** id查询单条人员信息** @author Jshuai* @date 2025-06-01* @param id 主键id* @return TbUserInfoVO*/@Overridepublic TbUserInfoVO getOne(Long id){return Mapper.map(getById(id), TbUserInfoVO.class);}/*** dto条件查询获取人员信息List** @author Jshuai* @date 2025-06-01* @param searchDto 查询实体参数* @return List<TbUserInfoVO>*/@Overridepublic List<TbUserInfoVO> getList(TbUserInfoSearchDTO searchDto){List<TbUserInfo> list = getLambdaWrapper(searchDto, null).list();List<TbUserInfoVO> infoVOList = Mapper.map(list, TbUserInfoVO.class);return infoVOList;}/*** dto条件查询获取人员信息Page** @author Jshuai* @date 2025-06-01* @param pageSearchDto 分页查询实体参数* @return Page<TbUserInfoVO>*/@Overridepublic Page<TbUserInfoVO> getPage(TbUserInfoPageSearchDTO pageSearchDto){return Mapper.map(getLambdaWrapper(pageSearchDto, null).page(new Page<>(pageSearchDto.getCurrent(), pageSearchDto.getSize())), TbUserInfoVO.class);}}

✅ 7. 对象转换工具

Mapper
package com.js.core.toolkit;import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.js.core.toolkit.util.BeanCopierUtil;
import com.js.core.toolkit.util.CollectionUtil;
import com.js.core.toolkit.util.ReflectUtil;
import lombok.extern.slf4j.Slf4j;import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;/*** <p>* 实体类转换工具类* </p>** @author Jshuai* @date 2025/6/4 9:41* @since JDK 1.8*/@Slf4j
public class Mapper {/*** 实体转换** @param object 实体* @param cls    转换目标类型*/public static <T> T map(Object object, Class<T> cls) {log.debug("Mapping from {} to {}",object != null ? object.getClass().getSimpleName() : "null",cls.getSimpleName());if (object == null) {return null;}try {T result = cls.newInstance();Field[] fields = object.getClass().getDeclaredFields();for (Field field : fields) {int modifiers = field.getModifiers();if (Modifier.isFinal(modifiers) || Modifier.isStatic(modifiers)) {continue;}field.setAccessible(true);Object value = field.get(object);Field targetField;try {targetField = cls.getDeclaredField(field.getName());} catch (NoSuchFieldException e) {continue;}targetField.setAccessible(true);targetField.set(result, value);}log.debug("Mapping result: {}", result);return result;} catch (Exception e) {log.error("Mapping failed", e);return null;}}/*** 实体转换(带枚举属性)** @param object 实体* @param cls    转换目标类型(带枚举属性)* @return T* @author Jshuai* @date 2021/12/17 20:34*/public static <T> T mapWithEnumProp(Object object, Class<T> cls) throws IllegalAccessException {Map<String, Object> propertyMap = map(object);return map(propertyMap, cls, false);}/*** 分页转换** @param objectPage 分页数据* @param cls        转换目标类型*/public static <T> Page<T> map(IPage<?> objectPage, Class<T> cls) {Page<T> page = new Page<>();List<T> list = objectPage.getRecords().stream().map(o -> map(o, cls)).collect(Collectors.toList());page.setRecords(list);page.setSize(objectPage.getSize());page.setTotal(objectPage.getTotal());return page;}/*** 集合转换** @param objects 实体* @param cls     转换目标类型*/public static <T> List<T> map(List<?> objects, Class<T> cls) {log.debug("Mapping list of size {} to {}", objects.size(), cls.getSimpleName());List<T> result = objects.stream().peek(o -> log.debug("Source object: {}", o)).map(o -> map(o, cls)).peek(r -> log.debug("Mapped object: {}", r)).collect(Collectors.toList());log.debug("Mapped list result size: {}", result.size());return result;}/*** map转对象** @param beanMap   属性map* @param beanClazz 目标对象类* @return T* @author Jshuai* @date 2021/9/24 15:55*/public static <T> T map(Map<String, Object> beanMap, Class<T> beanClazz) {return map(beanMap, beanClazz, true);}/*** map转对象** @param beanMap     属性map* @param beanClazz   目标对象类* @param isUnderline 是否需要下划线命名* @return T* @author Jshuai* @date 2021/9/24 15:55*/public static <T> T map(Map<String, Object> beanMap, Class<T> beanClazz, boolean isUnderline) {if (CollectionUtil.isEmpty(beanMap)) {return null;}T bean = null;try {Field[] fields = ReflectUtil.getFields(beanClazz);bean = beanClazz.newInstance();for (Field field : fields) {int modifiers = field.getModifiers();if (Modifier.isFinal(modifiers) || Modifier.isStatic(modifiers)) {continue;}field.setAccessible(true);if (isUnderline) {ReflectUtil.setFieldValue(field, bean, beanMap.get(StringTools.fromCamelCase(field.getName())));} else {ReflectUtil.setFieldValue(field, bean, beanMap.get(StringTools.firstToLowerCase(field.getName())));}}} catch (IllegalAccessException | InstantiationException e) {LogTools.error(log, e, "map:%s转->对象:%s失败。", beanMap, beanClazz);}return bean;}/*** map转对象** @param beanMap     属性map* @param beanClazz   目标对象类* @param isUnderline 是否需要下划线命名* @return T* @author Jshuai* @date 2021/9/24 15:55*/public static <T> T mapError(Map<String, Object> beanMap, Class<T> beanClazz, boolean isUnderline) throws InstantiationException, IllegalAccessException {if (CollectionUtil.isEmpty(beanMap)) {return null;}T bean = null;Field[] fields = ReflectUtil.getFields(beanClazz);bean = beanClazz.newInstance();for (Field field : fields) {int modifiers = field.getModifiers();if (Modifier.isFinal(modifiers) || Modifier.isStatic(modifiers)) {continue;}field.setAccessible(true);if (isUnderline) {ReflectUtil.setFieldValue(field, bean, beanMap.get(StringTools.fromCamelCase(field.getName())));} else {ReflectUtil.setFieldValue(field, bean, beanMap.get(StringTools.firstToLowerCase(field.getName())));}}return bean;}/*** 对象转属性map** @param bean  带转换对象* @param isAll 是否包含父类私有属性* @return java.util.Map<java.lang.String, java.lang.Object>* @author Jshuai* @date 2021/9/24 16:00*/public static Map<String, Object> map(Object bean, boolean isAll) throws IllegalAccessException {if (bean == null) {return new HashMap<>(24);}Map<String, Object> map = new HashMap<>(24);Field[] declaredFields = isAll ? ReflectUtil.getFields(bean.getClass()): ReflectUtil.getAccessibleFields(bean.getClass());for (Field field : declaredFields) {field.setAccessible(true);map.put(field.getName(), field.get(bean));}return map;}/*** 对象转属性map(包含父类私有属性)** @param bean 带转换对象* @return java.util.Map<java.lang.String, java.lang.Object>* @author Jshuai* @date 2021/9/24 16:00*/public static Map<String, Object> map(Object bean) throws IllegalAccessException {return map(bean, true);}/*** object类型转换成数组**/public static <T> List<T> castList(Object obj, Class<T> clazz) {List<T> result = new ArrayList<>();if (obj instanceof List<?>) {for (Object o : (List<?>) obj) {result.add(clazz.cast(o));}return result;}return new ArrayList<>();}}
BeanCopierUtil
package com.js.core.toolkit.util;import com.baomidou.mybatisplus.extension.activerecord.Model;
import com.js.core.toolkit.StringPool;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cglib.beans.BeanCopier;
import org.springframework.cglib.core.Converter;import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.time.LocalDateTime;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;/*** <p>* BeanCopier工具类* </p>** @author Jshuai* @date 2025/6/4 9:41* @since JDK 1.8*/
@Slf4j
public class BeanCopierUtil {/*** 以String为Key存放BeanCopier对象HashMap集合*/private static Map<String, BeanCopier> beanCopierMap = new ConcurrentHashMap<>();/*** 对象属性复制** @param source 资源对象* @param target 目标对象类*/public static void copy(Object source, Object target) {if (source == null || target == null) {log.warn("Bean copy failed: source or target is null");return;}log.debug("Copying from {} to {}", source.getClass().getSimpleName(), target.getClass().getSimpleName());log.debug("Source object: {}", source);String beanKey = generateKey(source.getClass(), target.getClass());if (!beanCopierMap.containsKey(beanKey)) {log.debug("Creating new BeanCopier for {}", beanKey);BeanCopier beanCopier = BeanCopier.create(source.getClass(), target.getClass(), false);beanCopierMap.put(beanKey, beanCopier);}beanCopierMap.get(beanKey).copy(source, target, new BeanConverter());log.debug("Target object after copy: {}", target);log.debug("Copy completed");}/*** 对象属性复制** @param source    资源对象* @param targetCls 目标对象类*/@SuppressWarnings("rawtypes")public static <T> T copy(Object source, Class<T> targetCls) {if (source == null || targetCls == null) {return null;}try {T target = targetCls.newInstance();copy(source, target);return target;} catch (Exception e) {e.printStackTrace();}return null;}/*** 生成唯一key** @param sourceCls 资源类* @param targetCls 目标类* @return 资源类路径名称 + "_" + 目标类路径名称*/private static String generateKey(Class<?> sourceCls, Class<?> targetCls) {return sourceCls.getName() + "_" + targetCls.getName();}private static class BeanConverter implements Converter {@Overridepublic Object convert(Object sourceValue, Class targetType, Object context) {log.debug("Converting value: {} from {} to {}", sourceValue,sourceValue != null ? sourceValue.getClass().getSimpleName() : "null",targetType.getSimpleName());if (sourceValue == null) {return null;}// 处理基本类型和包装类型转换if (targetType.isPrimitive() ||Number.class.isAssignableFrom(targetType) ||targetType == String.class ||targetType == Boolean.class) {return sourceValue;}// 处理日期时间类型转换if (sourceValue instanceof LocalDateTime && targetType == LocalDateTime.class) {return sourceValue;}// 处理Model类型转换if (Model.class.isAssignableFrom(sourceValue.getClass()) && Model.class.isAssignableFrom(targetType)) {return copy(sourceValue, targetType);}log.warn("Unsupported type conversion from {} to {}",sourceValue.getClass().getName(), targetType.getName());return null;}}/*** 用于源对象中部分字段是String,而目标对象是枚举类型时的copy** @param source 源对象* @param target 目标对象* @throws NoSuchMethodException* @throws InvocationTargetException* @throws IllegalAccessException*/public static void differentTypeCopy(Object source, Object target) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {Class sourceClass = source.getClass();Class targetClass = target.getClass();Field[] sourceFields = sourceClass.getDeclaredFields();Field[] targetFields = targetClass.getDeclaredFields();for (Field targetField : targetFields) {String propertyName = targetField.getName();boolean isNotExist = false;for (Field sourceField : sourceFields) {if (sourceField.getName().equals(targetField.getName())) {isNotExist = true;}}if (isNotExist) {if (StringPool.ENUM_DATA.contains(propertyName)) {String methodName = propertyName.substring(0, 1).toUpperCase() + propertyName.substring(1);Method getMethod = sourceClass.getMethod(StringPool.GET + methodName);Object value = getMethod.invoke(source);if (null != value) {Class type = (Class) targetField.getGenericType();Method setMethod = targetClass.getMethod(StringPool.SET + methodName, type);setMethod.invoke(target, Enum.valueOf(type, String.valueOf(value)));}}}}}
}

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

二、优化改成符合多种类型转换

✅ 步骤二:封装一个通用字典转换工具类改进方案结构图:

返回数据结构:R<T> -> T 可能是:- 普通对象(VO)- List<VO>- Page<VO>- 复杂对象(包含 List、Map、嵌套对象)我们要做:✅ 找到所有标了 @Dict 的字段✅ 获取对应字段的 value 值✅ 查找字典值✅ 写入目标 text 字段(如 sexText)

✅ 步骤二:封装一个通用字典转换工具类步骤一:注解保留不变

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Dict {String type(); // 字典类型String targetField() default ""; // 转换结果输出字段,如 sexText
}

✅ 步骤二:封装一个查询字典工具类

Mapper.xml

<select id="getLabel" resultType="String">SELECT dict_label FROM sys_dict_data WHERE dict_type = #{type} AND dict_value = #{value}LIMIT 1
</select>

Mapper 接口

@Mapper
public interface DictMapper {String getLabel(@Param("type") String type, @Param("value") String value);
}

ServiceImpl

@Service
public class DictServiceImpl implements DictService {@Autowiredprivate DictMapper dictMapper;@Overridepublic String getLabel(String dictType, String dictValue) {return dictMapper.getLabel(dictType, dictValue);}
}

✅ 步骤三:AOP 对返回数据进行增强处理

👉 环绕通知处理 Controller 的返回数据

@Aspect
@Component
public class DictAspect {@Autowiredprivate DictService dictService;@Around("execution(* com.hy..controller..*.*(..))")public Object doAround(ProceedingJoinPoint point) throws Throwable {Object result = point.proceed();if (result instanceof R<?>) {R<?> response = (R<?>) result;Object data = response.getData();handleDict(data);}return result;}private void handleDict(Object data) {if (data == null) return;if (data instanceof List<?>) {for (Object item : (List<?>) data) {processFields(item);}} else if (data instanceof IPage<?>) {List<?> records = ((IPage<?>) data).getRecords();for (Object item : records) {processFields(item);}} else {processFields(data);}}private void processFields(Object obj) {if (obj == null) return;for (Field field : obj.getClass().getDeclaredFields()) {Dict dict = field.getAnnotation(Dict.class);if (dict != null) {try {field.setAccessible(true);Object value = field.get(obj);if (value != null) {// 查询数据库获取中文标签String label = dictService.getLabel(dict.type(), value.toString());// 自动找目标字段(默认为 fieldName + "Text")String targetFieldName = dict.targetField().isEmpty()? field.getName() + "Text": dict.targetField();try {Field targetField = obj.getClass().getDeclaredField(targetFieldName);targetField.setAccessible(true);targetField.set(obj, label);} catch (NoSuchFieldException ignored) {// 如果没有目标字段,不抛异常,继续}}} catch (IllegalAccessException ignored) {}}}}
}

✅ 步骤四:VO 示例

@Data
public class UserVO {private Long id;@Dict(type = "sex")private String sex;private String sexText;@Dict(type = "status", targetField = "statusDesc")private String status;private String statusDesc;
}

✅ 示例 Controller

@GetMapping("/user")
public R<UserVO> getUser() {UserVO user = new UserVO();user.setId(1L);user.setSex("1");user.setStatus("1");return R.ok(user);
}@GetMapping("/users")
public R<List<UserVO>> getUsers() {List<UserVO> users = new ArrayList<>();// 添加多个用户return R.ok(users);
}@GetMapping("/users/page")
public R<IPage<UserVO>> getPagedUsers() {IPage<UserVO> page = new Page<>();// 设置分页记录return R.ok(page);
}

✅ 示例返回 JSON:

json复制编辑{"code": 0,"msg": "success","data": {"id": 1,"sex": "1","sexText": "男","status": "1","statusDesc": "正常"}
}

✅ 总结要点

模块功能
@Dict 注解标注字段需要字典转换
AOP 切面拦截 Controller 返回,动态补充字段值
工具类提供字典类型到文本的映射逻辑
支持结构普通 VO、List、IPage、R 嵌套结构

三、通用的递归遍历版本(最终版 v1)

✅它可以处理以下情况:

  1. 单个对象(如 UserVO
  2. 嵌套集合(如 List<StudentVO>List<PostVO>
  3. 嵌套 IPage<T>(MyBatis-Plus 分页对象)
  4. 嵌套 Map<String, Object>
  5. 嵌套的任意自定义 VO(只要它们有 @Dict 注解或者内部还包含可遍历的集合/对象)

✅核心思路是:

  • 对传入的 data(可能是单个 VO、List、Page 等)做一次递归:
    • 若是基本类型(String、包装类、枚举等)就跳过
    • 若是 CollectionListSet、数组)就对每个元素递归
    • 若是 Map 就对每个 value 递归
    • 若是 IPage<?> 就对它的 getRecords() (一个 List<VO>)递归
    • 若是普通对象(POJO),先把自己身上所有带 @Dict 注解的字段做一次转换(调用 DictService 查询数据库拿到中文),再对“所有字段的值”再递归一层(防止嵌套 VO 或集合)

下面给出完整的实现示例(假设你已经有了之前提到的 @Dict 注解、DictService.getLabel(type, value)、以及通用响应类 R<T>)。请把它放在 Spring Boot 项目的某个 @Component 包下,并确保你的 Controller 都返回 R<...>

1. @Dict 注解

package com.hy.core.annotation;import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;/*** 用于数据字典类型转换:*  - type: 字典类型(对应数据库中 sys_dict_data.dict_type)*  - targetField: 转换后要赋值的字段名(如果不填,默认就是 “原字段名 + Text”)*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Dict {String type();String targetField() default "";
}

2. 示例 VO(你的“复杂数据”场景)

package com.hy.core.domain;import com.hy.core.annotation.Dict;
import lombok.Data;
import java.util.List;@Data
public class UserVO {private Long id;@Dict(type = "sex", targetField = "sexDesc")private String sex;private String sexDesc;@Dict(type = "status", targetField = "statusDesc")private String status;private String statusDesc;private List<StudentVO> students;private List<PostVO> posts;
}@Data
public class StudentVO {private Long id;private String name;private Integer age;@Dict(type = "stuType", targetField = "stuTypeDesc")private String stuType;private String stuTypeDesc;
}@Data
public class PostVO {private String userNickname;private String userPhone;private String userEmail;private Integer userGender;@Dict(type = "post_type", targetField = "postTypeDesc")private String postType;private String postTypeDesc;
}

注意

所有需要字典转换的字段都要加上 @Dict(type="...", targetField="..."),并在类里预留一个对应的 targetField(例如 sexDescstatusDescstuTypeDescpostTypeDesc)来接收转换之后的中文。


3. DictService 接口及实现(从数据库查询字典值)

package com.hy.core.service;public interface DictService {/*** 根据 dict_type 和 dict_value 查库返回 dict_label* @param dictType  比如 "sex"、"status"、"post_type" 等* @param dictValue 比如 "1"、"0" 等* @return 对应的中文标签,如果查不到就返回 null 或者原值*/String getLabel(String dictType, String dictValue);
}
package com.hy.core.service.impl;import com.hy.core.mapper.DictMapper;
import com.hy.core.service.DictService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;@Service
public class DictServiceImpl implements DictService {@Autowiredprivate DictMapper dictMapper;@Overridepublic String getLabel(String dictType, String dictValue) {// 从数据库 sys_dict_data 表里查 dict_labelString label = dictMapper.getLabel(dictType, dictValue);return label != null ? label : dictValue;}
}
package com.hy.core.mapper;import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;/*** 用 MyBatis Mapper 直接写简单 SQL*/
@Mapper
public interface DictMapper {@Select("SELECT dict_label FROM sys_dict_data WHERE dict_type = #{type} AND dict_value = #{value} LIMIT 1")String getLabel(@Param("type") String type, @Param("value") String value);
}

4. 核心:通用的 DictAspect(AOP 切面)

package com.hy.core.aspect;import com.baomidou.mybatisplus.core.metadata.IPage;
import com.hy.core.annotation.Dict;
import com.hy.core.domain.R;
import com.hy.core.service.DictService;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.util.Collection;
import java.util.Map;@Aspect
@Component
public class DictAspect {@Autowiredprivate DictService dictService;/*** 拦截所有 controller 层返回 R<...> 的方法*/@Around("execution(* com.hy..controller..*.*(..))")public Object aroundController(ProceedingJoinPoint pjp) throws Throwable {// 先执行业务方法,得到返回值Object result = pjp.proceed();if (result instanceof R<?>) {R<?> r = (R<?>) result;Object data = r.getData();if (data != null) {// 从 data 开始做递归处理processValue(data);}}return result;}/*** 入口:根据值的类型做不同处理*/private void processValue(Object value) {if (value == null) {return;}// 1. 如果是集合(List、Set,都实现了 Collection)if (value instanceof Collection<?>) {for (Object item : (Collection<?>) value) {processValue(item);}return;}// 2. 如果是数组if (value.getClass().isArray()) {int len = Array.getLength(value);for (int i = 0; i < len; i++) {Object element = Array.get(value, i);processValue(element);}return;}// 3. 如果是 MyBatis-Plus 的分页对象 IPage<?>if (value instanceof IPage<?>) {IPage<?> page = (IPage<?>) value;if (page.getRecords() != null) {for (Object item : page.getRecords()) {processValue(item);}}return;}// 4. 如果是 Map<String, ?>,就对 value 集合做递归if (value instanceof Map<?, ?>) {for (Object obj : ((Map<?, ?>) value).values()) {processValue(obj);}return;}// 5. 如果是“基本类型”或者 String、枚举、包装类,直接跳过if (isPrimitiveOrWrapper(value.getClass()) || value instanceof String || value.getClass().isEnum()) {return;}// 6. 剩下的就是普通 VO/POJO 对象,去处理它的字段processObjectFields(value);}/*** 处理对象的所有字段:*  (1) 先把带 @Dict 注解的字段转换(调用 dictService 查库填充对应的 targetField)*  (2) 再对每个字段的值继续递归 processValue(...)(以防嵌套更多 VO 或集合)*/private void processObjectFields(Object obj) {if (obj == null) return;Class<?> clazz = obj.getClass();Field[] fields = clazz.getDeclaredFields();for (Field field : fields) {field.setAccessible(true);// —— Step1: 如果字段上标注了 @Dict,就先做字典转换 —— Dict dictAnno = field.getAnnotation(Dict.class);if (dictAnno != null) {try {Object rawVal = field.get(obj);if (rawVal != null) {String type = dictAnno.type();String dictVal = rawVal.toString();// 从数据库查询中文标签String label = dictService.getLabel(type, dictVal);// 目标字段名:如果 targetField 不为空则用它,否则默认 “原字段名 + Text”String targetFieldName = dictAnno.targetField().isEmpty()? field.getName() + "Text": dictAnno.targetField();// 给目标字段赋值(如果找得到)try {Field targetField = clazz.getDeclaredField(targetFieldName);targetField.setAccessible(true);targetField.set(obj, label);} catch (NoSuchFieldException ignored) {// 没有找到 targetField,就跳过}}} catch (IllegalAccessException ignored) {}}// —— Step2: 对该字段的值继续递归 —— try {Object fieldValue = field.get(obj);if (fieldValue != null) {processValue(fieldValue);}} catch (IllegalAccessException ignored) {}}}/*** 判断是否是 Java 基本类型或者它的包装类*/private boolean isPrimitiveOrWrapper(Class<?> clz) {return clz.isPrimitive()|| clz == Boolean.class|| clz == Byte.class|| clz == Character.class|| clz == Short.class|| clz == Integer.class|| clz == Long.class|| clz == Float.class|| clz == Double.class|| clz == Void.class;}
}

✅讲解

  1. 切面拦截
    • @Around("execution(* com.hy..controller..*.*(..))"):只要你 Controller 中返回 R<T>,就会进到 aroundController()
    • 先执行 proceed() 拿到原始返回结果,接着取出 R<?>data 字段,开始做递归处理。
  2. processValue(Object value)
    • 这是一个万能递归入口
      • 如果 valueCollection(例如 List<UserVO>Set<XXX>),就对每个元素再调用 processValue
      • 如果是 数组Object[]UserVO[] 等),用 Array.get(...) 一一展开递归;
      • 如果是 IPage<?>(MyBatis-Plus 分页对象),拿到 IPage#getRecords()(返回的是 List<VO>)再递归;
      • 如果是 Map<?,?>,遍历 valueSet(),对每个 map 值递归;
      • 如果是 “Java 原始类型” 或者 String、枚举、包装类型,就不处理(因为不会有 @Dict);
      • 剩下就是“自定义 VO/POJO”了,调用 processObjectFields(obj)
  3. processObjectFields(Object obj)
    • 遍历对象 obj所有字段(使用反射 clazz.getDeclaredFields())。
    • Step1: 如果字段上有 @Dict 注解,就取出它的 “type” 和原始值(field.get(obj)),再调用 dictService.getLabel(type, rawVal) 一次数据库查询,拿到中文标签。
      • 然后算出目标字段名:如果 targetField 不为空,就用它;否则默认 原字段名 + "Text"
      • 找到这个 “目标字段” 并赋值(targetField.set(obj, label))。
    • Step2: 拿到字段的原始值 fieldValue = field.get(obj),如果不为空,继续走 processValue(fieldValue) —— 这样可以向下深入:比如 UserVO 里有一个 List<StudentVO>,就会继续进入那个集合,对每个 StudentVO 再做同样的 @Dict 检测。
  4. 递归终止条件
    • 如果 value 是基本类型、String、枚举,就不会再深入。
    • 如果是 Collection/数组/IPage/Map,最终会拆到它们的元素或值,继续查 Unicode。如果遇到自定义 VO,就在 processObjectFields 中一次性把自己身上的注解给干了,然后再继续往字段里钻。

5. 最终在 Controller 中的使用示例

package com.hy.controller;import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.hy.core.domain.R;
import com.hy.core.domain.UserVO;
import com.hy.core.domain.PostVO;
import com.hy.core.domain.StudentVO;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;import java.util.Arrays;
import java.util.List;@RestController
public class UserController {@GetMapping("/user")public R<UserVO> getUser() {UserVO user = new UserVO();user.setId(1L);user.setSex("1");      // 对应字典表中:dict_type="sex", dict_value="1" → dict_label="男"user.setStatus("0");   // dict_type="status", dict_value="0" → dict_label="停用"// 还可以给 students 和 posts 准备测试数据StudentVO stu = new StudentVO();stu.setId(100L);stu.setName("小明");stu.setAge(18);stu.setStuType("A");   // 假设 dict_type="stuType", dict_value="A" → dict_label="一类学生"PostVO post = new PostVO();post.setUserNickname("张三");post.setUserPhone("13800000000");post.setUserEmail("zhangsan@example.com");post.setUserGender(1);post.setPostType("X"); // 假设 dict_type="post_type", dict_value="X" → dict_label="图文帖"user.setStudents(List.of(stu));user.setPosts(List.of(post));return R.ok(user);}@GetMapping("/users")public R<List<UserVO>> getUsers() {UserVO u1 = new UserVO();u1.setId(1L);u1.setSex("2");      // dict_type="sex", dict_value="2" → dict_label="女"u1.setStatus("1");   // dict_type="status", dict_value="1" → dict_label="正常"u1.setStudents(List.<StudentVO>of());u1.setPosts(List.<PostVO>of());UserVO u2 = new UserVO();u2.setId(2L);u2.setSex("1");u2.setStatus("1");u2.setStudents(List.<StudentVO>of());u2.setPosts(List.<PostVO>of());return R.ok(Arrays.asList(u1, u2));}@GetMapping("/users/page")public R<IPage<UserVO>> getPagedUsers() {// 构造 MyBatis-Plus 的 Page 对象IPage<UserVO> page = new Page<>(1, 10);UserVO u = new UserVO();u.setId(3L);u.setSex("2");u.setStatus("0");u.setStudents(List.<StudentVO>of());u.setPosts(List.<PostVO>of());page.setRecords(List.of(u));page.setTotal(1);return R.ok(page);}
}

请求 GET /user,返回示例(假设数据库中已有对应字典数据):

{"code": 0,"msg": "success","data": {"id": 1,"sex": "1","sexDesc": "男","status": "0","statusDesc": "停用","students": [{"id": 100,"name": "小明","age": 18,"stuType": "A","stuTypeDesc": "一类学生"}],"posts": [{"userNickname": "张三","userPhone": "13800000000","userEmail": "zhangsan@example.com","userGender": 1,"postType": "X","postTypeDesc": "图文帖"}]}
}
  • sexDescstatusDescstuTypeDescpostTypeDesc 都由 AOP 自动调用 DictService 从数据库查询并赋值。
  • 嵌套在 students 列表里的 StudentVOposts 列表里的 PostVO 同样自动被递归处理。
  • 如果你改成返回 R<List<UserVO>>R<IPage<UserVO>>,同样适配,因为最外层 processValue(...) 会识别并把列表/分页拆开,进而递归。

6. ✅ 总结

  • @Around 切面:只要你的 Controller 方法返回类型是 R<T>,就会被切面拦截。
  • processValue(...):万能递归分发,可自动识别到 List<?>、数组、IPage<?>Map<?,?>,以及普通 VO。
  • processObjectFields(...):先把当前对象上所有的 @Dict 注解字段取值并查库赋予对应的“翻译”到 targetField,然后再继续对“对象内部的其他字段”做递归。
  • 数据库查询:由 DictService.getLabel(type, value) 实现,尽量在 sys_dict_data 中做 SELECT dict_label
  • 无需手动遍历每一层嵌套,只要 VO/嵌套 VO 里再出现带 @Dict 注解的字段,都会自动生效。

这样就完成了一个“通用、可扩展、支持任意深度嵌套集合/分页/Map/对象”的数据字典 AOP 转换方案。 只要在每个需要字典转换的字段上加上注解,就能自动从数据库查询并把“中文标签”写到指定的 targetField

四、只拦截特定 Controller 类(最终版 v2)

要实现通用的数据字典转换,并且只拦截特定 Controller 类(如使用 @DictConvertTarget 注解标记的),又能自动解析嵌套的 List<>Page<>、复杂嵌套结构等,以下是一个 完整方案

✅ 1. 自定义注解标记 Controller

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface DictConvertTarget {
}

✅ 2. 自定义字段注解 @Dict

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Dict {String type(); // 字典类型String targetField() default ""; // 字典文本字段
}

✅ 3. 在 Controller 上使用标记注解

@DictConvertTarget
@RestController
@RequestMapping("/user")
public class UserController {@GetMapping("/detail")public R<UserVO> detail() {// 模拟数据return R.ok(userVO);}
}

✅ 4. 字典查询接口(模拟数据库)

public interface DictService {String getLabel(String type, String value);
}

实现类可以连接数据库或缓存:

@Service
public class DictServiceImpl implements DictService {@Overridepublic String getLabel(String type, String value) {// 伪代码 - 实际应从数据库中查if ("sex".equals(type)) {return "1".equals(value) ? "男" : "女";}if ("status".equals(type)) {return "0".equals(value) ? "启用" : "禁用";}// ...其他类型return null;}
}

✅ 5. 通用字典转换工具类(支持递归、List、Page、Map 等)

public class DictValueConverter {private static DictService dictService;public static void setDictService(DictService service) {dictService = service;}public static void convert(Object obj) {if (obj == null) return;if (obj instanceof Collection) {((Collection<?>) obj).forEach(DictValueConverter::convert);} else if (obj instanceof Page) {((Page<?>) obj).getRecords().forEach(DictValueConverter::convert);} else if (obj instanceof Map) {((Map<?, ?>) obj).values().forEach(DictValueConverter::convert);} else {processSingleObject(obj);}}private static void processSingleObject(Object obj) {Field[] fields = obj.getClass().getDeclaredFields();for (Field field : fields) {field.setAccessible(true);try {// 处理 @Dict 字段if (field.isAnnotationPresent(Dict.class)) {Dict dict = field.getAnnotation(Dict.class);String value = String.valueOf(field.get(obj));String label = dictService.getLabel(dict.type(), value);if (StringUtils.isNotBlank(dict.targetField())) {Field targetField = obj.getClass().getDeclaredField(dict.targetField());targetField.setAccessible(true);targetField.set(obj, label);}}// 递归嵌套 List / VO 类型字段Object fieldValue = field.get(obj);if (fieldValue != null) {if (fieldValue instanceof Collection || fieldValue instanceof Page || fieldValue instanceof Map) {convert(fieldValue);} else if (field.getType().getPackageName().startsWith("com.hy")) {convert(fieldValue);}}} catch (Exception e) {// 忽略异常}}}
}

⚠️ com.hy 是你自己的业务包路径,用于排除系统类。


✅ 6. AOP 拦截特定 Controller 并自动转换

@Aspect
@Component
public class DictAspect {@Autowiredpublic void setDictService(DictService dictService) {DictValueConverter.setDictService(dictService);}@Around("@within(com.hy.annotation.DictConvertTarget)")public Object handleDictConversion(ProceedingJoinPoint joinPoint) throws Throwable {Object result = joinPoint.proceed();if (result instanceof R<?>) {Object data = ((R<?>) result).getData();DictValueConverter.convert(data);}return result;}
}

✅ 7. 测试返回结构

支持以下结构自动转换:

  • R<UserVO>
  • R<List<UserVO>>
  • R<Page<UserVO>>
  • 嵌套 List、VO、Page、Map 都支持。

✅ 效果示例

🔍原始:

{"data": {"sex": "1","sexDesc": null,"status": "0","statusDesc": null,"students": [{"stuType": "1","stuTypeDesc": null}],"posts": [{"postType": "news","postTypeDesc": null}]}
}

自动转换后输出:

{"data": {"sex": "1","sexDesc": "男","status": "0","statusDesc": "启用","students": [{"stuType": "1","stuTypeDesc": "全日制"}],"posts": [{"postType": "news","postTypeDesc": "新闻类"}]}
}

✅ 完整代码

src
├── main
│   ├── java
│   │   └── com
│   │       └── hy
│   │           ├── DictConversionDemoApplication.java
│   │           ├── annotation
│   │           │   ├── Dict.java
│   │           │   └── DictConvertTarget.java
│   │           ├── aop
│   │           │   └── DictAspect.java
│   │           ├── controller
│   │           │   └── UserController.java
│   │           ├── domain
│   │           │   ├── R.java
│   │           │   ├── UserVO.java
│   │           │   ├── PostVO.java
│   │           │   └── StudentVO.java
│   │           ├── service
│   │           │   ├── DictService.java
│   │           │   └── impl
│   │           │       └── DictServiceImpl.java
│   │           └── util
│   │               └── DictValueConverter.java
│   └── resources
│       └── application.yml
# DictConversionDemoApplication.java
package com.hy;import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;@SpringBootApplication
public class DictConversionDemoApplication {public static void main(String[] args) {SpringApplication.run(DictConversionDemoApplication.class, args);}
}# annotation/Dict.java
package com.hy.annotation;import java.lang.annotation.*;@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Dict {String type();String targetField() default "";
}# annotation/DictConvertTarget.java
package com.hy.annotation;import java.lang.annotation.*;@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface DictConvertTarget {
}# aop/DictAspect.java
package com.hy.aop;import com.hy.annotation.DictConvertTarget;
import com.hy.domain.R;
import com.hy.service.DictService;
import com.hy.util.DictValueConverter;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;@Aspect
@Component
public class DictAspect {@Autowiredpublic void setDictService(DictService dictService) {DictValueConverter.setDictService(dictService);}@Around("@within(com.hy.annotation.DictConvertTarget)")public Object handleDictConversion(ProceedingJoinPoint joinPoint) throws Throwable {Object result = joinPoint.proceed();if (result instanceof R<?>) {Object data = ((R<?>) result).getData();DictValueConverter.convert(data);}return result;}
}# controller/UserController.java
package com.hy.controller;import com.hy.annotation.DictConvertTarget;
import com.hy.domain.*;
import com.hy.domain.R;
import org.springframework.web.bind.annotation.*;import java.util.Arrays;@DictConvertTarget
@RestController
@RequestMapping("/user")
public class UserController {@GetMapping("/detail")public R<UserVO> detail() {UserVO user = new UserVO();user.setId(1L);user.setSex("1");user.setStatus("0");user.setStudents(Arrays.asList(new StudentVO(1L, "小明", 18, "1")));user.setPosts(Arrays.asList(new PostVO("张三", "123456", "zs@example.com", 1, "news")));return R.ok(user);}
}# domain/R.java
package com.hy.domain;import lombok.Data;@Data
public class R<T> {private int code;private String msg;private T data;public static <T> R<T> ok(T data) {R<T> r = new R<>();r.setCode(0);r.setMsg("success");r.setData(data);return r;}
}# domain/UserVO.java
package com.hy.domain;import com.hy.annotation.Dict;
import lombok.Data;import java.util.List;@Data
public class UserVO {private Long id;@Dict(type = "sex", targetField = "sexDesc")private String sex;private String sexDesc;@Dict(type = "status", targetField = "statusDesc")private String status;private String statusDesc;private List<StudentVO> students;private List<PostVO> posts;
}# domain/PostVO.java
package com.hy.domain;import com.hy.annotation.Dict;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;@Data
@NoArgsConstructor
@AllArgsConstructor
public class PostVO {private String userNickname;private String userPhone;private String userEmail;private Integer userGender;@Dict(type = "post_type", targetField = "postTypeDesc")private String postType;private String postTypeDesc;
}# domain/StudentVO.java
package com.hy.domain;import com.hy.annotation.Dict;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;@Data
@NoArgsConstructor
@AllArgsConstructor
public class StudentVO {private Long id;private String name;private Integer age;@Dict(type = "stuType", targetField = "stuTypeDesc")private String stuType;private String stuTypeDesc;
}# service/DictService.java
package com.hy.service;public interface DictService {String getLabel(String type, String value);
}# service/impl/DictServiceImpl.java
package com.hy.service.impl;import com.hy.service.DictService;
import org.springframework.stereotype.Service;@Service
public class DictServiceImpl implements DictService {@Overridepublic String getLabel(String type, String value) {return switch (type) {case "sex" -> "1".equals(value) ? "男" : "女";case "status" -> "0".equals(value) ? "启用" : "禁用";case "stuType" -> "1".equals(value) ? "全日制" : "非全日制";case "post_type" -> "news".equals(value) ? "新闻类" : "其他";default -> null;};}
}# util/DictValueConverter.java
package com.hy.util;import com.hy.annotation.Dict;
import com.hy.service.DictService;
import org.springframework.util.StringUtils;import java.lang.reflect.Field;
import java.util.*;public class DictValueConverter {private static DictService dictService;public static void setDictService(DictService service) {dictService = service;}public static void convert(Object obj) {if (obj == null) return;if (obj instanceof Collection<?>) {((Collection<?>) obj).forEach(DictValueConverter::convert);} else if (obj instanceof Map<?, ?>) {((Map<?, ?>) obj).values().forEach(DictValueConverter::convert);} else {processSingleObject(obj);}}private static void processSingleObject(Object obj) {Field[] fields = obj.getClass().getDeclaredFields();for (Field field : fields) {field.setAccessible(true);try {if (field.isAnnotationPresent(Dict.class)) {Dict dict = field.getAnnotation(Dict.class);Object value = field.get(obj);if (value != null && StringUtils.hasText(dict.targetField())) {String label = dictService.getLabel(dict.type(), value.toString());Field target = obj.getClass().getDeclaredField(dict.targetField());target.setAccessible(true);target.set(obj, label);}}Object val = field.get(obj);if (val != null && (val instanceof Collection || val.getClass().getPackageName().startsWith("com.hy"))) {convert(val);}} catch (Exception ignored) {}}}
}# resources/application.yml
serverport: 8080

五、只拦截特定 Controller 类中指定的方法(最终版 v3)

✅ 该方法适应以下场景:

  1. 单个对象,如 R<UserVO>
  2. 嵌套集合,如 List<StudentVO>List<PostVO>
  3. 嵌套 MyBatis-Plus 分页对象 IPage<UserVO>
  4. 嵌套 Map<String, Object>
  5. 任意深度的自定义 VO 嵌套

并且所有字典数据都 不预加载到内存,而是通过 JdbcTemplate 从 H2 内存数据库的 sys_dict_data 表按需查询。

提示:以下代码片段请按路径逐一创建,并保持目录结构不变。创建完后,直接用 IDE 或命令行启动 Spring Boot 即可。

1. pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><groupId>com.hy.dictconvert</groupId><artifactId>dict-convert-demo</artifactId><version>0.0.1-SNAPSHOT</version><name>字典转换示例</name><description>Spring Boot 示例:AOP+注解按需从数据库查询字典并处理嵌套结构</description><properties><java.version>11</java.version><spring.boot.version>2.7.5</spring.boot.version><mybatis.plus.version>3.5.1</mybatis.plus.version></properties><dependencies><!-- Spring Boot Starter Web --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId><version>${spring.boot.version}</version></dependency><!-- Spring AOP --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId><version>${spring.boot.version}</version></dependency><!-- Spring JDBC & H2 Database --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-jdbc</artifactId><version>${spring.boot.version}</version></dependency><dependency><groupId>com.h2database</groupId><artifactId>h2</artifactId><scope>runtime</scope></dependency><!-- MyBatis-Plus (仅用于 IPage、Page 对象演示) --><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>${mybatis.plus.version}</version></dependency><!-- Lombok --><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>1.18.24</version><scope>provided</scope></dependency><!-- 测试 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><version>${spring.boot.version}</version><scope>test</scope></dependency></dependencies><build><plugins><!-- Spring Boot Maven Plugin --><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId><version>${spring.boot.version}</version></plugin></plugins></build>
</project>

2. src/main/resources/application.properties

# H2 内存数据库配置
spring.datasource.url=jdbc:h2:mem:dictdb;DB_CLOSE_DELAY=-1;MODE=MYSQL
spring.datasource.username=sa
spring.datasource.password=
spring.datasource.driver-class-name=org.h2.Driver# 显示 H2 控制台(可选)
spring.h2.console.enabled=true
spring.h2.console.path=/h2-console# MyBatis-Plus 应用包扫描(仅为了 Page 对象可用,无实际 Mapper)
mybatis-plus.configuration.map-underscore-to-camel-case=true# 日志级别(可选)
logging.level.org.springframework.jdbc.core.JdbcTemplate=DEBUG

3. src/main/resources/schema.sql

-- 创建 sys_dict_data 表
CREATE TABLE sys_dict_data (id BIGINT AUTO_INCREMENT PRIMARY KEY,dict_type VARCHAR(64) NOT NULL,dict_value VARCHAR(64) NOT NULL,dict_label VARCHAR(128) NOT NULL
);

4. src/main/resources/data.sql

-- 插入示例字典数据
INSERT INTO sys_dict_data(dict_type, dict_value, dict_label) VALUES 
('sex', '1', '男'),
('sex', '2', '女'),
('status', '0', '停用'),
('status', '1', '正常'),
('stuType', 'A', '一类学生'),
('stuType', 'B', '二类学生'),
('post_type', 'news', '新闻帖'),
('post_type', 'article', '文章帖');

H2 会在应用启动后自动执行 schema.sqldata.sql,将 sys_dict_data 表建立并插入示例数据。


✅5. 目录结构

src
└─ main├─ java│  └─ com│     └─ hy│        └─ dictconvert│           ├─ DemoApplication.java│           ├─ annotation│           │  ├─ Dict.java│           │  └─ EnableDictConvert.java│           ├─ aspect│           │  └─ DictAspect.java│           ├─ controller│           │  └─ UserController.java│           ├─ domain│           │  ├─ R.java│           │  ├─ UserVO.java│           │  ├─ StudentVO.java│           │  └─ PostVO.java│           ├─ service│           │  ├─ DictService.java│           │  └─ DictServiceImpl.java│           └─ util│              └─ DictValueConverter.java└─ resources├─ application.properties├─ data.sql└─ schema.sql

5.1 DemoApplication.java

package com.hy.dictconvert;import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;@SpringBootApplication
public class DemoApplication {public static void main(String[] args) {SpringApplication.run(DemoApplication.class, args);}
}

5.2 注解:Dict.java

package com.hy.dictconvert.annotation;import java.lang.annotation.*;/*** 字典转换注解,标记在需要转换的字段上* type: 对应 sys_dict_data 表里的 dict_type* targetField: 转换后要写入的字段名。如果为空,则默认 “原字段名 + Text”*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Dict {String type();String targetField() default "";
}

5.3 注解:EnableDictConvert.java

package com.hy.dictconvert.annotation;import java.lang.annotation.*;/*** 方法级注解:标记在 Controller 的接口方法上,表示该接口方法返回值需要进行字典转换*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface EnableDictConvert {
}

5.4 切面:DictAspect.java

package com.hy.dictconvert.aspect;import com.baomidou.mybatisplus.core.metadata.IPage;
import com.hy.dictconvert.annotation.EnableDictConvert;
import com.hy.dictconvert.domain.R;
import com.hy.dictconvert.service.DictService;
import com.hy.dictconvert.util.DictValueConverter;
import lombok.RequiredArgsConstructor;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;/*** 切面:只拦截标注了 @EnableDictConvert 的方法*/
@Aspect
@Component
@RequiredArgsConstructor
public class DictAspect {private final DictService dictService;@Around("@annotation(enableDictConvert)")public Object aroundEnableDict(ProceedingJoinPoint pjp, EnableDictConvert enableDictConvert) throws Throwable {// 先执行业务逻辑Object result = pjp.proceed();// 只有当返回值是 R<?> 时才进一步处理if (result instanceof R<?>) {R<?> r = (R<?>) result;Object data = r.getData();if (data != null) {// 设置 DictService 到工具类DictValueConverter.setDictService(dictService);// 递归转换 data 中所有标了 @Dict 的字段DictValueConverter.convert(data);}}return result;}
}

5.5 工具类:DictValueConverter.java

package com.hy.dictconvert.util;import com.baomidou.mybatisplus.core.metadata.IPage;
import com.hy.dictconvert.annotation.Dict;
import com.hy.dictconvert.service.DictService;
import org.springframework.util.ClassUtils;import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.util.Collection;
import java.util.Map;/*** 通用字典值转换工具类:*   - 支持递归:Collection、数组、IPage、Map、普通对象*   - 对所有带 @Dict 注解的字段,从数据库按需查询 label 写入指定字段*/
public class DictValueConverter {private static DictService dictService;public static void setDictService(DictService service) {dictService = service;}/*** 入口:递归处理传入的 value*/@SuppressWarnings("unchecked")public static void convert(Object value) {if (value == null) {return;}// 1. 如果是 Collection(List、Set)if (value instanceof Collection<?>) {for (Object item : (Collection<?>) value) {convert(item);}return;}// 2. 如果是 数组if (value.getClass().isArray()) {int len = Array.getLength(value);for (int i = 0; i < len; i++) {convert(Array.get(value, i));}return;}// 3. 如果是 MyBatis-Plus 分页对象 IPage<?>if (value instanceof IPage<?>) {IPage<?> page = (IPage<?>) value;if (page.getRecords() != null) {for (Object item : page.getRecords()) {convert(item);}}return;}// 4. 如果是 Map<?, ?>if (value instanceof Map<?, ?>) {for (Object mapVal : ((Map<?, ?>) value).values()) {convert(mapVal);}return;}// 5. 如果是“Java 基本类型” 或者 String、枚举,则跳过Class<?> clazz = value.getClass();if (ClassUtils.isPrimitiveOrWrapper(clazz) || clazz == String.class || clazz.isEnum()) {return;}// 6. 普通 POJO 对象,处理它的字段processFields(value);}/*** 遍历对象的所有字段,先对带 @Dict 的字段进行转换,再对所有字段的值递归*/private static void processFields(Object obj) {if (obj == null) return;Field[] fields = obj.getClass().getDeclaredFields();for (Field field : fields) {field.setAccessible(true);// 6.1 如果当前字段有 @Dict 注解,先从数据库查询 label,写入目标字段Dict dictAnno = field.getAnnotation(Dict.class);if (dictAnno != null) {try {Object rawVal = field.get(obj);if (rawVal != null) {String dictType = dictAnno.type();String dictValue = rawVal.toString();// 按需查询数据库,获取 labelString label = dictService.getLabel(dictType, dictValue);// 目标字段名:如果 annotation 中指定了 targetField,则用它;否则用“字段名+Text”String targetFieldName = dictAnno.targetField().isEmpty()? field.getName() + "Text": dictAnno.targetField();try {Field targetField = obj.getClass().getDeclaredField(targetFieldName);targetField.setAccessible(true);targetField.set(obj, label);} catch (NoSuchFieldException ignored) {// 如果找不到 targetField,就跳过}}} catch (Exception ignored) {}}// 6.2 对该字段的值做递归(以处理嵌套结构)try {Object fieldVal = field.get(obj);if (fieldVal != null) {convert(fieldVal);}} catch (Exception ignored) {}}}
}

上面递归过深容易堆栈溢出,改一下

package com.hy.archive.util;import com.baomidou.mybatisplus.core.metadata.IPage;
import com.hy.archive.annotation.Dict;
import com.hy.archive.service.SysDictDataService;import org.springframework.util.ClassUtils;import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.util.Collection;
import java.util.Map;
import java.util.IdentityHashMap;/*** 通用字典值转换工具类:*   - 支持递归:Collection、数组、IPage、Map、普通对象*   - 对所有带 @Dict 注解的字段,从数据库按需查询 label 写入指定字段*/
public class DictValueConverter {private static SysDictDataService dictService;public static void setDictService(SysDictDataService service) {dictService = service;}private static final int MAX_RECURSION_DEPTH = 20;private static final ThreadLocal<IdentityHashMap<Object, Boolean>> processedObjects =ThreadLocal.withInitial(IdentityHashMap::new);/*** 入口:递归处理传入的 value*/@SuppressWarnings("unchecked")public static void convert(Object value) {convert(value, 0);}private static void convert(Object value, int depth) {if (value == null || depth > MAX_RECURSION_DEPTH) {return;}// 检查是否已处理过该对象if (processedObjects.get().putIfAbsent(value, Boolean.TRUE) != null) {return;}// 1. 如果是 Collection(List、Set)if (value instanceof Collection<?>) {for (Object item : (Collection<?>) value) {convert(item);}return;}// 2. 如果是 数组if (value.getClass().isArray()) {int len = Array.getLength(value);for (int i = 0; i < len; i++) {convert(Array.get(value, i));}return;}// 3. 如果是 MyBatis-Plus 分页对象 IPage<?>if (value instanceof IPage<?>) {IPage<?> page = (IPage<?>) value;if (page.getRecords() != null) {for (Object item : page.getRecords()) {convert(item);}}return;}// 4. 如果是 Map<?, ?>if (value instanceof Map<?, ?>) {for (Object mapVal : ((Map<?, ?>) value).values()) {convert(mapVal);}return;}// 5. 如果是“Java 基本类型” 或者 String、枚举,则跳过Class<?> clazz = value.getClass();if (ClassUtils.isPrimitiveOrWrapper(clazz) || clazz == String.class || clazz.isEnum()) {return;}// 6. 普通 POJO 对象,处理它的字段processFields(value, depth);}/*** 遍历对象的所有字段,处理带 @Dict 注解的字段并对字段值递归处理*/private static void processFields(Object obj, int depth) {if (obj == null) return;Field[] fields = obj.getClass().getDeclaredFields();for (Field field : fields) {field.setAccessible(true);// 处理带有@Dict注解的字段Dict dictAnno = field.getAnnotation(Dict.class);if (dictAnno != null) {try {Object rawVal = field.get(obj);if (rawVal != null) {String dictType = dictAnno.type();String dictValue = rawVal.toString();// 按需查询数据库,获取 labelString label = dictService.getLabel(dictType, dictValue);// 目标字段名:如果 annotation 中指定了 targetField,则用它;否则用"字段名+Text"String targetFieldName = dictAnno.targetField().isEmpty()? field.getName() + "Text": dictAnno.targetField();try {Field targetField = obj.getClass().getDeclaredField(targetFieldName);targetField.setAccessible(true);targetField.set(obj, label);} catch (NoSuchFieldException ignored) {// 如果找不到 targetField,就跳过}}} catch (Exception ignored) {}}// 对该字段的值做递归处理(以处理嵌套结构)try {Object fieldVal = field.get(obj);if (fieldVal != null) {convert(fieldVal, depth + 1);}} catch (Exception ignored) {}}// 清理当前对象的处理记录,避免内存泄漏if (depth == 0) {processedObjects.get().clear();}}
}

5.6 服务层接口:DictService.java

package com.hy.dictconvert.service;/*** 字典服务:按需从数据库查询 dict_type + dict_value 对应的 dict_label*/
public interface DictService {/*** @param dictType  字典类型(sys_dict_data.dict_type)* @param dictValue 字典值(sys_dict_data.dict_value)* @return 对应的 dict_label;如果查不到,返回 null*/String getLabel(String dictType, String dictValue);
}

5.7 服务层实现:DictServiceImpl.java

package com.hy.dictconvert.service;import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;/*** 使用 JdbcTemplate 从 H2 数据库的 sys_dict_data 表按需查询字典标签*/
@Service
public class DictServiceImpl implements DictService {@Autowiredprivate JdbcTemplate jdbcTemplate;@Overridepublic String getLabel(String dictType, String dictValue) {String sql = "SELECT dict_label FROM sys_dict_data WHERE dict_type = ? AND dict_value = ? LIMIT 1";try {return jdbcTemplate.queryForObject(sql,new Object[]{dictType, dictValue},String.class);} catch (Exception e) {// 查不到或出错时,返回 nullreturn null;}}
}

5.8 通用响应体:R.java

package com.hy.dictconvert.domain;import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;/*** 通用响应封装类 R<T>*/
@Data
@ApiModel("响应信息主体")
public class R<T> implements Serializable {private static final long serialVersionUID = 1L;@ApiModelProperty("返回标记:成功标记=0,失败标记=1")private int code;@ApiModelProperty("返回信息")private String msg;@ApiModelProperty("数据")private T data;public static <T> R<T> ok() {return restResult(null, 0, null);}public static <T> R<T> ok(T data) {return restResult(data, 0, null);}public static <T> R<T> ok(T data, String msg) {return restResult(data, 0, msg);}public static <T> R<T> failed() {return restResult(null, 1, null);}public static <T> R<T> failed(String msg) {return restResult(null, 1, msg);}public static <T> R<T> failed(T data) {return restResult(data, 1, null);}public static <T> R<T> failed(T data, String msg) {return restResult(data, 1, msg);}private static <T> R<T> restResult(T data, int code, String msg) {R<T> r = new R<>();r.setCode(code);r.setData(data);r.setMsg(msg);return r;}
}

5.9 域对象:UserVO.java

package com.hy.dictconvert.domain;import com.hy.dictconvert.annotation.Dict;
import lombok.Data;import java.util.List;@Data
public class UserVO {private Long id;@Dict(type = "sex", targetField = "sexDesc")private String sex;private String sexDesc;@Dict(type = "status", targetField = "statusDesc")private String status;private String statusDesc;private List<StudentVO> students;private List<PostVO> posts;
}

5.10 域对象:StudentVO.java

package com.hy.dictconvert.domain;import com.hy.dictconvert.annotation.Dict;
import lombok.Data;@Data
public class StudentVO {private Long id;private String name;private Integer age;@Dict(type = "stuType", targetField = "stuTypeDesc")private String stuType;private String stuTypeDesc;
}

5.11 域对象:PostVO.java

package com.hy.dictconvert.domain;import com.hy.dictconvert.annotation.Dict;
import lombok.Data;@Data
public class PostVO {private String userNickname;private String userPhone;private String userEmail;private Integer userGender;@Dict(type = "post_type", targetField = "postTypeDesc")private String postType;private String postTypeDesc;
}

5.12 控制器:UserController.java

package com.hy.dictconvert.controller;import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.hy.dictconvert.annotation.EnableDictConvert;
import com.hy.dictconvert.domain.R;
import com.hy.dictconvert.domain.UserVO;
import com.hy.dictconvert.domain.StudentVO;
import com.hy.dictconvert.domain.PostVO;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;import java.util.List;
import java.util.Map;/*** 演示 Controller:只对标注了 @EnableDictConvert 的接口方法进行字典转换*/
@RestController
@RequestMapping("/user")
public class UserController {/*** 1. 单个 UserVO 对象演示*/@EnableDictConvert@GetMapping("/detail")public R<UserVO> detail() {// 模拟数据UserVO user = new UserVO();user.setId(1L);user.setSex("1");      // 对应 sys_dict_data(dict_type='sex', dict_value='1') -> '男'user.setStatus("0");   // 对应 sys_dict_data('status','0')->'停用'// 嵌套 students 列表StudentVO s1 = new StudentVO();s1.setId(101L);s1.setName("小明");s1.setAge(18);s1.setStuType("A");    // 对应 '一类学生'user.setStudents(List.of(s1));// 嵌套 posts 列表PostVO p1 = new PostVO();p1.setUserNickname("张三");p1.setUserPhone("13800000000");p1.setUserEmail("zhangsan@example.com");p1.setUserGender(1);p1.setPostType("news"); // 对应 '新闻帖'user.setPosts(List.of(p1));return R.ok(user);}/*** 2. List<UserVO> 演示*/@EnableDictConvert@GetMapping("/list")public R<List<UserVO>> list() {UserVO u1 = new UserVO();u1.setId(2L);u1.setSex("2");      u1.setStatus("1");   // 正常u1.setStudents(List.of());u1.setPosts(List.of());UserVO u2 = new UserVO();u2.setId(3L);u2.setSex("1");u2.setStatus("1");u2.setStudents(List.of());u2.setPosts(List.of());return R.ok(List.of(u1, u2));}/*** 3. IPage<UserVO> 分页演示(手动构造 Page 对象)*/@EnableDictConvert@GetMapping("/page")public R<Page<UserVO>> page() {// MyBatis-Plus 的 Page<T> 对象,第二个参数是 pageSizePage<UserVO> page = new Page<>(1, 2);UserVO u1 = new UserVO();u1.setId(4L);u1.setSex("2");u1.setStatus("0"); // 停用u1.setStudents(List.of());u1.setPosts(List.of());page.setRecords(List.of(u1));page.setTotal(1);return R.ok(page);}/*** 4. Map<String, Object> 演示*/@EnableDictConvert@GetMapping("/map")public R<Map<String, Object>> map() {UserVO user = new UserVO();user.setId(5L);user.setSex("1");user.setStatus("1");user.setStudents(List.of());user.setPosts(List.of());// 放到 map 里再返回return R.ok(Map.of("userKey", user));}/*** 5. 不加 @EnableDictConvert 的接口不会被拦截*/@GetMapping("/raw")public R<UserVO> raw() {UserVO u = new UserVO();u.setId(6L);u.setSex("2");u.setStatus("0");u.setStudents(List.of());u.setPosts(List.of());return R.ok(u);}
}

6. 启动与验证

测试各个接口

  • GET http://localhost:8080/user/detail

    {"code": 0,"msg": null,"data": {"id": 1,"sex": "1","sexDesc": "男","status": "0","statusDesc": "停用","students": [{"id": 101,"name": "小明","age": 18,"stuType": "A","stuTypeDesc": "一类学生"}],"posts": [{"userNickname": "张三","userPhone": "13800000000","userEmail": "zhangsan@example.com","userGender": 1,"postType": "news","postTypeDesc": "新闻帖"}]}
    }
    
  • GET http://localhost:8080/user/list

    {"code": 0,"msg": null,"data": [{"id": 2,"sex": "2","sexDesc": "女","status": "1","statusDesc": "正常","students": [],"posts": []},{"id": 3,"sex": "1","sexDesc": "男","status": "1","statusDesc": "正常","students": [],"posts": []}]
    }
    
  • GET http://localhost:8080/user/page

    {"code": 0,"msg": null,"data": {"records": [{"id": 4,"sex": "2","sexDesc": "女","status": "0","statusDesc": "停用","students": [],"posts": []}],"total": 1,"size": 2,"current": 1,"orders": [],"optimizeCountSql": true,"searchCount": true,"countId": null,"maxLimit": null,"pages": 1,"ordersBy": null}
    }
    
  • GET http://localhost:8080/user/map

    {"code": 0,"msg": null,"data": {"userKey": {"id": 5,"sex": "1","sexDesc": "男","status": "1","statusDesc": "正常","students": [],"posts": []}}
    }
    
  • GET http://localhost:8080/user/raw
    (没有加 @EnableDictConvert,因此不会被切面拦截,返回原始字段)

    {"code": 0,"msg": null,"data": {"id": 6,"sex": "2","sexDesc": null,"status": "0","statusDesc": null,"students": [],"posts": []}
    }
    

7. ✅ 说明与可扩展点

  1. 按需查询
    • DictServiceImpl 使用 JdbcTemplatesys_dict_data 表按需查找 dict_label,不会把全部字典加载到内存。
    • 如果生产环境需要加缓存,可在 getLabel(...) 方法上加 @Cacheable 或自行使用 Redis/Caffeine 缓存。
  2. 嵌套处理
    • DictValueConverter.convert(Object) 方法会递归检测传入的值:
      • 如果是 Collection → 遍历每个元素
      • 如果是 数组 → 遍历每个元素
      • 如果是 IPage<?> → 遍历 page.getRecords()
      • 如果是 Map<?,?> → 遍历 map.values()
      • 如果是简单类型(基本类型、包装类型、String、枚举) → 跳过
      • 否则视为普通 POJO → 反射遍历它的所有字段:
        • 先对带 @Dict 注解的字段,调用 DictService.getLabel(...) 查库并写入对应的 targetField
        • 再对该字段值本身做递归(处理更深层的嵌套)
  3. 接口级开关
    • 只有在 Controller 方法上加了 @EnableDictConvert 注解时,才会触发 AOP 切面 DictAspect
    • 其他方法不受影响,也不会触发字典转换。
  4. 分页示例
    • 引入了 MyBatis-Plus 依赖,仅为了使用 Page<T> / IPage<T> 类,便于演示分页场景。
    • 真实项目里可配合 MyBatis-Plus 或任意实现 IPage<T> 的分页插件。
  5. 数据源
    • 使用 H2 内存数据库,启动时执行 schema.sqldata.sql 建表并插入字典示例。
    • 你可以将其替换为 MySQL/Oracle,并自行修改 application.properties 和数据库初始化脚本。

至此,整个示例项目已经完备:

  • 按路径创建所有文件
  • 启动后可在 /user/detail, /user/list, /user/page, /user/map 等接口测试“数据字典转换”功能
  • 无需预加载所有字典数据,仅按需从数据库查询

六、EasyExcel 导出嵌套数据并动态映射字典

1. 在导出前通过自定义注解和 AOP 机制对字段进行数据字典转换。

✅主要功能如下:

  • 使用 EasyExcel 将数据写入 HttpServletResponse,实现浏览器下载。
  • VO 对象中含有嵌套的 List<PostVO> 结构,导出时需要对嵌套列表进行平铺输出(EasyExcel 默认只支持平铺结构))。
  • 定义自定义注解 @Dict(type, targetField) 标注需要做字典转换的字段,type 对应字典类型,targetField 指定转换结果要写入的字段名。
  • 不预加载字典值,转换时动态查询 MySQL 表 sys_dict_data(dict_type, dict_value, dict_label) 获得标签。
  • 通过 AOP 切面结合注解扫描,递归处理嵌套对象(集合、Map、自定义 VO 等)中标注 @Dict 的字段。
  • 在导出 Controller 中手动调用字典解析服务(如 dictResolverService.translate(dataList))对数据进行转换。
  • 前端使用 Thymeleaf 渲染页面,提供一个“导出”按钮,点击后触发导出接口。

2. 项目结构

项目目录结构示例:

springboot-easyexcel-dict-demo/
├── src/
│   ├── main/
│   │   ├── java/com/example/demo/
│   │   │   ├── DemoApplication.java            # Spring Boot 主类
│   │   │   ├── entity/User.java                # 数据库实体(可选)
│   │   │   ├── vo/UserExportVO.java            # 导出用 VO(含嵌套列表)
│   │   │   ├── vo/PostVO.java                  # 嵌套列表对象 VO
│   │   │   ├── annotation/Dict.java            # 自定义 @Dict 注解
│   │   │   ├── service/DictResolverService.java# 字典解析服务(执行转换)
│   │   │   ├── aspect/DictAspect.java         # AOP 切面(扫描并触发转换)
│   │   │   ├── controller/ExportController.java# 导出接口 Controller
│   │   │   └── mapper/SysDictDataMapper.java   # MyBatis Mapper 查询字典表
│   │   └── resources/
│   │       ├── application.yml                # Spring Boot 配置(数据库连接等)
│   │       └── templates/export.html          # Thymeleaf 前端页面
│   └── test/...
└── sql/└── sys_dict_data.sql                     # 建表与初始数据脚本

其中 vo/ 包存放导出数据的 VO 类,支持通过注解标注字段。annotation/ 包为自定义注解定义,aspect/ 包为 AOP 切面处理,mapper/ 包为 MyBatis 接口查询字典表。templates/export.html 为前端页面,包含一个“导出”按钮。

3. 数据库与字典表

示例使用 MySQL 存储字典数据,假设有数据字典表 sys_dict_data,结构包括 dict_type(类型)、dict_value(键)、dict_label(标签)等字段。示例建表及插入脚本:

-- sys_dict_data 表,用于存储字典映射
CREATE TABLE sys_dict_data (id INT AUTO_INCREMENT PRIMARY KEY,dict_type VARCHAR(100) NOT NULL,dict_value VARCHAR(100) NOT NULL,dict_label VARCHAR(100) NOT NULL
);
-- 插入示例字典数据
INSERT INTO sys_dict_data(dict_type, dict_value, dict_label) VALUES
('user_gender', 'M', '男'),
('user_gender', 'F', '女'),
('post_type', 'BLOG', '博客'),
('post_type', 'NEWS', '新闻');

application.yml 中配置 MySQL 数据源,例如:

spring:datasource:url: jdbc:mysql://localhost:3306/demo_db?useSSL=false&serverTimezone=UTCusername: your_userpassword: your_passworddriver-class-name: com.mysql.cj.jdbc.Driver

4. 实体/VO 定义

  1. UserExportVO.java:导出用的用户信息 VO,包含基本字段和嵌套的帖子列表。对需要字典转换的字段添加 @Dict 注解,并指定 dictType 和要写入的目标字段名称。使用 EasyExcel 注解(如 @ExcelProperty)指定 Excel 列名。示例:
@Data
public class UserExportVO {@ExcelProperty("用户名")private String username;@Dict(type = "user_gender", targetField = "genderLabel")  // 标注性别需转换@ExcelProperty("性别") private String gender;   // 存储字典值(代码)@ExcelProperty("性别标签")private String genderLabel; // 用于输出性别文本@ExcelProperty("年龄")private Integer age;// 嵌套列表,一个用户的多个帖子@ExcelProperty("帖子列表")private List<PostVO> posts;
}
  1. PostVO.java:用户帖子信息 VO,同样可以对需要转换的字段加注解。示例:
@Data
public class PostVO {@ExcelProperty("帖子标题")private String title;@ExcelProperty("帖子内容")private String content;@Dict(type = "post_type", targetField = "typeLabel")@ExcelProperty("帖子类型")private String type;     // 存储字典值,如 "BLOG","NEWS"@ExcelProperty("帖子类型标签")private String typeLabel;
}

在导出时,EasyExcel 会将 UserExportVO 中的每个字段映射为 Excel 列,并对嵌套 posts 列表中的元素进行行级输出。注意 EasyExcel 默认只支持平铺结构,所以需要自行处理嵌套列表的行展开(可在业务层将嵌套数据平铺成多行,或编写复杂的自定义表头逻辑)。

5. 自定义注解 @Dict

annotation/Dict.java 中定义字典注解,用于标识需要进行字典转换的字段,例如:

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Dict {/** 字典类型,对应 sys_dict_data.dict_type */String type();/** 目标字段名,用于存储转换后的标签值 */String targetField();
}

该注解添加到字段上后,后续通过 AOP 或工具类扫描时,可以知道需要将此字段的值(如性别代码)替换成字典表中的标签,并将结果写入指定的目标字段上。

6. 字典解析服务(DictResolverService)

实现一个服务类,用反射递归遍历对象,对所有标有 @Dict 的字段进行值替换。示例逻辑:

@Service
public class DictResolverService {@Autowiredprivate SysDictDataMapper dictMapper; // MyBatis Mapper 查询 sys_dict_data/*** 对给定数据(实体、列表或分页等)中的字典字段进行值翻译。*/public void translate(Object data) {if (data == null) return;// 如果是列表,则遍历每个元素翻译if (data instanceof Iterable<?>) {for (Object item : (Iterable<?>) data) {translate(item);}return;}// 如果是Map,则遍历值进行翻译if (data instanceof Map<?, ?>) {for (Object value : ((Map<?, ?>) data).values()) {translate(value);}return;}// 对象类型:遍历其字段Class<?> clazz = data.getClass();for (Field field : clazz.getDeclaredFields()) {field.setAccessible(true);try {// 如果字段有 @Dict 注解,则进行转换if (field.isAnnotationPresent(Dict.class)) {Dict dict = field.getAnnotation(Dict.class);Object key = field.get(data);if (key != null) {// 查询字典表获取标签String dictType = dict.type();String dictValue = key.toString();String label = dictMapper.selectLabel(dictType, dictValue);// 将结果写入目标字段String targetFieldName = dict.targetField();Field targetField = clazz.getDeclaredField(targetFieldName);targetField.setAccessible(true);targetField.set(data, label);}} // 如果字段本身是集合或自定义对象,递归处理else if (field.get(data) != null &&!(field.getType().isPrimitive() || field.getType() == String.class)) {translate(field.get(data));}} catch (Exception e) {// 忽略或记录错误}}}
}

其中 SysDictDataMapper.selectLabel(type, value) 为自定义 MyBatis 查询方法,用于从数据库查出对应 dict_label。如:

@Mapper
public interface SysDictDataMapper {@Select("SELECT dict_label FROM sys_dict_data WHERE dict_type = #{type} AND dict_value = #{value} LIMIT 1")String selectLabel(@Param("type") String type, @Param("value") String value);
}

上述服务不预加载所有字典值,而是按需查询数据库。它能够递归处理对象内部的嵌套结构(包括 ListMap 或自定义 VO),在找到 @Dict 注解字段时执行查询替换。这种思路与常见的 AOP 切面方式相似。

7. AOP 切面(可选)

可以使用 AOP 切面结合自定义注解,在方法返回时自动触发字典解析。比如定义一个方法注解 @TranslateDict,在切面中拦截该注解并调用 dictResolverService.translate(result)。示例:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface TranslateDict {}
java复制编辑@Aspect
@Component
public class DictAspect {@Autowiredprivate DictResolverService dictResolverService;@Around("@annotation(TranslateDict)")public Object aroundDict(ProceedingJoinPoint joinPoint) throws Throwable {Object result = joinPoint.proceed();dictResolverService.translate(result);return result;}
}

这样标注了 @TranslateDict 的 Controller 或 Service 方法会在返回结果后自动进行字典转换。在本示例中,导出接口也可使用此注解自动转换,或者直接在代码中手动调用转换。

8. 导出接口 Controller

在 Controller 中定义导出接口,接收 HTTP 请求并将生成的 Excel 文件写入响应流。例如:

@RestController
@RequestMapping("/export")
public class ExportController {@Autowiredprivate DictResolverService dictResolverService;// 获取要导出的数据,通常从数据库查询private List<UserExportVO> fetchData() {// 示例数据List<UserExportVO> list = new ArrayList<>();// ...填充示例用户及帖子数据return list;}@GetMapping("/users")public void exportUsers(HttpServletResponse response) {List<UserExportVO> users = fetchData();// 执行字典转换(性别、帖子类型等)dictResolverService.translate(users);// 设置响应头response.setContentType("application/vnd.ms-excel");response.setCharacterEncoding("utf-8");response.setHeader("Content-Disposition", "attachment;filename=users.xlsx");// 使用 EasyExcel 写出try {EasyExcel.write(response.getOutputStream(), UserExportVO.class).sheet("用户信息").doWrite(users);} catch (IOException e) {// 处理异常}}
}

上例中,首先调用 fetchData() 获取待导出的用户列表,然后调用 dictResolverService.translate(users) 进行字典值转换。最后通过 EasyExcel.write(response.getOutputStream(), UserExportVO.class)...doWrite(users) 将数据写入响应[blog.csdn.net](https://blog.csdn.net/qq_65597930/article/details/144252958#:~:text=%2F%2F 使用EasyExcel进行数据写入操作 EasyExcel.write(response.getOutputStream()%2C ExportPlanInformationVo.class) .sheet(,指定工作表名称)。响应头需设置为下载文件格式,前端浏览器即可接收并下载生成的 Excel。

9. 前端页面(Thymeleaf)

创建一个简单的 Thymeleaf 页面 export.html,提供“导出”按钮触发下载:

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head><title>导出示例</title>
</head>
<body>
<h2>用户数据导出</h2>
<!-- 点击按钮向 /export/users 发送请求,浏览器将下载 Excel -->
<form th:action="@{/export/users}" method="get"><button type="submit">导出用户信息</button>
</form>
</body>
</html>

访问页面后,点击“导出用户信息”按钮,即会调用 GET /export/users 接口,浏览器自动开始下载 Excel 文件。


版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com

热搜词