当前位置: 代码网 > it编程>编程语言>Java > 基于SpringBoot的自定义注解+AOP实现数据字典自动翻译方案

基于SpringBoot的自定义注解+AOP实现数据字典自动翻译方案

2026年08月06日 Java 我要评论
一、背景与痛点在企业级开发中,数据字典翻译是绕不开的需求:数据库存的是 code(如性别 1),前端要展示文本(如 男)。常见做法:前端自己做字典映射 → 维护成本高,字典变更要改前端后端

一、背景与痛点

在企业级开发中,数据字典翻译是绕不开的需求:数据库存的是 code(如性别 1),前端要展示文本(如 )。

常见做法:

  • 前端自己做字典映射 → 维护成本高,字典变更要改前端
  • 后端 service 层手动查字典赋值 → 代码侵入性强,到处重复
  • 全局 aop 自动翻译 → 一次配置,处处生效

本文实现一套基于自定义注解 @dict + spring aop 的字典自动翻译方案,特点如下:

特性说明
不修改原始值追加 _dictstr 后缀字段,前端同时拿到 code 和文本
支持多种返回类型pageinfopageresultlist、单对象
递归嵌套翻译嵌套对象和 list 内部的字典字段也能自动处理
多值支持逗号分隔的值(如 "1,2,3")自动逐个翻译
降级容错翻译异常不阻断业务,返回原始数据
redis 缓存按 dictcode 细粒度缓存,主动清除保证一致性
切面路径可配置不同项目通过 yml 配置不同的拦截包路径
多项目复用接口化数据加载,不绑定具体字典表

二、效果演示

vo 字段标注 @dict

@dict(dictcode = "gender")
private string gender;  // 数据库值: "1"

接口返回 json:

{
  "gender": "1",
  "gender_dictstr": "男"
}

前端可以直接用 gender_dictstr 展示,也可以用 gender 做筛选条件,互不影响。

三、整体架构

controller 返回 result
        ↓
   dictaspectconfig(配置化切面,读取 yml 中的 pointcut 表达式)
        ↓
   dicttranslateinterceptor(核心翻译逻辑)
        ↓
   解析返回数据类型(pageinfo / pageresult / list / 单对象)
        ↓
   递归遍历字段,收集 @dict 注解的 dictcode
        ↓
   dictcacheservice(redis 缓存)
        ↓
   dictdataprovider(接口,各项目实现自己的数据加载)
        ↓
   追加 "字段名_dictstr" 到 jsonobject 返回

四、核心代码实现

4.1@dict注解

package com.shinho.eccp.entry.api.dto.annotation;

import java.lang.annotation.*;

@target(elementtype.field)
@retention(retentionpolicy.runtime)
@documented
public @interface dict {

    /**
     * 字典类型编码(对应字典表的类型字段)
     * 例如: "gender", "area", "education_background"
     */
    string dictcode();

    /**
     * 翻译结果存放的字段名(可选)
     * 默认为 "原字段名_dictstr",指定后使用自定义字段名
     */
    string dicttext() default "";
}

4.2dictdataprovider接口(数据加载抽象)

各项目实现此接口,查自己的字典表,不做任何绑定:

package com.shinho.eccp.entry.api.dto.annotation;

import java.util.list;

public interface dictdataprovider {

    /**
     * 根据字典类型编码查询字典项列表
     */
    list<dictitem> getdictitemsbycode(string dictcode);

    /**
     * 字典项数据结构
     */
    class dictitem {
        private string value; // 字典编码值
        private string label; // 字典显示文本

        public dictitem() {}

        public dictitem(string value, string label) {
            this.value = value;
            this.label = label;
        }

        // getter / setter 省略
    }
}

4.3 具体项目的 provider 实现

@service
public class entrydictdataprovider implements dictdataprovider {

    @autowired
    private datadictionarydao datadictionarydao;

    @override
    public list<dictitem> getdictitemsbycode(string dictcode) {
        list<dropdownvaluesvo> volist = datadictionarydao.getvaluesbyfield(dictcode);
        if (volist == null || volist.isempty()) {
            return collections.emptylist();
        }
        list<dictitem> items = new arraylist<>(volist.size());
        for (dropdownvaluesvo vo : volist) {
            items.add(new dictitem(vo.getvalue(), vo.getname()));
        }
        return items;
    }
}

其他项目只需实现 dictdataprovider,可以查不同的表、调 feign 接口、读配置文件等,不受限制。

4.4dictcacheservice缓存服务

@service
public class dictcacheservice {

    private static final logger log = loggerfactory.getlogger(dictcacheservice.class);

    @value("${eccp.dict.redis-key-prefix:eccp:dict:}")
    private string rediskeyprefix;

    @value("${eccp.dict.redis-expire-seconds:2592000}")
    private int redisexpireseconds;

    @autowired
    private jedisutil jedisutil;

    @autowired
    private dictdataprovider dictdataprovider;

