关于jdbc:面试被问JDBC底层是如何连接数据库的

36次阅读

共计 11007 个字符,预计需要花费 28 分钟才能阅读完成。

关注“Java 后端技术全栈”

回复“面试”获取全套面试材料

背景

前两天一个小伙伴面试的时候,被问 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 文件。

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 的长处:

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

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

3、其可能无效避免 SQL 注入攻打。

execute 和 executeUpdate 的区别

相同点:二者都可能执行减少、删除、批改等操作。

不同点:

1、execute 能够执行查问语句,而后通过 getResult 把后果取出来。executeUpdate 不能执行查问语句。

2、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 连贯数据库,讲了到了底层是怎么连贯数据库的?
  • 后果集解决的罕用办法

正文完
 0