字典与枚举自动翻译:@AutoDict/@AutoEnum 注解 + AOP 零侵入方案

作者:忆笙智云官方 | 发布时间:2026-04-30 11:20 | 更新时间:2026-06-30 11:20

字典与枚举自动翻译注解驱动方案

引言

在企业级应用中,数据字典和枚举值的翻译是极其常见的开发需求。例如:数据库中存储的性别字段值是 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: '已完成' }

痛点

二、注解驱动方案设计

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="男"}

结论与建议

方案优势

  1. 零侵入:业务代码无需手动翻译,注解标注即可
  2. 统一管理:字典数据由后端统一维护,前端直接使用翻译后的文本
  3. 高性能:Redis 缓存字典数据,避免频繁查询数据库
  4. 易扩展:新增字典类型只需添加注解,无需修改翻译逻辑

最佳实践

相关资源