    public list<dictitem> getdictitems(string dictcode) {
        if (stringutils.isblank(dictcode)) {
            return collections.emptylist();
        }

        // 1. 优先从 redis 获取
        try {
            string rediskey = rediskeyprefix + dictcode;
            string json = jedisutil.getcurrentservice(rediskey);
            if (stringutils.isnotblank(json)) {
                list<dictitem> items = json.parsearray(json, dictitem.class);
                if (items != null) {
                		return items;
                }
            }
        } catch (exception e) {
            log.warn("redis获取字典缓存异常, dictcode={}", dictcode);
        }

        // 2. 缓存未命中,通过 provider 加载
        list<dictitem> items;
        try {
            items = dictdataprovider.getdictitemsbycode(dictcode);
        } catch (exception e) {
            log.error("查询字典数据异常, dictcode={}", dictcode, e);
            return collections.emptylist();
        }
        if (items == null) {
        		items = collections.emptylist();
        }

        // 3. 回写 redis
        try {
            string rediskey = rediskeyprefix + dictcode;
            jedisutil.setcurrentservice(rediskey, json.tojsonstring(items), redisexpireseconds);
        } catch (exception e) {
            log.warn("回写redis异常, dictcode={}", dictcode);
        }

        return items;
    }

    public map<string, list<dictitem>> batchgetdictitems(set<string> dictcodes) {
        map<string, list<dictitem>> result = new hashmap<>(dictcodes.size());
        for (string code : dictcodes) {
            result.put(code, getdictitems(code));
        }
        return result;
    }

    /** 清除缓存(字典变更时调用) */
    public void evict(string dictcode) {
        jedisutil.delcurrentservice(rediskeyprefix + dictcode);
    }

    /** 清除并重新加载 */
    public void reload(string dictcode) {
        evict(dictcode);
        getdictitems(dictcode);
    }
}

4.5dictaspectconfig配置化切面(关键)

这是本方案的核心亮点之一:切面拦截路径通过 application.yml 配置,不同项目无需修改代码,只改配置即可。

@configuration
public class dictaspectconfig {

    private static final logger log = loggerfactory.getlogger(dictaspectconfig.class);

    /**
     * 切面拦截表达式,各项目通过配置自定义
     * 默认拦截 com.shinho.eccp 下所有 web 包的 controller 方法
     */
    @value("${eccp.dict.pointcut-expression:execution(* com.shinho.eccp..web.*.*(..))}")
    private string pointcutexpression;

    @autowired
    private dicttranslateinterceptor dicttranslateinterceptor;

    @bean
    @order(2)
    public defaultpointcutadvisor dictadvisor() {
        aspectjexpressionpointcut pointcut = new aspectjexpressionpointcut();
        pointcut.setexpression(pointcutexpression);
        log.info("字典翻译切面已注册, pointcut={}", pointcutexpression);
        return new defaultpointcutadvisor(pointcut, dicttranslateinterceptor);
    }
}

为什么不用 @aspect + @pointcut

因为 @pointcut 的表达式必须是编译时常量,无法通过 @value 注入。
使用 defaultpointcutadvisor + aspectjexpressionpointcut 是 spring aop 的编程式方式,
pointcut 表达式可以在运行时从配置文件读取,真正做到零代码改动,不同项目只改 yml

4.6dicttranslateinterceptor核心翻译逻辑

@component
public class dicttranslateinterceptor implements methodinterceptor {

    private static final string dict_suffix = "_dictstr";
    private static final map<class<?>, field[]> field_cache = new concurrenthashmap<>();
    private static final map<class<?>, list<field>> dict_field_cache = new concurrenthashmap<>();

    @autowired
    private dictcacheservice dictcacheservice;

    @override
    public object invoke(methodinvocation invocation) throws throwable {
        object result = invocation.proceed();
        try {
            result = translatedict(result);
        } catch (exception e) {
            log.warn("字典翻译异常,返回原始数据: {}", e.getmessage());
        }
        return result;
    }

    private object translatedict(object result) {
        if (!(result instanceof result)) return result;

        object data = ((result<?>) result).getdata();
        if (data == null) {
        		return result;
        }

        if (data instanceof pageinfo) {
            pageinfo pageinfo = (pageinfo) data;
            if (collectionutils.isnotempty(pageinfo.getlist())) {
                pageinfo.setlist(processrecords(pageinfo.getlist()));
            }
        } else if (data instanceof pageresult) {
            pageresult pageresult = (pageresult) data;
            if (collectionutils.isnotempty(pageresult.getrows())) {
                pageresult.setrows(processrecords(pageresult.getrows()));
            }
        } else if (data instanceof list) {
            list<?> list = (list<?>) data;
            if (collectionutils.isnotempty(list)) {
                ((result) result).setdata(processrecords(list));
            }
        } else if (!isjavabasictype(data.getclass())) {
            list<object> single = processrecords(collections.singletonlist(data));
            if (collectionutils.isnotempty(single)) {
                ((result) result).setdata(single.get(0));
            }
        }
        return result;
    }
}

