Java JDBC下执行SQL的不同方式、参数化预编译防御

相关学习资料

http://zh.wikipedia.org/wiki/Java数据库连接
http://lavasoft.blog.51cto.com/62575/20588
http://blog.csdn.net/cxwen78/article/details/6863696
http://www.ibm.com/developerworks/cn/java/jdbc-objects/index.html?ca=drs
http://www.moon-soft.com/doc/37897.htm

 

目录

1. Java JDBC简介
2. Java JDBC下执行SQL的不同方式
3. Java JDBC编程实践

 

1. Java JDBC简介

数据库驱动程序是JDBC程序和数据库之间的转换层,数据库驱动程序负责将JDBC调用映射成特定的数据库调用,类似PHP中的"数据库抽象层"

http://www.php.net/manual/zh/refs.database.vendors.php

使用Java JDBC API进行编程,可以为多种关系数据库提供统一访问,为我们带来跨平台、跨数据库系统的好处

总的来说,JDBC驱动通常有如下4种类型

1. JDBC-ODPC桥:
它将JDBC API映射到ODPC API。再让JDBC-ODPC调用数据库本地驱动代码(也就是数据库厂商提供的数据库操作二进制代码库,例如Oracle中的oci.dll)
2. 本地API驱动
直接将JDBC API映射成数据库特定的客户端API,即通过客户端加载数据库厂商提供的本地代码库(C/C++等) 
3. 网络协议驱动
这种类型的驱动给客户端提供了一个网络API,客户端上的JDBC驱动程序使用套接字(Socket)来调用服务器上的中间件程序,后者在将其请求转化为所需的具体API调用。
4. 本地协议驱动
这种类型的驱动使用Socket,直接在客户端和数据库间通信。它是一种直接与数据库实例交互的JDBC
这种驱动是智能的,它知道数据库使用的底层协议,也是目前最主流使用的JDBC驱动,我们本章的重点就是它

JDBC编程(连接数据库)步骤

1) 加载数据库驱动
通常我们使用Class类的forName()静态方法来加载驱动(由各个数据库厂商自己实现)
Class.forName("com.mysql.jdbc.Driver");
Class.forName("oracle.jdbc.driver.OracleDriver");
"com.mysql.jdbc.Driver""oracle.jdbc.driver.OracleDriver"代表了数据库驱动类对应的字符串
2) 通过DriverManager获取数据库连接
DriverManager.getConnection(String url, String user, String pass);
    2.1) url: 数据库连接字符串
        2.1.1) Mysql
        jdbc:mysql://hostname:port/databasename
        2.1.2) Oracle
        jdbc:oracle:thin:@hostname:port:databasename
    2.2) user: 数据库的系统用户名
    2.3) pass: 数据库的系统密码
3) 通过Connection对象创建Statement对象,Connection创建Statement对象的方法有如下3个
    3.1) createStatement(String sql):创建基本的Statement对象
    3.2) prepareStatement(String sql): 根据传入的SQL语句创建预编译的Statement对象
    3.3) prepareCall(String sql): 根据传入的SQL语句创建CallableStatement对象
4) 使用Statement执行SQL语句
所有的Statement都有如下3个方法来执行SQL语句
    4.1) execute(): 可以执行任何SQL语句,但比较麻烦
    4.2) executeUpdate(): 主要用于执行DML和DDL语句。执行DML语句返回受SQL影响的行数,执行DDL语句返回0
    4.3) executeQuery(): 只能执行查询语句,执行后返回代表查询结果的ResultSet对象
5) 操作结果集
如果执行的SQL语句是查询语句,则执行结果将返回一个ResultSet对象,该对象里保存了SQL语句查询的结果。程序可以通过操作该ResultSet对象来取出查询结果。ResultSet对象主要提供了如
下方法
5.1) 移动记录指针的方法 5.1.1) next() 5.1.2) previous() 5.1.3) first() 5.1.4) last() 5.1.5) beforeFirst() 5.1.6) afterLast() 5.1.7) absolute() 5.2) 获取指针指向的某行的"特定的列值" 5.2.1) getInt()
     5.2.2) getString()
     ...
该方法既可以使用列索引作为参数,也可以使用列名作为参数
6) 回收数据库资源 包括关闭ResultSet、Statement、Connection等资源

 

