fhs-framework springboot mybatis 解决表关联查询问题的关键方案-翻译服务

简介

    开发中会经常遇到这样的场景:比如在成绩表有一个student_id,如果我要查看成绩列表需要学生的名称,一般写法就是使用join,现在大部分ORM框架对于表关联支持并不是很友好,所以很多时候我们都要自己写SQL去实现。

   翻译服务即:通过id,将对应的title/name 字段翻译出,装载到VO中用于前端展示的技术.

1 FHS 提供的翻译服务如何使用?

     a  定义翻译数据源.

        

@Service
@DataSource("base_business")
@AutoTrans(namespace = BaseTransConstant.ORG, fields = "name", useRedis = true, defaultAlias = "org")
public class UcenterMsOrganizationServiceImpl extends BaseServiceImpl implements UcenterMsOrganizationService {

      通过以上定义,我们就能知道,组织机构的表,对外开放组织机构名称这个字段给其他的业务使用.

      b  标记业务pojo orgid字段使用此翻译数据源.

          

/**
     * 所属机构
     */
    @NotNull(message = "所属机构字段不可为null", groups = {Update.class, Delete.class})
    @Length(message = "所属机构字段的长度最大为32", groups = {Add.class, Update.class}, max = 32)
    @Column(name = "organization_id")
    @Trans(type = TransType.AUTO_TRANS,key = BaseTransConstant.ORG )
    private String organizationId;

    c   调用transService的trans相关方法即可.

         

 public V d2v(D d) {
        try {
            if (d == null) {
                return null;
            }
            V vo = voClass.newInstance();
            BeanUtils.copyProperties(d, vo);
            transService.transOne(vo);
            return vo;
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
        return null;
    }

 public List dos2vos(List dos) {
        List vos = ListUtils.copyListToList(dos, this.getVOClass());
        transService.transMore(vos);
        return vos;
    }

     c VO中需要有getTransMap 方法返回一个map,翻译服务会吧翻译的结果放到此map中.

     d 翻译后的结果示例:

    

 {
            "between": {},
            "createTime": "2019-03-25 00:00:00",
            "createUser": "1",
            "dataPermissin": {},
            "dataPermissions": "{\"parkIds\":\"0d601eb23f0e11e99571d02788407b5e\"}",
            "groupCode": null,
            "inFilter": {},
            "isDelete": null,
            "isEnable": 1,
            "methods": null,
            "organizationId": "001",
            "pkey": 1,
            "remark": "12123",
            "roleId": 1,
            "roleName": "1231",
            "state": null,
            "transMap": {
                "orgName": "机构",
                "isEnableName": "启用",
                "createUserUserName": "admin22"
            },
            "updateTime": "2019-03-29 00:00:00",
            "updateUser": "62b5870c510c4e9da3f72460001c42fa"
        },

      2  翻译服务实现

                 A 定义注解

                              自动翻译注解,把一个service当做数据源的注解,用于service实现类之上,比如上面的ORGservice

@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE})
public @interface AutoTrans
{
    /**
     *  命名空间
     * @return
     */
    String namespace();

    /**
     * 字段集合
     * @return
     */
    String[] fields();

    /**
     * 是否使用缓存翻译
     * @return  默认为true 如果是false的话
     */
    boolean useCache() default true;

    /**
     * 是否使用redis存放缓存
     * @return 默认false
     */
    boolean useRedis() default false;

    /**
     * 默认的别名
     * @return
     */
    String defaultAlias() default "";
}

          字段翻译注解,定义这个字段使用哪个翻译,比如fhs提供了auto(自动翻译),和wordbook(字典)

@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.FIELD })
public @interface Trans {

    /**
     * 获取翻译类型,比如 wordbook 是字典
     * @return 类型
     */
    String type();

    /**
     * 字段 比如  要翻译男女 上面的type写wordbook 此key写sex即可
     * @return
     */
    String key() default "";
}

         翻译类型集合,用于标记到 pojo上,代表我使用了哪些类型的翻译.

@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE})
public @interface TransTypes 
{
    /**
     * 获取需要翻译的类型
     * @return
     */
    String[] types();
}

           B 定义翻译服务类

                    主要负责根据翻译的类型决定调用哪个ITransTypeService的实现类去翻译某些字段.


@Service("transService")
public class TransService {
    /**
     * key type  val是对应type的service
     */
    private static Map transTypeServiceMap = new HashMap();

    /**
     * 注册一个trans服务
     *
     * @param type             类型
     * @param transTypeService 对应的trans接口实现
     */
    public static void registerTransType(String type, ITransTypeService transTypeService) {
        transTypeServiceMap.put(type, transTypeService);
    }

