说明
在前后端分离开发的场景下,后端经常会需要将状态或者类型这样的数据以阿拉伯数字返回,比如1:生效,2:失效;3:发布中等等;以往我们的做法都是前后端沟通好前端通过数字和中文自己对应并显示。后端如果修改字典前端也需要一起跟着改
字典回显插件主要解决上述问题,在返回状态数字时候通过一个注解自动将数字对应的中文标识返回前端
原理
- 为了实现引用及能使用的原则,借助SpringBoot自动配置原理
- 通过AOP拦截系统返回的JSON参数,根据类型注入字典值(目前支持data的返回格式为:数组,分页、对象、对象中嵌套list)
- 通过Mybatis返回数据增加字典数据绑定;
实现
一、自动配置
在resources->META-INF->新建spring.factories
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.yangxc.common.dict.DictAutoConfiguration
DictAutoConfiguration对应的代码片段
@Configuration
@RequiredArgsConstructor
public class DictAutoConfiguration {
@Bean
public DictAspect dictAspect(){
return new DictAspect();
}
@Bean
public DictInterceptor dictInterceptor() {
return new DictInterceptor();
}
}
二、相关注解
此处提供两种方式,AOP和Mybatis,提供如下注解
@Documented
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DictTransByAop {
/**
* 字典的type类型,为空时默认为当前属性名
* @return
*/
String type() default "";
/**
* 目标属性名,需要同时增加属性
* 例如:target="sexDesc" 需增加属性
* private String sexDesc;
* @return
*/
String target() default "";
}
Mybatis方式:
@Documented
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DictTransByMybatis {
String type() default "";
String target() default "";
}
是否启用字典回显注解,为了提高系统性能只有配置开启了字典回显功能的才处理相关逻辑
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
public @interface NeedReturnDict {
}
三、逻辑实现
AOP方式切面逻辑实现,通过拦截NeedReturnDict注解,解析返回参数中标注了DictTransByAop注解的字段根据声明的key获取字典中的值,赋值给desc:
@Slf4j
@Aspect
@SuppressWarnings({"unused"})
public class DictAspect {
@Resource
private DictCache dictCache;
private volatile Map<String, Object> dictInfoMap = new ConcurrentHashMap<>();
@Around("@annotation(needReturnDict)")
public Object translation(final ProceedingJoinPoint pjp, NeedReturnDict needReturnDict) throws Throwable {
//目标方法执行
Object resultR = pjp.proceed();
if (ObjectUtil.isNull(resultR)) {
return resultR;
}
//获取返回data值
Object result = ((R) resultR).getData();
if (result instanceof IPage) {
// 分页的情况
IPage page = (IPage) result;
result = ((IPage) result).getRecords();
result = translate(result);
page.setRecords((List) result);
((R) resultR).setData(page);
return resultR;
}
result = translate(result);
((R) resultR).setData(result);
return resultR;
}
/**
* 返回值转换,增加字典回显
* @param result
* @return
*/
private Object translate(Object result) {
if (result instanceof List || result instanceof ArrayList) {
List<JSONObject> items = new ArrayList<>();
for (Object entity : (List) result) {
to(entity);
}
} else {
to(result);
}
return result;
}
/**
* 根据类上注解,设置目标属性值
* @param entity 返回对象
*/
public void to(Object entity) {
Class c = entity.getClass();
for (; c != Object.class; c = c.getSuperclass()){
try {
Field[] fields = c.getDeclaredFields();
for (Field field : fields){
field.setAccessible(true);
Object preValue = field.get(entity);
Class<?> type = field.getType();
if (ObjectUtil.isNotNull(preValue)) {
//如果对象中包含list,判断list中是否包含注解
if (type.equals(List.class) || type.equals(ArrayList.class)) {
// 当前集合的泛型类型
Type genericType = field.getGenericType();
if (null == genericType) {
continue;
}
if (genericType instanceof ParameterizedType) {
for (Object o : (List) preValue) {
to(o);
}
}
}/*else if(type.isArray()){
Object[] objArr = (Object[]) preValue;
if (objArr != null && objArr.length > 0) {
for (Object arr : objArr) {
to(arr);
}
}
}*/
//todo 自定义对象方式
if (field.isAnnotationPresent(DictTransByAop.class)) {
DictTransByAop dictTranslation = field.getAnnotation(DictTransByAop.class);
// 需要赋值的字段
Field fvalue = c.getDeclaredField(dictTranslation.target());
fvalue.setAccessible(true);
//当前属性名
String currentFiledName = StrUtil.isEmpty(dictTranslation.type())
? field.getName() : dictTranslation.type();
Object dictValue = dictCache.get(currentFiledName,preValue);
//设置字典内容
fvalue.set(entity, dictValue);
}
}
}
} catch (NoSuchFieldException e){
//父类存在子类不存在情况
}catch (Exception e) {
log.error("字典回显失败:{}", JSONUtil.toJsonStr(entity));
e.printStackTrace();
}
}
}
}
Mybatis拦截器实现方式,数据库返回数据后去给相关字段赋值
@Intercepts({ @Signature(type = ResultSetHandler.class, method = "handleResultSets", args = { Statement.class }) })
public class DictInterceptor implements Interceptor {
@Resource
private DictCache dictCache;
@Override
public Object intercept(Invocation invocation) throws Throwable {
return setFieldValue(invocation.proceed());
}
@Override
public Object plugin(Object target) {
if (target instanceof ResultSetHandler) {
return Plugin.wrap(target, this);
}
return target;
}
@Override
public void setProperties(Properties properties) {
}
private Object setFieldValue(Object result){
if (result instanceof Collection) {
List<Object> list = new ArrayList<>();
for (Object obj : (Collection)result) {
list.add(setFieldValue(obj));
}
return list;
}
// 此处可根据需要加上一些判断优化处理,如result为基本数据类型对象、Map时不再执行后续代码,或只处理限定类等
Field[] fs = ReflectUtil.getFields(result.getClass());
for (Field f : fs) {
DictTransByMybatis dictTransByMybatis = f.getAnnotation(DictTransByMybatis.class);
if (dictTransByMybatis == null) {
continue;
}
// 获取@DictTransByMybatis标记字段具体值
Object value = ReflectUtil.getFieldValue(result, f);
//当前属性名
String currentFiledName = StrUtil.isEmpty(dictTransByMybatis.type())
? f.getName() : dictTransByMybatis.type();
Object dictValue = dictCache.get(currentFiledName,value);
// 设置目标字段的转换值
ReflectUtil.setFieldValue(result, dictTransByMybatis.target(), dictValue);
}
return result;
}
}
上述dictCache为一个查询字典内容的扩展接口,大家可以根据自己系统自定义实现、接口定义如下:
public interface DictCache {
/**
* 获取所有缓存
* @return
*/
public Map<String, Object> getAll();
/**
* 根据字典类型,字典值获取字典
* @param type
* @param value
* @return
*/
public Object get(String type,Object value);
/**
* 保存字典
* @param type
* @param map
*/
public void save(String type,Map<String, Object> map);
/**
* 清空缓存
*/
public void removeDict();
}
总结
大家可以根据此原理封装为自己的starter,直接pom引入使用。
评论区