2. Java JDBC下执行SQL的不同方式

JDBC是一个相对比较底层的API接口,它只提供给我们一个执行原生SQL语句的输入接口。所以和JDBC的API接口都定义在

java.sql
javax.sql

我们知道,Java JDBC编程中有3种方法进行DDL、DML语句的执行

1. createStatement
返回Statement
    1) execute() 
    2) executeUpdate()
    3) executeQuery()
2. prepareStatement
返回PrepareStatement
    1) execute() 
    2) executeUpdate()
    3) executeQuery()
3. prepareCall
返回CallableStatement

我们来逐一学习

 

0x1: 基本的Statement对象

Statement提供了3个方法来执行SQL语句,它们都可以用于执行DDL(执行后返回值为0)、DML(执行后返回受影响行数)语句

使用execute执行DDL、DML语句

Statement的execute()方法几乎可以执行任何原生SQL语句,但它执行SQL语句时比较麻烦(它无法直接返回一个ResultSet,而是需要我们手工再次获取),通常情况下,使用executeQuery()、或executeUpdate()方法更加简单。但如果程序员不清楚SQL语句的类型,则只能使用execute()方法来执行该SQL语句了。

注意,使用execute()方法执行SQL语句的返回值"只是"boolean值,它表明了执行该SQL语句是否返回了ResultSet对象,而我们要得到具体的ResultSet对象还需要额外的方法

1. getResult(): 获取该Statement执行查询语句所返回的ResultSet对象
2. getUpdateCount(): 获取该Statement执行DML语句所影响的记录行数

code:

import java.util.*;
import java.io.*;
import java.sql.*;

public class ExecuteSQL
{
    private String driver;
    private String url;
    private String user;
    private String pass;
    public void initParam(String paramFile)throws Exception
    {
        // 使用Properties类来加载属性文件
        Properties props = new Properties();
        props.load(new FileInputStream(paramFile));
        driver = props.getProperty("driver");
        url = props.getProperty("url");
        user = props.getProperty("user");
        pass = props.getProperty("pass");
    }
    public void executeSql(String sql)throws Exception
    {
        // 加载驱动
        Class.forName(driver);
        try(
            // 获取数据库连接
            Connection conn = DriverManager.getConnection(url
                , user , pass);
            // 使用Connection来创建一个Statement对象
            Statement stmt = conn.createStatement())
        {
            // 执行SQL,返回boolean值表示是否包含ResultSet
            boolean hasResultSet = stmt.execute(sql);
            // 如果执行后有ResultSet结果集
            if (hasResultSet)
            {
                try(
                    // 获取结果集
                    ResultSet rs = stmt.getResultSet())
                {
                    // ResultSetMetaData是用于分析结果集的元数据接口
                    ResultSetMetaData rsmd = rs.getMetaData();
                    int columnCount = rsmd.getColumnCount();
                    // 迭代输出ResultSet对象
                    while (rs.next())
                    {
                        // 依次输出每列的值
                        for (int i = 0 ; i < columnCount ; i++ )
                        {
                            System.out.print(rs.getString(i + 1) + "\t");
                        }
                        System.out.print("\n");
                    }
                }
            }
            else
            {
                System.out.println("该SQL语句影响的记录有" 
                    + stmt.getUpdateCount() + "");
            }
        }
    }
    public static void main(String[] args) throws Exception
    {
        ExecuteSQL es = new ExecuteSQL();
        es.initParam("mysql.ini");
        System.out.println("------执行删除表的DDL语句-----");
        es.executeSql("drop table if exists my_test");
        System.out.println("------执行建表的DDL语句-----");
        es.executeSql("create table my_test"
        + "(test_id int auto_increment primary key, "
        + "test_name varchar(255))");
        System.out.println("------执行插入数据的DML语句-----");
        es.executeSql("insert into my_test(test_name) "
        + "select student_name from student_table");
        System.out.println("------执行查询数据的查询语句-----");
        es.executeSql("select * from my_test");
    }
}

使用executeUpdate执行DDL、DML语句

import java.util.*;
import java.io.*;
import java.sql.*;

