一、背景

经常在项目中,数据库根据范式设计,数据库表中仅保存了用于关联的字段id,中文显示信息需要再进行关联查询,并且往往在前端显示的时候,需要用中文显示,那么就不得不在查询的时候进行连表查询。

二、问题

对于连接信息较少的时候,倒是写起来也好弄,但是如果有4,5个字段甚至更多字段需要转换中文时,不仅连表写的比较麻烦,也会影响查询效率,并且也不得不自己写很长的查询语句,额外的去写很多VO,所以就开始思考能不能搞一个通用的代码解决这个问题,经过一番搜索和思考后,选择用aop通过切面和自定义注解来实现。

三、技术路线
  • 分别定义TranslateResult和TranslateField,用于标记切入点和翻译字段
  • 编写切面方法
  • 在待翻译的方法和对象上分别标注注解

基于aop实现查询结果中字典项的自动翻译_aop

四、代码实现
  1. 定义切面,并编写获取字典、翻译字段的方法
@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);
    }
}
  1. 分别定义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. 代码调用

(1) 在需要翻译的查询方法上标记上@TranslateResult注解

@TranslateResult
    List<GtPlanTaskVo> taskDistributePage(@Param("taskvo") GtPlanTaskVo distributeVo);

(2) 在对象的需要翻译的字段上标注@TranslateField注解

//此处注解可以添加对应字典及翻译后赋值字段的后缀,赋值逻辑是将翻译后的值赋值给翻译字段名+后缀的字段上
    @TranslateField(dictionaryKey = DeptDict.class)
    private Long deptId;
五、总结
  1. 利用AOP特性实现了数据对象中部分字段从code->name的翻译;
  2. 对代码侵入较小
  3. 对输出结果进行翻译,可支持分页,效率上较好
  4. 添加字典缓存,避免多次查询
  5. 字典类型可扩展