面試被問:JDBC底層是如何鏈接數據庫的?

關注「Java後端技術全棧」java

回覆「面試」獲取全套面試資料mysql

背景

前兩天一個小夥伴面試的時候,被問JDBC底層是如何鏈接數據庫的?面試

他頓時一臉懵逼,由於大部分人只知道JDBC的幾個步驟,至於底層究竟是怎麼鏈接數據庫的,還真不知道。sql

因爲小夥伴是面試高級開發,問這種問題倒也不能說面試官過度,若是是初級或者中級,那問着問題就確實有些過度了。數據庫

可是若是你在初級或者中級的階段,就知道了答案,豈不是爽歪歪麼?後端

估計大部分人都不知道這個問題該怎麼回答,稍微發散一下思惟,卻是能夠猜想一下,今天咱們就來搞清楚JDBC底層究竟是如何鏈接數據庫的。日後別再猜了。服務器

反過來,若是面試官問你JDBC的時候,你能知道底層是怎麼鏈接數據庫的,估計,不少相對較水的面試官也會一臉懵逼。網絡

何爲 JDBC ?

JDBC(Java DataBase Connectivity)是Java和數據庫之間的一個橋樑,是一個「規範」而不是一個實現,可以執行SQL語句。JDBC由一組用Java語言編寫的類和接口組成。各類不一樣類型的數據庫都有相應的實現,注意:本文中的代碼都是針對MySQL數據庫實現的。session

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文件。

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」進行鏈接數據庫的。

經常使用方法

方法

描述

createStatement()

建立向數據庫發送sql的statement對象。

prepareStatement(sql)

建立向數據庫發送預編譯sql的PrepareSatement對象。

prepareCall(sql)

建立執行存儲過程的callableStatement對象。

setAutoCommit(boolean autoCommit)

設置事務是否自動提交。

commit()

在連接上提交事務。

rollback()

在此連接上回滾事務。

獲取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(? , ?)}") ;
經常使用方法

方法

含義

executeQuery(String sql)

用於向數據發送查詢語句。

executeUpdate(String sql)

用於向數據庫發送insert、update或delete語句

execute(String sql)

用於向數據庫發送任意sql語句

addBatch(String sql)

把多條sql語句放到一個批處理中。

executeBatch()

向數據庫發送一批sql語句執行。

Statement和PreparedStatement的異同及優缺點

同:二者都是用來執SQL語句的

異:PreparedStatement須要根據SQL語句來建立,它可以經過設置參數,指定相應的值,不是像Statement那樣使用字符串拼接的方式。

PreparedStatement的優勢:

一、其使用參數設置,可讀性好,不易記錯。在statement中使用字符串拼接,可讀性和維護性比較差。

二、其具備預編譯機制,性能比statement更快。

三、其可以有效防止SQL注入攻擊。

execute和executeUpdate的區別

相同點:兩者都可以執行增長、刪除、修改等操做。

不一樣點:

一、execute能夠執行查詢語句,而後經過getResult把結果取出來。executeUpdate不能執行查詢語句。

二、execute返回Boolean類型,true表示執行的是查詢語句,false表示執行的insert、delete、update等。executeUpdate的返回值是int,表示有多少條數據受到了影響。

ResultSet結果集處理

前面的入門案例中這裏返回的結果集是ResultSetImpl

圖片

ResultSetImpl類圖

圖片

經常使用獲取值方法

  • 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的最後面。

經常使用數據類型轉換

SQL類型

Jdbc對應方法

返回類型

bit(1),bit(n)

getBoolean,getBytes()

Boolean,byte[]

tinyint

getByte()

Byte

smallint

getShort()

Short

int

getInt

Int

bigint

getLong()

Long

char,varchar,longvarchar

getString

String

text(clob) blob

getClob(),getblob()

Clob,blob

date

getDate()

java.sql.Date

time

getTime()

java.sql.Time

timestamp

getTimestamp

java.sql.Timestamp

以上即是結果集的處理,就這麼多了。

資源關閉

資源關閉不在業務代碼這一塊,主要是針對一些資源進行關閉,省得一直持有資源。另外咱們處理的資源關閉通常都是在finally中處理。

總結

本文主要講了以下內容:

  • 什麼是JDBC?
  • 數據庫驅動的加載和註冊是如何處理的?
  • 精彩點是咱們一般說的JDBC鏈接數據庫,講了到了底層是怎麼鏈接數據庫的?
  • 結果集處理的經常使用方法
相關文章
相關標籤/搜索