【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;
}
}
【Java】Springboot + Redis +(AOP & 响应外切)切面实现字典翻译的更多相关文章
- Redis-基本概念、java操作redis、springboot整合redis,分布式缓存,分布式session管理等
NoSQL的引言 Redis数据库相关指令 Redis持久化相关机制 SpringBoot操作Redis Redis分布式缓存实现 Resis中主从复制架构和哨兵机制 Redis集群搭建 Redis实 ...
- 【Java分享客栈】超简洁SpringBoot使用AOP统一日志管理-纯干货干到便秘
前言 请问今天您便秘了吗?程序员坐久了真的会便秘哦,如果偶然点进了这篇小干货,就麻烦您喝杯水然后去趟厕所一边用左手托起对准嘘嘘,一边用右手滑动手机看完本篇吧. 实现 本篇AOP统一日志管理写法来源于国 ...
- 【Other】最近在研究的, Java/Springboot/RPC/JPA等
我的Springboot框架,欢迎关注: https://github.com/junneyang/common-web-starter Dubbo-大波-服务化框架 dubbo_百度搜索 Dubbo ...
- Springboot的日志管理&Springboot整合Junit测试&Springboot中AOP的使用
==============Springboot的日志管理============= springboot无需引入日志的包,springboot默认已经依赖了slf4j.logback.log4j等日 ...
- 理解AOP思想(面向切面编程)
AOP:面向切面编程,相信很多刚接触这个词的同行都不是很明白什么,百度一下看到下面这几句话: 在软件业,AOP为Aspect Oriented Programming的缩写,意为:面向切面编程,通过预 ...
- SpringBoot学习笔记(七):SpringBoot使用AOP统一处理请求日志、SpringBoot定时任务@Scheduled、SpringBoot异步调用Async、自定义参数
SpringBoot使用AOP统一处理请求日志 这里就提到了我们Spring当中的AOP,也就是面向切面编程,今天我们使用AOP去对我们的所有请求进行一个统一处理.首先在pom.xml中引入我们需要的 ...
- springboot + redis + 注解 + 拦截器 实现接口幂等性校验
一.概念 幂等性, 通俗的说就是一个接口, 多次发起同一个请求, 必须保证操作只能执行一次 比如: 订单接口, 不能多次创建订单 支付接口, 重复支付同一笔订单只能扣一次钱 支付宝回调接口, 可能会多 ...
- Springboot + redis + 注解 + 拦截器来实现接口幂等性校验
Springboot + redis + 注解 + 拦截器来实现接口幂等性校验 1. SpringBoot 整合篇 2. 手写一套迷你版HTTP服务器 3. 记住:永远不要在MySQL中使用UTF ...
- Spring全家桶——SpringBoot之AOP详解
Spring全家桶--SpringBoot之AOP详解 面向方面编程(AOP)通过提供另一种思考程序结构的方式来补充面向对象编程(OOP). OOP中模块化的关键单元是类,而在AOP中,模块化单元是方 ...
- HDU 4720 Naive and Silly Muggles (外切圆心)
Naive and Silly Muggles Time Limit: 2000/1000 MS (Java/Others) Memory Limit: 32768/32768 K (Java/Oth ...
随机推荐
- js 禁用右键菜单和禁止复制
大江东去,浪淘尽,千古风流人物.故垒西边,人道是,三国周郎赤壁.乱石穿空,惊涛拍岸,卷起千堆雪.江山如画,一时多少豪杰.遥想公瑾当年,小乔初嫁了,雄姿英发.羽扇纶巾,谈笑间,樯橹灰飞烟灭.故国神游,多 ...
- 数据结构 顺序表(C语言 与 Java实现)以及部分练习题
目录 数据结构 数组(顺序表) 特点 使用Java实现更高级的数组 C语言实现 总结 优点 缺点 例题 26. 删除有序数组中的重复项 1. 两数之和 27. 移除元素 153. 寻找旋转排序数组中的 ...
- kettle从入门到精通 第二十三课 kettle carte 错误(java.lang.OutOfMemoryError: GC overhead limit exceeded,Could not emit buffer due to lack of requests,java heap space)分析
1.Could not emit buffer due to lack of requests(无法发出缓冲区,因为请求不足.) 原因有两点:1)消费者处理数据能力较弱,如表输出步骤.2)消费者没有处 ...
- Python + redis操作Redis数据库
Redis redis是一个key-value存储系统.和Memcached类似,它支持存储的value类型相对更多,包括string(字符串).list(链表).set(集合).zset(sorte ...
- 使用WinSW把nginx做成windows服务
1.下载nginx:http://nginx.org/en/download.html 2.下载win sw:https://github.com/winsw/winsw/releases/tag/v ...
- 实时数据同步Inofity、sersync、lsyncd
数据备份方案 企业网站和应用都得有完全的数据备份方案确保数据不丢失,通常企业有如下的数据备份方案 定时任务定期备份 需要周期性备份的数据可以分两类: 后台程序代码.运维配置文件修改,一般会定时任务执行 ...
- Postman 的 Basic Auth 如何通过 Feign 实现
Postman 的 Basic Auth: 分析 根据以上图片分析: Postman 的 Authorization 实际为: header 中添加 Authorization: ******* ** ...
- 仓颉语言HelloWorld内测【仅需三步】
2024年6月21日,华为仓颉正式公开发布.还记的19年和王学智的团队做过接触,他们反馈说16年我出版的<自己动手构造编译系统>一书对他们的研发很有帮助,身为作者听到这个消息还是很开心的. ...
- nginx 反向代理(proxy)与负载均衡(upstream)应用实践
集群介绍 集群就是指一组(若干个)相互独立的计算机,利用高速通信网络组成的一个较大的计算机服务系统,每个集群节点(即集群中的每台计算机)都是运行各自服务的独立服务器.这些服务器之间可以彼此通信,协同向 ...
- Linux 提权-LXD 容器
本文通过 Google 翻译 LXD Container – Linux Privilege Escalation 这篇文章所产生,本人仅是对机器翻译中部分表达别扭的字词进行了校正及个别注释补充. 0 ...