zoukankan      html  css  js  c++  java
  • jdbc经典问题

    背景

    前两天一个小伙伴面试的时候,被问JDBC底层是如何连接数据库的?

    他顿时一脸懵逼,因为大部分人只知道JDBC的几个步骤,至于底层到底是怎么连接数据库的,还真不知道。

    由于小伙伴是面试高级开发,问这种问题倒也不能说面试官过分,如果是初级或者中级,那问着问题就确实有些过分了。

    但是如果你在初级或者中级的阶段,就知道了答案,岂不是爽歪歪么?

    估计大部分人都不知道这个问题该怎么回答,稍微发散一下思维,倒是可以猜测一下,今天我们就来搞清楚JDBC底层到底是如何连接数据库的。往后别再猜了。

    反过来,如果面试官问你JDBC的时候,你能知道底层是怎么连接数据库的,估计,很多相对较水的面试官也会一脸懵逼。

    何为 JDBC ?

    JDBC(Java DataBase Connectivity)是Java和数据库之间的一个桥梁,是一个「规范」而不是一个实现,能够执行SQL语句。JDBC由一组用Java语言编写的类和接口组成。各种不同类型的数据库都有相应的实现,注意:本文中的代码都是针对MySQL数据库实现的。

    JDBC 架构

    分为双层架构和三层架构。

    双层

    作用:此架构中,Java Applet 或应用直接访问数据源。

    条件:要求 Driver 能与访问的数据库交互。

    机制:用户命令传给数据库或其他数据源,随之结果被返回。

    部署:数据源可以在另一台机器上,用户通过网络连接,称为 C/S配置(可以是内联网或互联网)。

    三层

    侧架构特殊之处在于,引入中间层服务。

    流程:命令和结构都会经过该层。

    吸引:可以增加企业数据的访问控制,以及多种类型的更新;另外,也可简化应用的部署,并在多数情况下有性能优势。

    历史趋势:以往,因性能问题,中间层都用 C 或 C++ 编写,随着优化编译器(将 Java 字节码 转为 高效的 特定机器码)和技术的发展,如EJB,Java 开始用于中间层的开发这也让 Java 的优势突显出现出来,使用 Java 作为服务器代码语言,JDBC随之被重视。

    入门案例

    下面给出一个JDBC入门级案例:

    public class JdbcDemo {
        public static final String URL = "jdbc:mysql://localhost:3306/mblog";
        public static final String USER = "root";
        public static final String PASSWORD = "123456";
    
        public static void main(String[] args) throws Exception { 
            Class.forName("com.mysql.jdbc.Driver"); 
            Connection conn = DriverManager.getConnection(URL, USER, PASSWORD); 
            Statement stmt = conn.createStatement(); 
            ResultSet rs = stmt.executeQuery("SELECT id, name, age FROM m_user where id =1"); 
            while(rs.next()){
                System.out.println("name: "+rs.getString("name")+" 年龄:"+rs.getInt("age"));
            }
        }
    }
    

    JDBC 步骤

    数据库驱动:

    Class.forName("com.mysql.jdbc.Driver"); 
    

    获取连接:

    Connection conn = DriverManager.getConnection(URL, USER, PASSWORD); 
    

    创建Statement或者PreparedStatement对象:

    Statement stmt = conn.createStatement(); 
    

    执行sql数据库查询:

    ResultSet rs = stmt.executeQuery("SELECT id, name, age FROM m_user where id =1"); 
    

    解析结果集:

    System.out.println("name: "+rs.getString("name")+" 年龄:"+rs.getInt("age"));
    

    最后就是各种资源的关闭。

    数据库驱动

    加载MySql的驱动类 :

    Class.forName("com.mysql.jdbc.Driver"); 
    

    我们安装好数据库之后,我们的应用程序也是不能直接使用数据库的,必须要通过相应的数据库驱动程序,通过驱动程序去和数据库打交道。其实也就是数据库厂商的JDBC接口实现,即对Connection等接口的实现类的jar文件。

    img

    Driver接口

    java.sql.Driver此接口是提供给数据库厂商实现的。比如说MySQL的,需要依赖对应的jar包。

    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>8.0.16</version>
    </dependency>
    

    MySQL数据库对应的实现驱动实现类:

    package com.mysql.cj.jdbc;
    import java.sql.SQLException; 
    public class Driver extends NonRegisteringDriver implements java.sql.Driver {
        static {
            try {
                //注册驱动
                java.sql.DriverManager.registerDriver(new Driver());
            } catch (SQLException E) {
                throw new RuntimeException("Can't register driver!");
            }
        } 
        public Driver() throws SQLException { 
        }
    }
    

    DriverManager是rt.jar包下的类,(rt=runtime),把我们需要驱动类注册进去。

    //DriverManager类中的方法
    public static synchronized void registerDriver(java.sql.Driver driver, DriverAction da)
        throws SQLException {
         /* Register the driver if it has not already been added to our list */
         if(driver != null) {
              registeredDrivers.addIfAbsent(new DriverInfo(driver, da));
          } else {
              // This is for compatibility with the original DriverManager
              throw new NullPointerException();
          }
          println("registerDriver: " + driver);
    }
    

    相应装载Oracle驱动:

    Class.forName("oracle.jdbc.driver.OracleDriver"); 
    

    Sql Server驱动:

    Class.forName("com.microsoft.jdbc.sqlserver.SQLServerDriver");
    

    获取链接

    给我们看起来就这一行代码:

    Connection conn = DriverManager.getConnection(URL, USER, PASSWORD);
    

    下面我们进行深入聊聊这行代码,到底底层是怎么连接数据库的?

    getConnection方法三个参数:链接地址,用户名和密码。

    public static Connection getConnection(String url,
         String user, String password) throws SQLException {
         java.util.Properties info = new java.util.Properties();
         if (user != null) {
             info.put("user", user);
         }
         if (password != null) {
             info.put("password", password);
         }
       return (getConnection(url, info, Reflection.getCallerClass()));
     }
    

    创建一个Properties对象,Properties是HashTable的子类。

    public class Properties extends Hashtable<Object,Object> {
        //.....
    }
    

    再看getConnection方法:

    //  Worker method called by the public getConnection() methods.
    private static Connection getConnection(
            String url, java.util.Properties info, Class<?> caller) throws SQLException {
      ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;
      SQLException reason = null;
      //遍历气门注册的数据库驱动
      for(DriverInfo aDriver : registeredDrivers) {  
               try { 
                    //获取连接
                    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;
                     }
                }  
        }
    }
    

    这段代码的关键是这一句代码:

    Connection con = aDriver.driver.connect(url, info);
    

    connet()方法是每个数据库驱动自己的实现的。

    package com.mysql.cj.jdbc;
    public class NonRegisteringDriver implements java.sql.Driver {
         @Override
        public java.sql.Connection connect(String url, Properties info) throws SQLException { 
            //部分无关键要的代码省略
            //下面是重点
            ConnectionUrl conStr = ConnectionUrl.getConnectionUrlInstance(url, info);
            switch (conStr.getType()) {
                    //SINGLE_CONNECTION("jdbc:mysql:", HostsCardinality.SINGLE), //
                    case SINGLE_CONNECTION:
                        return com.mysql.cj.jdbc.ConnectionImpl.getInstance(conStr.getMainHost());
                    case LOADBALANCE_CONNECTION:
                        return LoadBalancedConnectionProxy.createProxyInstance((LoadbalanceConnectionUrl) conStr);
                    case FAILOVER_CONNECTION:
                        return FailoverConnectionProxy.createProxyInstance(conStr);
                    case REPLICATION_CONNECTION:
                        return ReplicationConnectionProxy.createProxyInstance((ReplicationConnectionUrl) conStr);
                    default:
                        return null;
            } 
        }
    }    
    

    ConnectionUrl从这个类名应该能猜到还不到真正连接的,只是创建一个连接Url相关信息封装。

    public abstract class ConnectionUrl implements DatabaseUrlContainer {
        private static final String DEFAULT_HOST = "localhost";
        private static final int DEFAULT_PORT = 3306;
        //...
    }    
    

    熟悉的身影,MySQL数据库默认端口。我们继续看下一行重要的代码:

    ConnectionImpl.getInstance(conStr.getMainHost());

    这里就是获取一个实例,不出意外,连接就在这里面产生的。继续:

    //ConnectionImpl
    public static JdbcConnection getInstance(HostInfo hostInfo) throws SQLException {
         return new ConnectionImpl(hostInfo);
    }
    

    ConnectionImpl构造方法里有调用createNewIO方法:

        @Override
        public void createNewIO(boolean isForReconnect) {
            synchronized (getConnectionMutex()) {  
                try {
                    if (!this.autoReconnect.getValue()) {
                        connectOneTryOnly(isForReconnect);
                        return;
                    }
                    connectWithRetries(isForReconnect);
                } catch (SQLException ex) { 
                }
            }
        }
    private void connectOneTryOnly(boolean isForReconnect) throws SQLException {
            Exception connectionNotEstablishedBecause = null; 
                JdbcConnection c = getProxy();
                //又看到熟悉的connet方法,
                this.session.connect(this.origHostInfo, this.user, this.password, this.database, DriverManager.getLoginTimeout() * 1000, c); 
    
                this.session.setQueryInterceptors(this.queryInterceptors); 
    
        }
    

    其中,这里的session是NativeSession。

    public void connect(HostInfo hi, String user, String password, String database, int loginTimeout, TransactionEventHandler transactionManager)
                throws IOException {  
        SocketConnection socketConnection = new NativeSocketConnection();
        socketConnection.connect(this.hostInfo.getHost(), this.hostInfo.getPort(), this.propertySet, getExceptionInterceptor(), this.log, loginTimeout); 
        this.protocol.connect(user, password, database);                     this.protocol.getServerSession().setErrorMessageEncoding(this.protocol.getAuthenticationProvider().getEncodingForHandshake()); 
    }
    

    在这个方法里,我们看到了Socket的命名开头的类,哈哈,是不是就是使用Socket进行通信的呢?

    精彩继续:

     socketConnection.connect(this.hostInfo.getHost(), this.hostInfo.getPort(), ...); 
    

    来到NativeSocketConnection类中方法:

    //com.mysql.cj.protocol.a.NativeSocketConnection
    @Override
    public void connect(String hostName, int portNumber, PropertySet propSet, ExceptionInterceptor excInterceptor, Log log, int loginTimeout) {  
      this.mysqlSocket = this.socketFactory.connect(this.host, this.port, propSet, loginTimeout);
      //... 
    }            
    

    这里的socketFactory是StandardSocketFactory。所以也就是调用的是StandardSocketFactory的connect方法:

    //StandardSocketFactory
    public <T extends Closeable> T connect(String hostname, int portNumber, PropertySet pset, int loginTimeout) throws IOException {
        this.rawSocket = createSocket(pset);
        this.rawSocket.connect(sockAddr, getRealTimeout(connectTimeout));
    }   
    protected Socket createSocket(PropertySet props) {
         return new Socket();
    }
    

    这里就算到底了,说白JDBC的底层就是使用「Socket」进行连接数据库的。

    常用方法

    获取Statement

    三种类型

    要执行SQL语句,必须获得java.sql.Statement实例,Statement实例分为以下3 种类型:

    • 执行静态SQL语句。通常通过Statement实例实现。
    • 执行动态SQL语句。通常通过PreparedStatement实例实现。
    • 执行数据库存储过程。通常通过CallableStatement实例实现。

    具体获取方式

    Statement stmt = con.createStatement() ;   
    PreparedStatement pstmt = con.prepareStatement(sql) ;   
    CallableStatement cstmt =  con.prepareCall("{CALL demoSp(? , ?)}") ;   
    

    常用方法Statement和PreparedStatement的异同及优缺点

    同:两者都是用来执SQL语句的

    异:PreparedStatement需要根据SQL语句来创建,它能够通过设置参数,指定相应的值,不是像Statement那样使用字符串拼接的方式。

    PreparedStatement的优点:

    1、其使用参数设置,可读性好,不易记错。在statement中使用字符串拼接,可读性和维护性比较差。

    2、其具有预编译机制,性能比statement更快。

    3、其能够有效防止SQL注入攻击。

    execute和executeUpdate的区别

    相同点:二者都能够执行增加、删除、修改等操作。

    不同点:

    1、execute可以执行查询语句,然后通过getResult把结果取出来。executeUpdate不能执行查询语句。

    2、execute返回Boolean类型,true表示执行的是查询语句,false表示执行的insert、delete、update等。executeUpdate的返回值是int,表示有多少条数据受到了影响。

    ResultSet结果集处理

    前面的入门案例中这里返回的结果集是ResultSetImpl

    img

    ResultSetImpl类图

    img

    常用获取值方法

    • getString(int index)、getString(String columnName):获得在数据库里是varchar、char等类型的数据对象。
    • getFloat(int index)、getFloat(String columnName):获得在数据库里是Float类型的数据对象。
    • getDate(int index)、getDate(String columnName):获得在数据库里是Date类型的数据。
    • getBoolean(int index)、getBoolean(String columnName):获得在数据库里是Boolean类型的数据。
    • getObject(int index)、getObject(String columnName):获取在数据库里任意类型的数据。

    常用获取行方法

    • next():移动到下一行
    • Previous():移动到前一行
    • absolute(int row):移动到指定行
    • beforeFirst():移动resultSet的最前面。
    • afterLast() :移动到resultSet的最后面。

    常用数据类型转换

    以上便是结果集的处理,就这么多了。

    资源关闭

    资源关闭不在业务代码这一块,主要是针对一些资源进行关闭,免得一直持有资源。另外我们处理的资源关闭一般都是在finally中处理。

    总结

    本文主要讲了如下内容:

    • 什么是JDBC?
    • 数据库驱动的加载和注册是如何处理的?
    • 精彩点是我们通常说的JDBC连接数据库,讲了到了底层是怎么连接数据库的?
    • 结果集处理的常用方法
  • 相关阅读:
    英语生活箴言
    Javascript中最常用的55个经典技巧
    深刻理解Java编程的7个例子
    定制Apache索引样式
    【Androidin全球首发】国产Android Broncho A1 评测,第一印象
    系统程序员成长计划写得又快又好的秘诀(五)
    让adb logcat打印内核调试信息
    系统程序员成长计划写得又快又好的秘诀(三)
    Projects owned by limodev.cn
    Apache Direcotry Indexes目录列表显示样式定制
  • 原文地址:https://www.cnblogs.com/tianweichang/p/14193502.html
Copyright © 2011-2022 走看看