简介
开发中会经常遇到这样的场景:比如在成绩表有一个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 extends VO> 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 extends VO> 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 extends Annotation> 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