递归构建带 _dictstr 的 jsonobject:

private jsonobject buildjsonwithdict(object obj, map<string, list<dictitem>> dictdatamap) {
    jsonobject json = new jsonobject(true); // 保持字段顺序
    field[] allfields = getallfields(obj.getclass());

    for (field field : allfields) {
        field.setaccessible(true);
        object value = getfieldvaluesafe(obj, field);

        // 嵌套对象递归处理(list / 单对象)
        if (value != null && !isjavabasictype(field.gettype())) {
            if (value instanceof list) {
                list<?> listvalue = (list<?>) value;
                if (collectionutils.isnotempty(listvalue) && !isjavabasictype(listvalue.get(0).getclass())) {
                    set<string> nestedcodes = collectalldictcodes(listvalue.get(0).getclass());
                    if (!nestedcodes.isempty()) {
                        map<string, list<dictitem>> mergedmap = ensuredictloaded(dictdatamap, nestedcodes);
                        list<object> nestedresult = new arraylist<>();
                        for (object item : listvalue) {
                            nestedresult.add(buildjsonwithdict(item, mergedmap));
                        }
                        json.put(field.getname(), nestedresult);
                    } else {
                        json.put(field.getname(), value);
                    }
                } else {
                    json.put(field.getname(), value);
                }
            } else if (!field.gettype().isenum()) {
                set<string> nestedcodes = collectalldictcodes(field.gettype());
                if (!nestedcodes.isempty()) {
                    map<string, list<dictitem>> mergedmap = ensuredictloaded(dictdatamap, nestedcodes);
                    json.put(field.getname(), buildjsonwithdict(value, mergedmap));
                } else {
                    json.put(field.getname(), value);
                }
            } else {
                json.put(field.getname(), value);
            }
        } else {
            json.put(field.getname(), value);
        }

        // 追加 _dictstr 字段
        dict dict = field.getannotation(dict.class);
        if (dict != null && value != null) {
            string textvalue = translatevalue(dictdatamap, dict.dictcode(), value.tostring());
            string dictfieldname = stringutils.isnotblank(dict.dicttext())
                    ? dict.dicttext()
                    : (field.getname() + dict_suffix);
            json.put(dictfieldname, textvalue);
        }
    }
    return json;
}

字典值翻译(支持逗号分隔多值 + 降级):

private string translatevalue(map<string, list<dictitem>> dictdatamap, string dictcode, string value) {
    list<dictitem> items = dictdatamap.get(dictcode);
    // 降级
    if (items == null || items.isempty()) {
    		return value; 
    }

    map<string, string> valuetolabelmap = new hashmap<>(items.size());
    for (dictitem item : items) {
        if (item.getvalue() != null) {
            valuetolabelmap.put(item.getvalue(), item.getlabel());
        }
    }

    string[] codes = value.split(",");
    stringbuilder sb = new stringbuilder();
    for (int i = 0; i < codes.length; i++) {
        string code = codes[i].trim();
        string text = valuetolabelmap.getordefault(code, code); // 找不到返回原值
        if (i > 0) sb.append(",");
        sb.append(text);
    }
    return sb.tostring();
}

4.7 缓存管理接口

字典数据变更后需要主动刷新缓存:

@restcontroller
@requestmapping("/dict/cache")
@api(tags = "字典缓存管理")
public class dictcachecontroller {

    @autowired
    private dictcacheservice dictcacheservice;

    @postmapping("/evict")
    public result<string> evict(@requestparam string dictcode) {
        dictcacheservice.evict(dictcode);
        return result.ok("缓存已清除: " + dictcode);
    }

    @postmapping("/reload")
    public result<string> reload(@requestparam string dictcode) {
        dictcacheservice.reload(dictcode);
        return result.ok("缓存已重新加载: " + dictcode);
    }
}

五、配置文件说明

eccp:
  dict:
    # redis 缓存 key 前缀(不同项目配置不同值,避免冲突)
    redis-key-prefix: eccp:entry:dict:
    # 缓存过期时间(秒),默认30天
    redis-expire-seconds: 2592000
    # 切面拦截的包路径表达式(不同项目配置不同的 controller 包路径)
    pointcut-expression: execution(* com.shinho.eccp.entry.web.*.*(..))

六、使用方式

6.1 在 vo 上标注注解

@data
public class memberlistvo {

    @dict(dictcode = "gender")
    private string gender;

    @dict(dictcode = "legal_person_company")
    private string legalpersoncompany;

    @dict(dictcode = "status")
    private string status;

    // 自定义翻译字段名
    @dict(dictcode = "recruitment_source", dicttext = "recruitmentsourcetext")
    private string recruitmentsource;
}

6.2 接口返回示例

{
  "code": 200,
  "data": {
    "list": [
      {
        "gender": "1",
        "gender_dictstr": "男",
        "legalpersoncompany": "01",
        "legalpersoncompany_dictstr": "xx食品有限公司",
        "status": "2",
        "status_dictstr": "待办理入职",
        "recruitmentsource": "3",
        "recruitmentsourcetext": "内部推荐"
      }
    ]
  }
}

