码迷,mamicode.com
首页 > 数据库 > 详细

JDBC接口讲解与底层实现分析(上)

时间:2015-07-31 01:32:01      阅读:1058      评论:0      收藏:0      [点我收藏+]

标签:jdbc   preparedstatement   源码分析   sql预编译   java驱动   

一、JDBC为什么需要数据库驱动?

数据库是一个产品,想要访问它,就得通过它定义的方式去访问。你可能觉得平时操作时好像并没有按照什么协议访问呀,就是敲了下SQL命令,就有返回结果了。但你记得你是在什么环境下访问的吗?你在cmd终端下输入mysql启动的就是mysql client,是一个客户端,这个客户端其实就是一段程序,它的内部逻辑对你来说是透明的,你给它一段SQL,其实它是会做一定包装的,然后通过一些协议发送给mysql的server服务端。

抓包工具:WireShark

然后打脸了,并没有抓到3306端口的网络包

然后查阅了下《MySQL技术内幕InnoDB存储引擎》:

常用的进程间通信方式有:管道,命名管道,命名字,TCP套接字,Unix域套接字。而MySQL提供的连接方式从本质上看都是上述提及的进程通信方式

1.TCP/IP

TCP/IP套接字的方式是MySQL在任何平台上都提供的连接方式,也是用的最多的。一般情况客户端一台机器去连接服务器另一台机器。两台机器之间就是通过TCP/IP连接。客户端会向服务器MySQL实例发出TCP/IP连接请求,并连接成功

2,。命名管道和共享内存

Windows2003、vista及在此之上的平台,如果两个需要进程通信的进程在同一台机器上,那么可以使用命名管道,配置文件启用--enable-named-pipe。也可以使用共享内存的连接方式,只需要进行配置。



无论哪种方式,与数据库服务器通信肯定还是有一定的协议的

那么mysql,oracle,DB2他们的协议是一样的吗?不是。那么JAVA为每一个协议都去写一个类,是不切合实际的。首先这个协议是别人定的,第二数据库产品太多了。所以JAVA定义了一套接口,也就是JDBC。而协议的实现与通讯就由数据库厂商来提供了,这也就是驱动程序的JAR包。


看一下标准的连接语句

//声明Connection对象
        Connection con;
        //驱动程序名
        String driver = "com.mysql.jdbc.Driver";
        //URL指向要访问的数据库名test
        String url = "jdbc:mysql://localhost:3306/test";
        String user = "root";
        String password = "root";
        //遍历查询结果集
        try {
            //加载驱动程序
            Class.forName(driver);
            //1.getConnection()方法,连接MySQL数据库
            con = DriverManager.getConnection(url,user,password);

            //2.创建statement类对象,用来执行SQL语句
            Statement statement = con.createStatement();
            //要执行的SQL语句
            String sql = "select * from student";
            //3.ResultSet类,用来存放获取的结果集!!
            ResultSet rs = statement.executeQuery(sql);
 
            while(rs.next()){
                //获取name这列数据
                name = rs.getString("name");
                //获取uid这列数据
                id = rs.getString("uid");
            }
            rs.close();
            con.close();
        } catch(ClassNotFoundException e) {   
            //数据库驱动类异常处理
            System.out.println("Sorry,can`t find the Driver!");   
            e.printStackTrace();   
            } catch(SQLException e) {
            //数据库连接失败异常处理
            e.printStackTrace();  
            }catch (Exception e) {
            // TODO: handle exception
            e.printStackTrace();
        }finally{
            System.out.println("数据库数据成功获取!!");
        }
    }

所以这个过程总结起来就是:

1.注册一个Driver

2.建立一个到数据库的连接

3.创建一个Statement

4.执行SQL语句

5.处理结果

6.关闭JDBC对象


但是我们都是面向接口编程的,但运行时的对象还是使用的底层实现