public class ExecuteDDL
{
    private String driver;
    private String url;
    private String user;
    private String pass;
    public void initParam(String paramFile) throws Exception
    {
        // 使用Properties类来加载属性文件
        Properties props = new Properties();
        props.load(new FileInputStream(paramFile));
        driver = props.getProperty("driver");
        url = props.getProperty("url");
        user = props.getProperty("user");
        pass = props.getProperty("pass");
    }
    public void createTable(String sql)throws Exception
    {
        // 加载驱动
        Class.forName(driver);
        try(
        // 获取数据库连接
        Connection conn = DriverManager.getConnection(url , user , pass);
        // 使用Connection来创建一个Statment对象
        Statement stmt = conn.createStatement())
        {
            // 执行DDL,创建数据表
            stmt.executeUpdate(sql);
        }
    }
    public static void main(String[] args) throws Exception
    {
        ExecuteDDL ed = new ExecuteDDL();
        ed.initParam("mysql.ini");
        ed.createTable("create table jdbc_test "
            + "( jdbc_id int auto_increment primary key, " 
            + "jdbc_name varchar(255), "
            + "jdbc_desc text);");
        System.out.println("-----建表成功-----");
    }
}

从代码中我们可以看到,我们并没有把数据库连接信息写在程序里,而是使用一个mysql.ini文件来保存数据库连接信息,这样当需要把程序从开发环境移植到生产环境时,无须修改源代码,只需要修改mysql.ini配置文件即可

mysql.ini:

driver=com.mysql.jdbc.Driver
url=jdbc:mysql://127.0.0.1:3306/select_test
user=root
pass=111

使用executeQuery执行DDL、DML语句

import java.sql.*;

public class ConnMySql
{
    public static void main(String[] args) throws Exception
    {
        // 1.加载驱动,使用反射的知识,现在记住这么写。
        Class.forName("com.mysql.jdbc.Driver");
        try(
            // 2.使用DriverManager获取数据库连接,
            // 其中返回的Connection就代表了Java程序和数据库的连接
            // 不同数据库的URL写法需要查驱动文档知道,用户名、密码由DBA分配
            Connection conn = DriverManager.getConnection(
                "jdbc:mysql://127.0.0.1:3306/company"
                , "root" , "111");
            // 3.使用Connection来创建一个Statment对象
            Statement stmt = conn.createStatement();
            // 4.执行SQL语句
            /*
            Statement有三种执行sql语句的方法:
            1 execute 可执行任何SQL语句。- 返回一个boolean值,
              如果执行后第一个结果是ResultSet,则返回true,否则返回false
            2 executeQuery 执行Select语句 - 返回查询到的结果集
            3 executeUpdate 用于执行DML语句。- 返回一个整数,
              代表被SQL语句影响的记录条数
            */
            ResultSet rs = stmt.executeQuery("select *"
                + " from p8_ad_user"))
        {
            // ResultSet有系列的getXxx(列索引 | 列名),用于获取记录指针
            // 指向行、特定列的值,不断地使用next()将记录指针下移一行,
            // 如果移动之后记录指针依然指向有效行,则next()方法返回true。
            while(rs.next())
            {
                System.out.println(rs.getInt(1) + "\t"
                    + rs.getString(2) + "\t"
                    + rs.getString(3) + "\t"
                    + rs.getString(4));
            }
        }
    }
}

 

0x2: prepareStatement预编译对象

JDBC中的这个类对于安全人员、和黑客都需要重点关注,它不仅可以提供SQL执行性能,同时还有防御SQL注入的功能。

当我们的业务层中和数据库相关的代码经常要执行一些相似度很高的SQL语句,它们的结果基本相似,只是插入时插入的值不同而已

insert into student_table values(null, "LittleHann", 1);
insert into student_table values(null, "LittleHann", 2);

在这种情况下,我们可以使用带占位符(?)参数的SQL语句来代替它

insert into student_table values(null, ?, ?);

为了满足这种功能,JDBC提供了prepareStatement接口,它是Statement接口的子接口,它可以预编译SQL语句,预编译后的SQL语句被存储在prepareStatement对象中,然后可以使用该对象多次高效地执行该语句。prepareStatement同样也使用execute()、executeUpdate()、executeQuery()来执行SQL语句,但这三个方法无须参数,因为prepareStatement在创建的时候已经存储了预编译的SQL语句,在执行SQL语句的时候只要传入参数值即可(setXxx)

