Mybatis字典枚举自动翻译

1,754 阅读5分钟

背景

写crud业务的时候,枚举和字典处不论是前端处理还是后端处理都挺麻烦。前端翻译势必要得到所有的枚举,然后自己去找,后端还要给一个接口去查枚举。

究其原因就是数据设计的时候只存了code,没有存desc,存desc有的时候要经常变,导致旧数据错,也浪费空间

因此后端翻译是最正确,如何优雅的翻译枚举或者字典,是个问题。

方法

orm工具使用mybatis我觉得手写sql在一些复杂场景还是很重要的),mybatis提供了强大的拦截器Intercepter,可以拦截sql执行之前,执行之后,映射完成之后等功能。我们常用的PageHelpler就是使用了拦截器,拦截sql执行之前,将原来要执行的sql,使用select count语句包装了一次。

下图摘自mybatis官网

image.png

因为mybatis提供了映射完成之后的拦截器,我们只需要使用拦截器得到最终结果,然后通过反射,去修改对象中的属性就可以了。

效果

User表中有一个status列,0表示关闭,1表示正常 image.png 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;

自动翻译了~ image.png

提示

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对象过程中做很多事情,不仅仅是翻译枚举,和分页。还可以做例如脱敏之类,全凭想象。