来看一个图,看我们是如何使用驱动的(很明显的体现出来了面向接口编程

技术分享





二、为什么使用Class.forName来加载数据库驱动,DriverManager的作用


我们平时用Class.forName去加载驱动,可能很多初学者都没太懂这个底层。所以我们来解释下

我们先用最直观的方式连接一下:

<pre name="code" class="java">Driver driver=new com.mysql.jdbc.Driver();
Connection conn=driver.connect(url, info);
System.out.println(conn);


读者可以用这种方式去试试,看能不能获取连接(肯定是可以的啦 (@???)

这个方式就非常直观了,前面是接口,后面是实现类。也就是驱动类。

那么为什么我们平时不用这种直观的方式呢?

因为这样我们的类就与com.mysql.jdbc.Driver这个类直接耦合了

所以我们可以这样改一下:

String driverString="com.mysql.jdbc.Driver";
Driver driver=Class.forName(driverString).newInstance();
Connection conn=driver.connect(url, info);
System.out.println(conn);
使用反射获取对应的实现类,并创建对象


当系统中多个Driver时,我们手动控制是很麻烦的

DriverManager就是java.sql中的一个工具类,你给它注册进去所有的Driver,它帮你管理,你调用它的getConnection

会智能帮你选择合适的Driver并创建连接Connection

String driverString="com.mysql.jdbc.Driver";
Driver driver=Class.forName(driverString).newInstance();
String driverString2="oracle.jdbc.driver.OracleDriver";
Driver driver2=Class.forName(driverString).newInstance();
DriverManager.registerDriver(driver1);
DriverManager.registerDriver(driver2);
con = DriverManager.getConnection(url,user,password);

但这样去写其实也是很繁琐的,进一步简化就成为第一部分的简化方案:

//加载驱动程序
            Class.forName(driver);
            //1.getConnection()方法,连接MySQL数据库
            con = DriverManager.getConnection(url,user,password);
那DriverManager是如何获得的呢:

源码:

它自己会保存一份所有driver的列表

// List of registered JDBC drivers
    private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<DriverInfo>();

我们前面已经调用Class.forName(),系统已经加载了我们所需要的实现类


DriverManager加载进来时会执行以下静态语句

 static {
        loadInitialDrivers();
        println("JDBC DriverManager initialized");
    }
loadInitialDrivers方法会加载系统中已经加载的所有驱动Driver存放到刚刚那个list里面
然后我们调用getConnection时,它就会遍历列表的驱动,帮我们找到合适的驱动Driver并使用

(下面是部分重点代码,完整的请参考Java源码)


//  Worker method called by the public getConnection() methods.
    private static Connection getConnection(
        String url, java.util.Properties info, Class<?> caller) throws SQLException {
        

        // Walk through the loaded registeredDrivers attempting to make a connection.
        // Remember the first exception that gets raised so we can reraise it.
        SQLException reason = null;

        for(DriverInfo aDriver : registeredDrivers) {
            // If the caller does not have permission to load the driver then
            // skip it.
            if(isDriverAllowed(aDriver.driver, callerCL)) {
                try {
                    println("    trying " + aDriver.driver.getClass().getName());
                    Connection con = aDriver.driver.connect(url, info);
                    if (con != null) {
                        // Success!
                        println("getConnection returning " + aDriver.driver.getClass().getName());
                        return (con);
                    }
                } catch (SQLException ex) {
                    if (reason == null) {
                        reason = ex;
                    }
                }

            } else {
                println("    skipping: " + aDriver.getClass().getName());
            }

        }

        // if we got here nobody could connect.
        if (reason != null)    {
            println("getConnection failed: " + reason);
            throw reason;
        }

        println("getConnection: no suitable driver found for "+ url);
        throw new SQLException("No suitable driver found for "+ url, "08001");
    }
}




三、Statement与PreparedStatment区别


PreparedStatement是带预编译功能的


使用的规律就是他们都是connection创建出来,

但Statement是没有参数的,

而PreparedStatement的参数是sql的字符串(因为它要预编译sql嘛,所以必须要有sql参数)


摘取http://www.52testing.com/showart.asp?id=67

1.什么叫预编译:

通过Statement对象执行SQL语句时,需要将SQL语句发送给DBMS,由DBMS首先进行编译后再执行。

预编译语句和Statement不同,在创建PreparedStatement 对象时就指定了SQL语句,该语句立即发送给DBMS进行编译。当该编译语句被执行时,DBMS直接运行编译后的SQL语句,而不需要像其他SQL语句那样首先将其编译。
   

2、什么时候使用预编译语句

   一般是在需要反复使用一个SQL语句时才使用预编译语句,预编译语句常常放在一个fo r或者while循环里面使用,通过反复设置参数从而多次使用该SQL语句。为了防止SQL注入漏洞,在某些数据操作中也使用预编译语句。

3、为什么使用预编译语句
   (1) 防止SQL注入
   (2)提高效率
   数据库处理一个SQL语句,需要完成解析SQL语句、检查语法和语义以及生成代码,一般说来,处理时间要比执行语句所需要的时间长。预编译语句在创建的时候已经是将指定的SQL语句发送给了DBMS,完成了解析、检查、编译等工作。因此,当一个SQL语句需要执行多次时,使用预编译语句可以减少处理时间,提高执行效率。
   (3)提高代码的可读性和可维护性 
   将参数与SQL语句分离出来,这样就可以方便对程序的更改和扩展,同样,也可以减少不必要的错误。 


http://www.cnblogs.com/springside-example/archive/2008/02/03/2529943.html

这样生成数据库底层的内部命令(编译后的SQL),并将该命令封装在preparedStatement对象中,可以减轻数据库负担,提高访问数据库速度。

并没有找到很多这块的资料,所以个人理解是:

Statement创建时不需要传参数,只有execute时才一条一条SQL发送给服务器然后从服务器获取结果

而PreparedStatement则创建时需要创建参数,它会先发送这个SQL给数据库,进行解析检查编译成编译后的语句。关键就在这里:存的不是SQL,而是编译后的语句。这个编译出来的语句还会留出坑:放参数

这样使用起来就不需要再编译了,将坑填满,放入参数。所以效率很高,尤其在于反复使用同一个语句。

那么如何防止SQL注入:我的理解是

(1) 首先在PreparedStatement中可以有相关的过滤,类型不同。比如应该传入int型的,传入了123‘ or 1=1‘,肯定是能判断出来的。而Statement是不做这种判断的,直接拼接传入数据库了,就SQL注入攻击成功了

(2) 我们刚刚说了存放的编译的语句,然后有放参数的坑,这个参数就非常严格了。也许  or 这样的SQL语句编译之后已经不是or 这样的字符串。所以你传入123‘ or 1=1‘  (or是没有编译前的SQL),和已经编译后的语句拼接,那肯定是不能成功被解析为or这个逻辑的(仅仅是个人理解!!!!!!)


所以说字符串的SQL注入也是不能成功的,也就是说类型相同,如本该传入String型‘aaa’   而传入aaa‘ or 1=1‘。按照刚刚的第一条过滤应该判断不了,所以第二条的原因,还是不能成功注入

(证明见下面的使用API的代码)


贴一段JAVA JDK源码

这是Connection.prepareStatement(sql)语句

/**
     * Creates a <code>PreparedStatement</code> object for sending
     * parameterized SQL statements to the database.
     * <P>
     * A SQL statement with or without IN parameters can be
     * pre-compiled and stored in a <code>PreparedStatement</code> object. This
     * object can then be used to efficiently execute this statement
     * multiple times.
     *
     * <P><B>Note:</B> This method is optimized for handling
     * parametric SQL statements that benefit from precompilation. If
     * the driver supports precompilation,
     * the method <code>prepareStatement</code> will send
     * the statement to the database for precompilation. Some drivers
     * may not support precompilation. In this case, the statement may
     * not be sent to the database until the <code>PreparedStatement</code>
     * object is executed.  This has no direct effect on users; however, it does
     * affect which methods throw certain <code>SQLException</code> objects.
     * <P>
     * Result sets created using the returned <code>PreparedStatement</code>
     * object will by default be type <code>TYPE_FORWARD_ONLY</code>
     * and have a concurrency level of <code>CONCUR_READ_ONLY</code>.
     * The holdability of the created result sets can be determined by
     * calling {@link #getHoldability}.
     *
     * @param sql an SQL statement that may contain one or more '?' IN
     * parameter placeholders
     * @return a new default <code>PreparedStatement</code> object containing the
     * pre-compiled SQL statement
     * @exception SQLException if a database access error occurs
     * or this method is called on a closed connection
     */
    PreparedStatement prepareStatement(String sql)
        throws SQLException;

注释也说了,是把预编译的语句存放在PreparedStatement对象中的


那么我们看下PreparedStatement这个类中是不是有呢:

 class ParseInfo {
        char firstStmtChar = 0;

        boolean foundLoadData = false;

        long lastUsed = 0;

        int statementLength = 0;

        int statementStartPos = 0;

        boolean canRewriteAsMultiValueInsert = false;

        byte[][] staticSql = null;

        boolean isOnDuplicateKeyUpdate = false;

        int locationOfOnDuplicateKeyUpdate = -1;

        String valuesClause;

        boolean parametersInDuplicateKeyClause = false;

        /**
         * Represents the "parsed" state of a client-side prepared statement, with the statement broken up into it's static and dynamic (where parameters are
         * bound) parts.
         */
        ParseInfo(String sql, MySQLConnection conn, java.sql.DatabaseMetaData dbmd, String encoding, SingleByteCharsetConverter converter) throws SQLException {
            this(sql, conn, dbmd, encoding, converter, true);
        }

我觉得上述内部类和方法就是存放编译后的SQL语句了,存放方式蛮复杂的,二进制数组等等



看下使用API方法

        Connection conn=driver.connect(url, p);			
	
	while(rs.next())
	{
		System.out.println(rs.getString(1));
	}
	Statement s=conn.createStatement();
	rs=s.executeQuery("select * from wk where username='xx' or 1=1");
	System.out.println("using Statement");
	while(rs.next())
	{
		System.out.println(rs.getString(1));
	}

	PreparedStatement ps=conn.prepareStatement("select * from wk where username=?");
	ps.setString(1, "xx' or 1=1");
	ResultSet rs=ps.executeQuery();
	System.out.println("using PreparedStatement"); 

这个过程:

技术分享



(这个程序运行可以发现字符串的SQL注入是没成功的)


好了 ,剩下的部分下一篇再写吧,要不然文章太长了


四、

那么这个实现类在系统中是否是单例呢?


五、连接池

使用了动态代理模式


六、spring中如何使用JDBC


版权声明:本文为博主原创文章,未经博主允许不得转载。

JDBC接口讲解与底层实现分析(上)

标签:jdbc   preparedstatement   源码分析   sql预编译   java驱动   

原文地址:http://blog.csdn.net/silviakafka/article/details/46225045

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!