Mybatis何时了,占位符你知多少

目录

1.前序

2.数据准备

2.1 sql脚本

2.2 Java相关类及配置文件

3 初识Mybatis占位符

3.1 #{}类型

3.2 ${}类型

3.3 初识总结

3.4 常见问题

4. 源码浅析

4.1 GenericTokenParser初始化

4.2 BindingTokenParser初始化

4.2 ParameterMappingTokenHandler初始化

4.3 工作原理

4.4 启示


1.前序

在Mybatis的世界里,存在两个我们经常用到的占位符,分别是${}和#{}。我们知道在实际开发过程中#{}的使用频率还是极高的;${}使用不当,极为容易引起Sql注入,然而他并没有在Mybatis框架中消失,足见他在框架的地位也不容小觑。他除了保证框架的兼容性,那是否还有其他存在的必要性呢?那么就跟着我的脚步,一起揭开Mybatis中占位符的神秘面纱。

2.数据准备

2.1 sql脚本

--创建用户表
CREATE TABLE USER (
  ID INT(11) NOT NULL AUTO_INCREMENT,
  USERNAME VARCHAR(32) NOT NULL COMMENT '用户名称',
  BIRTHDAY DATETIME DEFAULT NULL COMMENT '生日',
  SEX CHAR(1) DEFAULT NULL COMMENT '性别',
  ADDRESS VARCHAR(256) DEFAULT NULL COMMENT '地址',
  PRIMARY KEY  (ID)
) ENGINE=INNODB DEFAULT CHARSET=UTF8;

--创建用户数据
INSERT  INTO USER(ID,USERNAME,BIRTHDAY,SEX,ADDRESS) VALUES 
(41,'老王','2018-02-27 17:47:08','男','北京'),
(42,'小二王','2018-03-02 15:09:37','女','北京三元桥'),
(43,'小三王','2018-03-04 11:34:34','女','北京三元桥'),
(45,'李四','2018-03-04 12:04:06','男','北京三元桥'),
(46,'老王','2018-03-07 17:37:26','男','北京四元桥'),
(48,'小马宝莉','2018-03-08 11:44:00','女','北京五元桥');
COMMIT;

2.2 Java相关类及配置文件

2.2.1 pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <artifactId>studycode</artifactId>
    <dependencies>
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
            <version>3.5.4</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.16.10</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.6</version>
        </dependency>
        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>1.2.12</version>
        </dependency>
        <dependency>
            <groupId>dom4j</groupId>
            <artifactId>dom4j</artifactId>
            <version>1.6.1</version>
        </dependency>
        <dependency>
            <groupId>jaxen</groupId>
            <artifactId>jaxen</artifactId>
            <version>1.1.6</version>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
        </dependency>
    </dependencies>
</project>

2.2.2 Java对象User

public class User implements Serializable {
    private Integer id;
    private String username;
    private Date birthday;
    private String sex;
    private String address;
    //get/set
    //toString()
}

2.2.3 sqlMapConfig.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<!--mybatis的主配置文件-->
<configuration>

    <properties resource="jdbcConfig.properties"/>
    <settings>
        <setting name="lazyLoadingEnabled" value="true"/>
        <setting name="aggressiveLazyLoading" value="false"/>
    </settings>
    
    <typeAliases>
        <typeAlias type="cn.surpass.domain.User" alias="user"/>
    </typeAliases>

    <!--配置环境-->
    <environments default="mysql">
        <!--配置环境-->
        <environment id="mysql">
            <!--配置事务类型-->
            <transactionManager type="JDBC"></transactionManager>
            <dataSource type="POOLED">
                <!--配置连接数据库的基本信息-->
                <property name="driver" value="${jdbc.driver}"/>
                <property name="url" value="${jdbc.url}"/>
                <property name="username" value="${jdbc.username}"/>
                <property name="password" value="${jdbc.password}"/>
            </dataSource>
        </environment>
    </environments>

    <!--指定映射配置文件的位置,映射配置文件指的是每个独立的配置文件-->
    <mappers>
        <package name="cn.surpass.skill.dao"/>
    </mappers>
</configuration>

2.2.4 Mapper文件

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="cn.surpass.skill.dao.UserDao">

