mybatis中的#{}与${}在原理上的区别

一、MyBatis中${}和#{}的区别
1.1 ${}和#{}演示
数据库数据:
在这里插入图片描述
dao接口:

List<User> findByUsername(String username);

List<User> findByUsername2(String username);

Mapper.xml:

<!-- 使用#{} -->
<select id="findByUsername" parameterType="java.lang.String" resultType="com.lscl.entity.User">
    select * from user where username like #{username}
</select>

<!-- 使用${},注意${}中的值必须要填value -->
<select id="findByUsername2" parameterType="java.lang.String" resultType="com.lscl.entity.User">
    select * from user where username like '%${value}%'
</select>

执行测试代码:

@Test
public void findByUsername() throws Exception {
    InputStream in = Resources.getResourceAsStream("SqlMapConfig.xml");

    SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();

    SqlSessionFactory factory = builder.build(in);

    // true:自动提交
    SqlSession session = factory.openSession(true);

    UserDao userDao = session.getMapper(UserDao.class);

    List<User> userList = userDao.findByUsername("%小%");
    List<User> userList2 = userDao.findByUsername2("小");

    System.out.println("userList: ");
    for (User user : userList) {
        System.out.println(user);
    }

    System.out.println("userList2: ");

    for (User user : userList2) {
        System.out.println(user);
    }

    session.close();
    in.close();
}

查看执行结果:
在这里插入图片描述
发现都能够查询出来

1.2 SQL注入问题
${}会产生SQL注入,#{}不会产生SQL注入问题

我们做一个测试

List<User> userList2 = userDao.findByUsername2(" aaa' or 1=1 -- ");

System.out.println("userList2: ");

for (User user : userList2) {
    System.out.println(user);
}

查询生成的SQL语句:
在这里插入图片描述
我们传递的参数是aaa’ or 1=1 --,导致查询出来了全部的数据。

大家可以想象一下,如果我是要根据id删除呢?

delete from user where id='${value}'

如果我传递的是:1’ or 1=1; --,结果会是什么样,我想大家应该已经知道了。

我这里id是Integer类型,不好测试,就不带大家测试了,大家有兴趣可以自己私下测试。

如果上面使用的是#{}就不会出现SQL注入的问题了
在这里插入图片描述
1.3 ${}和#{}的区别
#{}匹配的是一个占位符,相当于JDBC中的一个?,会对一些敏感的字符进行过滤,编译过后会对传递的值加上双引号,因此可以防止SQL注入问题。

匹 配 的 是 真 实 传 递 的 值 , 传 递 过 后 , 会 与 s q l 语 句 进 行 字 符 串 拼 接 。 {}匹配的是真实传递的值,传递过后,会与sql语句进行字符串拼接。 sql{}会与其他sql进行字符串拼接,不能预防sql注入问题。

查看#{}和${}生成的SQL语句:
在这里插入图片描述

String abc=123;

#{abc}="123"

${value}=123;

1.4 #{}底层是如何防止SQL注入的?
1.4.1 网上的答案
网上关于这类问题非常多,总结出来就两个原因:

1)#{}底层采用的是PreparedStatement,会预编译,因此不会产生SQL注入问题;

其实预编译是MySQL自己本身的功能,和PreparedStatement没关系;而且预编译也不是咱们理解的那个预编译,再者PreparedStatement底层默认根本没有用到预编译(要我们手动开启)!详细往下看

2)#{}不会产生字符串拼接, 会 产 生 字 符 串 拼 接 , 因 此 {}会产生字符串拼接,因此 {}会出现SQL注入问题;

这两个答案都经不起深究,最终答案也只是停留在表面,也没人知道具体是为什么。

1.4.2 为什么能防止SQL注入?
我们翻开MySQL驱动的源码一看究竟;

