1、PreparedStatement效率为什么高

PreparedStatement会讲SQL语句进行预编译,每次执行的时候只需要将参数设置给相应的占位符就可以运行。而使用Statement时,SQL语句时每次都要进行编译,所以PreparedStatement的效率相对较高。

1.1、具体分析

SQL在DB中的执行步骤

  1. 将SQL 语句转化为DB形式(语法树结构)
  2. 检查语法
  3. 检查语义
  4. 准备执行计划(也是优化的过程,这个步骤比较重要,关系到你SQL文的效率)
  5. 设置运行时的参数
  6. 执行查询并取得结果

所谓的==预编译==就是同样的SQL【包括不同参数的】,1-4步骤只在第一次执行,所以大大提高了执行效率。

2、PreparedStatement如何防止SQL注入

使用Statement时,如果SQL当中出现了'或者-等符号时,需要使用转义字符来进行转义,而在PreparedStatement当中,如果占位符的值当中有这些符号,PreparedStatement会自动的进行转义。

  1. 不使用PreparedStatement的set方法设置参数(效果跟Statement相似,相当于执行静态SQL)
String param = "'test' or 1=1";
String sql = "select file from file where name = " + param; // 拼接SQL参数
PreparedStatement preparedStatement = connection.prepareStatement(sql);
ResultSet resultSet = preparedStatement.executeQuery();
System.out.println(resultSet.next());

输出结果为true,DB中执行的SQL为

-- 永真条件1=1成为了查询条件的一部分,可以返回所有数据,造成了SQL注入问题
select file from file where name = 'test' or 1=1 
  1. 使用PreparedStatement的set方法设置参数
String param = "'test' or 1=1";
String sql = "select file from file where name = ?";
PreparedStatement preparedStatement = connection.prepareStatement(sql);
preparedStatement.setString(1, param);
ResultSet resultSet = preparedStatement.executeQuery();
System.out.println(resultSet.next());

输出结果为false,DB中执行的SQL为

select file from file where name = '\'test\' or 1=1'

我们可以看到输出的SQL文是把==整个参数用引号包起来,并把参数中的引号作为转义字符==,从而避免了参数也作为条件的一部分。

具体分析

查看源码:com.mysql.cj.ClientPreparedQueryBindings#setString

public void setString(int parameterIndex, String x) throws SQLException {
        synchronized (checkClosed().getConnectionMutex()) {
            // if the passed string is null, then set this column to null
            if (x == null) {
                setNull(parameterIndex, Types.CHAR);
            } else {
                checkClosed();

                int stringLength = x.length();

                if (this.connection.isNoBackslashEscapesSet()) {
                    // Scan for any nasty chars                
                    // 判断是否需要转义处理(比如包含引号,换行等字符)
                    boolean needsHexEscape = isEscapeNeededForString(x, stringLength);                
                    // 如果不需要转义,则在两边加上单引号
                    if (!needsHexEscape) {
                        byte[] parameterAsBytes = null;

                        StringBuilder quotedString = new StringBuilder(x.length() + 2);
                        quotedString.append('\'');
                        quotedString.append(x);
                        quotedString.append('\'');

                        ...
                    } else {
                        ...
                    }
                }

                String parameterAsString = x;
                boolean needsQuoted = true;
                // 如果需要转义,则做转义处理
                if (this.isLoadDataQuery || isEscapeNeededForString(x, stringLength)) {
                ...
                }
}