</mapper>

2.2.5测试类

public class PlaceholderTest {
    private InputStream in;
    private SqlSession sqlSession;
    private SqlSessionFactory factory;
    @Before
    public void init() throws IOException {
        in= Resources.getResourceAsStream("SqlMapConfig.xml");
        factory = new SqlSessionFactoryBuilder().build(in);
        sqlSession = factory.openSession();
    }

    @After
    public void destroy()throws Exception{
        sqlSession.commit();
        sqlSession.close();
        in.close();
    }
    
    @Test
    public void test1(){
        
    }
}

2.2.5 jdbcConfig.properties

jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://IP:3306/mybatis?useUnicode=true&characterEncoding=utf8&rewriteBatchedStatements=true
jdbc.username=XXX
jdbc.password=XXX

3 初识Mybatis占位符

3.1 #{}类型

#{}占位符用来设置参数,参数的类型可以有3种,基本类型,自定义类型和map类型。基本类型作为参数,参数与占位符中的名称无关。自定义类型作为参数,自定义类中需要为为属性提供get方法(有必要吗?),如果没有提供get方法,那么会根据占位符中的名称去反射获取值,如果占位符中的名称和属性不一致,那么报ReflectionException。Map作为参数类型,key和占位符中的名称一致即可,如果名称不一致那么将会把null,传递到占位符中。

3.1.1 基本类型

上文提到基本类型参数与占位符中的名称无关,下面为了说明这个问题,我随便输出一串内容(不建议这么做),在实际开发过程中中我们一般用#{VALUE}来获取参数的值,如下:

上面我们传递了一个参数,如果我们想传递两个参数呢?对于面向对象的Java来说,我们可以封装对象,也可以将参数变为Map对象。然而我就不想这么做呢,有没有直接传递两个参数的做法呢?答案肯定是有的。看下面的例子:

讲到这里,不知大家是否发现没有,这里并没有在标签使用parameterType属性,Mybatis可以自动识别,这也是Mybatis的强大之处,能很好做到自动识别。在这里,有两个问题大家可以想一下:

1.如果没有resultType或者resultMap是否可以?(报错,)

org.apache.ibatis.exceptions.PersistenceException: 
### Error querying database.  Cause: org.apache.ibatis.executor.ExecutorException: A query was run and no Result Maps were found for the Mapped Statement 'cn.surpass.skill.dao.UserDao.queryUserByAddressAndSex'.  It's likely that neither a Result Type nor a Result Map was specified.
### The error may exist in cn/surpass/skill/dao/UserDao.xml
### The error may involve cn.surpass.skill.dao.UserDao.queryUserByAddressAndSex

2.大家把mapper的select标签改成insert、update或者delete标签是否可以?如下图,违反了mybatis-3-mapper.dtd约束了。

3.1.2 自定义类型

自定义类型作为参数,自定义类中需要为为属性提供get方法(有必要吗?),如果没有提供get方法,那么会根据占位符中的名称去反射获取值,如果占位符中的名称和属性不一致,那么报ReflectionException。我们先看一下简单的例子:

在本例中,我们终于见到了parameterType参数,他可以传递一个对象。通过对象的值我们解析所对应的属性。是不是很简单?到这里,我们仍然有几个问题需要考虑一下:

1.如果我们将User对象的getAddress()和getSex()方法删除,还能正常输出结果吗?通过上面的解释,好像是可以输出结果的。而实际确实如此。然而,如果我们将getAddress方法改成getHome()呢?好像也能正常输出。

所以我们猜想,即使有get方法,MyBatis也没有使用他,而是通过反射的方式直接设置值。

2.如果在接口类中的参数中我们加了Param注解,那么可以成功调用吗?看下图。

呵呵,最终还是被我玩坏会了。那问题怎么解决呢?其实很简单,在Mapper的#{}中加入user前缀就可以,这里我就不截图了,只把mapper的最终形式展现给大家。

<select id="queryUserByUserAddressAndSex" parameterType="user" resultType="user">
    SELECT * FROM USER WHERE ADDRESS = #{user.address} AND SEX = #{user.sex}
</select>

3.1.3 Map作为参数类型

