什么是字典?
开发者更加倾向于使用短小的字段类型比如 tinyint、char(1)等来存储一些类型字段,以让每个数据都能够尽可能少的占用空间。 而用户当然不买单,希望能够看到每个字段都真正含义(比如 status = 1时,其真正含义是状态进行中, status = 2时,其真正含义是状态完成)
而数据从1
到进行中
的数据变换过程我们称之为字典翻译。
实现方案
而字典是任何后台管理系统比不可少的系统功能模块之一。 而根据架构选型的不同,其内容翻译的主要实现方案有两种:
- 基于后端查询时自动进行翻译(本文主要讨论内容)
在进行查询、列表时后端能够自动对字段进行翻译,把对于status字段,能够有一个statusName(存储翻译结果的名称,随你定)来说明status的含义,并展示给用户。
- 基于前端展示时的自动翻译
在后端返回结果后,通过status字段再次去调用后端的API或者是本地的字典缓存,来获取status字段的真实含义,然后展示给用户。
而本文的剩下内容将讨论如何基于Spring AOP来实现方案1(后端自动翻译)功能。
使用方式
- 添加字段用于填充翻译后的文本
- 添加
@Dict
注解,并指定翻译的字典类型 - 在API的返回方法中添加
@DictTranslation
注解
class Entity{
...
// 未翻译的字典值
private String type;
@Dict(EVENT_TYPE)
private String typeName;
...
}
class Controller{
...
@GetMapping("/detail")
@DictTranslation
public ApiResult getById(@NotBlank @RequestParam("id") String id) {
return ApiResult.ok().data(service.getInfo(id));
}
...
}
实现结果
实现步骤
- 添加注解
@Dict
用于指定字典的元数据(字典类型和字典值)
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Dict {
/**
* 字典类型:t_sys_dict_type中的dict_type字段.
*/
String value();
String codeField() default "";
}
- 添加注解
@DictTranslation
用于手动指定哪些方法会通过AOP实现自动翻译
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface DictTranslation {
}
- 实现注解AOP切面
/**
* @Author yaowenbin
* @Date 2022/11/18
*/
@Aspect
@Component
@Slf4j
public class DictAspect {
@Resource
private SysDictDataService dictDataService;
@Pointcut("@annotation(dictTranslation)")
public void pointcut(DictTranslation dictTranslation) {
}
@Around("pointcut(dictTranslation)")
public Object doAround(ProceedingJoinPoint pjp, DictTranslation dictTranslation) throws Throwable {
Object proceed = pjp.proceed();
if (! (proceed instanceof ApiResult) ) {
return proceed;
}
ApiResult result = (ApiResult) proceed;
// 翻译单个对象
notNull(result.get(ApiResult.FIELD_DATA), val -> {
translationDict(result.get("data"));
});
// 翻译列表对象
notNull(result.get(ApiResult.FIELD_PAGE), val -> {
PageVo pageVo = (PageVo) result.get("page");
List dataList = pageVo.getList();
for (Object o : dataList) {
translationDict(o);
}
});
return proceed;
}
public void translationDict(Object data) {
Field[] fields = data.getClass().getDeclaredFields();
for (Field field : fields) {
if (field.isAnnotationPresent(Dict.class)) {
Dict dict = field.getAnnotation(Dict.class);
String codeFieldName;
if (dict.codeField().equals("")) {
String fieldName = field.getName();
codeFieldName = fieldName.substring(0, fieldName.length() - 4);
} else {
codeFieldName = dict.codeField();
}
char[] chars = codeFieldName.toCharArray();
chars[0] = toUpperCase(chars[0]);
codeFieldName = String.valueOf(chars);
String getterName = "get" + codeFieldName;
Method codeGetter;
try {
codeGetter = data.getClass().getMethod(getterName);
} catch (NoSuchMethodException | SecurityException e) {
log.warn("翻译失败, {}未找到{}()方法,无法翻译该字段", data.getClass(), getterName);
continue;
}
String code;
try {
code = (String) codeGetter.invoke(data);
} catch (IllegalAccessException | InvocationTargetException e) {
log.warn("翻译失败, {}调用{}()异常", data.getClass(), getterName);
continue;
}
DropdownVo dropdown = dictDataService.getDataByType(dict.value(), code);
if (dropdown == null) {
log.warn("翻译失败, {}类中的{}未找到数据库中{}对应字典数据", getClass(), dict, code);
continue;
}
ReflectUtil.setFieldValue(data, field.getName(), dropdown.getLabel());
}
}
}
public <V> void notNull(V obj, Consumer<V> consumer) {
if (obj != null) {
consumer.accept(obj);
}
}
private char toUpperCase(char c) {
if (97 <= c && c<= 122) {
c ^= 32;
}
return c;
}
}