【Java】Springboot + Redis +(AOP & 响应外切)切面实现字典翻译
使用案例演示:
先开发了一个简单的Demo:
普通DTO类注解翻译的字段和翻译来源
在需要翻译的方法上注解@Translate
接口返回结果:
框架思路:
1、标记的注解需要通过AOP切面在调用的时候处理翻译
2、翻译的来源是Redis的缓存,需要有数据来源,应用启动之后就需要初始化
一、配置Redis
pom.xml的相关依赖:
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-data-redis --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> <version>${spring.boot.version}</version> </dependency> <!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-aop --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> <version>${spring.boot.version}</version> </dependency> <!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> <version>${spring.boot.version}</version> </dependency> <!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-web --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <version>${spring.boot.version}</version> </dependency> <!-- https://mvnrepository.com/artifact/redis.clients/jedis --> <dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> <version>4.3.1</version> </dependency> <!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.30</version> </dependency> <!-- https://mvnrepository.com/artifact/com.baomidou/mybatis-plus-boot-starter --> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.2</version> </dependency> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.8.4</version> </dependency> <properties> <maven.compiler.source>8</maven.compiler.source> <maven.compiler.target>8</maven.compiler.target> <spring.boot.version>2.3.10.RELEASE</spring.boot.version> <durid.version>1.2.14</durid.version> </properties>
Redis的yml配置:
spring: redis: host: 192.168.124.8 database: 0 timeout: 3000 password: 123456 jedis: pool: max-active: 29 max-wait: -1 max-idle: 10 min-idle: 0
RedisTemplate配置类:
package cn.cloud9.server.struct.redis; import cn.hutool.core.date.DateUtil; import com.alibaba.fastjson.parser.Feature; import com.alibaba.fastjson.serializer.SerializeConfig; import com.alibaba.fastjson.serializer.SerializeWriter; import com.alibaba.fastjson.serializer.SerializerFeature; import com.alibaba.fastjson.support.config.FastJsonConfig; import com.alibaba.fastjson.support.spring.FastJsonRedisSerializer; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Lazy; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.serializer.GenericToStringSerializer; import org.springframework.data.redis.serializer.RedisSerializer; import org.springframework.data.redis.serializer.StringRedisSerializer; import javax.annotation.Resource; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; import java.time.format.DateTimeFormatter; import java.util.Date; /** * @author OnCloud9 * @description * @project tt-server * @date 2022年11月10日 下午 10:20 */ @Configuration public class RedisConfiguration { /** * 改用fastjson redis序列化,请删除redis数据后使用此序列化 * @param redisConnectionFactory * @return */ @Bean public RedisTemplate<String, ?> redisTemplate(@Lazy RedisConnectionFactory redisConnectionFactory) { RedisTemplate<String, ?> redisTemplate = new RedisTemplate<>(); redisTemplate.setConnectionFactory(redisConnectionFactory); GenericToStringSerializer<String> stringRedisSerializer = new GenericToStringSerializer<>(String.class); redisTemplate.setKeySerializer(stringRedisSerializer); redisTemplate.setHashKeySerializer(stringRedisSerializer); FastJsonRedisSerializer<?> fastJsonRedisSerializer = new FastJsonRedisSerializer<>(Object.class); FastJsonConfig fastJsonConfig = fastJsonRedisSerializer.getFastJsonConfig(); SerializeConfig serializeConfig = fastJsonConfig.getSerializeConfig(); /* 加入的LocalDateTime序列化,也可以不加(但是要用@JSONField(format = "yyyy-MM-dd HH:mm:ss"))格式化 */ serializeConfig.put(LocalDateTime.class, (serializer, object, fieldName, fieldType, features) -> { SerializeWriter out = serializer.out; if (object == null) { out.writeNull(); return; } out.writeString(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").format((LocalDateTime) object)); }); serializeConfig.put(LocalDate.class, (serializer, object, fieldName, fieldType, features) -> { SerializeWriter out = serializer.out; if (object == null) { out.writeNull(); return; } out.writeString(DateTimeFormatter.ofPattern("yyyy-MM-dd").format((LocalDate) object)); }); serializeConfig.put(LocalTime.class, (serializer, object, fieldName, fieldType, features) -> { SerializeWriter out = serializer.out; if (object == null) { out.writeNull(); return; } out.writeString(DateTimeFormatter.ofPattern("HH:mm:ss").format((LocalTime) object)); }); serializeConfig.put(Date.class, (serializer, object, fieldName, fieldType, features) -> { SerializeWriter out = serializer.out; if (object == null) { out.writeNull(); return; } out.write("\"" + DateUtil.format(((Date)object),"yyyy-MM-dd HH:mm:ss") + "\""); }); fastJsonConfig.setSerializeConfig(serializeConfig); fastJsonConfig.setFeatures(Feature.SupportAutoType); fastJsonConfig.setSerializerFeatures(SerializerFeature.WriteClassName); redisTemplate.setValueSerializer(fastJsonRedisSerializer); redisTemplate.setHashValueSerializer(fastJsonRedisSerializer); redisTemplate.afterPropertiesSet(); return redisTemplate; } }
二、数据源来源获取
数据源配置:
spring: datasource: url: jdbc:mysql://192.168.124.8:3308/tt?serverTimeZone=Asia/Shanghai driver-class-name: com.mysql.cj.jdbc.Driver username: root password: 123456
创建字典表的DTO,Mapper
package cn.cloud9.server.struct.dict.dto; import com.alibaba.fastjson.annotation.JSONField; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; import java.time.LocalDateTime; /** * @author OnCloud9 * @description * @project tt-server * @date 2022年11月13日 下午 09:31 */ @Data @TableName("system_dict") public class DictDTO { @TableId(value = "DICT_ID", type = IdType.AUTO) private Integer dictId; @TableField("DICT_CODE") private Integer dictCode; @TableField("DICT_TYPE") private String dictType; @TableField("DICT_ALIAS") private String dictAlias; @TableField("DICT_NAME") private String dictName; @TableField("DICT_TYPE_NAME") private String dictTypeName; @TableField("DICT_TYPE_ALIAS") private String dictTypeAlias; @TableField("DICT_PARENT_ID") private String dictParentId; @JSONField(format = "yyyy-MM-dd HH:mm:ss") @TableField("GEN_TIME") private LocalDateTime genTime; }
Mapper配置一个自定义查询SQL的方法:
package cn.cloud9.server.struct.dict.mapper; import cn.cloud9.server.struct.dict.dto.DictDTO; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Select; import java.util.List; public interface DictMapper extends BaseMapper<DictDTO> { @Select("${SQL}") List<DictDTO> queryUsingCustomSql(@Param("SQL") String sql); }
一般来说是加载字典表放入缓存中,但是还有类似行政区域表,也是需要缓存放入的
字典表:
SELECT * FROM `system_dict`
非字典,但是也可以按照字典表结构存储的表:
SELECT `NAME` AS `DICT_CODE`, 'AB_WORD' AS `DICT_TYPE`, `MEANING` AS `DICT_NAME` FROM abridge_word
只要字段适配,同样可以按照字典装载
我们可以有若干个需要装载的表,那需要写在配置文件中解除硬编码控制:
package cn.cloud9.server.struct.dict.cache; import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Configuration; import java.util.Map; /** * 缓存配置读取类,用于读取需要Redis装载的缓存 * @author OnCloud9 * @description * @project tt-server * @date 2022年11月13日 下午 10:10 */ @Data @Configuration @ConfigurationProperties(prefix = "cache") public class CacheProperty { private Map<String, String> sqlMap; }
则yml的配置声明如下:
cache: sql-map: default: SELECT * FROM `system_dict` # ab-word: SELECT `NAME` AS `DICT_CODE`, 'AB_WORD' AS `DICT_TYPE`, `MEANING` AS `DICT_NAME` FROM abridge_word
在需要加载的时候可以遍历配置Bean的Map,依次SQL查询需要装载的数据
// 1、注入配置Bean 和 mapper @Resource private CacheProperty cacheProperty; @Resource private DictMapper dictMapper; // 2、方法中获取map交给mapper执行 /* 读取配置文件的缓存SQL */ final Map<String, String> sqlMap = cacheProperty.getSqlMap(); for (String sqlKey : sqlMap.keySet()) { final String sql = sqlMap.get(sqlKey); final List<DictDTO> dictList = baseMapper.queryUsingCustomSql(sql) }
三、缓存抽象与实现
缓存的功能抽象成接口,最主要的三个功能:
1、初始化
2、按字典编码获取翻译名称
3、按字典类别获取这个类别的集合
package cn.cloud9.server.struct.dict.cache; import cn.cloud9.server.struct.dict.dto.DictDTO; import java.util.List; /** * 缓存服务接口 * */ public interface CacheService { void initializeCacheDataToRedis(); String findNameFromRedis(String dictCode); List<DictDTO> findListFromRedis(String dictCate); }
存入的Redis的结构是采用Hash,即 Key + Hkey + Hvalue
Hvalue又分成了两种类型,String 和 List<DictDTO>
第一种,获取翻译名称的时候,存入需要定义一个根Key, 和组合的Hkey
规则是这样: 根Key写死在Bean中不变,组合的Hkey = 表名(配置SQL的Key键) + 分隔符 + 字典类别 + 分割符 + 字典编号,Hvalue 是字典名称
第二种,需要获取某一个类别的集合,用于下拉列表,或者在前台翻译
规则是这样: 根Key写死在Bean中不变,组合的Hkey = 表名(配置SQL的Key键) + 分隔符 + 字典类别, Hvalue 是这个类别的集合
了解上述规则后,这个缓存服务接口,交给DictService来实现:
package cn.cloud9.server.struct.dict.service; import cn.cloud9.server.struct.dict.cache.CacheProperty; import cn.cloud9.server.struct.dict.cache.CacheService; import cn.cloud9.server.struct.dict.dto.DictDTO; import cn.cloud9.server.struct.dict.mapper.DictMapper; import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import lombok.extern.slf4j.Slf4j; import org.apache.commons.collections.CollectionUtils; import org.springframework.data.redis.core.HashOperations; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Service; import javax.annotation.Resource; import java.util.*; import java.util.concurrent.ConcurrentHashMap; /** * * 字典服务 * @author OnCloud9 * @description * @project tt-server * @date 2022年11月13日 下午 09:21 */ @Slf4j @Service public class DictService extends ServiceImpl<DictMapper, DictDTO> implements CacheService { public static final String KEY_LISTS = "REDIS-LISTS-CACHE"; public static final String KEY_MAP = "REDIS-MAPS-CACHE"; public static final String SEPARATOR = "@"; @Resource private CacheProperty cacheProperty; @Resource private StringRedisTemplate stringTemplate; @Resource private RedisTemplate<String, Map<String, String>> mapTemplate; /** * 缓存初始化处理 */ @Override public void initializeCacheDataToRedis() { final HashOperations<String, Object, Object> hashOps = mapTemplate.opsForHash(); /* 清空缓存 */ stringTemplate.delete(KEY_MAP); stringTemplate.delete(KEY_LISTS); /* 读取配置文件的缓存SQL */ final Map<String, String> sqlMap = cacheProperty.getSqlMap(); /* 准备缓存结构容器, 并装载数据 */ Map<String, String> mapTank = new ConcurrentHashMap<>(); Map<String, List<DictDTO>> listTank = new ConcurrentHashMap<>(); for (String sqlKey : sqlMap.keySet()) { final String sql = sqlMap.get(sqlKey); final List<DictDTO> dictList = baseMapper.queryUsingCustomSql(sql); for (DictDTO dict : dictList) { final Integer dictCode = dict.getDictCode(); final String dictName = dict.getDictName(); final String dictType = dict.getDictType(); /* 装载 key -> h-key -> h-value */ final String mapKey = sqlKey + SEPARATOR + dictType + SEPARATOR + dictCode; mapTank.put(mapKey, dictName); /* 装载 key -> h-key -> h-list */ final String listKey = sqlKey + SEPARATOR + dictType; List<DictDTO> cateList = listTank.get(listKey); if (CollectionUtils.isEmpty(cateList)) { cateList = new ArrayList<>(); listTank.put(listKey, cateList); } cateList.add(dict); } } /* 装填到Redis中 */ hashOps.putAll(KEY_MAP, mapTank); hashOps.putAll(KEY_LISTS, listTank); log.info("Redis 缓存装载完毕 ...... "); } /** * * @param dictCode 格式:sqlKey@字典类别@字典编码 * @return 字典名称 找不到为null */ @Override public String findNameFromRedis(String dictCode) { final HashOperations<String, Object, Object> hashOps = mapTemplate.opsForHash(); final Object o = hashOps.get(KEY_MAP, dictCode); final boolean isEmpty = Objects.isNull(o); return !isEmpty ? (String) o : ""; } /** * * @param dictCate 格式:sqlKey@字典类别 * @return 字典类别集合 找不到为空集合 */ @SuppressWarnings("unchecked") @Override public List<DictDTO> findListFromRedis(String dictCate) { final HashOperations<String, Object, Object> hashOps = mapTemplate.opsForHash(); final Object o = hashOps.get(KEY_LISTS, dictCate); final boolean isEmpty = Objects.isNull(o); return !isEmpty ? (List<DictDTO>) o : Collections.EMPTY_LIST; } }
四、解决初始化加载的问题
初始化的实现有了,Bean也有了,那怎么才能让应用一启动的时候就开始执行呢?
而且执行一次通常是类静态资源调用的做法,于是就用到了
import org.springframework.context.ApplicationContextAware;
所有Bean装入Spring完毕后会执行Aware,通过Aware可以获取容器上下文对象
通过上下文对象,根据类型和Bean名称,可以静态的获取对应的Bean,
有了DictServiceBean之后,就可以调用初始化了
package cn.cloud9.server.struct.spring; import org.jetbrains.annotations.NotNull; import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; /** * Spring上下文持有器类,用于静态方式获取Bean实例 * @author OnCloud9 * @description * @project tt-server * @date 2022年11月13日 下午 11:04 */ @Service @Lazy(value = false) public class SpringContextHolder implements ApplicationContextAware { /** * spring上下文 */ private static ApplicationContext applicationContext; @Override public void setApplicationContext(@NotNull ApplicationContext applicationContext) throws BeansException { SpringContextHolder.applicationContext = applicationContext; } public static ApplicationContext getApplicationContext() { return applicationContext; } /** * 获取bean * @param name bean名称 * @param <T> * @return */ public static <T> T getBean(String name){ return (T) applicationContext.getBean(name); } /** * 获取bean * @param requiredType bean类型 * @param <T> * @return */ public static <T> T getBean(Class<T> requiredType){ return applicationContext.getBean(requiredType); } /** * 获取bean * @param name bean名称 * @param requiredType bean类型 * @param <T> * @return */ public static <T> T getBean(String name, Class<T> requiredType){ return applicationContext.getBean(name,requiredType); } }
五、管理缓存
初始化是可以复用的,涉及字典相关的数据一旦更新发生变化,Redis的缓存也需要同步
这里最简单的做法就是刷新处理,结合上面的Bean持有器类,可以这样实现:
package cn.cloud9.server.struct.dict.cache; import cn.cloud9.server.struct.spring.SpringContextHolder; /** * 缓存管理器类,用于刷新缓存 * @author OnCloud9 * @description * @project tt-server * @date 2022年11月13日 下午 11:08 */ public class CacheManager { public static void refreshCache() { final CacheService cacheService = SpringContextHolder.getBean("dictService", CacheService.class); new Thread(cacheService::initializeCacheDataToRedis).start(); } }
放在Boot主启动类完成后调用:
package cn.cloud9.server; import cn.cloud9.server.struct.dict.cache.CacheManager; import cn.cloud9.server.struct.validator.EnableFormValidator; import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; /** * @author OnCloud9 * @description * @project tt-server * @date 2022年11月06日 下午 04:18 */ @MapperScan(basePackages = "cn.cloud9.server.*") @SpringBootApplication public class MainApplication { public static void main(String[] args) { SpringApplication.run(MainApplication.class, args); CacheManager.refreshCache(); } }
重启运行看看能不能触发加载
查看Redis是否按照规则存入了字典:
六、设计注解
两个问题:在哪里翻译? 翻译什么?
对应两个注解:@Translate @DictFrom
这里@Translate注解 加了类对象声明,好像不需要,先无视把
声明在方法上标记,用来给AOP定位目标方法
package cn.cloud9.server.struct.dict.annotation; import java.lang.annotation.*; /** * 标记此注解时翻译PO对象 */ @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface Translate { /** * 翻译的DTO类 */ Class<?> dtoClass(); }
@DictFrom,用来标记翻译字段
package cn.cloud9.server.struct.dict.annotation; import java.lang.annotation.*; @Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface DictFrom { /* 翻译的来源表 */ String srcTable() default "default"; /* 翻译的指定类别 */ String srcCate(); /* 翻译的来源字段 */ String srcField(); /* 翻译的字段是否是多个的, 默认单个 */ boolean isMulti() default false; /* 如果是多个的,每个值的分隔符是? */ String separator() default ","; }
七、编写字典翻译切面:
因为是个Demo, 切面这里的作用就是判断类型,翻译单独交给反射工具类来完成了
暂时只考虑单个Bean, 集合接口和翻页对象三种,也没有考虑嵌套Bean的情况
package cn.cloud9.server.struct.dict.aspect; import cn.cloud9.server.struct.dict.annotation.Translate; import cn.cloud9.server.struct.dict.reflect.ReflectUtil; import cn.cloud9.server.test.model.DictAspectModel; import com.alibaba.fastjson.JSON; import com.baomidou.mybatisplus.core.metadata.IPage; import lombok.extern.slf4j.Slf4j; import org.apache.commons.collections.CollectionUtils; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.Signature; import org.aspectj.lang.annotation.AfterReturning; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Pointcut; import org.aspectj.lang.reflect.MethodSignature; import org.springframework.stereotype.Component; import javax.annotation.Resource; import java.lang.reflect.Method; import java.util.Collection; import java.util.List; /** * @author OnCloud9 * @description * @project tt-server * @date 2022年11月12日 下午 10:02 */ @Slf4j @Aspect @Component public class DictAspect { @Resource private ReflectUtil reflectUtil; @Pointcut(value = "@annotation(translate)", argNames = "translate") public void doTranslate(Translate translate) { } @AfterReturning(pointcut = "doTranslate(translate)", returning = "result", argNames = "point,result,translate") public Object translation(final JoinPoint point, Object result, Translate translate) throws Throwable { Signature signature = point.getSignature(); MethodSignature methodSignature = (MethodSignature) signature; Method method = methodSignature.getMethod(); final Class<?> aClass = translate.dtoClass(); final boolean isCollection = result instanceof Collection; final boolean isPage = result instanceof IPage; final boolean isTargetClass = aClass.equals(result.getClass()); if (!isCollection && !isPage && !isTargetClass) return result; else if (isCollection) { List<Object> list = (List<Object>) result; if (CollectionUtils.isEmpty(list)) return result; for (Object row : list) reflectUtil.translateDTO(row, aClass); } else if (isPage) { IPage<Object> page = (IPage<Object>) result; if (CollectionUtils.isEmpty(page.getRecords())) return result; final List<Object> records = page.getRecords(); for (Object record : records) reflectUtil.translateDTO(record, aClass); } else if (isTargetClass) { reflectUtil.translateDTO(result, aClass); } return result; } }
八、反射工具类:
反射工具类为了优化反射操作,这里用了hutool的工具
package cn.cloud9.server.struct.dict.reflect; import cn.cloud9.server.struct.dict.annotation.DictFrom; import cn.cloud9.server.struct.dict.annotation.Translate; import cn.cloud9.server.struct.dict.service.DictService; import org.springframework.stereotype.Component; import javax.annotation.Resource; import java.lang.reflect.Field; import java.util.Objects; import cn.hutool.core.bean.BeanUtil; /** * @author OnCloud9 * @description * @project tt-server * @date 2022年11月13日 下午 08:28 */ @Component public class ReflectUtil { @Resource private DictService dictService; /** * * @param result * @param aClass */ public void translateDTO(Object result, Class<?> aClass) { /* 获取这个类下的所有字段 */ final Field[] declaredFields = aClass.getDeclaredFields(); for (Field field : declaredFields) { /* 获取类上的@Translate注解 */ final DictFrom dictFrom = field.getAnnotation(DictFrom.class); /* 如果没有此注解则跳过 */ if (Objects.isNull(dictFrom)) continue; /* 获取声明的字典来源信息 */ final String srcTable = dictFrom.srcTable(); final String srcCate = dictFrom.srcCate(); final String srcField = dictFrom.srcField(); final boolean isMulti = dictFrom.isMulti(); final String separator = dictFrom.separator(); /* 取出目标对象对应字段的值 */ final Object fieldValue = BeanUtil.getFieldValue(result, srcField); /* 如果没有值则跳过, 或者值类型不是字符串或者整形 */ if (Objects.isNull(fieldValue) ) continue; else if (!(fieldValue instanceof String) && !(fieldValue instanceof Integer)) continue; if (!isMulti) { /* 调用Redis资源开始翻译 */ final String key = srcTable + DictService.SEPARATOR + srcCate + DictService.SEPARATOR + String.valueOf(fieldValue); final String translateName = dictService.findNameFromRedis(key); /* 赋值翻译字段 */ BeanUtil.setFieldValue(result, field.getName(), translateName); } else { final String[] split = ((String)fieldValue).split(separator); final StringBuilder builder = new StringBuilder(); for (int i = 0; i < split.length; i++) { final String key = srcTable + DictService.SEPARATOR + srcCate + DictService.SEPARATOR + split[i].trim(); if (i == split.length - 1) { final String fromRedis = dictService.findNameFromRedis(key); builder.append(fromRedis); } else { final String fromRedis = dictService.findNameFromRedis(key); builder.append(fromRedis); builder.append(separator); } } /* 赋值翻译字段 (多个) */ BeanUtil.setFieldValue(result, field.getName(), builder.toString()); } } } }
九、补充嵌套Bean的情况:
因为有第一个翻译的方法逻辑,后面实现起来就很容易了
只需要在前面判断当前字段是不是集合或者翻译类型的,然后逐个遍历判断类型
当然这个判断没有那么严谨,一般开发的情况不会装载一般数据类型,如果确实碰到了,可以结合实际情况再添加判断补充处理
最后递归调用翻译方法就可以了
package cn.cloud9.server.struct.dict.reflect; import cn.cloud9.server.struct.dict.annotation.DictFrom; import cn.cloud9.server.struct.dict.annotation.Translate; import cn.cloud9.server.struct.dict.service.DictService; import com.baomidou.mybatisplus.core.metadata.IPage; import org.apache.commons.collections.CollectionUtils; import org.springframework.stereotype.Component; import javax.annotation.Resource; import java.lang.reflect.Field; import java.util.Collection; import java.util.List; import java.util.Objects; import java.util.Set; import cn.hutool.core.bean.BeanUtil; /** * @author OnCloud9 * @description * @project tt-server * @date 2022年11月13日 下午 08:28 */ @Component public class ReflectUtil { @Resource private DictService dictService; /** * 翻译DTO * @param result */ public void translateDTO(Object result) { /* 获取这个类下的所有字段 */ final Field[] declaredFields = result.getClass().getDeclaredFields(); for (Field field : declaredFields) { /* 处理嵌套在目标对象类中的集合类型翻译 */ final Object fieldValue = BeanUtil.getFieldValue(result, field.getName()); final boolean isCollection = fieldValue instanceof Collection; final boolean isPage = fieldValue instanceof IPage; if (isCollection) { Collection<Object> list = (Collection<Object>) fieldValue; if (CollectionUtils.isEmpty(list)) continue; for (Object row : list) { if (row.getClass().isPrimitive()) continue; this.translateDTO(row); } } else if (isPage) { IPage<Object> page = (IPage<Object>) fieldValue; if (CollectionUtils.isEmpty(page.getRecords())) continue; final List<Object> records = page.getRecords(); for (Object record : records) { if (record.getClass().isPrimitive()) continue; this.translateDTO(record); } } /* 获取类上的@Translate注解 */ final DictFrom dictFrom = field.getAnnotation(DictFrom.class); /* 如果没有此注解则跳过 */ if (Objects.isNull(dictFrom)) continue; /* 获取声明的字典来源信息 */ final String srcTable = dictFrom.srcTable(); final String srcCate = dictFrom.srcCate(); final String srcField = dictFrom.srcField(); final boolean isMulti = dictFrom.isMulti(); final String separator = dictFrom.separator(); /* 取出目标对象对应字段的值 */ final Object resultFieldValue = BeanUtil.getFieldValue(result, srcField); /* 如果没有值则跳过, 或者值类型不是字符串或者整形 */ if (Objects.isNull(resultFieldValue) ) continue; else if (!(resultFieldValue instanceof String) && !(resultFieldValue instanceof Integer)) continue; if (!isMulti) { /* 调用Redis资源开始翻译 */ final String key = srcTable + DictService.SEPARATOR + srcCate + DictService.SEPARATOR + String.valueOf(resultFieldValue); final String translateName = dictService.findNameFromRedis(key); /* 赋值翻译字段 */ BeanUtil.setFieldValue(result, field.getName(), translateName); } else if (resultFieldValue instanceof String && isMulti) { /* 按照注解声明的分割符对目标值进行切割,如果标签 */ final String[] split = ((String)resultFieldValue).split(separator); /* 对切片逐一翻译,再拼接回来 */ final StringBuilder builder = new StringBuilder(); for (int i = 0; i < split.length; i++) { final String key = srcTable + DictService.SEPARATOR + srcCate + DictService.SEPARATOR + split[i].trim(); if (i == split.length - 1) { final String fromRedis = dictService.findNameFromRedis(key); builder.append(fromRedis); } else { final String fromRedis = dictService.findNameFromRedis(key); builder.append(fromRedis); builder.append(separator); } } /* 赋值翻译字段 (多个) */ BeanUtil.setFieldValue(result, field.getName(), builder.toString()); } } } }
这里重写测试Controller的方法验证一下我们的递归:
package cn.cloud9.server.test.controller; import cn.cloud9.server.struct.dict.annotation.Translate; import cn.cloud9.server.struct.dict.dto.DictDTO; import cn.cloud9.server.struct.dict.mapper.DictMapper; import cn.cloud9.server.test.model.DictAspectModel; import com.alibaba.fastjson.JSON; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import javax.annotation.Resource; import java.util.ArrayList; import java.util.List; /** * @author OnCloud9 * @description * @project tt-server * @date 2022年11月12日 下午 10:15 */ @Slf4j @RestController(value = "testDictController") @RequestMapping("/test/dict") public class DictController { @Resource private DictMapper dictMapper; @Translate @GetMapping("/tran") public List<DictAspectModel> translateAspectTest() { /* 测试集合内的DTO能否翻译 */ List<DictAspectModel> models = new ArrayList<>(); for (int i = 1; i < 2; i++) { final DictAspectModel model = new DictAspectModel(); model.setDictCode(1006000 + i); model.setMovieType("1013013 , 1013015 , 1013017 , 1013020 "); models.add(model); } /* 设置嵌套DTO,测试能否翻译内嵌对象 */ final DictAspectModel model = new DictAspectModel(); model.setDictCode(1006003); model.setMovieType("1013013 , 1013015 , 1013017 , 1013020 "); final ArrayList<DictAspectModel> innerList = new ArrayList<>(); innerList.add(model); models.get(0).setModels(innerList); log.info("翻译切面之前:models {}", JSON.toJSONString(models)); return models; } /** * 测试我们编写的SQL执行是否有效 * @param sql 自定义SQL * @return 字典集合 */ @GetMapping("/sql") public List<DictDTO> getDictListBySql(@RequestBody String sql) { return dictMapper.queryUsingCustomSql(sql); } }
Postman请求结果:
可以看到内嵌的集合DTO也能被翻译出来
十、使用ResponseBodyAdvice取代AOP
AOP切入点是针对方法层级的,如果需要扩大切点颗粒细度,还是使用响应外切来完成
1、默认所有模型实体响应给前端就是需要翻译的
只有特别情况才不需要翻译,通过此注解标记来控制
可以定义在Controller上或者方法上
package cn.cloud9.server.struct.dict.annotation; import java.lang.annotation.*; /** * 禁用字典翻译标记 */ @Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD, ElementType.TYPE}) public @interface DisableTranslate { }
2、切点判断逻辑:
package cn.cloud9.server.struct.dict.hook; import cn.cloud9.server.struct.dict.annotation.DisableTranslate; import cn.cloud9.server.struct.dict.reflect.ReflectUtil; import com.baomidou.mybatisplus.core.metadata.IPage; import org.apache.commons.collections.CollectionUtils; import org.springframework.core.MethodParameter; import org.springframework.core.annotation.Order; import org.springframework.http.MediaType; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.server.ServerHttpRequest; import org.springframework.http.server.ServerHttpResponse; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice; import javax.annotation.Resource; import java.util.Collection; import java.util.List; import java.util.Objects; /** * @author OnCloud9 * @description 使用响应外切实现翻译入口 * @project tt-server * @date 2022年11月25日 下午 07:19 */ @Order(2) @ControllerAdvice(annotations = RestController.class) public class DictAdvice implements ResponseBodyAdvice<Object> { @Resource private ReflectUtil reflectUtil; /** * 增加入口颗粒度, 可标注在类或方法上控制是否翻译 * @param methodParameter * @param aClass * @return */ @Override public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> aClass) { final Class<DisableTranslate> dtClass = DisableTranslate.class; /* 1、判断是否在类上标记 */ DisableTranslate disableTranslate = methodParameter.getContainingClass().getAnnotation(dtClass); boolean isMarkOnClass = Objects.nonNull(disableTranslate); /* 2、判断是否在方法上标记 */ disableTranslate = methodParameter.getMethod().getAnnotation(dtClass); boolean isMarkOnMethod = Objects.nonNull(disableTranslate); /* 3、只要在类或者方法上标记,则表示不使用翻译 */ return !(isMarkOnClass || isMarkOnMethod); } @SuppressWarnings("all") @Override public Object beforeBodyWrite( Object result, MethodParameter methodParameter, MediaType mediaType, Class<? extends HttpMessageConverter<?>> aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse ) { /* 是否为空 */ final boolean isEmpty = Objects.isNull(result); if (isEmpty) return result; /* 返回的结果类型是否为基本类型 */ final boolean isPrimitive = result.getClass().isPrimitive(); if (isPrimitive) return result; /* 返回的结果类型是否为集合 */ final boolean isCollection = result instanceof Collection; /* 返回的结果类型是否为翻页对象 */ final boolean isPage = result instanceof IPage; if (isCollection) { Collection<Object> list = (Collection<Object>) result; if (CollectionUtils.isEmpty(list)) return result; for (Object row : list) reflectUtil.translateDTO(row); } else if (isPage) { IPage<Object> page = (IPage<Object>) result; if (CollectionUtils.isEmpty(page.getRecords())) return result; final List<Object> records = page.getRecords(); for (Object record : records) reflectUtil.translateDTO(record); } else { reflectUtil.translateDTO(result); } return result; } }