# plugin-transform **Repository Path**: myqf_site/plugin-transform ## Basic Information - **Project Name**: plugin-transform - **Description**: 自动翻译VO层对象里的的属性,让属性自动赋上对应中文值。 - **Primary Language**: Java - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 1 - **Forks**: 0 - **Created**: 2022-10-20 - **Last Updated**: 2024-09-26 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README > 源代码:[https://gitee.com/myqf_site/plugin-transform.git](https://gitee.com/myqf_site/plugin-transform.git) ## 产生的背景 前后端联调时,后端将会返回VO对象到页面展示,在这个过程中后端经常需要经历对象内的一些属性做手动挡的显式转换,这些操作可能会涉及到跨服务调用(如果说是微服务项目需要通过Dubbo或者FeignClient调用其它服务)、联表关联查询等问题,这会将开发流程复杂化且基础代码写的烦不想写,我们应该期望方式是将这些属性的转换交给框架自动处理,即自动挡的隐式转换。 ## 能处理的业务行为 1. 所有实现指定接口的枚举自动翻译: 例如:店铺类型枚举 shopTpeEnum(1:成本型;2:服务型;3:专职型),原来的处理方式是需要手动一个个去设置枚举对应的中文值:xxVO.setShopTypeName(ShopTypeEnum.name())。 2. 翻译静态数据字典表的字典项: eg:sex(0:女;1:男)
静态数据字典表里存储的往往都是公用的枚举类型,一般都集中在公用服务Service里,当其它服务需要翻译 它时往往需要跨服务调用它的service。 3. 某个ID需要翻译成ID对应的名字: eg:店铺shop表里关联了一个业务组group_id,表示这个店铺属于这个业务组,那我们在前端页面显示的时候,如果需要将业务组groupId转换成业务组名展示,那我需要再查一次业务组表或者sql里联表查出来。 针对上面的问题有一种可替代方案是让前端自己翻译匹配对应数据,如字典项,枚举,但这种方式还是尽量不要去选择,前端无法即时感知需要翻译对象的变化,应该让前端尽量脱离业务做单纯的展示。 ## 对它的定义 Translator将是一款功能全面的数据翻译插件,只需要几个简单的注解配置,就可以实现各种姿势的数据翻译,彻底抛弃联表查询与硬编码翻译逻辑。 ## 业务流程 举个栗子,定义一个ShopVO对象,他的属性如下: ```java @Data @Accessors(chain = true) public class ShopVO { /** * 店铺的自增主健 */ private Long id; /** * 店铺名 */ private String name; /** * 店铺所属业务组ID */ private Long groupId; /** * 店铺所属业务组名 */ @Transform(from = "groupId", transformer = GroupTransformer.class) private String groupName; } ``` 它除了有自己的ID与名称外,还有一个业务组ID与业务组名,我们现在需要实现:在返回这个对象时,将业务组名也一起返回。以下是翻译它的时序图: ![TranslatorAspect_transform.svg](doc/TranslatorAspect_transform.svg)
概要流程: 1. 定义一个切面 Aspect,重写 @AfterReturning 方法,当目标方法执行后,会去调用该方法实现 2. 请求执行完成,将要返回 3. 进入切面 4. 找到被 **Transform **注解标志的Controlelr层方法 5. 从方法里获取该方法的响应对象 6. 根据响应对象类型向工厂中拿对应的处理类型类(TransformTypeConvert)进一步获取后端返回的响应体里的数据 7. 通过反射获取被 **@Transform **注解的字段以及注解里面的属性值 8. 获取注解里的属性值之一:**transformer**。这个属性值是你要告诉好框架该向哪里去拿目标值 9. 拿到目标值之后,通过反射把值设置到目标属性 10. 切面返回,请求返回前端 ## 核心代码设计 1)定义注解:**Transform,**将该注解放在需要翻译的对象的字段上,填入必填的几个属性: ```java @Retention(RetentionPolicy.RUNTIME) @Documented @Target({ElementType.FIELD, ElementType.METHOD}) public @interface Transform { /** * 翻译的字段 */ Class transformer() default Transformer.class; /** * 数据源,即你要告诉注解它要取得信息在哪里 */ Class dataSource() default Void.class; /** * 默认是当前字段或”字段+Code“或“字段+Id” */ String from() default ""; /** * 参数 */ String param() default ""; } ```
2)定义顶层转换接口:**Transformer****,**它是作为所有转换能力实现的顶层抽象,比如,你需要翻译某个枚举类型、翻译某个ID成Name,那具备着两种能力的类需要实现这个接口。 ```java public interface Transformer { /** * 翻译 * * @param original 原始值 * @param datasource 翻译数据源 * @param param 翻译后取值参数名字 * @return 翻译后的值 */ String transform(T original, Class datasource, String param); } ``` 例如,具备翻译枚举的能力类:**EnumTransformer:** ```java @Component public class EnumTransformer implements Transformer { @Override @SuppressWarnings("unchecked") public String transform(Object original, Class datasource, String param) { Class> iDict = (Class>) datasource; return Stream.of(iDict.getEnumConstants()).filter(i -> i.getCode().equals(original)).map(IDict::getText).findFirst().orElse(null); } } // 注意这里面有个自己定义的 IDict 枚举接口,所有枚举都需要实现它 具体实现将放在代码里 ``` 3)定义转换工厂:**TransformConvertFactory,**它主要的作用是生产与存储具备不同转换能力的转换类。 ```java @Component public class TransformConvertFactory { /** * 用来存储生成的转换类 */ private final Map, TransformTypeConvert> map = new ConcurrentHashMap<>(); /** * 将生成的转换功能函数缓存起来 * * @param transformTypeConvert * @param 字节码泛型 */ public void addConvert(TransformTypeConvert transformTypeConvert) { Type genericInterface = transformTypeConvert.getClass().getGenericInterfaces()[0]; ParameterizedType parameterizedType = (ParameterizedType) genericInterface; ParameterizedType actualTypeArgument = (ParameterizedType) parameterizedType.getActualTypeArguments()[0]; map.put((Class) actualTypeArgument.getRawType(), transformTypeConvert); } /** * 从缓存里获取生成的功能函数 * * @param convertClass 转换类的字节码标识 * @param 字节码泛型 * @return TransformTypeConvert */ public TransformTypeConvert getTranslateTypeConvert(Class convertClass) { return map.get(convertClass); } } ``` 4)定义切面:TranslatorAspect,切面内要做的事就是处理需要转换的属性 ```java /** * 翻译切面 * * @author landon_shi * @date 2022/9/27 */ @Aspect @Component @Order(2) @Slf4j public class TranslatorAspect { @Resource private TransformConvertFactory transformConvertFactory; /** * 目标方法返回之后执行的后置通知 * * @param object object */ @SuppressWarnings("unchecked") @AfterReturning(pointcut = "@annotation(com.example.plugin.anonation.Transform)", returning = "object") public void doAfter(Object object) { // 从转换工厂里获取对应转换器 TransformTypeConvert transformTypeConvert = transformConvertFactory.getTranslateTypeConvert((Class) object.getClass()); Object result; if (transformTypeConvert != null) { result = transformTypeConvert.convert(object); } else { result = object; } // 一般情况下,我们项目中经常会返回一个List集合或者Page类型给前端 if (result instanceof Collection || result instanceof IPage) { // 如果是List类型直接循环处理,如果是Page类型,那我们需要把里面的List拿出来再循环处理 Collection collection = result instanceof IPage ? ((IPage) result).getRecords() : (Collection) result; if (collection.isEmpty()) { return; } collection.forEach((Consumer) this::translateObject); log.info("返回列表结果 {}", collection); } else { this.translateObject(object); log.info("返回对象结果 {}", object); } } /** * 转换对象 * * @param result 源对象 */ private void translateObject(Object result) { if (Objects.isNull(result)) { return; } // 通过反射获取所有的属性 Field[] fields = ReflectUtil.getFields(result.getClass()); // 翻译当前对象字段 Arrays.stream(fields) // 获取所有需要翻译的属性 .filter(field -> field.getAnnotation(Transform.class) != null) // 再循环里处理 .forEach(field -> this.transform(result, field)); // 翻译内部属性字段 Arrays.stream(fields).filter(field -> field.getAnnotation(Transform.class) != null).forEach(field -> { try { Object subObject = ReflectUtil.invokeGet(result, field); if (subObject instanceof Collection) { ((Collection) subObject).forEach(this::translateObject); } else { this.translateObject(subObject); } } catch (Exception e) { log.error("翻译错误", e); } }); } /** * 核心处理 对象转换 * * @param vo 源VO对象 * @param field 被注解了的属性 */ @SuppressWarnings("unchecked") private void transform(Object vo, Field field) { try { // 获取目标属性上的注解信息 Transform annotation = field.getAnnotation(Transform.class); // 源字段的值(我们要根据这个字段的值作为一个条件去向目标数据源获取结果值)) Object fieldValue = ReflectUtil.invokeGet(vo, annotation.from()); if (Objects.isNull(fieldValue)) { return; } // 获取目标数据源 Class aClass = annotation.dataSource(); String translateValue; // 我们需要按需定义好几种需要转换的类型:例如 枚举、静态字典 if (IDict.class.isAssignableFrom(aClass)) { // EnumTransformer 是需要我们自己定义的枚举转换类 translateValue = new CommonEnumTransformer().transform(fieldValue, aClass, null); } else if (LocalCache.class.isAssignableFrom(aClass)) { // LocalCache 是从本地缓存里获取,本地缓存里的值在项目启动的时候就会被加载好(缓存预热) CommonLocalCacheTransformer commonLocalCacheTransformer = SpringContextUtil.getBean(CommonLocalCacheTransformer.class); translateValue = commonLocalCacheTransformer.transform(fieldValue, annotation.dataSource(), annotation.param()); } else { Transformer transformer = SpringContextUtil.getBean(annotation.transformer()); translateValue = transformer.transform(fieldValue, annotation.dataSource(), annotation.param()); } if (StringUtils.isEmpty(translateValue)) { log.error("translate为空"); return; } // 将翻译后的值设置进去 ReflectUtil.invokeSet(vo, field, translateValue); } catch (Exception e) { log.error("翻译错误", e); } } } ``` ## 性能利器:本地缓存 现在,我们已经可以简单的实现对VO对象里的字段翻译,但是在实际开发中,我们经常会遇到Controller层返回的是一个 Page 或者 List 类型,对于这两种类型,我们定义的切面实现逻辑里不可能每次都要跨服务向数据源循环拿目标数据。因此,我们需要引入一个机制,使其能够减少跨服务调用的频率。 依据互联网的万金油做法,我们可以很轻松的想到这种机制的实现就是使用缓存,但缓存的类型有很多种,那么该使用什么类型缓存呢?是基于Redis实现的分布式缓存,还是基本JVM内存实现的本地缓存? 考虑到使用分布式缓存会增加一次网络IO与序列化与反序列化,所以本业务实现中使用本地缓存最佳。 我们想要实现的效果是:当项目启动完成之后,调用一个被注解注解了的接口,即可实现字段翻译,因此,我们必须要在接口调用之前实现数据的预热,可称为缓存预热。 Spring中提供的一个现成的接口:SmartLifecycle。当Spring的所有的Bean初始化加载完成之后,Spring将会调用所有实现了该拓展接口的子类。 ```java public interface LocalCache extends SmartLifecycle { /** * 初始化缓存 */ void initCache(); /** * 当Spring中所有的Bean初始化完成之后会掉用所有实现了 SmartLifecycle 接口子类的 start 方法 */ @Override default void start() { initCache(); } /** * 停止 */ @Override default void stop() { } /** * 是都在运行 * * @return boolean */ @Override default boolean isRunning() { return false; } } ```
接下来,我们需要定义一个缓存的模版,该模版主要的作用是对所有缓存的共同行为进行抽象: ```java /** * 本地缓存操作模版 * SchedulingConfigurer接口为Spring提供的定时任务接口 * * @author landon_shi * @date 2022/9/27 */ @EnableScheduling @Slf4j public abstract class AbstractLocalCacheTemplate implements LocalCache, SchedulingConfigurer { /** * 目标数据缓存 */ protected final Map cache = new ConcurrentHashMap<>(40000); /** * 更新时间 (用来判断当前缓存是不是需要更新)) */ private Long lastUpdateTime; /** * 初始化缓存 */ @Override public void initCache() { // 取当前系统时间作为标志用户区分那些数据是需要新增的数据 lastUpdateTime = System.currentTimeMillis(); boolean result = this.allCacheData(); if (!result) { lastUpdateTime = 0L; } } @Override public void configureTasks(@Nullable ScheduledTaskRegistrar scheduledTaskRegistrar) { log.info("初始化cache任务..."); if (scheduledTaskRegistrar == null) { return; } // 添加要在给定的固定延迟内触发的定时任务 scheduledTaskRegistrar.addFixedDelayTask(this::incrementCacheData, incrementCacheCron()); // 添加根据给定触发器触发的定时任务 scheduledTaskRegistrar.addTriggerTask(this::allCacheData, allCacheCron()); } /** * 增量数据缓存 */ private void incrementCacheData() { Long lastUpdate = lastUpdateTime; lastUpdateTime = System.currentTimeMillis(); // 调用子类实现的缓存增量数据 this.incrementCacheData(lastUpdate); } /** * 拉取增量数据 * * @param lastUpdateTime lastUpdateTime */ protected abstract void incrementCacheData(Long lastUpdateTime); /** * 增量数据的执行延迟间隔时间 * * @return long 毫秒单位 */ protected abstract long incrementCacheCron(); /** * 全量数据加载的触发器,eg:0 0/1 * * * ?" 一分钟拉一次 * * @return CronTrigger */ protected abstract CronTrigger allCacheCron(); /** * 全量数据 (这个方法里需要自己去实现当容器加载完成之后那些数据需要缓存下来,可以理解为缓存预热) * * @return 缓存结果 true成功/false失败 */ protected abstract boolean allCacheData(); /** * 制定cache key * * @return Function */ protected abstract Function cacheKey(); /** * 如果cache没有命中的处理方法 * * @param cacheId cacheId * @return V */ protected abstract V ifNull(K cacheId); /** * 通过缓存的key获取缓存 * * @param cacheId 缓存key * @return V */ public V getById(K cacheId) { return getCache(cacheId); } @Nullable public V getCache(K cacheId) { if (cacheId == null) { log.warn("UserCache查询用户缓存时传入的userId为null,请确认是否传参正确"); return null; } return Optional.ofNullable(cache.get(cacheId)).orElse(ifNull(cacheId)); } /** * 判断缓存的key是否存在 * * @param cacheId cacheId * @param consumer consumer */ public void ifPresent(K cacheId, Consumer consumer) { Optional.ofNullable(getCache(cacheId)).ifPresent(consumer); } public T getCacheProperty(K userId, Function mapper) { return Optional.ofNullable(getCache(userId)).map(mapper).orElse(null); } public void putCache(K k, V v) { cache.put(k, v); } public void pushCache(List cacheData) { lastUpdateTime = System.currentTimeMillis(); cacheData.forEach(v -> { Optional k = Optional.ofNullable(v).map(cacheKey()); k.ifPresent(k1 -> putCache(k1, v)); }); } public List findIdsBy(Predicate predicate) { return cache.values().stream().filter(predicate).map(cacheKey()).collect(Collectors.toList()); } public Map findMapBy(Predicate predicate) { return cache.values().stream().filter(predicate).collect(Collectors.toMap(cacheKey(), Function.identity())); } public List findBy(Predicate predicate) { return cache.values().stream().filter(predicate).collect(Collectors.toList()); } } ``` 至此,翻译插件的核心逻辑已经完成了,更多的内容可在代码里探索。
欢迎各位大佬指正或提交分支扩展。