打开PreparedStatement类的setString()方法(MyBatis在#{}传递参数时,是借助setString()方法来完成,${}则不是):
在这里插入图片描述
setString()方法全部源码:

public void setString(int parameterIndex, String x) throws SQLException {
        synchronized(this.checkClosed().getConnectionMutex()) {
            if (x == null) {
                this.setNull(parameterIndex, 1);
            } else {
                this.checkClosed();
                int stringLength = x.length();
                StringBuilder buf;
                if (this.connection.isNoBackslashEscapesSet()) {
                    boolean needsHexEscape = this.isEscapeNeededForString(x, stringLength);
                    Object parameterAsBytes;
                    byte[] parameterAsBytes;
                    if (!needsHexEscape) {
                        parameterAsBytes = null;
                        buf = new StringBuilder(x.length() + 2);
                        buf.append(''');
                        buf.append(x);
                        buf.append(''');
                        if (!this.isLoadDataQuery) {
                            parameterAsBytes = StringUtils.getBytes(buf.toString(), this.charConverter, this.charEncoding, this.connection.getServerCharset(), this.connection.parserKnowsUnicode(), this.getExceptionInterceptor());
                        } else {
                            parameterAsBytes = StringUtils.getBytes(buf.toString());
                        }

                        this.setInternal(parameterIndex, parameterAsBytes);
                    } else {
                        parameterAsBytes = null;
                        if (!this.isLoadDataQuery) {
                            parameterAsBytes = StringUtils.getBytes(x, this.charConverter, this.charEncoding, this.connection.getServerCharset(), this.connection.parserKnowsUnicode(), this.getExceptionInterceptor());
                        } else {
                            parameterAsBytes = StringUtils.getBytes(x);
                        }

                        this.setBytes(parameterIndex, parameterAsBytes);
                    }

                    return;
                }

                String parameterAsString = x;
                boolean needsQuoted = true;
                if (this.isLoadDataQuery || this.isEscapeNeededForString(x, stringLength)) {
                    needsQuoted = false;
                    buf = new StringBuilder((int)((double)x.length() * 1.1D));
                    buf.append(''');

                    for(int i = 0; i < stringLength; ++i) {  //遍历字符串,获取到每个字符
                        char c = x.charAt(i);
                        switch(c) {
                        case 'u0000':
                            buf.append('\');
                            buf.append('0');
                            break;
                        case 'n':
                            buf.append('\');
                            buf.append('n');
                            break;
                        case 'r':
                            buf.append('\');
                            buf.append('r');
                            break;
                        case 'u001a':
                            buf.append('\');
                            buf.append('Z');
                            break;
                        case '"':
                            if (this.usingAnsiMode) {
                                buf.append('\');
                            }

                            buf.append('"');
                            break;
                        case ''':
                            buf.append('\');
                            buf.append(''');
                            break;
                        case '\':
                            buf.append('\');
                            buf.append('\');
                            break;
                        case '¥':
                        case '₩':
                            if (this.charsetEncoder != null) {
                                CharBuffer cbuf = CharBuffer.allocate(1);
                                ByteBuffer bbuf = ByteBuffer.allocate(1);
                                cbuf.put(c);
                                cbuf.position(0);
                                this.charsetEncoder.encode(cbuf, bbuf, true);
                                if (bbuf.get(0) == 92) {
                                    buf.append('\');
                                }
                            }

                            buf.append(c);
                            break;
                        default:
                            buf.append(c);
                        }
                    }

                    buf.append(''');
                    parameterAsString = buf.toString();
                }

                buf = null;
                byte[] parameterAsBytes;
                if (!this.isLoadDataQuery) {
                    if (needsQuoted) {
                        parameterAsBytes = StringUtils.getBytesWrapped(parameterAsString, ''', ''', this.charConverter, this.charEncoding, this.connection.getServerCharset(), this.connection.parserKnowsUnicode(), this.getExceptionInterceptor());
                    } else {
                        parameterAsBytes = StringUtils.getBytes(parameterAsString, this.charConverter, this.charEncoding, this.connection.getServerCharset(), this.connection.parserKnowsUnicode(), this.getExceptionInterceptor());
                    }
                } else {
                    parameterAsBytes = StringUtils.getBytes(parameterAsString);
                }

                this.setInternal(parameterIndex, parameterAsBytes);
                this.parameterTypes[parameterIndex - 1 + this.getParameterIndexOffset()] = 12;
            }

        }
    }

我们执行#{}的查询语句,打断点观察:
在这里插入图片描述
最终传递的参数如下:
在这里插入图片描述
最终传递的参数为:‘aaa’ or 1=1 –

咱们在数据库中执行如下SQL语句(肯定是查询不到数据的):
在这里插入图片描述
如果把PreparedStatement加的那根"/"去掉呢?我们执行SQL试试:
在这里插入图片描述
我们也可以通过MySQL的日志来观察#{}和${}产生的SQL语句来分析问题:

1)开启MySQL日志:

在MySQL配置文件中的[mysqld]下增加如下配置:

# 是否开启mysql日志  0:关闭(默认值) 1:开启
general-log=1

# mysql 日志的存放位置
general_log_file="D:/query.log"

在这里插入图片描述
2)重启MySQL服务(要以管理员身份运行):
在这里插入图片描述

net stop mysql

net start mysql

使用mybatis分别执行如下两条SQL语句:
在这里插入图片描述
查看MySQL日志:
在这里插入图片描述
1.5 #{}和KaTeX parse error: Expected 'EOF', got '#' at position 11: {}的应用场景 既然#̲{}比{}好那么多,那为什么还要有${}这个东西存在呢?干脆都用#{}不就万事大吉吗?

其实不是的, 也 有 用 武 之 地 , 我 们 都 知 道 {}也有用武之地,我们都知道 {}会产生字符串拼接,来生成一个新的字符串

1.5.1 ${}和#{}用法上的区别
例如现在要进行模糊查询,查询user表中姓张的所有员工的信息

sql语句为:select * from user where name like ‘张%’

此时如果传入的参数是 “张”

如果使用 : s e l e c t ∗ f r o m u s e r w h e r e n a m e l i k e ′ {}:select * from user where name like ' selectfromuserwherenamelike{value}%’

生成的sql语句:select * from user where name like ‘张%’

如果使用#{}:select * from user where name like #{value}"%"

生成的sql语句:select * from user where name like ‘张’"%"

如果传入的参数是 “张%”

使用#{}:select * from user where name like #{value}

生成的sql语句:select * from user where name like ‘张%’

使用 : s e l e c t ∗ f r o m u s e r w h e r e n a m e l i k e ′ {}:select * from user where name like ' selectfromuserwherenamelike{value}’

生成的sql语句:select * from user where name like ‘张%’

通过上面的SQL语句我们能够发现#{}是会加上双引号,而${}匹配的是真实的值。

还有一点就是如果使用 的 话 , 里 面 必 须 要 填 v a l u e , 即 : {}的话,里面必须要填value,即: value{value},#{}则随意

1.5.2 什么情况下用${}?
场景举例:
在这里插入图片描述
代码测试:
在这里插入图片描述
执行之后,发现执行成功
在这里插入图片描述

我们可以切换一下,把${}改成#{},会出现SQL语法错误的异常
在这里插入图片描述

1.6 总结
1.6.1 SQL注入问题
MyBatis的#{}之所以能够预防SQL注入是因为底层使用了PreparedStatement类的setString()方法来设置参数,此方法会获取传递进来的参数的每个字符,然后进行循环对比,如果发现有敏感字符(如:单引号、双引号等),则会在前面加上一个’/'代表转义此符号,让其变为一个普通的字符串,不参与SQL语句的生成,达到防止SQL注入的效果。

其次${}本身设计的初衷就是为了参与SQL语句的语法生成,自然而然会导致SQL注入的问题(不会考虑字符过滤问题)。

1.6.2 #{}和KaTeX parse error: Expected 'EOF', got '#' at position 10: {}用法总结 1)#̲{}在使用时,会根据传递进来的…{}则不会,我们需要手动加

2)在传递一个参数时,我们说了#{}中可以写任意的值, 则 必 须 使 用 v a l u e ; 即 : {}则必须使用value;即: 使value{value}

