一、背景
经常在项目中,数据库根据范式设计,数据库表中仅保存了用于关联的字段id,中文显示信息需要再进行关联查询,并且往往在前端显示的时候,需要用中文显示,那么就不得不在查询的时候进行连表查询。
二、问题
对于连接信息较少的时候,倒是写起来也好弄,但是如果有4,5个字段甚至更多字段需要转换中文时,不仅连表写的比较麻烦,也会影响查询效率,并且也不得不自己写很长的查询语句,额外的去写很多VO,所以就开始思考能不能搞一个通用的代码解决这个问题,经过一番搜索和思考后,选择用aop通过切面和自定义注解来实现。
三、技术路线
- 分别定义TranslateResult和TranslateField,用于标记切入点和翻译字段
- 编写切面方法
- 在待翻译的方法和对象上分别标注注解
四、代码实现
- 定义切面,并编写获取字典、翻译字段的方法
@Aspect
@Component
@Slf4j
public class TranslationAspect {
@Autowired
private ApplicationContext applicationContext;
@Autowired
NormalRedisGtCacheOperator redisGtCacheOperator;
String suffix="Display";
/**
* 切入函数
* @param joinPoint 切入点
* @return 增强后的数据对象
* @throws Throwable 异常抛出
*/
@Around("execution(* cn.test.nrms..*.*(..)) && @annotation(cn.test.nrms.common.dict.TranslateResult)")
public Object translateResult(ProceedingJoinPoint joinPoint) throws Throwable {
Object result = joinPoint.proceed(); // 执行原方法
if(result instanceof PageInfo){
PageInfo<?> pageInfo=(PageInfo<?>)result;
for (Object item:pageInfo.getList()){
translateItem(item);
}
}else if(result instanceof Collection){
Collection<?> collection=(Collection<?>) result;
for(Object item:collection){
translateItem(item);
}
}
else {
translateItem(result);
}
// 返回翻译后的结果
return result;
}
/**
* 转换单个对象
* @param item 待转换对象
*/
private void translateItem(Object item){
// 检查结果对象是否为null,并且是否包含需要翻译的字段
if (item != null) {
// 使用反射遍历对象的所有字段
List<Field> fields=new ArrayList<>();
Class tempclass=item.getClass();
while (tempclass!=null){
fields.addAll(Arrays.asList(tempclass.getDeclaredFields()));
tempclass=tempclass.getSuperclass();
}
//Field[] fields = item.getClass().getDeclaredFields();
for (Field field : fields) {
if (field.isAnnotationPresent(TranslateField.class)) {
// 获取注解中的字典键
TranslateField translateField = field.getAnnotation(TranslateField.class);
Class<? extends DictQuery> dictionaryKey = translateField.dictionaryKey();
if(StringUtils.isNotEmpty(translateField.suffix())){
suffix=translateField.suffix();
}
String code = getOriginalCode(item, field);
// 翻译字段
String translatedValue = translateValue((Class<DictQuery>) dictionaryKey, code);
setTransValue(item,field.getName()+suffix,translatedValue);
}
}
}
}
/**
* 设置翻译后的值
* @param result 待翻译对象
* @param fieldName 翻译后赋值字段
* @param translatedValue 翻译后的值
*/
private void setTransValue(Object result, String fieldName, String translatedValue) {
try{
Class<? extends Object> c=result.getClass();
Field f =c.getDeclaredField(fieldName);
f.setAccessible(true);
f.set(result,translatedValue);
}catch (Exception ex){
log.error(ex.getMessage());
}
}
/**
* 根据code获取翻译值
* @param dictionaryKey 字典对象
* @param code 待翻译code
* @return 翻译后的值
*/
private String translateValue(Class<DictQuery> dictionaryKey, String code) {
String catchKey= dictionaryKey.getName();
Map<String,String> dictPair;
DictCache dictCache= DictCache.getInstance(catchKey);
if(dictCache==null){
DictQuery dictQuery=applicationContext.getBean(dictionaryKey);
dictPair=dictQuery.dictMap();
DictCache.put(catchKey,dictPair);
}else {
ObjectMapper objectMapper = new ObjectMapper();
dictPair = objectMapper.convertValue(dictCache.data, new TypeReference<Map<String, String>>() {
});
}
if(dictPair!=null&&dictPair.containsKey(code)){
return dictPair.get(code);
}
return code;
}
/**
* 反射获取字段值
* @param result 数据对象
* @param field 字段
* @return 字段值
*/
private String getOriginalCode(Object result, Field field) {
try {
PropertyDescriptor pd = new PropertyDescriptor(field.getName(), result.getClass());
Method getMethod=pd.getReadMethod();
Object invoke=getMethod.invoke(result);
return invoke==null?"":invoke.toString();
}catch (Exception ex){
log.error(ex.getMessage());
return "";
}
}
}
- 此处根据字典类获取到字典后,会放到缓存中
@Data
public class DictCache {
String key;
LocalDateTime createTime;
//过期时间(秒)
Long expire;
Object data;
public boolean isEnable() {
return DateTimeUtil.daysBetweenInSecond(createTime, LocalDateTime.now()) < expire;
}
private static Map<String, DictCache> cachePair = new HashMap<>();
public static DictCache getInstance(String key) {
if (cachePair.containsKey(key) && cachePair.get(key).isEnable()) {
return cachePair.get(key);
}
synchronized (DictCache.class) {
cachePair.remove(key);
}
return null;
}
public static synchronized void put(String key, Object data) {
put(key, data, 300L);
}
public static synchronized void put(String key, Object data, Long expire) {
DictCache dictCache = new DictCache();
dictCache.setKey(key);
dictCache.setData(data);
dictCache.setExpire(expire);
dictCache.setCreateTime(LocalDateTime.now());
cachePair.put(key, dictCache);
}
}
- 分别定义TranslateResult和TranslateField,用于标记切入点和翻译字段
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface TranslateField {
/**
* 字典对象
* @return 字典对象
*/
Class<? extends DictQuery> dictionaryKey() default NormalDict.class; // 用于指定字典中的键
String suffix() default "Display";
}
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface TranslateResult {
}
*此处有一个小小的设计,TranslateField的dictionaryKey参数用的是类,并限定了是实现DictQuery接口的类,用于其他字典获取方法的扩展。因为考虑到字典翻译需要的是一个key/value的map,所以该接口只有一个获取字典的方法,返回的是该字典的map。 3. 实现一个常规的字典查询类
//常规的字典查询类型
@Slf4j
@Service
public class NormalDict implements DictQuery {
@Autowired
NormalDictMapper normalDictMapper;
@Override
public Map<String, String> dictMap() {
try {
List<Map<String, String>> dictResult = normalDictMapper.queryDict();
Map<String, String> dictPair = new HashMap<>();
if (dictResult != null && !dictResult.isEmpty()) {
for (Map<String, String> item : dictResult) {
dictPair.put(String.valueOf(item.get("code")), item.get("name"));
}
return dictPair;
}
return null;
}catch (Exception ex){
log.error(ex.getMessage());
return null;
}
}
}
//查询mapper
@Mapper
public interface NormalDictMapper {
@Select("select f_id code,f_name name from tg_enum_domain")
List<Map<String, String>> queryDict();
}
- 代码调用
(1) 在需要翻译的查询方法上标记上@TranslateResult注解
@TranslateResult
List<GtPlanTaskVo> taskDistributePage(@Param("taskvo") GtPlanTaskVo distributeVo);
(2) 在对象的需要翻译的字段上标注@TranslateField注解
//此处注解可以添加对应字典及翻译后赋值字段的后缀,赋值逻辑是将翻译后的值赋值给翻译字段名+后缀的字段上
@TranslateField(dictionaryKey = DeptDict.class)
private Long deptId;
五、总结
- 利用AOP特性实现了数据对象中部分字段从code->name的翻译;
- 对代码侵入较小
- 对输出结果进行翻译,可支持分页,效率上较好
- 添加字典缓存,避免多次查询
- 字典类型可扩展