    /**
     * 翻译一个字段
     *
     * @param obj 需要翻译的对象
     */
    public void transOne(VO obj) {
        if (obj == null) {
            return;
        }
        ClassInfo info = ClassManager.getClassInfoByName(obj.getClass());
        String[] transTypes = info.getTransTypes();
        if (transTypes == null) {
            return;
        }
        List transFieldList = null;
        for (String type : transTypes) {
            transFieldList = info.getTransField(type);
            if (transFieldList == null || transFieldList.size() == 0) {
                continue;
            }
            transTypeServiceMap.get(type).transOne(obj, transFieldList);
        }
    }

    /**
     * 翻译多个 字段
     *
     * @param objList 需要翻译的对象集合
     * @param objList 需要翻译的字段集合
     */
    public void transMore(List objList) {
        if (objList == null || objList.size() == 0) {
            return;
        }
        Object object = objList.get(0);
        ClassInfo info = ClassManager.getClassInfoByName(object.getClass());
        String[] transTypes = info.getTransTypes();
        if (transTypes == null) {
            return;
        }
        List transFieldList = null;
        for (String type : transTypes) {
            transFieldList = info.getTransField(type);
            if (transFieldList == null || transFieldList.size() == 0) {
                continue;
            }
            transTypeServiceMap.get(type).transMore(objList, transFieldList);
        }
    }

}

            D 自动翻译实现

                springboot启动成功后,扫描service包,被autotrans注解标记的service,解析autotrans的内容,然后把数据库中数据缓存到内存和redis中,当业务需要翻译的时候,根据id把缓存的数据拿出来,放到transmap中.

 

    


/**
 * 本接类使用需要配合Autotrans 注解和autoTransAble的实现类
 *
 * @Description: 自动翻译服务
 * @Author: Wanglei
 * @Date: Created in 10:14 2019/10/15
 */
@Data
@Service
public class AutoTransService implements ITransTypeService, InitializingBean, ApplicationListener {

    public static final Logger LOGGER = LoggerFactory.getLogger(AutoTransService.class);


    /**
     * service的包路径
     */
    @Value("${fhs.autotrans.package:com.*.*.service.impl}")
    private String[] packageNames;

    /**
     * 翻译数据缓存map
     */
    private Map> cacheMap = new HashMap<>();


    /**
     * 缓存 默认时间:半个小时
     */
    @CreateCache(expire = 1800, name = "trans:cache:", cacheType = CacheType.REMOTE)
    private Cache> transCache;

    /**
     * 基础服务
     */
    private Map baseServiceMap = new HashMap<>();

    /**
     * 配置
     */
    private Map transSettMap = new HashMap<>();

    /**
     * 如果直接去表里查询,放到这个cache中
     */
    private ThreadLocal>> threadLocalCache = new ThreadLocal<>();

    /**
     * 翻译字段配置map
     */
    private Map transFieldSettMap = new HashMap<>();

    @Override
    public void transOne(VO obj, List toTransList) {
        Trans tempTrans = null;
        for (Field tempField : toTransList) {
            TransFieldSett transFieldSett = transFieldSettMap.containsKey(tempField) ? transFieldSettMap.get(tempField) : new TransFieldSett(tempField);
            tempTrans = transFieldSett.getTrans();
            String namespace = transFieldSett.getNamespace();
            String alias = transFieldSett.getAlias();
            if (transSettMap.containsKey(namespace) && CheckUtils.isNullOrEmpty(alias)) {
                alias = transSettMap.get(namespace).defaultAlias();
            }
            String pkey = ConverterUtils.toString(ReflectUtils.getValue(obj, tempField.getName()));
            if (StringUtils.isEmpty(pkey)) {
                continue;
            }
            Map transCache = null;
            // 主键可能是数组
            pkey = pkey.replace("[", "").replace("]", "");
            if (pkey.contains(",")) {
                String[] pkeys = pkey.split(",");
                transCache = new HashMap<>();
                Map tempTransCache = null;
                for (String tempPkey : pkeys) {
                    tempTransCache = getTempTransCacheMap(namespace, ConverterUtils.toInteger(tempPkey));
                    if (tempTransCache == null) {
                        LOGGER.error("auto trans缓存未命中:" + namespace + "_" + tempPkey);
                        continue;
                    }
                    // 比如学生表  可能有name和age 2个字段
                    for (String key : tempTransCache.keySet()) {
                        transCache.put(key, transCache.containsKey(key) ? transCache.get(key) + "," + tempTransCache.get(key) : tempTransCache.get(key));
                    }
                }
            } else {
                transCache = getTempTransCacheMap(namespace, pkey);
                if (transCache == null) {
                    LOGGER.error("auto trans缓存未命中:" + namespace + "_" + pkey);
                    continue;
                }
            }
            if (!CheckUtils.isNullOrEmpty(alias)) {
                Map tempMap = new HashMap<>();
                Set keys = transCache.keySet();
                for (String key : keys) {
                    tempMap.put(alias + key.substring(0, 1).toUpperCase() + key.substring(1), transCache.get(key));
                }
                transCache = tempMap;
            }
            Map transMap = obj.getTransMap();
            Set keys = transCache.keySet();
            for (String key : keys) {
                if (CheckUtils.isNullOrEmpty(transMap.get(key))) {
                    transMap.put(key, transCache.get(key));
                }
            }
        }
    }

