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中.
【fhs-framework springboot mybatis 解决表关联查询问题的关键方案-翻译服务】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 typeval是对应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 goodStuAgecustomer#customercustomerName 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 popo * @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); returnnew 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 goodStuAgecustomer#customercustomerName 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

    推荐阅读