MyBatis入坑之OGNL乱写

3,695 阅读4分钟

这是我参与11月更文挑战的第9天,活动详情查看:2021最后一次更文挑战

Mybatis支持OGNL的语法

OGNL 是 Object-Graph Navigation Language 的缩写,对象-图行导航语言,语法为:#{ }。

是不是有点懵,不知道这是个啥?

OGNL 作用是在对象和视图之间做数据的交互,可以存取对象的属性和调用对象的方法,通过表达式可以迭代出整个对象的结构图。

e1 or e2
e1 and e2
e1 == e2,e1 eq e2
e1 != e2,e1 neq e2
e1 lt e2:小于
e1 lte e2:小于等于,其他gt(大于),gte(大于等于)
e1 in e2
e1 not in e2
e1 + e2,e1 * e2,e1/e2,e1 - e2,e1%e2
!e,not e:非,求反
e.method(args)调用对象方法
e.property对象属性值
e1[ e2 ]按索引取值,List,数组和Map
@class@method(args)调用类的静态方法
@class@field调用类的静态字段值

还有一些表达式用来赋值或者增强属性。经常用来做模糊搜索的 bind标签:

<bind name="nameLike" value="'%'+ name + '%'"/>

这里的value也属于OGNL表达式 e1+e2,字符串是拼接,数字的话就是加法运算,我们可以引申出肯定还有:

  • e1*e2 乘法
  • e1/e2 除法
  • e1-e2 减法
  • e1%e2 取模

用于解析静态方法

org.apache.ibatis.scripting.xmltags.TextSqlNode.BindingTokenParser#handleToken
org.apache.ibatis.scripting.xmltags.OgnlCache#getValue
org.apache.ibatis.scripting.xmltags.OgnlCache#parseExpression解析表达式
org.apache.ibatis.ognl.Ognl#parseExpression
org.apache.ibatis.ognl.OgnlParser#staticReference
org.apache.ibatis.ognl.OgnlParser#staticMethodCall
org.apache.ibatis.ognl.OgnlRuntime#callStaticMethod

在SQL映射语句中可以支持引入以下几种方式:

<select id="getUserById" resultMap="BaseResultMap">
       select * from user
       <if test="id != null">
           <where>
                 name = #{name}
                 and id =${id}
                 and id = ${user.id}
                 and id = ${@@abs(-12345678)}
                 and id = ${@@parseInt("654")}
                 and id='${@cn.followtry.mybatis.bean.User@name()}'
                 and id='${new cn.followtry.mybatis.bean.User()}'
                 and id=${@cn.followtry.mybatis.bean.User@haha}
                 and id='${@cn.followtry.mybatis.bean.User@arr[1]}'
                 and id='${@cn.followtry.mybatis.bean.User@list[1]}'
                 and id='${@cn.followtry.mybatis.bean.User@map.get("123")}'
                 and id='${@cn.followtry.mybatis.bean.CodeTypeEnum@THREE.ordinal()}'
             </where>
         </if>
       limit 100
</select>
  • 变量:id =${id}

  • 属性:id = ${user.id}

  • 静态方法(public):id='${@cn.followtry.mybatis.bean.User@name()}'

  • 静态属性(public):id=${@cn.followtry.mybatis.bean.User@aaa}

  • 数组索引:id='${@cn.followtry.mybatis.bean.User@arr[1]}'

  • 集合:'${@cn.followtry.mybatis.bean.User@list[1]}'

  • Map:

    • id='${@cn.followtry.mybatis.bean.User@map.get("123")}'
    • id='${@cn.followtry.mybatis.bean.User@map}'
    • Enum:id=${@cn.followtry.mybatis.bean.CodeTypeEnum@THREE.ordinal()}
  • 构造方法:id='${new cn.followtry.mybatis.bean.User()}'

  • java.lang.Math方法:id = ${@@abs(-12345678)} 可以省略class的编写,方法的默认class是java.lang.Math

${}语法中通过两个@字符,前者定位到Java类,后者定位到类中的方法或属性,这里只列出的其中一部分,对于Mybatis支持的${}语法,可以参见OGNL语法手册。


  • 做一个小示例
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
 
import com.hotent.core.util.BeanUtils;
 
public class Ognl {
    public Ognl() {
    }
 
    public static boolean isEmpty(Object o) throws IllegalArgumentException {
        return BeanUtils.isEmpty(o);
    }
 
    public static boolean isNotEmpty(Object o) {
        return !isEmpty(o);
    }
 
    public static boolean isNotEmpty(Long o) {
        return !isEmpty(o);
    }
 
