目录
4.2 ParameterMappingTokenHandler初始化
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方法得到不同的计算结果。值得我们学习和借鉴。