七、多项目复用

不同项目只需三步:

1. 实现 dictdataprovider 接口(查自己的字典表):

@service
public class bffdictdataprovider implements dictdataprovider {
    @autowired
    private somebffdictdao bffdictdao;

    @override
    public list<dictitem> getdictitemsbycode(string dictcode) {
        return bffdictdao.querybytype(dictcode).stream()
            .map(e -> new dictitem(e.getcode(), e.getname()))
            .collect(collectors.tolist());
    }
}

2. 配置 yml(不同前缀 + 不同切面路径):

# 项目a - entry 服务
eccp:
  dict:
    redis-key-prefix: eccp:entry:dict:
    pointcut-expression: execution(* com.shinho.eccp.entry.web.*.*(..))
# 项目b - bff 服务
eccp:
  dict:
    redis-key-prefix: eccp:bff:dict:
    pointcut-expression: execution(* com.shinho.eccp.bff.web.*.*(..))
# 项目c - 多个包路径
eccp:
  dict:
    redis-key-prefix: eccp:hr:dict:
    pointcut-expression: execution(* com.shinho.eccp.hr.controller.*.*(..)) || execution(* com.shinho.eccp.hr.api.*.*(..))

3. 在 vo 字段上加 @dict 注解即可。

各项目字典缓存隔离,切面路径独立,互不干扰。

八、性能优化细节

优化点做法
避免重复反射concurrenthashmap 缓存 class → field[] 映射
避免 n+1 查询先收集所有 dictcode,批量从缓存获取
翻译效率list<dictitem>hashmap<value, label>,o(1) 查找
嵌套按需加载外层已加载的字典传递给内层,缺失时才补充
无注解短路没有 @dict 注解的对象直接跳过,零开销
redis 持久化按 dictcode 细粒度缓存,变更时主动清除
可改造方向如果各个项目字典表结构一致,查询字典数据可以传一个tablename参数即可,但会涉及到多数据源

九、缺点与性能优化(待实现)

  1. 性能相关
    json 序列化开销 每次请求都把返回对象通过反射构建 jsonobject,相比直接返回 java 对象多了一次序列化过程。数据量小时无感,但如果是大分页(比如一次返回几百条,每条几十个字段+嵌套),开销会明显。反射遍历所有字段 即使对象只有 1 个 @dict 字段,也要遍历全部字段(含父类链)。虽然有 field_cache 缓存 field 数组,但每次还是要逐字段 field.get() 取值并放入 jsonobject。无注解对象也被处理 当前逻辑对所有走过切面的 controller 方法都会触发 translatedict,即使返回的 vo 上没有任何 @dict 注解,也会执行 instanceof 判断和类型检查。
  2. 解决方案 目前有两种解决思路
  • 通过在方法或类级别加开关注解来彻底短路, 在 vo 类中上增加 @dictenable 注解判断是否需要提前短路。
  • dicttranslateinterceptor.java 这个类中增加一个map<class<?>, boolean> 用来缓存 vo 判断是否需要提前短路

十、文件清单

文件位置说明
dict.javaapi 模块注解定义
dictdataprovider.javaapi 模块数据加载接口 + dictitem
dictaspectconfig.javaservice 模块配置化切面注册
dicttranslateinterceptor.javaservice 模块核心翻译逻辑
dictcacheservice.javaservice 模块redis 缓存服务
entrydictdataprovider.javaservice 模块本项目的 provider 实现
dictcachecontroller.javaservice 模块缓存管理接口

十一、总结

本方案的核心设计思想:
一套代码,多项目复用,不同的表、不同的包路径,只改配置文件。

  1. 注解驱动 — 加 @dict 即生效,零侵入业务代码
  2. 接口隔离dictdataprovider 让数据来源与翻译逻辑彻底解耦
  3. 全面配置化 — redis 前缀、过期时间、切面拦截路径均通过 yml 配置
  4. 防御性编程 — 任何环节异常都降级处理,绝不阻断业务
  5. 缺点 —性能相关:

— 解决方案

十二、具体代码实现

  1. dict.java
import java.lang.annotation.*;

/**
 * 数据字典翻译注解
 * <p>
 * 标注在 vo/dto 字段上,aop 切面会自动将字典 code 翻译为文本,
 * 追加到返回 json 中(默认字段名: 原字段名_dictstr),原始值保持不变。
 * </p>
 *
 * <pre>
 * 使用示例:
 * {@code @dict(dictcode = "gender")}
 * private string gender;  // 数据库值: "1"
 *
 * 返回 json:
 * { "gender": "1", "gender_dictstr": "男" }
 * </pre>
 */
@target(elementtype.field)
@retention(retentionpolicy.runtime)
@documented
public @interface dict {

    /**
     * 字典类型编码(对应字典表的类型字段)
     * 例如: "gender", "area", "education_background"
     */
    string dictcode();