    @Override
    public void transMore(List objList, List toTransList) {
        threadLocalCache.set(new HashMap<>());
        // 由于一些表数据比较多,所以部分数据不是从缓存取的,是从db先放入缓存的,翻译完了释放掉本次缓存的数据
        for (Field tempField : toTransList) {
            tempField.setAccessible(true);
            Trans tempTrans = tempField.getAnnotation(Trans.class);
            String namespace = tempTrans.key();
            // 如果是 good#student  翻译出来应该是 goodStuName goodStuAge  customer#customer  customerName
            if (namespace.contains("#")) {
                namespace = namespace.substring(0, namespace.indexOf("#"));
            }
            if (!this.baseServiceMap.containsKey(namespace)) {
                LOGGER.warn("namesapce对应的service没有标记autotrans:" + namespace);
                continue;
            }
            AutoTrans autoTransSett = this.transSettMap.get(namespace);
            if (autoTransSett.useCache()) {
                continue;
            }
            Set ids = new HashSet<>();
            objList.forEach(obj -> {
                try {
                    ids.add(tempField.get(obj));
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                }
            });
            List dbDatas = baseServiceMap.get(namespace).findByIds(new ArrayList<>(ids));
            for (VO vo : dbDatas) {
                threadLocalCache.get().put(namespace + "_" + vo.getPkey(), createTempTransCacheMap(vo, autoTransSett));
            }
        }
        objList.forEach(obj -> {
            this.transOne(obj, toTransList);
        });
        threadLocalCache.set(null);
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        TransService.registerTransType("auto", this);
        TransMessageListener.regTransRefresher("auto", this::refreshCache);
    }


    public void init(ApplicationReadyEvent applicationReadyEvent) {
        //spring容器初始化完成之后,就会自行此方法。
        Set> entitySet = scan(AutoTrans.class, packageNames);
        // 遍历所有class,获取所有用@autowareYLM注释的字段
        if (entitySet != null) {
            for (Class entity : entitySet) {
                // 获取该类
                Object baseService = SpringContextUtil.getBeanByClass(entity);
                if (!(baseService instanceof AutoTransAble)) {
                    LOGGER.warn("AutoTrans 只能用到实现AutoTransAble的类上,不能用到:" + baseService.getClass());
                    continue;
                }
                AutoTrans autoTransSett = entity.getAnnotation(AutoTrans.class);
                this.baseServiceMap.put(autoTransSett.namespace(), (AutoTransAble) baseService);
                this.transSettMap.put(autoTransSett.namespace(), autoTransSett);
            }
        }
        refreshCache(new HashMap<>());
    }

