背景
写crud业务的时候,枚举和字典处不论是前端处理还是后端处理都挺麻烦。前端翻译势必要得到所有的枚举,然后自己去找,后端还要给一个接口去查枚举。
究其原因就是数据设计的时候只存了code,没有存desc,存desc有的时候要经常变,导致旧数据错,也浪费空间
因此后端翻译是最正确,如何优雅的翻译枚举或者字典,是个问题。
方法
orm工具使用mybatis(我觉得手写sql在一些复杂场景还是很重要的),mybatis提供了强大的拦截器Intercepter,可以拦截sql执行之前,执行之后,映射完成之后等功能。我们常用的PageHelpler就是使用了拦截器,拦截sql执行之前,将原来要执行的sql,使用select count语句包装了一次。
下图摘自mybatis官网
因为mybatis提供了映射完成之后的拦截器,我们只需要使用拦截器得到最终结果,然后通过反射,去修改对象中的属性就可以了。
效果
User表中有一个status列,0表示关闭,1表示正常 User Pojo类
public class User implements Serializable {
private Integer id;
private String name;
private Date createDate;
private transient Integer age;
//使用自定义注解,标记字典为user_status,翻译的属性的statusText
@DemoUtil.FieldBind(type = DemoDataBIndImpl.BindType.USER_STATUS,target = "statusText")
private Integer status;
//翻译的属性
private String statusText;
自动翻译了~
提示
MetaObject是mybatis提供一个反射操作工具,非常好用,mybatis需要反射操作的时候,都是用的它。
实现
定义一个注解
@Documented
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.ANNOTATION_TYPE})
public @interface FieldBind {
String type(); //字典的分类
String target(); //翻译的目标属性
}
使用FieldProperty将注解和加注解的Field,封装一下
@Data
@AllArgsConstructor
class FieldProperty{
private String name;//属性名
private FieldBind fieldBind;//属性绑定的注解
}
字典来源
定一个DataBind,数据绑定接口
public interface DemoDataBind {
/**
* 使用反射对象MetaObject,操作目标对象Object
* @param fieldBind 自定义的注解
* @param o 原始对象
* @param metaObject 原始对象的mybatis反射对象
*/
void setMetaObject(FieldBind field, Object o, MetaObject metaObject);
}
实现,这里的字典来源,这里是一个Map,可以在Spring启动的时候加载进来
public class DemoDataBIndImpl implements DemoDataBind{
public interface BindType {
String USER_STATUS = "user_status";
}
/**
* 可以在系统启动的时候,将数据库的所有字典和java代码中的枚举缓存起来
* 分布式记得用redis,更新的时候记得要更新redis(一般很少更新字典)
*/
private Map<String, String> STATUS_MAP = new ConcurrentHashMap<String, String>() {{
put("0", "关闭");
put("1", "正常");
}};
@Override
public void setMetaObject(DemoUtil.FieldBind fieldBind, Object fieldValue, MetaObject metaObject) {
// 数据库中数据转换
if (BindType.USER_STATUS.equals(fieldBind.type())) {
//使用反射工具类修改target属性,值从缓存中取到对应的结果
metaObject.setValue(fieldBind.target(), STATUS_MAP.get(String.valueOf(fieldValue)));
}
}
}
拦截器
实现一个mybatis的拦截器,拦截对象是ResultSetHandler,拦截方法是handleResultSets
@Intercepts({@Signature(
type = ResultSetHandler.class,
method = "handleResultSets",
args = {Statement.class}
)})
public class DemoInterceptor implements Interceptor {
private DemoDataBind demoDataBind;//字典数据绑定
public DemoInterceptor(DemoDataBind demoDataBind) {
this.demoDataBind = demoDataBind;
}
@Override
public Object intercept(Invocation invocation) throws Throwable {
//一定是list结果
List proceed = (List) invocation.proceed();
if(proceed.isEmpty())
return proceed;
else {
//获取mybatis的Configuration,用于获得MetaData
DefaultResultSetHandler resultSetHandler= (DefaultResultSetHandler) invocation.getTarget();
Field mappedStatement = resultSetHandler.getClass().getDeclaredField("mappedStatement");
mappedStatement.setAccessible(true);
MappedStatement o = (MappedStatement) mappedStatement.get(resultSetHandler);
Configuration configuration = o.getConfiguration();
// 迭代处理
Iterator iterator = proceed.iterator();
while (iterator.hasNext()){
Object next = iterator.next();
//检查是否需要翻译,是否需要翻译的标准是,检查目标对象的Class是否有自定义的注解,
//有的话,调用字典数据绑定,取修改对象的target属性
if(null!=next && !DemoUtil.needTranslate(configuration,next,
(m,f)->{
//得到自定义注解
DemoUtil.FieldBind fieldBind = f.getFieldBind();
//得到具体属性的值
Object value = m.getValue(f.getName());
demoDataBind.setMetaObject(fieldBind,value,m);
}
)){
//不需要翻译的话,跳过
break;
}
}
return proceed;
}
}
}
工具类,用于找属性
public class DemoUtil {
private static Map<Class<?>,List<FieldProperty>> fieldProMaps;
private static Set<Class<?>> invalidClass;
static {
//缓存class和属性,不用每次都便利查找
fieldProMaps=new ConcurrentHashMap<>();
//不合法的class,有些class天生就不可能有我们自定义的注解,例如HashMap
//如果第一个便利class属性之后发现没有自定义注解,也会被标记为不合法的class
invalidClass=new CopyOnWriteArraySet<>();
invalidClass.add(HashMap.class);//不校验HashMap
}
/**
* 是否需要翻译,通过寻找class上的自定义注解
*/
public static boolean needTranslate(Configuration configuration, Object o, BiConsumer<MetaObject, FieldProperty> biConsumer){
//根据对象的
List<FieldProperty> fieldPropertyList = getFieldPropertyList(o.getClass());
if(!ObjectUtils.isEmpty(fieldPropertyList)){
//如果不为空的话,调用BiConsumer的apply了
//创建元数据对象(为什么要花很大功夫得到mybatis的Configuration?自己写反射不也可以完成吗?因为mybatis可能还有很多其他配置,自己可能写会丢失那些功能,这些配置都在Configuration里了,newMetaObject也会有缓存在其中)
MetaObject metaObject = configuration.newMetaObject(o);
//多线程处理,fork join
fieldPropertyList.parallelStream().forEach(fieldProperty -> {
biConsumer.accept(metaObject,fieldProperty);
});
return true;
}
return false;
}
@Documented
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.ANNOTATION_TYPE})
public @interface FieldBind {
String type(); //字典类型
String target(); //目标属性
}
@Data
@AllArgsConstructor
static class FieldProperty{
private String name;
private FieldBind fieldBind;
}
/**
* 找翻译注解
*/
private static List<FieldProperty> getFieldPropertyList(Class<?> c){
if(invalidClass.contains(c)){
//检查是否合法
return null;
}
//缓存检查
List<FieldProperty> fieldProperties = fieldProMaps.get(c);
if(fieldProperties!=null)
return fieldProperties;
//获取到所有的属性
List<Field> allField = getAllField(c);
//过滤出有FieldBind的属性,并且封装成FieldProperty
List<FieldProperty> collect = allField.stream().filter(i -> {
FieldBind annotation = i.getAnnotation(FieldBind.class);
return annotation != null;
}).map(i -> new FieldProperty(i.getName(), i.getAnnotation(FieldBind.class)))
.collect(Collectors.toList());
//空的话,不合法
if(ObjectUtils.isEmpty(collect))
invalidClass.add(c);
//不为空,存缓存
else fieldProMaps.put(c,collect);
return collect;
}
/**
* 找到所有field(这里没有处理父类的属性,可以自行修改)
* @return
*/
private static List<Field> getAllField(Class c){
Field[] declaredFields = c.getDeclaredFields();
return Arrays.stream(declaredFields).filter((var0x) -> {
//去除static属性
return !Modifier.isStatic(var0x.getModifiers());
}).filter((var0x) -> {
//去除transient属性
return !Modifier.isTransient(var0x.getModifiers());
}).collect(Collectors.toList());
}
加载拦截器
我用的是springBoot,从容器里找到mybatis的sqlSessionFactory,调用增加拦截器的方法即可
//注入字典绑定的数据源,生成拦截器
//(这里的字典数据源也可以放在容器里,通过监听spring的启动,例如Spring的ApplicationReadyEvent
//加载数据库和java枚举代码,存入缓存,自由发挥)
DemoInterceptor demoInterceptor = new DemoInterceptor(new DemoDataBIndImpl());
//增加拦截器
sqlSessionFactory.getConfiguration().addInterceptor(demoInterceptor)
总结
mybatis提供的拦截器,可以在整个sql语句到映射成java对象过程中做很多事情,不仅仅是翻译枚举,和分页。还可以做例如脱敏之类,全凭想象。