    /**
     * 翻译结果存放的字段名(可选)
     * 默认为 "原字段名_dictstr",指定后使用自定义字段名
     */
    string dicttext() default "";
}
  1. dictdataprovider.java
import java.util.list;

/**
 * 字典数据加载接口
 * <p>
 * 各项目实现此接口来提供字典数据,不绑定具体字典表结构。
 * 可以查本地数据库、调 feign 接口、读配置文件等。
 * </p>
 */
public interface dictdataprovider {

    /**
     * 根据字典类型编码查询字典项列表
     *
     * @param dictcode 字典类型编码
     * @return 字典项列表
     */
    list<dictitem> getdictitemsbycode(string dictcode);

    /**
     * 字典项数据结构 后面可以单独抽出去,不通过内部类实现
     */
    class dictitem {
        /** 字典编码值 */
        private string value;
        /** 字典显示文本 */
        private string label;

        public dictitem() {
        }

        public dictitem(string value, string label) {
            this.value = value;
            this.label = label;
        }

        public string getvalue() {
            return value;
        }

        public void setvalue(string value) {
            this.value = value;
        }

        public string getlabel() {
            return label;
        }

        public void setlabel(string label) {
            this.label = label;
        }
    }
}

  1. entrydictdataprovider
import org.springframework.beans.factory.annotation.autowired;
import org.springframework.stereotype.service;

import java.util.arraylist;
import java.util.collections;
import java.util.list;

/**
 * eccp-entry 项目的字典数据 provider 实现
 * <p>
 * 通过 datadictionarydao 查询本项目字典表,
 * 将 dropdownvaluesvo 转换为通用的 dictitem。
 * </p>
 */
@service
public class entrydictdataprovider implements dictdataprovider {

    @autowired
    private datadictionarydao datadictionarydao;

    @override
    public list<dictitem> getdictitemsbycode(string dictcode) {
        list<dropdownvaluesvo> volist = datadictionarydao.getvaluesbyfield(dictcode);
        if (volist == null || volist.isempty()) {
            return collections.emptylist();
        }
        list<dictitem> items = new arraylist<>(volist.size());
        for (dropdownvaluesvo vo : volist) {
            items.add(new dictitem(vo.getvalue(), vo.getname()));
        }
        return items;
    }
}
  1. dictcacheservice
import org.apache.commons.lang.stringutils;
import org.slf4j.logger;
import org.slf4j.loggerfactory;
import org.springframework.beans.factory.annotation.autowired;
import org.springframework.beans.factory.annotation.value;
import org.springframework.stereotype.service;

import java.util.*;

/**
 * 字典缓存服务
 * <p>
 * 按 dictcode 细粒度缓存字典数据到 redis,
 * 缓存未命中时通过 dictdataprovider 加载并回写。
 * </p>
 */
@service
public class dictcacheservice {

    private static final logger log = loggerfactory.getlogger(dictcacheservice.class);

    @value("${eccp.dict.redis-key-prefix:eccp:dict}")
    private string rediskeyprefix;

    @value("${eccp.dict.redis-expire-seconds:2592000}")
    private int redisexpireseconds;

    @autowired
    private jedisutil jedisutil;

    @autowired
    private dictdataprovider dictdataprovider;

    /**
     * 获取指定 dictcode 的字典项列表
     *
     * @param dictcode 字典类型编码
     * @return 字典项列表,不会返回 null
     */
    public list<dictitem> getdictitems(string dictcode) {
        if (stringutils.isblank(dictcode)) {
            return collections.emptylist();
        }

        // 1. 优先从 redis 获取
        try {
            string rediskey = rediskeyprefix + ":" + dictcode;
            string json = jedisutil.getcurrentservice(rediskey);
            if (stringutils.isnotblank(json)) {
                list<dictitem> items = json.parsearray(json, dictitem.class);
                if (items != null) {
                    return items;
                }
            }
        } catch (exception e) {
            log.warn("redis获取字典缓存异常, dictcode={}", dictcode);
        }

        // 2. 缓存未命中,通过 provider 加载
        list<dictitem> items;
        try {
            items = dictdataprovider.getdictitemsbycode(dictcode);
        } catch (exception e) {
            log.error("查询字典数据异常, dictcode={}", dictcode, e);
            return collections.emptylist();
        }
        if (items == null) {
            items = collections.emptylist();
        }

        // 3. 回写 redis
        try {
            string rediskey = rediskeyprefix + ":" + dictcode;
            jedisutil.setcurrentservice(rediskey, json.tojsonstring(items), redisexpireseconds);
        } catch (exception e) {
            log.warn("回写redis异常, dictcode={}", dictcode);
        }

        return items;
    }

    /**
     * 批量获取多个 dictcode 的字典数据
     *
     * @param dictcodes 字典编码集合
     * @return dictcode -> 字典项列表 的映射
     */
    public map<string, list<dictitem>> batchgetdictitems(set<string> dictcodes) {
        map<string, list<dictitem>> result = new hashmap<>(dictcodes.size());
        for (string code : dictcodes) {
            result.put(code, getdictitems(code));
        }
        return result;
    }