3)#{}针对SQL注入进行了字符过滤,${}则只是作为普通传值,并没有考虑到这些问题

4)#{}的应用场景是为给SQL语句的where字句传递条件值,${}的应用场景是为了传递一些需要参与SQL语句语法生成的值。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: MyBatis 是一款开源的持久层框架,用于简化 Java 应用程序数据存储层的开发。它使用简单的 XML 或注解配置和映射原语,将接口和 Java 的 POJO(Plain Old Java Object,普通 Java 对象)映射到数据库的记录。MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。MyBatis 可以使用简单的 XML 或注解来配置和映射原语,将接口和 Java 的 POJO 映射到数据库的记录。 ### 回答2: MyBatis是一个开源的持久层框架,它可以与关系型数据库进行交互。在使用MyBatis时,我们需要定义一个映射文件,这个映射文件定义了数据库表与Java对象之间的映射关系。 在映射文件,我们可以使用SQL语句来进行数据库的操作,包括增删改查等。MyBatis提供了丰富的标签来方便我们编写SQL语句,例如: - select标签用于查询操作,我们可以指定查询条件、排序方式等。 - insert标签用于插入操作,我们可以指定插入的字段和值。 - update标签用于更新操作,我们可以指定更新的字段和条件。 - delete标签用于删除操作,我们可以指定删除的条件。 除了上述标签外,MyBatis还提供了动态SQL的支持,可以根据不同的条件决定是否执行特定的SQL语句块。这使得我们可以灵活地构建复杂的SQL语句,满足各种需求。 在使用MyBatis时,我们需要配置数据源和SqlSessionFactory,数据源用于连接数据库,SqlSessionFactory用于创建SqlSession,通过SqlSession我们可以执行具体的SQL操作。 MyBatis还提供了事务管理的支持,可以确保数据库的一致性。我们可以通过配置来指定事务的隔离级别、提交方式等。 总的来说,MyBatis是一个功能强大、灵活易用的持久层框架,它可以帮助我们更加方便地进行数据库操作。无论是简单的查询还是复杂的多表关联,MyBatis都能提供满足需求的解决方案。 ### 回答3: MyBatis是一种功能强大的开源持久化框架,用于与关系型数据库进行交互。它提供了一种将 SQL 语句从代码分离出来的方式,使得开发人员可以更好地管理和维护 SQL 语句。 MyBatis的工作原理是通过配置文件来绑定 Java 对象和数据库表,从而实现数据的增删改查操作。在配置文件,我们可以定义各种 SQL 语句,并通过注解或xml方式将其与 Java 方法绑定起来。这样,我们就可以通过调用方法,来执行相应的 SQL 语句。 使用MyBatis时,我们首先需要配置数据源,并将其配置到MyBatis的配置文件。在配置文件,我们还可以配置一些 MyBatis 的全局属性,如类型别名、数据库方言等。 在代码,我们可以通过编写相应的映射接口和映射文件来完成数据的访问。映射接口的方法应该与映射文件定义的 SQL 语句一一对应,通过调用方法来执行相应的 SQL 语句。 MyBatis还提供了强大的动态 SQL 支持,可以根据不同的条件生成不同的 SQL 语句。这样,我们就可以根据具体的业务需求动态地拼接 SQL 语句,从而提高 SQL 的灵活性和可复用性。 总的来说,MyBatis是一种简单易用、灵活高效的持久化框架,广泛应用于 Java EE 开发。它能够帮助开发人员简化数据库访问的工作,并提升开发效率和系统性能。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值