【Java】Springboot + Redis +(AOP & 响应外切)切面实现字典翻译
使用案例演示:
先开发了一个简单的 Demo:
普通 DTO 类注解翻译的字段和翻译来源
在需要翻译的方法上注解 @Translate
接口返回结果:
框架思路:
1、标记的注解需要通过 AOP 切面在调用的时候处理翻译
2、翻译的来源是 Redis 的缓存,需要有数据来源,应用启动之后就需要初始化
一、配置 Redis
pom.xml 的相关依赖:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 | <!-- 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 配置:
1 2 3 4 5 6 7 8 9 10 11 12 | 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 配置类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 | 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🇲🇲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🇲🇲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🇲🇲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🇲🇲ss" ) + "\"" ); }); fastJsonConfig.setSerializeConfig(serializeConfig); fastJsonConfig.setFeatures(Feature.SupportAutoType); fastJsonConfig.setSerializerFeatures(SerializerFeature.WriteClassName); redisTemplate.setValueSerializer(fastJsonRedisSerializer); redisTemplate.setHashValueSerializer(fastJsonRedisSerializer); redisTemplate.afterPropertiesSet(); return redisTemplate; } } |
二、数据源来源获取
数据源配置:
1 2 3 4 5 6 | 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | 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🇲🇲ss" ) @TableField ( "GEN_TIME" ) private LocalDateTime genTime; } |
Mapper 配置一个自定义查询 SQL 的方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | 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); } |
一般来说是加载字典表放入缓存中,但是还有类似行政区域表,也是需要缓存放入的
字典表:
1 | SELECT * FROM `system_dict` |
非字典,但是也可以按照字典表结构存储的表:
1 | SELECT ` NAME ` AS `DICT_CODE`, 'AB_WORD' AS `DICT_TYPE`, `MEANING` AS `DICT_NAME` FROM abridge_word |
只要字段适配,同样可以按照字典装载
我们可以有若干个需要装载的表,那需要写在配置文件中解除硬编码控制:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | 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 的配置声明如下:
1 2 3 4 | 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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | // 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、按字典类别获取这个类别的集合
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | 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 来实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 | 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 也有了,那怎么才能让应用一启动的时候就开始执行呢?
而且执行一次通常是类静态资源调用的做法,于是就用到了
1 | import org.springframework.context.ApplicationContextAware; |
所有 Bean 装入 Spring 完毕后会执行 Aware,通过 Aware 可以获取容器上下文对象
通过上下文对象,根据类型和 Bean 名称,可以静态的获取对应的 Bean,
有了 DictServiceBean 之后,就可以调用初始化了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 | 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 持有器类,可以这样实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | 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 主启动类完成后调用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | 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 定位目标方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | 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,用来标记翻译字段
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | 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 的情况
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 | 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 的工具
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 | 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 的情况:
因为有第一个翻译的方法逻辑,后面实现起来就很容易了
只需要在前面判断当前字段是不是集合或者翻译类型的,然后逐个遍历判断类型
当然这个判断没有那么严谨,一般开发的情况不会装载一般数据类型,如果确实碰到了,可以结合实际情况再添加判断补充处理
最后递归调用翻译方法就可以了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 | 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 的方法验证一下我们的递归:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 | 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 上或者方法上
1 2 3 4 5 6 7 8 9 10 11 12 | package cn.cloud9.server.struct.dict.annotation; import java.lang.annotation.*; /** * 禁用字典翻译标记 */ @Documented @Retention (RetentionPolicy.RUNTIME) @Target ({ElementType.METHOD, ElementType.TYPE}) public @interface DisableTranslate { } |
2、切点判断逻辑:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 | 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; } } |