    /**
     * 清除指定 dictcode 的缓存
     *
     * @param dictcode 字典类型编码
     */
    public void evict(string dictcode) {
        try {
            jedisutil.delcurrentservice(rediskeyprefix + ":" + dictcode);
        } catch (exception e) {
            log.warn("清除字典缓存异常, dictcode={}", dictcode);
        }
    }

    /**
     * 清除并重新加载指定 dictcode 的缓存
     *
     * @param dictcode 字典类型编码
     */
    public void reload(string dictcode) {
        evict(dictcode);
        getdictitems(dictcode);
    }
}

  1. dictcachecontroller
import com.shinho.common.api.base.result;
import io.swagger.annotations.api;
import io.swagger.annotations.apioperation;
import io.swagger.annotations.apiparam;
import org.springframework.beans.factory.annotation.autowired;
import org.springframework.web.bind.annotation.*;

/**
 * 字典缓存管理接口
 * <p>
 * 字典数据变更后,通过此接口主动刷新 redis 缓存,保证一致性。
 * </p>
 */
@restcontroller
@requestmapping("/dict/cache")
@api(tags = "字典缓存管理")
public class dictcachecontroller {

    @autowired
    private dictcacheservice dictcacheservice;

    @postmapping("/evict")
    @apioperation("清除指定字典类型的缓存")
    public result<string> evict(@apiparam("字典类型编码") @requestparam string dictcode) {
        dictcacheservice.evict(dictcode);
        return result.ok("缓存已清除: " + dictcode);
    }

    @postmapping("/reload")
    @apioperation("清除并重新加载指定字典类型的缓存")
    public result<string> reload(@apiparam("字典类型编码") @requestparam string dictcode) {
        dictcacheservice.reload(dictcode);
        return result.ok("缓存已重新加载: " + dictcode);
    }
}

  1. dicttranslateinterceptor
import com.github.pagehelper.pageinfo;
import org.aopalliance.intercept.methodinterceptor;
import org.aopalliance.intercept.methodinvocation;
import org.apache.commons.collections4.collectionutils;
import org.apache.commons.lang.stringutils;
import org.slf4j.logger;
import org.slf4j.loggerfactory;
import org.springframework.beans.factory.annotation.autowired;
import org.springframework.stereotype.component;

import java.lang.reflect.field;
import java.util.*;
import java.util.concurrent.concurrenthashmap;

/**
 * 字典翻译核心拦截器
 * <p>
 * 拦截 controller 方法的返回值,自动将标注了 {@link dict} 注解的字段
 * 翻译为文本,追加 "_dictstr" 后缀字段到返回的 json 中。
 * </p>
 * <p>
 * 特性:
 * <ul>
 *   <li>支持 pageinfo、pageresult、list、单对象等多种返回类型, pageinfo、pageresult、result 换成自己项目中使用的</li>
 *   <li>递归处理嵌套对象和 list 内部的字典字段</li>
 *   <li>支持逗号分隔的多值翻译</li>
 *   <li>降级容错:翻译异常不阻断业务,返回原始数据</li>
 *   <li>concurrenthashmap 缓存反射结果,避免重复反射</li>
 * </ul>
 * </p>
 */
@component
public class dicttranslateinterceptor implements methodinterceptor {

    private static final logger log = loggerfactory.getlogger(dicttranslateinterceptor.class);

    private static final string dict_suffix = "_dictstr";

    /** class -> 所有字段(含父类)缓存 */
    private static final map<class<?>, field[]> field_cache = new concurrenthashmap<>();

    @autowired
    private dictcacheservice dictcacheservice;

    @override
    public object invoke(methodinvocation invocation) throws throwable {
        object result = invocation.proceed();
        try {
            result = translatedict(result);
        } catch (exception e) {
            log.warn("字典翻译异常,返回原始数据: {}", e.getmessage());
        }
        return result;
    }

    /**
     * 翻译字典入口
     */
    @suppresswarnings("unchecked")
    private object translatedict(object result) {
        if (!(result instanceof result)) {
            return result;
        }

        result<?> resultobj = (result<?>) result;
        object data = resultobj.getdata();
        if (data == null) {
            return result;
        }

        if (data instanceof pageinfo) {
            pageinfo<?> pageinfo = (pageinfo<?>) data;
            if (collectionutils.isnotempty(pageinfo.getlist())) {
                list<object> translated = processrecords(pageinfo.getlist());
                ((pageinfo) pageinfo).setlist(translated);
            }
        } else if (data instanceof pageresult) {
            pageresult pageresult = (pageresult) data;
            if (collectionutils.isnotempty(pageresult.getrows())) {
                list<object> translated = processrecords(pageresult.getrows());
                pageresult.setrows(translated);
            }
        } else if (data instanceof list) {
            list<?> list = (list<?>) data;
            if (collectionutils.isnotempty(list) && !isjavabasictype(list.get(0).getclass())) {
                ((result) resultobj).setdata(processrecords(list));
            }
        } else if (!isjavabasictype(data.getclass())) {
            list<object> single = processrecords(collections.singletonlist(data));
            if (collectionutils.isnotempty(single)) {
                ((result) resultobj).setdata(single.get(0));
            }
        }

