字典与枚举自动翻译:@AutoDict/@AutoEnum 注解 + AOP 零侵入方案
字典与枚举自动翻译注解驱动方案
引言
在企业级应用中,数据字典和枚举值的翻译是极其常见的开发需求。例如:数据库中存储的性别字段值是 1/2,前端需要展示为 男/女;订单状态存储为 0/1/2/3,前端需要展示为 待支付/已支付/已发货/已完成。
传统的做法是在业务代码中手动翻译,或者在前端维护映射表。这两种方式都存在维护成本高、容易遗漏的问题。本文将分享基于注解驱动的字典与枚举自动翻译方案,通过 AOP 拦截自动完成翻译,业务代码零侵入。
一、传统方案的痛点
1.1 手动翻译方式
// 每个接口都需要手动翻译,代码重复
public UserVO getUser(Long id) {
User user = userMapper.selectById(id);
UserVO vo = BeanUtil.copyProperties(user, UserVO.class);
// 手动翻译性别
vo.setGenderName(dictService.getLabel("sys_gender", user.getGender()));
// 手动翻译状态
vo.setStatusName(dictService.getLabel("sys_status", user.getStatus()));
// 手动翻译部门
vo.setDeptName(deptService.getNameById(user.getDeptId()));
return vo;
}
痛点:
- 每个接口都写翻译逻辑,代码重复
- 容易遗漏某个字段的翻译
- 新增字典类型时需要逐个接口修改
1.2 前端维护映射表
// 前端维护字典映射
const genderMap = { 1: '男', 2: '女' }
const statusMap = { 0: '待支付', 1: '已支付', 2: '已发货', 3: '已完成' }
痛点:
- 字典数据前后端重复维护
- 字典值变更时需前端发版
- 多端(Web/App/小程序)需各自维护
二、注解驱动方案设计
2.1 整体架构
flowchart TD
A[Controller返回数据] --> B[AOP拦截]
B --> C[扫描@AutoDict注解]
B --> D[扫描@AutoEnum注解]
C --> E[从Redis获取字典数据]
D --> F[从枚举类获取映射]
E --> G[翻译字段值]
F --> G
G --> H[设置翻译后的字段值]
H --> I[返回翻译后的数据]
style B fill:#e1f5fe,stroke:#0288d1
style E fill:#f3e5f5,stroke:#7b1fa2
style G fill:#fff9c4,stroke:#f9a825
2.2 注解设计
字典翻译注解
/**
* 字典自动翻译注解
* 标注在VO字段上,自动将字典值翻译为字典标签
*
* 使用示例:
* @AutoDict(dictType = "sys_gender", sourceField = "gender")
* private String genderName;
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoDict {
/**
* 字典类型编码
*/
String dictType();
/**
* 源字段名(存储字典值的字段)
*/
String sourceField();
}
/**
* 类级别字典翻译注解
* 标注在VO类上,批量翻译多个字段
*
* 使用示例:
* @DictTranslate({
* @AutoDict(dictType = "sys_gender", sourceField = "gender"),
* @AutoDict(dictType = "sys_status", sourceField = "status")
* })
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface DictTranslate {
AutoDict[] value();
}
枚举翻译注解
/**
* 枚举自动翻译注解
* 标注在VO字段上,自动将枚举值翻译为枚举描述
*
* 使用示例:
* @AutoEnum(enumClass = GenderEnum.class, sourceField = "gender")
* private String genderName;
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoEnum {
/**
* 枚举类
*/
Class<? extends Enum<?>> enumClass();
/**
* 源字段名
*/
String sourceField();
}
/**
* 类级别枚举翻译注解
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface EnumTranslate {
AutoEnum[] value();
}
三、枚举基类设计
3.1 可翻译枚举接口
/**
* 可翻译枚举接口
* 所有需要自动翻译的枚举类实现此接口
*/
public interface TranslatableEnum {
/**
* 获取枚举值(存储到数据库的值)
*/
Object getValue();
/**
* 获取枚举描述(翻译后的文本)
*/
String getLabel();
}
3.2 枚举实现示例
/**
* 性别枚举
*/
public enum GenderEnum implements TranslatableEnum {
MALE(1, "男"),
FEMALE(2, "女");
private final Integer value;
private final String label;
GenderEnum(Integer value, String label) {
this.value = value;
this.label = label;
}
@Override
public Object getValue() {
return value;
}
@Override
public String getLabel() {
return label;
}
/**
* 根据值获取描述
*/
public static String getLabelByValue(Integer value) {
for (GenderEnum e : values()) {
if (e.value.equals(value)) return e.label;
}
return null;
}
}
四、AOP 拦截器实现
4.1 字典翻译切面
/**
* 字典自动翻译切面
* 拦截Controller返回值,自动翻译标注了@AutoDict的字段
*/
@Aspect
@Component
@Slf4j
public class DictTranslateAspect {
private final DictService dictService;
public DictTranslateAspect(DictService dictService) {
this.dictService = dictService;
}
/**
* 拦截所有Controller方法
*/
@AfterReturning(pointcut = "execution(* com.example..controller..*.*(..))",
returning = "result")
public void translateDict(JoinPoint joinPoint, Object result) {
if (result == null) return;
// 处理R<T>包装类
Object data = unwrapResult(result);
if (data == null) return;
// 处理单个对象
if (data instanceof List<?> list) {
list.forEach(this::translateObject);
} else {
translateObject(data);
}
}
/**
* 翻译单个对象
*/
private void translateObject(Object obj) {
Class<?> clazz = obj.getClass();
// 处理类级别@DictTranslate注解
DictTranslate classDict = clazz.getAnnotation(DictTranslate.class);
if (classDict != null) {
for (AutoDict autoDict : classDict.value()) {
translateField(obj, autoDict);
}
}
// 处理字段级别@AutoDict注解
for (Field field : clazz.getDeclaredFields()) {
AutoDict autoDict = field.getAnnotation(AutoDict.class);
if (autoDict != null) {
translateField(obj, autoDict);
}
}
// 处理字段级别@AutoEnum注解
for (Field field : clazz.getDeclaredFields()) {
AutoEnum autoEnum = field.getAnnotation(AutoEnum.class);
if (autoEnum != null) {
translateEnumField(obj, autoEnum);
}
}
}
/**
* 翻译字典字段
*/
private void translateField(Object obj, AutoDict autoDict) {
try {
// 获取源字段值
Field sourceField = obj.getClass().getDeclaredField(autoDict.sourceField());
sourceField.setAccessible(true);
Object sourceValue = sourceField.get(obj);
if (sourceValue == null) return;
// 从字典服务获取翻译值
String label = dictService.getLabel(autoDict.dictType(),
sourceValue.toString());
// 设置翻译后的值到目标字段
// 目标字段名 = sourceField + "Name"(约定优于配置)
String targetFieldName = autoDict.sourceField() + "Name";
Field targetField = obj.getClass().getDeclaredField(targetFieldName);
targetField.setAccessible(true);
targetField.set(obj, label);
} catch (Exception e) {
log.warn("字典翻译失败: {}", e.getMessage());
}
}
/**
* 翻译枚举字段
*/
private void translateEnumField(Object obj, AutoEnum autoEnum) {
try {
Field sourceField = obj.getClass().getDeclaredField(autoEnum.sourceField());
sourceField.setAccessible(true);
Object sourceValue = sourceField.get(obj);
if (sourceValue == null) return;
// 从枚举类获取翻译值
Class<? extends Enum<?>> enumClass = autoEnum.enumClass();
String label = getEnumLabel(enumClass, sourceValue);
String targetFieldName = autoEnum.sourceField() + "Name";
Field targetField = obj.getClass().getDeclaredField(targetFieldName);
targetField.setAccessible(true);
targetField.set(obj, label);
} catch (Exception e) {
log.warn("枚举翻译失败: {}", e.getMessage());
}
}
/**
* 从枚举类获取标签
*/
private String getEnumLabel(Class<? extends Enum<?>> enumClass, Object value) {
for (Enum<?> e : enumClass.getEnumConstants()) {
if (e instanceof TranslatableEnum te) {
if (te.getValue().equals(value)) {
return te.getLabel();
}
}
}
return null;
}
}
五、Redis 缓存字典数据
5.1 字典缓存设计
flowchart LR
A[翻译请求] --> B{Redis有缓存?}
B -->|是| C[返回缓存数据]
B -->|否| D[查询数据库]
D --> E[写入Redis缓存]
E --> C
style B fill:#fff9c4,stroke:#f9a825
style E fill:#f3e5f5,stroke:#7b1fa2
5.2 字典服务实现
/**
* 字典服务实现
* 带Redis缓存
*/
@Service
public class DictServiceImpl implements DictService {
private final DictDataMapper dictDataMapper;
private final RedisTemplate<String, Object> redisTemplate;
/** 字典缓存Key前缀 */
private static final String DICT_CACHE_KEY = "dict:data:";
/** 缓存过期时间(小时) */
private static final long CACHE_EXPIRE_HOURS = 24;
@Override
public String getLabel(String dictType, String dictValue) {
String cacheKey = DICT_CACHE_KEY + dictType;
// 从缓存获取整个字典类型的Map
Map<Object, Object> dictMap = redisTemplate.opsForHash()
.entries(cacheKey);
if (dictMap.isEmpty()) {
// 缓存未命中,查询数据库
List<DictData> dictList = dictDataMapper.selectByDictType(dictType);
// 构建Map并写入缓存
dictMap = dictList.stream()
.collect(Collectors.toMap(
DictData::getDictValue,
DictData::getDictLabel,
(a, b) -> b
));
redisTemplate.opsForHash().putAll(cacheKey, dictMap);
redisTemplate.expire(cacheKey, CACHE_EXPIRE_HOURS, TimeUnit.HOURS);
}
Object label = dictMap.get(dictValue);
return label != null ? label.toString() : null;
}
/**
* 清除字典缓存
* 在字典数据变更时调用
*/
public void clearCache(String dictType) {
redisTemplate.delete(DICT_CACHE_KEY + dictType);
}
/**
* 清除所有字典缓存
*/
public void clearAllCache() {
Set<String> keys = redisTemplate.keys(DICT_CACHE_KEY + "*");
if (keys != null && !keys.isEmpty()) {
redisTemplate.delete(keys);
}
}
}
六、使用示例
6.1 VO 类定义
/**
* 用户VO - 使用类级别注解
*/
@DictTranslate({
@AutoDict(dictType = "sys_gender", sourceField = "gender"),
@AutoDict(dictType = "sys_status", sourceField = "status")
})
public class UserVO {
private Long id;
private String username;
private Integer gender; // 字典值:1/2
private String genderName; // 自动翻译:男/女
private Integer status; // 字典值:0/1
private String statusName; // 自动翻译:禁用/启用
}
/**
* 订单VO - 使用字段级别注解
*/
public class OrderVO {
private Long id;
private String orderNo;
@AutoEnum(enumClass = OrderStatusEnum.class, sourceField = "status")
private Integer status; // 枚举值:0/1/2/3
private String statusName; // 自动翻译:待支付/已支付/已发货/已完成
@AutoDict(dictType = "pay_type", sourceField = "payType")
private Integer payType; // 字典值:1/2/3
private String payTypeName; // 自动翻译:微信/支付宝/银行卡
}
6.2 Controller 无需额外处理
@RestController
@RequestMapping("/api/user")
public class UserController {
@GetMapping("/{id}")
public R<UserVO> getUser(@PathVariable Long id) {
// 直接返回,AOP会自动翻译
UserVO user = userService.getById(id);
return R.ok(user);
}
@GetMapping("/list")
public R<List<UserVO>> listUsers() {
// 列表数据也会自动翻译
List<UserVO> users = userService.listAll();
return R.ok(users);
}
}
七、翻译流程总览
sequenceDiagram
participant C as Client
participant CT as Controller
participant AOP as DictTranslateAspect
participant DS as DictService
participant R as Redis
participant DB as Database
C->>CT: GET /api/user/1
CT->>CT: 查询用户数据
CT-->>AOP: 返回 UserVO{gender=1}
AOP->>AOP: 扫描@AutoDict注解
AOP->>DS: getLabel("sys_gender", "1")
DS->>R: GET dict:data:sys_gender
R-->>DS: 缓存命中 {1:"男", 2:"女"}
DS-->>AOP: "男"
AOP->>AOP: 设置 genderName = "男"
AOP-->>C: UserVO{gender=1, genderName="男"}
结论与建议
方案优势
- 零侵入:业务代码无需手动翻译,注解标注即可
- 统一管理:字典数据由后端统一维护,前端直接使用翻译后的文本
- 高性能:Redis 缓存字典数据,避免频繁查询数据库
- 易扩展:新增字典类型只需添加注解,无需修改翻译逻辑
最佳实践
- 命名约定:翻译字段统一使用
源字段名 + Name的命名方式 - 缓存更新:字典数据变更时及时清除 Redis 缓存
- 空值处理:翻译结果为空时保留原始值,避免前端显示空白
- 性能优化:批量翻译时预加载字典数据到缓存,减少 Redis 请求次数
- 嵌套对象:AOP 切面需递归处理嵌套对象中的注解