Java 基于MyBatis-Plus使用反射和注解的方式来简化频繁的过滤条件

使用MyBatis-Plus为我们提供的过滤条件wrapper对象的便利,当需要频繁在多个业务中实现数量较多的过滤条件调用时。

我尝试着去使用了一下反射加注解的方式创建自己的工具类和自定义注解来解决这个问题。在最下面我会进行一个简易的说明。

定义自己的自定义注解,标记在封装过滤参数的对象的成员字段中,通过反射来封装到QueryWrapper中。

我使用的模板是AdminLTE中的一个预约界面的过滤查询为例子,这里主要是使用Spring Boot + MyBatis-Plus等框架。

定义自己的自定义注解,标记在封装过滤参数的对象的成员字段中,通过反射来封装到QueryWrapper中。

 

当我们可能遇到下面的情况:

 public PageVo<Appointment> pageResult(QueryObject qo) {
        AppointmentQueryObject temp = (AppointmentQueryObject) qo; //多态形象,强转获取当前类型对应的分页对象
        return new PageVo<>(this.page(new Page<>(qo.getCurrentPage(), qo.getPageSize()),
        new QueryWrapper<Appointment>()
                .like(StringUtils.hasLength(temp.getAno()),         "ano", temp.getAno())
                .like(StringUtils.hasLength(temp.getContactName()), "contact_name",     temp.getContactName())
                .like(StringUtils.hasLength(temp.getContactTel ()), "contact_tel",      temp.getContactTel())
                .eq(temp.getBusinessId() != null,                   "Business_id",      temp.getBusinessId())
                .eq(temp.getCategoryId() != null,                "Category_id",      temp.getCategoryId())
                .eq(temp.getStatusId  () != null,                   "status",           temp.getStatusId())
                .ge(temp.getOpenDate  () != null,               "appointment_time", temp.getOpenDate())
                .le(temp.getEndDate   () != null,                "appointment_time", temp.getEndDate())));

这个时候我们需要在QueryWrapper中调用一堆的方法,来封装我们的过滤参数。这种情况也算是一种重复的操作了。

先是定义一个自定义注解,其中的第一项很好看出,直接定义为可能和当前字段名称不一致的列名;

第二项我将它定义为AND与OR的拼接条件的枚举;

第三项我采用了与QueryWrapper为我们提供的过滤方法的名称作为名称并使用枚举的形式定义:

@Documented
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface FilterField {

    /**
     * 数据库对应的列名称
     */
    String[] name() default "";

    /**
     * 查询语句拼接类型
     * {@link PickType}
     */
    PickType type() default PickType.AND;

    /**
     * 过滤查询对应的方法
     * {@link WrapperScheme}
     */
    WrapperScheme[] scheme() default WrapperScheme.EQ;
}
@Getter
public enum PickType 
{
    /**
     * 过滤查询AND拼接
     */
    AND(false),
    /**
     * 过滤查询OR拼接
     */
    OR(true);

    private final boolean flag;

    PickType(boolean flag) { this.flag = flag; }
}
/**
 * 这个枚举类是用来对过滤查询工具方法中的Wrapper对应的方法进行了一个枚举
 * 如果后续需要增加 请在此添加对应的方法名称用于枚举名称
 * 然后后续请不要忘记了在工具类中添加对应的Swatch case的对应方法
 */
@Getter
public enum WrapperScheme 
{
    /** 全等判断 */
    EQ(0),
    /** 大于等于判断 */
    GE(1),
    /** 小于等于判断 */
    LE(2),
    /** 模糊查询条件 */
    LIKE(3),
    /** 升序排序*/
    ORDER_ASC(4),
    /** 降序排序*/
    ORDER_DESC(5),
    /** 为空判断*/
    NULL(6);

    private final Integer value;
    
    WrapperScheme(Integer value) { this.value = value; }
}

其实我有想过将PickType这个枚举类合并到WrapperScheme中,但是后面没有去做,或许可以尝试一下。

接着是编写我们的工具类了:

public class QueryWrapperFilterUtil<T, E> {

    private QueryWrapperFilterUtil() {
    }

    public static QueryWrapperFilterUtil getInstance() {
        return new QueryWrapperFilterUtil();
    }

    private QueryWrapper<T> wrapper;

    private QueryObject qo;

    /**
     * 此方法通过反射QO查询对象获取字段和字段上注解来封装调用wrapper对应的方法
     * 通过放射获取字段的名称和值 对{@link FilterField}的存在进行判断
     * 对{@FieldName name}属性的存在进行判断决定调用的方法的参数
     *
     * @param qo      持有过滤条件的对象
     * @param wrapper 过滤对象
     * @return 封装了对应过滤条件的过滤对象
     */
    public QueryWrapper<T> wrapper(QueryObject qo, QueryWrapper<T> wrapper) {
        this.wrapper = wrapper;
        this.qo = qo;
        Class<? extends QueryObject> clz = qo.getClass();
        Field[] fields = clz.getDeclaredFields();
        for (Field field : fields) {
            field.setAccessible(true);
            //调用handle方法获取当前字段的字段值
            E param = this.handle(field);
            //调用transformColumn方法获取字段的名称
            String name = this.transformColumn(field.getName());
            //或者字段上的注解
            FilterField annotation = field.getAnnotation(FilterField.class);
            //判断字段上是否拥有注解
            if (annotation == null) {
                wrapper = wrapper.eq(this.judgementCondition(param), name, param);
                continue;
            }
            //获取注解中的名称属性
            String[] parameter = annotation.name();
            //判断注解的type属性
            boolean bool = annotation.type().isFlag();
            //获取注解可能拥有的所有过滤方案
            WrapperScheme[] wrapperSchemes = annotation.scheme();
            //遍历封装注解的每一个过滤方案
            for (WrapperScheme wrapperScheme : wrapperSchemes) {
                Integer schemeValue = wrapperScheme.getValue();
                boolean ifName = "".equals(parameter[0].trim());
                //判断注解是否定义了自定义名称 否则采用默认字段名称转换_分隔符的字符串
                if (parameter.length == 1 && ifName) {
                    this.wrapper(schemeValue, name, param);
                    this.flag(bool, null, null);
                    continue;
                }
                //遍历注解中的每一个自定义名称
                for (int index = 0; index < parameter.length; index++) {
                    this.wrapper(schemeValue, parameter[index], param);
                    this.flag(bool, index, parameter.length - 1);
                }
            }
        }
        return wrapper;
    }

    /**
     * 通过{@link WrapperScheme}枚举中的值属性 进行判断语句确认调用方法类型
     * 如果在{@link WrapperScheme}枚举中添加了对应的枚名称类型
     * 请在此处书写对应的判断方法
     *
     * @param scheme    {@link WrapperScheme}枚举中对应名称的值
     * @param parameter {@link FilterField} 注解中对应的 name 参数
     * @param param     通过反射获取到的 {@link QueryObject} 对象中封装的参数值
     */
    private void wrapper(Integer scheme, String parameter, E param) {
        boolean flag = this.judgementCondition(param);
        switch (scheme) {
            case 1: wrapper.ge(flag, parameter, param); break;
            case 2: wrapper.le(flag, parameter, param); break;
            case 3: wrapper.like(flag, parameter, param); break;
            case 4: wrapper.orderByAsc(parameter); break;
            case 5: wrapper.orderByDesc(parameter); break;
            case 6:
                if(!flag) { break; }
                if (Boolean.parseBoolean(param.toString())) {
                    wrapper.isNotNull(parameter);
                    break;
                }
                wrapper.isNull(parameter);
                break;
            default: wrapper.eq(flag, parameter, param); break;
        }
    }

    /**
     * 这个方法是用来将 ele字符串中存在的大写字符
     * 即驼峰书写方式转化为小写并前置添加"_"符号 的分隔符号样式的列名称
     *
     * @param ele 被转换的字符串对象
     * @return 遵循了单词分隔符号的列名称
     */
    public String transformColumn(String ele) {
        StringBuffer sb = new StringBuffer();
        sb.append(ele);
        for (int i = 0; i < ele.length(); i++) {
            char c = sb.charAt(i);
            if (c < 97) {
                c = (char) (c + 32);
                sb.delete(i, i + 1);
                sb.insert(i, "_" + c);
            }
        }
        return sb.toString();
    }


    /**
     * 判断过滤查询对象中的字段是否拥有值
     *
     * @param param 字段对象
     * @return
     */
    private boolean judgementCondition(E param) {
        return param != null && StringUtils.hasLength(param.toString());
    }

    /**
     * 对可能出现的反射调用获取字段值进行异常捕捉处理
     *
     * @param field 反射获取道的字段
     * @return 反射调用获取到的对应字段的值
     */
    private E handle(Field field) {
        try {
            return (E) field.get(qo);
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 对{@link PickType} 中判断的AND与OR的拼接进行判断和方法调用
     *
     * @param flag  {@link PickType}的type默认是false
     *              如果为true则为OR方法调用
     *              只进行是否为OR的方法调用 因为wrapper默认是AND
     * @param index 查询对象中的所有字段的对应索引位置
     * @param len   查询对象中所有字段的数量 用来对应对象中所有字段的总数量
     *              警示最后一个字段并不需要在进行OR方法调用
     */
    private void flag(boolean flag, Integer index, Integer len) {
        boolean indexIf = (!flag || index != null);
        boolean lenIf = (len != null && index < len);
        if (indexIf && lenIf) {
            wrapper.or();
        }
    }

}

我采用了放射的方式来获取传入当前工具类方法中的类的字段,这个类就是我们的过滤条件参数的封装对象的类。

然后获取它的名称,值以及是否贴有注解,我会提供一个将当前字段名称驼峰转换为_下划线分隔符形式的字符串的方法,然后将值调用会产生的异常脱离出一个方法来处理,这样子代码不会因为多了一点try catch而有些难看,虽然已经有些很难看了。

我也尝试着直接获取字段的值,但是它返回的是一个Objcet在多次实验的时候封装总会出现一些问题,所以我使用了泛型的方式。

首先进行一步注解存在与否的判断,如果当前的字段没有注解,那么默认就是以当前字段名称的分隔符转换形式的字符串为名称,默认EQ的过滤条件,传递对应的参数调用wrapper方法封装。

存在注解的情况下,还是分两小步进行判断,第一步就是没有定义注解的名称属性,那么直接调用上面获取到的分隔符字符串的名称,然后获取对应的过滤条件,进行wrapper方法的调用封装。

如果有名称,那么就使用当前的注解上设定的名称来进行wrapper方法的调用。期间可能存在多个名称,以及多个过滤条件,这个我是采用了遍历的方式来解决的。

如果简化之后的效果就会如下:

    public PageVo<Appointment> pageResult(QueryObject qo) {
        return new PageVo<Appointment>(this.page(new Page<>(qo.getCurrentPage(), qo.getPageSize()),
                QueryWrapperFilterUtil.getInstance().wrapper(qo, new QueryWrapper<Appointment>())));
    }

这个时候可能会提示泛型需要的定义的问题,这个我还没有解决,主要是还没有在这种情况下遇到什么其他的问题,基本上能够满足使用。

实际使用的情况如下:

    @FilterField(name = {"e.name","email"}, type = PickType.OR, scheme = WrapperScheme.LIKE)
    private String keyword;
    
    @FilterField(name = "`status`")
    private Integer statusId;

    private Integer businessId;

    @FilterField(name = "appointment_ano", scheme = WrapperScheme.NULL)
    private Boolean whetherAppointment;

    @FilterField(scheme = WrapperScheme.LIKE)
    private String customerName;

    @FilterField(name = "pay_time", scheme = WrapperScheme.GE)
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm")
    private Date openDate;

 

上面也有过说明,可以在WrapperScheme枚举中添加新的需要的wrapper中提供的过滤方法名称,然后再在QueryWrapperFilterUtil工具类的wrapper方法的switch等值判断中添加对应的新需求吧。

其实有想过将这个方法抽离出一个单独的类,然后修改在那个类中,使用当前这个QueryWrapperFilterUtil工具类去继承它,调用父类方法就好了,本意上分离出来这个方法也是不想影响到里面的逻辑代码,这个部分是对应MyBatis-Plus的QueryWrapper为我们提供好的方法的一个参数封装罢了,配合上面定义的WrapperScheme枚举来进行CRUD。

试想过,默认的过滤条件为EQ,那么可不可以将对应的类型有一个默认的过滤条件呢?因为基本数据类型我们大部分都是做等值判断,而String类型大部分多是LIKE模糊查询使用,而且我们在使用需要进行LIKE过滤条件的字段的时候,可能名称就是定义的表中列对应的_下划线分隔符转驼峰的方式,这种就可以直接使用上述过的分隔符转换字符串从方法了。

因为我碰到过如下的情况,这里会使用很多的LIKE:

    @FilterField(scheme = WrapperScheme.LIKE)
    private String name;

    @FilterField(scheme = WrapperScheme.LIKE)
    private String scope;

    @FilterField(scheme = WrapperScheme.LIKE)
    private String tel;

    /**TODO: 加强字符串默认是LIKE条件 整型默认是EQ类型*/
    @FilterField(scheme = WrapperScheme.LIKE)
    private String legalName;  

如果进行了String的默认LIKE过滤条件的逻辑增加,那么我们在这种情况下,根本就不需要定义多余的注解了。,著需要在不同的点定义就好了。不过我还没有去实现它,这可以尝试下。

实现的方式到这里就结束了,再说明上和代码的表示方面如果有不足之处,谢谢指出。可能你会有更好的点子能与我分享,或者能对它进行完善。

上一篇:登录界面


下一篇:装饰器框架,实现一个装饰器,函数闭包加上参数,解压序列,函数闭包为函数加上认证功能,装饰器闭包模拟session