        return result;
    }

    /**
     * 批量处理记录列表
     */
    private list<object> processrecords(list<?> records) {
        if (collectionutils.isempty(records)) {
            return collections.emptylist();
        }

        // 1. 收集所有需要翻译的 dictcode
        object firstitem = records.get(0);
        if (isjavabasictype(firstitem.getclass())) {
            return new arraylist<>(records);
        }

        set<string> alldictcodes = collectalldictcodes(firstitem.getclass());
        if (alldictcodes.isempty()) {
            // 没有 @dict 注解,检查是否有嵌套对象需要处理
            if (!hasnesteddictfields(firstitem.getclass())) {
                return new arraylist<>(records);
            }
        }

        // 2. 批量获取字典数据
        map<string, list<dictitem>> dictdatamap = dictcacheservice.batchgetdictitems(alldictcodes);

        // 3. 逐条翻译
        list<object> resultlist = new arraylist<>(records.size());
        for (object record : records) {
            resultlist.add(buildjsonwithdict(record, dictdatamap));
        }
        return resultlist;
    }

    /**
     * 递归构建带 _dictstr 的 jsonobject
     */
    private jsonobject buildjsonwithdict(object obj, map<string, list<dictitem>> dictdatamap) {
        jsonobject json = new jsonobject(true); // 保持字段顺序
        field[] allfields = getallfields(obj.getclass());

        for (field field : allfields) {
            field.setaccessible(true);
            object value = getfieldvaluesafe(obj, field);

            // 处理嵌套对象
            if (value != null && !isjavabasictype(field.gettype())) {
                if (value instanceof list) {
                    list<?> listvalue = (list<?>) value;
                    if (collectionutils.isnotempty(listvalue) && !isjavabasictype(listvalue.get(0).getclass())) {
                        set<string> nestedcodes = collectalldictcodes(listvalue.get(0).getclass());
                        map<string, list<dictitem>> mergedmap = ensuredictloaded(dictdatamap, nestedcodes);
                        list<object> nestedresult = new arraylist<>();
                        for (object item : listvalue) {
                            nestedresult.add(buildjsonwithdict(item, mergedmap));
                        }
                        json.put(field.getname(), nestedresult);
                    } else {
                        json.put(field.getname(), value);
                    }
                } else if (!field.gettype().isenum()) {
                    set<string> nestedcodes = collectalldictcodes(field.gettype());
                    if (!nestedcodes.isempty() || hasnesteddictfields(field.gettype())) {
                        map<string, list<dictitem>> mergedmap = ensuredictloaded(dictdatamap, nestedcodes);
                        json.put(field.getname(), buildjsonwithdict(value, mergedmap));
                    } else {
                        json.put(field.getname(), value);
                    }
                } else {
                    json.put(field.getname(), value);
                }
            } else {
                json.put(field.getname(), value);
            }

            // 追加 _dictstr 字段
            dict dict = field.getannotation(dict.class);
            if (dict != null && value != null) {
                string textvalue = translatevalue(dictdatamap, dict.dictcode(), value.tostring());
                string dictfieldname = stringutils.isnotblank(dict.dicttext())
                        ? dict.dicttext()
                        : (field.getname() + dict_suffix);
                json.put(dictfieldname, textvalue);
            }
        }
        return json;
    }

    /**
     * 字典值翻译(支持逗号分隔多值)
     */
    private string translatevalue(map<string, list<dictitem>> dictdatamap, string dictcode, string value) {
        list<dictitem> items = dictdatamap.get(dictcode);
        // 降级:返回原值
        if (items == null || items.isempty()) {
            return value;
        }

        // 构建 value -> label 映射
        map<string, string> valuetolabelmap = new hashmap<>(items.size());
        for (dictitem item : items) {
            if (item.getvalue() != null) {
                valuetolabelmap.put(item.getvalue(), item.getlabel());
            }
        }

        // 支持逗号分隔的多值
        string[] codes = value.split(",");
        stringbuilder sb = new stringbuilder();
        for (int i = 0; i < codes.length; i++) {
            string code = codes[i].trim();
            // 找不到返回原值
            string text = valuetolabelmap.getordefault(code, code);
            if (i > 0) {
                sb.append(",");
            }
            sb.append(text);
        }
        return sb.tostring();
    }

    /**
     * 收集指定类及其嵌套类中所有 @dict 注解的 dictcode
     */
    private set<string> collectalldictcodes(class<?> clazz) {
        set<string> codes = new hashset<>();
        field[] fields = getallfields(clazz);
        for (field field : fields) {
            dict dict = field.getannotation(dict.class);
            if (dict != null) {
                codes.add(dict.dictcode());
            }
        }
        return codes;
    }