    /**
     * 刷新缓存
     *
     * @param messageMap 消息
     */
    public void refreshCache(Map messageMap) {
        //这里必须能拿到namespace 拿不到,就当作全部刷新
        String namespace = messageMap.get("namespace") != null ?
                messageMap.get("namespace").toString() : null;
        if (namespace == null) {
            Set namespaceSet = this.transSettMap.keySet();
            namespaceSet.forEach(temp -> {
                refreshOneNamespace(temp);
            });
        } else {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                LOGGER.error("刷新缓存错误:", e);
            }
            refreshOneNamespace(namespace);
        }
    }

    /**
     * 刷新一个namespace下的所有的缓存
     *
     * @param namespace namespace
     */
    public void refreshOneNamespace(String namespace) {
        LOGGER.info("开始刷新auto-trans缓存:" + namespace);
        if (!this.transSettMap.containsKey(namespace)) {
            LOGGER.info("本系统无需刷新此缓存namespace:" + namespace);
            return;
        }

        List vos = this.baseServiceMap.get(namespace).select();
        if (vos == null || vos.isEmpty()) {
            return;
        }
        Object pkeyVal = null;
        String fielVal = null;
        Map tempCacheTransMap = null;
        VO po = null;
        AutoTrans autoTrans = this.transSettMap.get(namespace);
        //不适用缓存的不做缓存
        if (!autoTrans.useCache()) {
            return;
        }
        for (int i = 0; i < vos.size(); i++) {
            po = vos.get(i);
            pkeyVal = po.getPkey();
            cacheMap.put(namespace + "_" + pkeyVal, createTempTransCacheMap(po, autoTrans));
            if(autoTrans.useCache()){
                this.transCache.put(namespace + "_" + pkeyVal, createTempTransCacheMap(po, autoTrans));
            }
        }
        LOGGER.info("刷新auto-trans缓存完成:" + namespace);
    }

    /**
     * 创建一个临时缓存map
     *
     * @param po        po
     * @param autoTrans 配置
     * @return
     */
    private Map createTempTransCacheMap(Object po, AutoTrans autoTrans) {
        String fielVal = null;
        Map tempCacheTransMap = new LinkedHashMap<>();
        for (String field : autoTrans.fields()) {
            fielVal = ConverterUtils.toString(ReflectUtils.getValue(po, field));
            tempCacheTransMap.put(field, fielVal);
        }
        return tempCacheTransMap;
    }

    /**
     * 获取用于翻译的缓存
     *
     * @param namespace namespace
     * @param pkey      主键
     * @return 缓存
     */
    private Map getTempTransCacheMap(String namespace, Object pkey) {
        AutoTrans autoTrans = this.transSettMap.get(namespace);
        //如果内存缓存中有,则优先用内存缓存
        if (cacheMap.containsKey(namespace + "_" + pkey)) {
            return cacheMap.get(namespace + "_" + pkey);
        }
        //如果注解为空,代表可能是其他的服务提供的翻译,尝试去redis获取缓存
        else if (autoTrans == null) {
            Map redisCacheResult = this.transCache.get(namespace + "_" + pkey);
            //如果获取到了返回
            if (redisCacheResult != null) {
                return redisCacheResult;
            }
            //redis获取不到返回空map
            return new HashMap<>();
        } else {
            if (autoTrans == null) {
                LOGGER.warn("namespace对应的service没有使用autotrans注解标记:" + namespace);
                return  new HashMap<>();
            }
            //如果强调使用缓存,则可能是还没刷新进来,直接返回空map,前端在刷新一下就好了
            if (autoTrans.useCache()) {
                return new HashMap<>();
            }
            if (this.threadLocalCache.get() == null) {
                VO vo = this.baseServiceMap.get(namespace).selectById(pkey);
                return createTempTransCacheMap(vo, autoTrans);
            }
            return this.threadLocalCache.get().get(namespace + "_" + pkey);
        }
    }

    /**
     * 翻译单个的key
     *
     * @param namespace namespace
     * @param pkeyVal   主键
     * @return
     */
    public String transKey(String namespace, String pkeyVal) {
        Map tempCacheTransMap = cacheMap.get(namespace + "_" + pkeyVal);
        if (tempCacheTransMap == null) {
            LOGGER.error("auto trans缓存未命中:" + namespace + "_" + pkeyVal);
        } else {
            for (String key : tempCacheTransMap.keySet()) {
                return tempCacheTransMap.get(key);
            }
        }
        return null;
    }

    /**
     * 类扫描器
     *
     * @param annotationClass 注解
     * @param packageNames    包
     * @return 符合条件的类
     */
    public static Set> scan(Class annotationClass, String[] packageNames) {
        TypeFilter entityFilter = AnnotationTypeFilterBuilder.build(annotationClass);
        SpringClassScanner entityScanner = new SpringClassScanner.Builder().typeFilter(entityFilter).build();
        for (String packageName : packageNames) {
            entityScanner.getScanPackages().add(packageName);
        }
        Set> entitySet = null;
        try {
            entitySet = entityScanner.scan();
        } catch (ClassNotFoundException | IOException e) {
            LOGGER.error("包扫描错误", e);
            // log or throw runTimeExp
            throw new RuntimeException(e);
        }
        return entitySet;
    }

    @Override
    public void onApplicationEvent(ApplicationReadyEvent applicationReadyEvent) {
        init(applicationReadyEvent);
    }

}

/**
 * 被翻译的字段的实体
 */
@Data
class TransFieldSett {
    /**
     * trans注解
     */
    private Trans trans;
    /**
     * 命名空间
     */
    private String namespace;
    /**
     * 别名
     */
    String alias;

    public TransFieldSett(Field transField) {
        transField.setAccessible(true);
        trans = transField.getAnnotation(Trans.class);
        namespace = trans.key();
        // 如果是 good#student  翻译出来应该是 goodStuName goodStuAge  customer#customer  customerName
        if (namespace.contains("#")) {
            alias = namespace.substring(namespace.indexOf("#") + 1);
            namespace = namespace.substring(0, namespace.indexOf("#"));
        }
    }
}

全部源码地址 https://gitee.com/fhs-opensource/fhs-framework/tree/v2.x/fhs_extends/fhs_trans

开源项目地址:https://gitee.com/fhs-opensource/fhs-framework

fhs framework qq群:976278956

你可能感兴趣的:(fhs_framework)