    public static boolean isNumber(Object o) {
        return BeanUtils.isNumber(o);
    }
}
<select id="getAll" parameterType="java.util.Map" resultMap="TaskEntity">
  SELECT task.*,run.subject subject,run.processName processName
  FROM ACT_RU_TASK task left join BPM_PRO_RUN run
  on task.PROC_INST_ID_=run.actInstId
  where 1=1
  <if test="@Ognl@isNotEmpty(name)"> AND task.name_ LIKE #{name} </if>
  <if test="@Ognl@isNotEmpty(subject)"> AND run.subject LIKE #{subject} </if>
  <if test="@Ognl@isNotEmpty(processName)"> AND run.processName LIKE #{processName} </if>
  <if test="@Ognl@isEmpty(orderField)">
    order by task.CREATE_TIME_ desc
  </if>
  <if test="@Ognl@isNotEmpty(orderField)">
    order by ${orderField} ${orderSeq}
  </if>		
</select>

类的内置方法

其实MybatisMapper.xml中还可以使用对象的内置方法,比如我们需要判断一个java.util.Collection集合是否为空,可以这么写:

<if test="collection!=null and collection.size()> 0">
  and some_col = #{some_val}
</if>

这里就使用了对象的内置方法Collection.size()

我们还可以调用自定义对象CollectionUtils静态方法来判断集合是否为空:

package cn.felord.util;

public final class CollectionUtils {
    public static boolean isNotEmpty( Collection<?> collection) {
        return (collection != null && !collection.isEmpty());
    }    
}

那么上面的<if>判断改为:

<if test="@cn.felord.util.CollectionUtils@isNotEmpty(collection)">
  and some_col = #{some_val}
</if>

不要忘了这里要带上类的全限定名。

取值操作

取值操作的话,如果是对象直接e.property,如果是集合或者Map可以e[index|key],通过索引或者键名来取值。分别举个例子:

# 对象取属性
user.username
# 集合取元素
array[1] 
# map 取值
map['username']

其实静态属性也能取值调用,跟上面的静态方法类似:

@cn.felord.Cache@user

对应Java代码:

package cn.felord;

public final class Cache {
    public static User user = new User ("felord.cn") ;
}

赋值操作

上面的取值除了可以做判断还可以用来SQL参数赋值:

     <where>
         <!-- 常用的赋值方式 -->
             username = #{username}
         <!-- $ 也可以赋值 -->
             and user_id =${userId}
         <!-- 对象取属性 -->
             and id = ${user.id}
         <!-- Math.abs  双@简写 -->
             and age = ${@@abs(-12345678)}
         <!-- 调用枚举 -->
             and gender =${@cn.felord.GenderEnum@MALE.ordinal()}
             and id=${@cn.felord.Cache@user.userId}
         </where>

通过${}符号可以用OGNL表达式给SQL参数赋值,不过感觉平常比较少用。


  • <bind>参数的调用可以通过#{}${} 方式获取,#{}可以防止注入:<bind>value值会使用OGNL计算
<bind name="username_bind" value='@java.util.UUID@randomUUID().toString().replace("-", "")' />
  • 使用OGNL实现单表的分表功能

分表这个功能是通用Mapper中的新功能,允许在运行的时候指定一个表名,通过指定的表名对表进行操作。这个功能实现就是使用了OGNL。

首先并不是所有的表都需要该功能,因此定义了一个接口,当参数(接口方法只有实体类一个参数)对象继承该接口的时候,就允许使用动态表名。

public interface IDynamicTableName {
    /**
     * 获取动态表名 - 只要有返回值,不是null和'',就会用返回值作为表名
     * @return
     */
    String getDynamicTableName();
}

然后在XML中写表名的时候使用:

<if test="@tk.mybatis.mapper.util.OGNL@isDynamicParameter(_parameter) 
            and dynamicTableName != null 
            and dynamicTableName != ''">
    ${dynamicTableName}
</if>
<if test="@tk.mybatis.mapper.util.OGNL@isNotDynamicParameter(_parameter) 
            or dynamicTableName == null 
            or dynamicTableName == ''">
    defaultTableName
</if>

由于我需要判断_parameter是否继承了IDynamicTableName接口,简单的写法已经无法实现,所以使用了静态方法,这两个方法如下:

/**
 * 判断参数是否支持动态表名
 *
 * @param parameter
 * @return true支持,false不支持
 */
public static boolean isDynamicParameter(Object parameter) {
    if (parameter != null && parameter instanceof IDynamicTableName) {
        return true;
    }
    return false;
}

/**
 * 判断参数是否b支持动态表名
 *
 * @param parameter
 * @return true不支持,false支持
 */
public static boolean isNotDynamicParameter(Object parameter) {
    return !isDynamicParameter(parameter);
}

根据<if>判断的结果来选择使用那个表名。另外注意XML判断中有一个dynamicTableName,这个参数是根据getDynamicTableName方法得到的,MyBatis使用属性对应的getter方法来获取值,不是根据field来获取值。