    /**
     * 检查类是否有嵌套对象中包含 @dict 字段
     */
    private boolean hasnesteddictfields(class<?> clazz) {
        field[] fields = getallfields(clazz);
        for (field field : fields) {
            if (!isjavabasictype(field.gettype()) && !field.gettype().isenum()
                    && field.gettype() != list.class) {
                set<string> nestedcodes = collectalldictcodes(field.gettype());
                if (!nestedcodes.isempty()) {
                    return true;
                }
            }
            // 检查 list 泛型内容太复杂,此处简化处理
        }
        return false;
    }

    /**
     * 确保所有需要的字典数据都已加载
     */
    private map<string, list<dictitem>> ensuredictloaded(map<string, list<dictitem>> existing, set<string> neededcodes) {
        set<string> missingcodes = new hashset<>();
        for (string code : neededcodes) {
            if (!existing.containskey(code)) {
                missingcodes.add(code);
            }
        }

        if (missingcodes.isempty()) {
            return existing;
        }

        // 加载缺失的字典数据
        map<string, list<dictitem>> additional = dictcacheservice.batchgetdictitems(missingcodes);
        map<string, list<dictitem>> merged = new hashmap<>(existing);
        merged.putall(additional);
        return merged;
    }

    /**
     * 获取类的所有字段(含父类),使用缓存
     */
    private field[] getallfields(class<?> clazz) {
        return field_cache.computeifabsent(clazz, c -> {
            list<field> fieldlist = new arraylist<>();
            class<?> tempclass = c;
            while (tempclass != null && tempclass != object.class) {
                fieldlist.addall(arrays.aslist(tempclass.getdeclaredfields()));
                tempclass = tempclass.getsuperclass();
            }
            return fieldlist.toarray(new field[0]);
        });
    }

    /**
     * 安全获取字段值
     */
    private object getfieldvaluesafe(object obj, field field) {
        try {
            return field.get(obj);
        } catch (exception e) {
            return null;
        }
    }

    /**
     * 判断是否是 java 基本类型或常见类型(不需要递归处理的类型)
     */
    private boolean isjavabasictype(class<?> clazz) {
        return clazz.isprimitive()
                || clazz == string.class
                || clazz == integer.class
                || clazz == long.class
                || clazz == double.class
                || clazz == float.class
                || clazz == boolean.class
                || clazz == short.class
                || clazz == byte.class
                || clazz == character.class
                || clazz == java.math.bigdecimal.class
                || clazz == java.math.biginteger.class
                || clazz == java.util.date.class
                || clazz == java.time.localdate.class
                || clazz == java.time.localdatetime.class
                || number.class.isassignablefrom(clazz)
                || clazz.getname().startswith("java.time.")
                || clazz == map.class
                || map.class.isassignablefrom(clazz);
    }
}

  1. dictaspectconfig
import org.slf4j.logger;
import org.slf4j.loggerfactory;
import org.springframework.aop.aspectj.aspectjexpressionpointcut;
import org.springframework.aop.support.defaultpointcutadvisor;
import org.springframework.beans.factory.annotation.autowired;
import org.springframework.beans.factory.annotation.value;
import org.springframework.context.annotation.bean;
import org.springframework.context.annotation.configuration;
import org.springframework.core.annotation.order;

/**
 * 字典翻译切面配置
 * <p>
 * 核心亮点:切面拦截路径通过 application.yml 配置,
 * 不同项目无需修改代码,只改配置即可复用。
 * </p>
 * <p>
 * 使用 {@link defaultpointcutadvisor} + {@link aspectjexpressionpointcut} 编程式方式,
 * 而非 @aspect + @pointcut,因为后者的表达式必须是编译时常量,无法通过 @value 注入。
 * </p>
 */
@configuration
public class dictaspectconfig {

    private static final logger log = loggerfactory.getlogger(dictaspectconfig.class);

    /**
     * 切面拦截表达式,各项目通过配置自定义
     * 默认拦截 com.shinho.eccp 下所有 web 包的 controller 方法
     */
    @value("${eccp.dict.pointcut-expression:execution(* com.shinho.eccp..web.*.*(..))}")
    private string pointcutexpression;

    @autowired
    private dicttranslateinterceptor dicttranslateinterceptor;

    @bean
    public defaultpointcutadvisor dictadvisor() {
        aspectjexpressionpointcut pointcut = new aspectjexpressionpointcut();
        pointcut.setexpression(pointcutexpression);
        log.info("字典翻译切面已注册, pointcut={}", pointcutexpression);
        return new defaultpointcutadvisor(pointcut, dicttranslateinterceptor);
    }
}

以上就是基于springboot的自定义注解+aop实现数据字典自动翻译方案的详细内容,更多关于springboot注解+aop数据字典翻译的资料请关注代码网其它相关文章!

(0)

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2026  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com