Map作为参数类型,key和占位符中的名称一致即可,如果名称不一致那么将会把null,传递到占位符中。我们先看一个示例:

是不是很简单。不过这里仍然有几个问题需要明确一下(我的问题为啥这么多)?

1.如果在接口类中的参数中我们加了Param注解,那么可以成功调用吗?【不可以】

2.如果mapper的xml中没有parameterType参数可以吗?【可以】

感兴趣的读者可以自行尝试一下。

那么问题来了,如果我们定义的接口中传入的参数是表名、字段名、排序的类名呢?正如下面的三条Sql语句,显然#{}闲的无能为力了,那么${}就闪亮登场。

SELECT * FROM #{TABLE_NAME}
SELECT #{COLUMN_NAME} FROM USER
SELECT * FROM USER ORDER BY #{COLUMU}

3.2 ${}类型

我们按照上面的思路依次试一下${}是否也有基本类型、自定义类型和map类型,看看是否和#{}一致。

3.2.1 基本类型

这里我们直接贴代码,看看运行结果,如下图,结果输出正常。

3.2.2 自定义类型

3.2.3 Map作为参数类型

通过上面三个案例我们知道用法基本和#{}差不多,其他的特殊情况我就不去尝试了,大家有兴趣可以自己玩一把。

3.3 初识总结

针对上面#{}和${},我们分别截取打印的sql部分。

通过上面我们列举的大量案例,总结出如下几点:

1.对于#{},注意要Sql的“?”,我们知道Mybatis是通过类似jdbc的占位符实现了;而${}是通过字符串拼接的形式拼接。

2.既然${}通过字符串凭借的方式实现的,他极易容易引起Sql注入的问题。所以,我们在对于以上两个占位符使用的时候,应该要注意一下几点:

1)如果是要传递参数,比如where条件里的值,我们可以使用#{};

2)  如果涉及到列名、表名、排序,我们可以考虑使用${};

3)  如果是where条件后的列名判断,就坚决不能使用${},因为这也会导致Sql注入,如下举例。是不是一件很恐怖的事情。这一点需要大家注意。

SELECT * FROM USER WHERE ${COLUMN_NAME} = '李四';
--当COLUMN_NAME为‘1 = 1 OR 1’ ,就变成
SELECT * FROM USER WHERE 1 = 1 OR 1 = '李四';

4)MyBatis是支持OGNL表达式的,如下面的代码,我们提取用户所对应的组织ID可以这么写:#{org.id}

public class User{
    private String name;
    private Org org;
    //get and set
}

public class Org{
    private String id;
    //get and set
}

3.4 常见问题

之前在我接触的项目中,涉及到模糊匹配的操作,他们值这么处理的:

 <select id="queryByUserName" resultType="user">
     SELECT * FROM USER WHERE NAME LIKE '%${name}%'
 </select>

根据上面的总结,${}是不能放到where后面的,我们很容易发现里面存在的问题。会引起Sql注入问题,此时我们应该如下处理:

<!-- ORACLE 的处理方式-->
<select id="queryByUserName" resultType="user">
    SELECT * FROM USER WHERE NAME LIKE '%' || #{name} || '%'
</select>

<!-- MySql 的处理方式-->
<select id="queryByUserName" resultType="user">
    SELECT * FROM USER WHERE USERNAME LIKE CONCAT('%',#{VALUE},'%')
</select>

4. 源码浅析

4.1 GenericTokenParser初始化

  /**
   * Token解析器
   * @param openToken  起始分隔符 如果是#{},则openToken为#{,如果是${},则openToken为${
   * @param closeToken 终结分隔符 为}
   * @param handler 处理器
   */
  public GenericTokenParser(String openToken, String closeToken, TokenHandler handler) {
    this.openToken = openToken;
    this.closeToken = closeToken;
    this.handler = handler;
  }

通过上图我们发现会将openToken、closeToken和handler传到成员变量中。通过注释我们很好理解openToken为占位符的前半部分,即针对【#{}】,则表示【#{】;如果是【${}】,则表示【${】。

4.2 BindingTokenParser初始化

BindingTokenParser是TextSqlNode的内部类,通过TextSqlNode的重写方法apply实现

@Override
public boolean apply(DynamicContext context) {
    GenericTokenParser parser = createParser(new BindingTokenParser(context, injectionFilter));
    context.appendSql(parser.parse(text));
    return true;
}
private GenericTokenParser createParser(TokenHandler handler) {
    return new GenericTokenParser("${", "}", handler);
}

看到了吧,这里看到的${}的影子。

4.2 ParameterMappingTokenHandler初始化

ParameterMappingTokenHandler是SqlSourceBuilder的内部类,通过SqlSourceBuilder的方法parse实现。

  public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) {
    ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters);
    GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
    String sql = parser.parse(originalSql);
    return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
  }

