关注“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):获取在数据库里任意类型的数据。
常用获取行方法
常用数据类型转换
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中处理。
总结
本文主要讲了如下内容:
评论 (1 条评论)