优雅翻译前端返回中文描述
痛点
查询或者导出:需要过多的关注编码(枚举、快码、数据字典)对应是什么中文(对开发来说这本不是该关注的或者应该减少关注的),导致带来以下问题:
-
增加mysql一些不必要的性能开销(虽然从cpu和内存的开销上增加比较小,但这部分确实没必
要) 太多次的重复性左连、子查询(看着都不舒服) -
代码编写多了一些麻烦
-
冗余了重复的非业务性代码在业务模块上
代码示例
每个应用都还得单独的写快码的查询方法,或者远程调用;
多个地方使用到这里需要名称的,如果不想xml去写连表的用wrapper查,也得如上去写查询快码再设置名称;
冗余了重复的非业务性代码在业务模块上,代码简洁性上看上去也不好
实现的目的
- 减少开发对编码中文翻译的关注,简化了部分编码,从而提高一点编码效率,提高一些代码的简洁性,减少不必要的性能损耗;
- 目前系统上也没有二级缓存的能力,引入也是方便后续其它功能的使用;
实现技术
J2cache二级缓存(caffeine、redission/redis)+ 序列化增强(自定义序列化器)+ JDBC + 反射 +
Bean策略模式;
功能操作说明
简单的枚举翻译
1、自动给前端一个拼接上Name后缀的字段
@ApiModelProperty(value = "预约单状态")
@DictionarySerialize(processorEnumClass = ApptsStatusTypeEnum.class)
private String apptsStatus = "1";
效果:{"apptsStatus":1,"apptsStatusName":"待入园"}2、这个字段给前端展示时是翻译后的值
@ApiModelProperty(value = "预约单状态")
@DictionarySerialize(overrideWriter = true,processorEnumClass =
ApptsStatusTypeEnum.class)
private String apptsStatus = "1";
效果:{"apptsStatus":"待入园"}3、用自己定义的字段展示给前端
@ApiModelProperty(value = "预约单状态")
@DictionarySerialize(processorEnumClass =
ApptsStatusTypeEnum.class,dynamicFieldName = "xzpApptsStatus")
private String apptsStatus = "1";
效果:{"apptsStatus":1,"xzpApptsStatus":"待入园"}4、多个枚举编码拼接的也可以按拼接的方式展示给前端
@ApiModelProperty(value = "预约单状态")
@DictionarySerialize(processorEnumClass = ApptsStatusTypeEnum.class)
private String apptsStatus = "1,2,3";
效果:{"apptsStatus":"1,2,3","xzpApptsStatus":"待入园,已入园,已取消"}5、自定义的拼接符号也可以
@ApiModelProperty(value = "预约单状态")
@DictionarySerialize(processorEnumClass = ApptsStatusTypeEnum.class,delimiter =
"-")
private String apptsStatus = "1-2-3";
效果:{"apptsStatus":"1-2-3","xzpApptsStatus":"待入园-已入园-已取消"}6、多个枚举编码拼接的也可以按集合的方式展示给前端
@ApiModelProperty(value = "预约单状态")
@DictionarySerialize(processorEnumClass = ApptsStatusTypeEnum.class,showListType
= true)
private String apptsStatus = "1,2,3";
效果:{"apptsStatus":"1,2,3","xzpApptsStatus":["待入园","已入园","已取消"]}7、集合的字段也可以按集合或者字符串的方式展示给前端
@ApiModelProperty(value = "预约单状态")
@DictionarySerialize(processorEnumClass = ApptsStatusTypeEnum.class)
private List<String> apptsStatus = Lists.newArrayList("1","2");
效果:{"apptsStatus":["1","2","3"],"xzpApptsStatus":"待入园,已入园,已取消"}
@ApiModelProperty(value = "预约单状态")
@DictionarySerialize(processorEnumClass = ApptsStatusTypeEnum.class,showListType
= true)
private List<String> apptsStatus = Lists.newArrayList("1","2");
效果:{"apptsStatus":["1","2","3"],"xzpApptsStatus":["待入园","已入园","已取消"]}
快码的翻译
(有如上的6种效果,使用方式略有点区别,以第一种来举例说明)
1、自动给前端一个拼接上Name后缀的字段
@ApiModelProperty(value = "角色快码")
@DictionarySerialize(queryType = 2,key = "MINI_PROGRAM_ROLE_TYPE")
private String roleTypeCode = "JGC";
效果:{"roleTypeCode":"JGC","roleTypeCodeName":"加工厂"}
数据字典的翻译
(有如上的6种效果,使用方式略有点区别,以第一种来举例说明)
1、自动给前端一个拼接上Name后缀的字段
@ApiModelProperty(value = "数据字典的通知类型")
@DictionarySerialize(queryType = 2,key = "sys_notice_type",dataTranslateBean =
"DictionarySerializeRedisDefaultHandler")
private String xzppppp = "14";
效果:{"xzppppp":"12","xzpppppName":"通知"}
自定义翻译
(可以自定义对字段的翻译,也有如上的6种效果)
@Component(value = "XzpTestHandler")
public class DictionarySerializeRedisDefaultHandler implements
DictionarySerializeHandler {
@Override
public String translate(Object idValue,String key,String lookUpFieldName) {
System.out.println("拓展功能:自定义对字段的翻译")
return "hahaha";
}
}
@ApiModelProperty(value = "数据字典的通知类型")
@DictionarySerialize(queryType = 2,dataTranslateBean = "XzpTestHandler")
private String xzppppp = "12";
效果:{"xzppppp":"12","xzpppppName":"hahaha"}
导出
(导出的使用如上面的注解)
@ApiModel(description = "预约角色用户导出数据VO")
@Data
public class MiniProgramApptsUserRoleExportVO {
@ColumnWidth(20)
@HeadFontStyle(fontHeightInPoints = 12, bold = false)
@ExcelProperty(value = "角色", order = 4)
@ApiModelProperty("角色")
@DictionarySerialize(queryType = 2, key = "MINI_PROGRAM_ROLE_TYPE")
private String roleTypeCode = "JGC";
}
1、快码翻译有没有性能影响?
没有,因为快码的翻译使用的二级缓存
2、是否影响现有导出?
不影响(或者说有一丢丢的影响,因为会对导出的那个对象去判断一次有没有贴那个翻译注解,但就判
断这么一次,影响基本可以忽略不计了)
3、大批数据量的导出对性能有没有影响?
基本上不会有太多的影响,因为第一条数据解析出来之后,后面要翻译的字段值都从当前线程的map中
取值了
4、如何更新二级缓存?
4.1、默认过期的时间:30分钟
4.2、快码的缓存每1小时自动刷新一次(@CacheRefresh(refresh = 60, timeUnit =
TimeUnit.MINUTES))
5、怎么清除二级缓存?
5.1 对使用到的应用服务提供后门接口,调用就清除二级缓存(使用@CacheInvalidate如下例子)
5.2 如果只使用远程的模式,那直接清除redis的key也行
6、多节点之前清除缓存另外的节点怎么同步清除的?
利用chanel管道的pub/sub,用应用名称来作为管道名称实现通讯
注解类:
import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.linsy.common.core.handler.DictionarySerializeTranslate;import java.lang.annotation.*;@JacksonAnnotationsInside
@JsonSerialize(using = DictionarySerializeTranslate.class
)
@Target({ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DictionarySerialize {/*** 添加的对象字段名(比如: statusName),如果没有就默认拼接当前字段名称name*/String dynamicFieldName() default "";/*** key(比如:数据字典KEY)*/String key() default "";/*** 查询类型 1枚举 2自定义转换/主数据相关*/byte queryType() default 1;/*** 枚举类*/Class<? extends Enum<?>> processorEnumClass();/*** 分割符*/String delimiter() default ",";/*** 查询数据源bean*/String dataTranslateBean() default "DictionarySerializeDefaultHandler";/*** 是否覆盖*/boolean overrideWriter() default false;/*** 如果没有翻译到(或者数据库默认值) 那么默认是返回原值,也可以自定义传入参数来展示** @return*/boolean useOriginalValue() default true;/*** 是否显示列表类型*/boolean showListType() default false;
}
返回数据序列化翻译
import cn.hutool.core.util.StrUtil;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.BeanProperty;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.ser.ContextualSerializer;
import com.linsy.common.core.annotation.DictionarySerialize;
import com.linsy.common.core.utils.SpringUtils;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.compress.utils.Lists;
import org.apache.commons.lang3.tuple.Pair;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;import javax.annotation.PostConstruct;
import java.io.IOException;
import java.lang.reflect.Method;
import java.math.BigDecimal;
import java.sql.Date;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;@Slf4j
@NoArgsConstructor
@Component
public class DictionarySerializeTranslate<E extends Enum<E>> extends JsonSerializer<Object> implements ContextualSerializer {private static List<Class<?>> elemClassList;/*** @PostConstruct 注解表示 initMethod() 方法会在该 Bean 被初始化完成后自动执行。 * 其作用是:在 DictionarySerializeTranslate 实例化后,初始化支持的元素类型列表(如 String、Integer 等),用于后续类型判断或序列化处理。*/@PostConstructpublic void initMethod() {elemClassList = new ArrayList<>();elemClassList.add(String.class);elemClassList.add(Boolean.class);elemClassList.add(boolean.class);elemClassList.add(Byte.class);elemClassList.add(byte.class);elemClassList.add(Integer.class);elemClassList.add(int.class);elemClassList.add(Long.class);elemClassList.add(long.class);elemClassList.add(BigDecimal.class);elemClassList.add(Date.class);elemClassList.add(Short.class);elemClassList.add(short.class);elemClassList.add(Float.class);elemClassList.add(float.class);elemClassList.add(Double.class);elemClassList.add(double.class);elemClassList.add(Character.class);elemClassList.add(char.class);log.info("数据翻译器初始化");}private static final String SPLITMARK = ",";/*** 添加的对象字段名(比如: statusName)*/private String dynamicFieldName;/*** key(比如:数据字典KEY),这里暂未使用*/private String key;/*** 查询类型 1枚举 2自定义转换/主数据相关*/private byte queryType;/*** 枚举类*/private Class<E> processorEnumClass;/*** 序列化字段的类型*/private Class<?> sourceClassType;/*** 字符串的分割符号* @return*/private String delimiter;/*** 查询数据源bean*/private String dataTranslateBean = "DictionarySerializeDefaultHandler";/*** 是否覆盖*/boolean overrideWriter;/*** 如果没有翻译到(或者数据库默认值) 那么默认是返回原值,也可以自定义传入参数来展示** @return*/boolean useOriginalValue;/*** 是否显示列表类型*/boolean showListType;/*** 先进行,拿到注解的各种属性和字段属性* @param prov* @param beanProperty* @return* @throws JsonMappingException*/@Overridepublic JsonSerializer<?> createContextual(SerializerProvider prov, BeanProperty beanProperty) throws JsonMappingException {if (beanProperty != null) {// String 类型DictionarySerialize dataTranslateEntity = beanProperty.getAnnotation(DictionarySerialize.class);if (dataTranslateEntity == null) {dataTranslateEntity = beanProperty.getContextAnnotation(DictionarySerialize.class);}if (dataTranslateEntity != null) {return new DictionarySerializeTranslate(//需要添加的字段名dataTranslateEntity.dynamicFieldName(),//序列化字段名beanProperty.getName(),//主数据查询keydataTranslateEntity.key(),//翻译类型 1默认,枚举/主数据 2自定义sao操作dataTranslateEntity.queryType(),//自定义处理的beandataTranslateEntity.dataTranslateBean(),//枚举dataTranslateEntity.processorEnumClass(),//序列化字段的类型beanProperty.getType().getRawClass(),//分割符dataTranslateEntity.delimiter(),//是否覆盖dataTranslateEntity.overrideWriter(),//是否显示列表类型dataTranslateEntity.showListType(),//主数据没有翻译到 传入的默认值dataTranslateEntity.useOriginalValue());}return prov.findValueSerializer(beanProperty.getType(), beanProperty);}return prov.findNullValueSerializer(null);}public DictionarySerializeTranslate(String dynamicFieldName,String sourceFieldName,String key,byte queryType,String dataTranslateBean,Class<E> processorEnumClass,Class<?> sourceClassType,String delimiter,boolean overrideWriter,boolean showListType,boolean useOriginalValue) {if (StrUtil.isEmpty(dynamicFieldName)) {//自定义字段为空的时候,默认给它拼接后缀Namethis.dynamicFieldName = sourceFieldName + "Name";} else {//自定义字段不为空的时候this.dynamicFieldName = dynamicFieldName;}this.queryType = queryType;this.dataTranslateBean = dataTranslateBean;this.processorEnumClass = processorEnumClass;this.sourceClassType = sourceClassType;this.key = key;this.delimiter = delimiter;this.overrideWriter = overrideWriter;this.useOriginalValue = useOriginalValue;this.showListType = showListType;}@Overridepublic void serialize(Object value, JsonGenerator jsonGenerator, SerializerProvider serializers) throws IOException {//一定要先写入原值jsonGenerator.writeObject(value);,// 然后再写入原值的条件上追加字段,否则报错"can not write file name ,expecting a value(源码需要原值赋值才能做其它操作)//如果是空值,也要写入null,writeObject底层帮忙做了if (StringUtils.hasText(dynamicFieldName) && Objects.nonNull(value)) {// 如果注解的配置为空,则不做转义serializeValue(dynamicFieldName, value, jsonGenerator);}}/*** 序列化非null值** @param dynamicFieldName 要设置值的字段名称* @param value 贴了注解的字段的值* @param jsonGenerator jsonGenerator* @throws IOException IOException*/private void serializeValue(String dynamicFieldName,Object value, JsonGenerator jsonGenerator) throws IOException {//提供两种种类型的转义,第一种:枚举形式/主数据或者字典相关 第二种:自定义sao操作转义commonTranslate(dynamicFieldName, value, jsonGenerator);}/*** 枚举/主数据/自定义翻译* @param dynamicFieldName* @param value* @param jsonGenerator*/private void commonTranslate(String dynamicFieldName, Object value, JsonGenerator jsonGenerator) {try {String fieldvalue = "";StringBuffer stringBuffer = new StringBuffer();//是否是集合或者数组或者字符串变更boolean comomType = false;List<String> enumResponseList = null;if (value instanceof List<?>) {comomType = true;//集合处理//判断序列化字段的类型,是否是集合或者数组、//fieldvalue = solveListType((List<?>) value, stringBuffer);Pair<String, List<String>> listTypePair = dealQueryTypeLogic((List<?>) value, stringBuffer);//自定义加入其它字段fieldvalue = listTypePair.getLeft();enumResponseList = listTypePair.getRight();} else if (value.getClass().isArray()) {comomType = true;//数组处理Pair<String, List<String>> arrayTypePair = dealQueryTypeLogic(Arrays.asList((Object[]) value), stringBuffer);//自定义加入其它字段fieldvalue = arrayTypePair.getLeft();enumResponseList = arrayTypePair.getRight();} else if (value instanceof String && StringUtils.hasText(delimiter)) {comomType = true;//这里如果类型是String并且有分割符号,会动态拼接String valueString = (String) value;String[] valueSplits = valueString.split(delimiter);Pair<String, List<String>> arrayTypePair = dealQueryTypeLogic(Arrays.asList(valueSplits), stringBuffer);//自定义加入其它字段fieldvalue = arrayTypePair.getLeft();enumResponseList = arrayTypePair.getRight();} else {//其它情况就单个处理fieldvalue = dealSimpleObject(value);}//是否覆盖原值if (!overrideWriter) {//不覆盖jsonGenerator.writeObject(value);if (comomType && showListType) {//是否是改造类型并且需要用集合表示jsonGenerator.writeObjectField(dynamicFieldName, enumResponseList);} else {//普通类型用字符串表示jsonGenerator.writeStringField(dynamicFieldName, fieldvalue);}} else {//覆盖if (comomType && showListType) {//是否是改造类型并且需要用集合表示jsonGenerator.writeObject(enumResponseList);} else {//普通类型用字符串表示,没有值就塞进原字段jsonGenerator.writeObject(fieldvalue);}}} catch (Exception e) {log.error("通过枚举转换错误,错误原因【{}】!", e);}}/*** 集合处理逻辑* @param value* @param stringBuffer* @return*/private Pair<String, List<String>> dealQueryTypeLogic(List<?> value, StringBuffer stringBuffer) {switch (queryType) {//默认值1,表示获取自定义枚举或者调用主数据case 1: return solveListType(value, stringBuffer);//注解上面的queryType如果定义了2,就可以去做sao操作了case 2: return solveOwnMeaningListType(value, stringBuffer);default: return Pair.of("", Lists.newArrayList());}}/*** 单个处理逻辑* @param value* @return*/private String dealSimpleObject(Object value) {switch (queryType) {//默认值1,表示获取自定义枚举或者调用主数据case 1: return dealComplexObject(value);//注解上面的queryType如果定义了2,就可以去做sao操作了case 2: return dealOwnMeaningObject(value);default: return "";}}/*** 处理集合类型* @param value* @param stringBuffer* @return*/private Pair<String,List<String>> solveListType(List<?> value, StringBuffer stringBuffer) {List<?> list = value;List<String> publicSerializeEnumResponseList = new ArrayList<>(value.size());if (!CollectionUtils.isEmpty(value)) {for (int index = 0; index < list.size(); index++) {Object target = list.get(index);if (Objects.nonNull(target) && elemClassList.contains(target.getClass())) {String fieldObjectvalue = "";//枚举或者主数据fieldObjectvalue = dealComplexObject(target);if (StringUtils.hasText(fieldObjectvalue)) {stringBuffer.append(fieldObjectvalue);publicSerializeEnumResponseList.add(fieldObjectvalue);if (index != (list.size() - 1)) {if (StringUtils.hasText(delimiter)) {stringBuffer.append(delimiter);} else {stringBuffer.append(SPLITMARK);}}}}}}if (!StringUtils.hasText(stringBuffer.toString())) {log.info(dynamicFieldName + "找不到枚举值");}return Pair.of(stringBuffer.toString(),publicSerializeEnumResponseList);}/*** 处理sao操作的集合类型* @param value* @param stringBuffer* @return*/private Pair<String,List<String>> solveOwnMeaningListType(List<?> value, StringBuffer stringBuffer) {List<?> list = value;List<String> publicSerializeEnumResponseList = new ArrayList<>(value.size());if (!CollectionUtils.isEmpty(value)) {for (int index = 0; index < list.size(); index++) {Object target = list.get(index);if (Objects.nonNull(target)) {String fieldObjectvalue = "";//自定义的sao操作fieldObjectvalue = dealOwnMeaningObject(target);if (StringUtils.hasText(fieldObjectvalue)) {stringBuffer.append(fieldObjectvalue);if (index != (list.size() - 1)) {stringBuffer.append(SPLITMARK);}publicSerializeEnumResponseList.add(fieldObjectvalue);}}}}if (!StringUtils.hasText(stringBuffer.toString())) {log.info(dynamicFieldName + "找不到枚举值");}return Pair.of(stringBuffer.toString(),publicSerializeEnumResponseList);}/*** 获取数组,集合,字符串拼接的枚举value* @param target* @return*/private String dealComplexObject(Object target) {String fieldvalue = "";if (StringUtils.hasText(key)) {//如果key有值就默认去查主数据DictionarySerializeHandler translateDefaultHandler = SpringUtils.getBean(dataTranslateBean);fieldvalue = translateDefaultHandler.translate(target, key);} else {//查自定义的枚举try {fieldvalue = getDesc(this.valueOf(target));} catch (Exception e) {log.error("增加的字段名称{}获取枚举value失败,失败原因{}", dynamicFieldName, e);}}return fieldvalue;}/*** sao操作获取数组,集合,字符串拼接的枚举value* @param target* @return*/private String dealOwnMeaningObject(Object target) {String fieldvalue = "";//查自定义的枚举try {DictionarySerializeHandler dataTranslateHandler = SpringUtils.getBean(dataTranslateBean);fieldvalue = dataTranslateHandler.translate(target, key);} catch (Exception e) {log.error("增加的字段名称{}获取枚举value失败,失败原因{}", dynamicFieldName, e);}return fieldvalue;}private E valueOf(Object valueparam) {E[] es = this.processorEnumClass.getEnumConstants();return Arrays.stream(es).filter((e) -> equalsValue(valueparam, getCode(e))).findAny().orElse(null);}private boolean equalsValue(Object sourceValue, Object targetValue) {String sValue = String.valueOf(sourceValue).trim();String tValue = String.valueOf(targetValue).trim();if (sValue.equals(tValue)) {return true;}return false;}/*** 系统历史规范问题,导致要兼容多个* @param object* @return*/private Object getCode(Object object) {try {if (Objects.isNull(object)) {return null;}// 优先尝试获取 "getCode" 方法Method getCodeMethod = this.processorEnumClass.getMethod("getCode");Object code = getCodeMethod.invoke(object);return String.valueOf(code);} catch (NoSuchMethodException e) {try {Method getCodeMethod = this.processorEnumClass.getMethod("getValue");Object code = getCodeMethod.invoke(object);return String.valueOf(code);} catch (Exception ex) {log.error("getValue方法转换失败", e);return null;}} catch (Exception e) {log.error("getCode或者getValue方法转换失败", e);return null;}}/*** 兼容种枚举类型,规范书写枚不需要多个实现* @param object* @return*/private String getDesc(Object object) {try {try {if (Objects.isNull(object)) {return null;}//优先getInfo方法找Method getInfoMethod = this.processorEnumClass.getMethod("getInfo");return String.valueOf(getInfoMethod.invoke(object));} catch (NoSuchMethodException e) {try {//尝试获取 "getName" 方法Method getNameMethod = this.processorEnumClass.getMethod("getName");return String.valueOf(getNameMethod.invoke(object));} catch (NoSuchMethodException ex) {Method getCodeMethod = this.processorEnumClass.getMethod("getDescription");Object code = getCodeMethod.invoke(object);return String.valueOf(code);}}} catch (Exception allException) {log.error("getDesc方法转换失败", allException);return null;}}}
默认实现
@Component(value = "DictionarySerializeDefaultHandler")
public class DictionarySerializeDefaultHandler implements DictionarySerializeHandler {@Overridepublic String translate(Object idValue,String key) {//调用主数据或者数据字典拿到对应的枚举值,这里待实现,要有redis缓存相关的接口才行,其它不能用,会很慢return "";}
}