一、背景与痛点
在企业级开发中,数据字典翻译是绕不开的需求:数据库存的是 code(如性别 1),前端要展示文本(如 男)。
常见做法:
- 前端自己做字典映射 → 维护成本高,字典变更要改前端
- 后端 service 层手动查字典赋值 → 代码侵入性强,到处重复
- 全局 aop 自动翻译 → 一次配置,处处生效
本文实现一套基于自定义注解 @dict + spring aop 的字典自动翻译方案,特点如下:
| 特性 | 说明 |
|---|---|
| 不修改原始值 | 追加 _dictstr 后缀字段,前端同时拿到 code 和文本 |
| 支持多种返回类型 | pageinfo、pageresult、list、单对象 |
| 递归嵌套翻译 | 嵌套对象和 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参数即可,但会涉及到多数据源 |
九、缺点与性能优化(待实现)
- 性能相关
json 序列化开销 每次请求都把返回对象通过反射构建 jsonobject,相比直接返回 java 对象多了一次序列化过程。数据量小时无感,但如果是大分页(比如一次返回几百条,每条几十个字段+嵌套),开销会明显。反射遍历所有字段 即使对象只有 1 个 @dict 字段,也要遍历全部字段(含父类链)。虽然有 field_cache 缓存 field 数组,但每次还是要逐字段 field.get() 取值并放入 jsonobject。无注解对象也被处理 当前逻辑对所有走过切面的 controller 方法都会触发 translatedict,即使返回的 vo 上没有任何 @dict 注解,也会执行 instanceof 判断和类型检查。 - 解决方案 目前有两种解决思路
- 通过在方法或类级别加开关注解来彻底短路, 在 vo 类中上增加
@dictenable注解判断是否需要提前短路。 - 在
dicttranslateinterceptor.java这个类中增加一个map<class<?>, boolean>用来缓存 vo 判断是否需要提前短路
十、文件清单
| 文件 | 位置 | 说明 |
|---|---|---|
dict.java | api 模块 | 注解定义 |
dictdataprovider.java | api 模块 | 数据加载接口 + dictitem |
dictaspectconfig.java | service 模块 | 配置化切面注册 |
dicttranslateinterceptor.java | service 模块 | 核心翻译逻辑 |
dictcacheservice.java | service 模块 | redis 缓存服务 |
entrydictdataprovider.java | service 模块 | 本项目的 provider 实现 |
dictcachecontroller.java | service 模块 | 缓存管理接口 |
十一、总结
本方案的核心设计思想:
一套代码,多项目复用,不同的表、不同的包路径,只改配置文件。
- 注解驱动 — 加
@dict即生效,零侵入业务代码 - 接口隔离 —
dictdataprovider让数据来源与翻译逻辑彻底解耦 - 全面配置化 — redis 前缀、过期时间、切面拦截路径均通过 yml 配置
- 防御性编程 — 任何环节异常都降级处理,绝不阻断业务
- 缺点 —性能相关:
— 解决方案
十二、具体代码实现
- 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 "";
}
- 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;
}
}
}
- 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;
}
}
- 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);
}
}
- 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);
}
}
- 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);
}
}
- 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数据字典翻译的资料请关注代码网其它相关文章!
发表评论