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)
✅它可以处理以下情况:
- 单个对象(如
UserVO
) - 嵌套集合(如
List<StudentVO>
、List<PostVO>
) - 嵌套
IPage<T>
(MyBatis-Plus 分页对象) - 嵌套
Map<String, Object>
- 嵌套的任意自定义 VO(只要它们有
@Dict
注解或者内部还包含可遍历的集合/对象)
✅核心思路是:
- 对传入的
data
(可能是单个 VO、List、Page 等)做一次递归:- 若是基本类型(
String
、包装类、枚举等)就跳过 - 若是
Collection
(List
、Set
、数组)就对每个元素递归 - 若是
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
(例如sexDesc
、statusDesc
、stuTypeDesc
、postTypeDesc
)来接收转换之后的中文。
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;}
}
✅讲解
- 切面拦截
@Around("execution(* com.hy..controller..*.*(..))")
:只要你 Controller 中返回R<T>
,就会进到aroundController()
。- 先执行
proceed()
拿到原始返回结果,接着取出R<?>
的data
字段,开始做递归处理。
processValue(Object value)
- 这是一个万能递归入口:
- 如果
value
是Collection
(例如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)
。
- 如果
- 这是一个万能递归入口:
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
检测。
- 遍历对象
- 递归终止条件
- 如果
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": "图文帖"}]}
}
sexDesc
、statusDesc
、stuTypeDesc
、postTypeDesc
都由 AOP 自动调用DictService
从数据库查询并赋值。- 嵌套在
students
列表里的StudentVO
、posts
列表里的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)
✅ 该方法适应以下场景:
- 单个对象,如
R<UserVO>
- 嵌套集合,如
List<StudentVO>
、List<PostVO>
- 嵌套 MyBatis-Plus 分页对象
IPage<UserVO>
- 嵌套
Map<String, Object>
- 任意深度的自定义 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.sql
和data.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. ✅ 说明与可扩展点
- 按需查询
DictServiceImpl
使用JdbcTemplate
从sys_dict_data
表按需查找dict_label
,不会把全部字典加载到内存。- 如果生产环境需要加缓存,可在
getLabel(...)
方法上加@Cacheable
或自行使用 Redis/Caffeine 缓存。
- 嵌套处理
DictValueConverter.convert(Object)
方法会递归检测传入的值:- 如果是
Collection
→ 遍历每个元素 - 如果是 数组 → 遍历每个元素
- 如果是
IPage<?>
→ 遍历page.getRecords()
- 如果是
Map<?,?>
→ 遍历map.values()
- 如果是简单类型(基本类型、包装类型、String、枚举) → 跳过
- 否则视为普通 POJO → 反射遍历它的所有字段:
- 先对带
@Dict
注解的字段,调用DictService.getLabel(...)
查库并写入对应的targetField
- 再对该字段值本身做递归(处理更深层的嵌套)
- 先对带
- 如果是
- 接口级开关
- 只有在 Controller 方法上加了
@EnableDictConvert
注解时,才会触发 AOP 切面DictAspect
。 - 其他方法不受影响,也不会触发字典转换。
- 只有在 Controller 方法上加了
- 分页示例
- 引入了 MyBatis-Plus 依赖,仅为了使用
Page<T>
/IPage<T>
类,便于演示分页场景。 - 真实项目里可配合 MyBatis-Plus 或任意实现
IPage<T>
的分页插件。
- 引入了 MyBatis-Plus 依赖,仅为了使用
- 数据源
- 使用 H2 内存数据库,启动时执行
schema.sql
和data.sql
建表并插入字典示例。 - 你可以将其替换为 MySQL/Oracle,并自行修改
application.properties
和数据库初始化脚本。
- 使用 H2 内存数据库,启动时执行
至此,整个示例项目已经完备:
- 按路径创建所有文件
- 启动后可在
/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 定义
- 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;
}
- 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);
}
上述服务不预加载所有字典值,而是按需查询数据库。它能够递归处理对象内部的嵌套结构(包括 List
、Map
或自定义 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 文件。