Statement和prepareStatement的性能对比

import java.util.*;
import java.io.*;
import java.sql.*;

public class PreparedStatementTest
{
    private String driver;
    private String url;
    private String user;
    private String pass;
    public void initParam(String paramFile)throws Exception
    {
        // 使用Properties类来加载属性文件
        Properties props = new Properties();
        props.load(new FileInputStream(paramFile));
        driver = props.getProperty("driver");
        url = props.getProperty("url");
        user = props.getProperty("user");
        pass = props.getProperty("pass");
        // 加载驱动
        Class.forName(driver);
    }
    public void insertUseStatement()throws Exception
    {
        long start = System.currentTimeMillis();
        try(
            // 获取数据库连接
            Connection conn = DriverManager.getConnection(url
                , user , pass);
            // 使用Connection来创建一个Statment对象
            Statement stmt = conn.createStatement())
        {
            // 需要使用100条SQL语句来插入100条记录
            for (int i = 0; i < 1000 ; i++ )
            {
                stmt.executeUpdate("insert into student_table values("
                    + " null ,'姓名" + i + "' , 1)");
            }
            System.out.println("使用Statement费时:" 
                + (System.currentTimeMillis() - start));
        }
    }
    public void insertUsePrepare()throws Exception
    {
        long start = System.currentTimeMillis();
        try(
            // 获取数据库连接
            Connection conn = DriverManager.getConnection(url
                , user , pass);
            // 使用Connection来创建一个PreparedStatement对象
            PreparedStatement pstmt = conn.prepareStatement(
                "insert into student_table values(null,?,1)"))

        {
            // 100次为PreparedStatement的参数设值,就可以插入100条记录
            for (int i = 0; i < 1000 ; i++ )
            {
                pstmt.setString(1 , "姓名" + i);
                pstmt.executeUpdate();
            }
            System.out.println("使用PreparedStatement费时:" 
                + (System.currentTimeMillis() - start));
        }
    }
    public static void main(String[] args) throws Exception
    {
        PreparedStatementTest pt = new PreparedStatementTest();
        pt.initParam("mysql.ini");
        pt.insertUseStatement();
        pt.insertUsePrepare();
    }
}

mysql.ini:

driver=com.mysql.jdbc.Driver
url=jdbc:mysql://127.0.0.1:3306/select_test
user=root
pass=111

result:

使用Statement费时:71269
使用PreparedStatement费时:69226

除了性能上的优势,prepareStatement还有另一个优势: 无须在SQL语句中"拼接"SQL参数,从安全的角度来看,这就从很大程序上避免了"SQL注入"的发生。使用prepareStatement时,所有的参数都变成了"问号占位符",也就避免了数据和代码的混淆(这是造成注入的根本原因)

关于使用参数化预编译防御SQL注入,这里要注意几点:

1. 参数化预编译之所以能防御住SQL注入,只要是基于以下2点:
    1) setString(): WEB程序接收字符串的场景
    将用户输入的参数全部强制转换为字符串,并进行适当的转义,防止了闭合的产生
    2) setInt(): WEB程序接收整型的场景
    将用户输入的非整型参数强制转换为整型,并去除潜在的"非整型注入字符",类似与PHP中的intVal()防御思路
2. 并不是说使用了参数化预编译方法执行SQL,就不会有注入的发生了,当WEB系统和DataBase系统的字符集配置不当,可能会导致宽字节注入的发生

 

0x3: prepareCall存储过程对象

调用存储过程可以使用CallableStatement,程序员通过Connection的prepareCall()方法创建CallableStatement对象

创建存储过程

delimiter //
create procedure add_pro(a int, b int, out sum int)
begin
set sum = a + b
end;
//

通过Connection的prepareCall()方法创建CallableStatement对象时需要传入"调用存储过程的SQL命令语句":

cstmt = conn.prepareCall("{call add_pro(?, ?, ?)}");

存储过程的参数既有:

1. 传入参数
java程序必须为这些参数传入值,可以通过CallableStatement的setXxx()方法为"传入参数"设置值
2. 传出参数
java程序可以通过该参数获取存储过程里的值,CallableStatement需要调用registerOutParameter()方法来注册该参数,执行结束后调用CallableStatement对象的getXxx(int index)
方法来获取指定传出参数的值