是不是看到了#{}的影子?

4.3 工作原理

4.3.1 当我们调用GenericTokenParser#parse()的方法中

public String parse(String text) {
    if (text == null || text.isEmpty()) {
      return "";
    }
    // search open token
    int start = text.indexOf(openToken);
    if (start == -1) {
      return text;
    }
    char[] src = text.toCharArray();
    int offset = 0;
    final StringBuilder builder = new StringBuilder();
    StringBuilder expression = null;
    while (start > -1) {
      if (start > 0 && src[start - 1] == '\\') {
        // this open token is escaped. remove the backslash and continue.
        builder.append(src, offset, start - offset - 1).append(openToken);
        offset = start + openToken.length();
      } else {
        // found open token. let's search close token.
        if (expression == null) {
          expression = new StringBuilder();
        } else {
          expression.setLength(0);
        }
        builder.append(src, offset, start - offset);
        offset = start + openToken.length();
        int end = text.indexOf(closeToken, offset);
        while (end > -1) {
          if (end > offset && src[end - 1] == '\\') {
            // this close token is escaped. remove the backslash and continue.
            expression.append(src, offset, end - offset - 1).append(closeToken);
            offset = end + closeToken.length();
            end = text.indexOf(closeToken, offset);
          } else {
            expression.append(src, offset, end - offset);
            break;
          }
        }
        if (end == -1) {
          // close token was not found.
          builder.append(src, start, src.length - start);
          offset = src.length;
        } else {
          builder.append(handler.handleToken(expression.toString()));
          offset = end + closeToken.length();
        }
      }
      start = text.indexOf(openToken, offset);
    }
    if (offset < src.length) {
      builder.append(src, offset, src.length - offset);
    }
    return builder.toString();
  }

我们看到上面代码45行【builder.append(handler.handleToken(expression.toString()));】,这里调用了handler的handleToken方法。

4.3.2 对于BindingTokenParser#handleToken解析

    @Override
    public String handleToken(String content) {
      Object parameter = context.getBindings().get("_parameter");
      if (parameter == null) {
        context.getBindings().put("value", null);
      } else if (SimpleTypeRegistry.isSimpleType(parameter.getClass())) {
        context.getBindings().put("value", parameter);
      }
      Object value = OgnlCache.getValue(content, context.getBindings());
      String srtValue = value == null ? "" : String.valueOf(value); // issue #274 return "" instead of "null"
      checkInjection(srtValue);
      return srtValue;
    }

上诉代码9行【Object value = OgnlCache.getValue(content, context.getBindings());】使用Ognl表格式进行解析。这里看到了吧,表名${}里面是支持OGNL表达式的。接下来就是10-12行代码,直接把映射的值返回。

4.3.3 对于BindingTokenParser解析

    @Override
    public String handleToken(String content) {
      parameterMappings.add(buildParameterMapping(content));
      return "?";
    }

代码3行将参数所对应的值放到parameterMappings中,目的映射Sql的占位符"?",而程序的最终返回?。

4.3.4 到目前我们明白,#{}在组装sql的时候是通过?拼接的,这也印证了3.3章节sql"?"出现的原因,而${}直接返回映射所对应的值srtValue。

4.4 启示

通过上面代码的浅析,发现这里用到了策略的设计模式,根据实例化不同的handleToken实例,在调用handleToken方法得到不同的计算结果。值得我们学习和借鉴。

发布了88 篇原创文章 · 获赞 97 · 访问量 10万+

猜你喜欢

转载自blog.csdn.net/oYinHeZhiGuang/article/details/104965329