code:

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import java.util.*;
import java.io.*;
import java.sql.*;

public class CallableStatementTest
{
    private String driver;
    private String url;
    private String user;
    private String pass;
    public void initParam(String paramFile)throws Exception
    {
        // 使用Properties类来加载属性文件
        Properties props = new Properties();
        props.load(new FileInputStream(paramFile));
        driver = props.getProperty("driver");
        url = props.getProperty("url");
        user = props.getProperty("user");
        pass = props.getProperty("pass");
    }
    public void callProcedure()throws Exception
    {
        // 加载驱动
        Class.forName(driver);
        try(
            // 获取数据库连接
            Connection conn = DriverManager.getConnection(url
                , user , pass);
            // 使用Connection来创建一个CallableStatment对象
            CallableStatement cstmt = conn.prepareCall(
                "{call add_pro(?,?,?)}"))
        {
            cstmt.setInt(1, 4);
            cstmt.setInt(2, 5);
            // 注册CallableStatement的第三个参数是int类型
            cstmt.registerOutParameter(3, Types.INTEGER);
            // 执行存储过程
            cstmt.execute();
            // 获取,并输出存储过程传出参数的值。
            System.out.println("执行结果是: " + cstmt.getInt(3));
        }
    }
    public static void main(String[] args) throws Exception
    {
        CallableStatementTest ct = new CallableStatementTest();
        ct.initParam("mysql.ini");
        ct.callProcedure();
    }
}

 

3. Java JDBC编程实践

0x1: 下载MySQL支持JDBC的驱动程序

我们前面说过,Java JDBC现在主流的做法是"本地协议驱动",为此,Java需要借助不同厂商提供的数据库驱动(本质上是一个JAR包)来和数据库进行连接,我们这里以Mysql为例,

前往MySQL官网(http://www.mysql.com/products/connector/ )下载驱动程序,,MySQL针对不同的平台提供了不同的连接器,我们需要的是JDBC Driver for MySQL (Connector/J)

0x2: 在MyEclips中创建工程项目,并添加驱动JAR包

import java.sql.*;

public class ConnMySql
{
    public static void main(String[] args) throws Exception
    {
        // 1.加载驱动,使用反射的知识,现在记住这么写。
        Class.forName("com.mysql.jdbc.Driver");
        try(
            // 2.使用DriverManager获取数据库连接,
            // 其中返回的Connection就代表了Java程序和数据库的连接
            // 不同数据库的URL写法需要查驱动文档知道,用户名、密码由DBA分配
            Connection conn = DriverManager.getConnection(
                "jdbc:mysql://127.0.0.1:3306/company"
                , "root" , "111");
            // 3.使用Connection来创建一个Statment对象
            Statement stmt = conn.createStatement();
            // 4.执行SQL语句
            /*
            Statement有三种执行sql语句的方法:
            1 execute 可执行任何SQL语句。- 返回一个boolean值,
              如果执行后第一个结果是ResultSet,则返回true,否则返回false
            2 executeQuery 执行Select语句 - 返回查询到的结果集
            3 executeUpdate 用于执行DML语句。- 返回一个整数,
              代表被SQL语句影响的记录条数
            */
            ResultSet rs = stmt.executeQuery("select *"
                + " from p8_ad_user where u_id=1"))
        {
            // ResultSet有系列的getXxx(列索引 | 列名),用于获取记录指针
            // 指向行、特定列的值,不断地使用next()将记录指针下移一行,
            // 如果移动之后记录指针依然指向有效行,则next()方法返回true。
            while(rs.next())
            {
                System.out.println(rs.getInt(1) + "\t"
                    + rs.getString(2) + "\t"
                    + rs.getString(3) + "\t"
                    + rs.getString(4));
            }
        }
    }
}

将mysql数据库连接驱动添加到当前项目的CLASSPATH中

 

0x3: 编译、运行Java代码

 

Copyright (c) 2014 LittleHann All rights reserved

 

 

 

posted @ 2014-04-28 11:12 郑瀚Andrew.Hann 阅读(...) 评论(...) 编辑 收藏