JBDC底层原理解析
一、底层原理解析
1、引入依赖
首先我们先在pom.xml文件中引入MySQL驱动依赖。
<dependency>
<groupId>mysql</groupId>//id
<artifactId>mysql-connector-java</artifactId>//在下图的Driver,class中可以找到该ID名
<version>8.0.30</version>//相应的版本可以根据IDEA给的提示进行修改
</dependency>
2、简单实现JBDC程序
引入依赖后我们先进行简单JDBC程序实现:
import com.itheima.pojo.User;
import org.junit.jupiter.api.Test;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;
public class JdbcTest {
@Test
public void testJdbc() throws Exception {
//1. 注册驱动
Class.forName("com.mysql.cj.jdbc.Driver");
//2. 获取数据库连接
String url="jdbc:mysql://127.0.0.1:3306/mybatis";
String username = "root";
String password = "1234";
Connection connection = DriverManager.getConnection(url, username, password);
//3. 执行SQL
Statement statement = connection.createStatement(); //操作SQL的对象
String sql="select id,name,age,gender,phone from user";
ResultSet rs = statement.executeQuery(sql);//SQL查询结果会封装在ResultSet对象中
List<User> userList = new ArrayList<>();//集合对象(用于存储User对象)
//4. 处理SQL执行结果
while (rs.next()){
//取出一行记录中id、name、age、gender、phone下的数据
int id = rs.getInt("id");
String name = rs.getString("name");
short age = rs.getShort("age");
short gender = rs.getShort("gender");
String phone = rs.getString("phone");
//把一行记录中的数据,封装到User对象中
User user = new User(id,name,age,gender,phone);
userList.add(user);//User对象添加到集合
}
//5. 释放资源
statement.close();
connection.close();
rs.close();
//遍历集合
for (User user : userList) {
System.out.println(user);
}
}
}
代码中主要接口对象是Connection、Statement、ResultSet。
DriverManager(类):数据库驱动管理类。
- 作用:
- 注册驱动
- 创建java代码和数据库之间的连接,即获取Connection对象
Connection(接口):建立数据库连接的对象
- 作用:用于建立java程序和数据库之间的连接
Statement(接口): 数据库操作对象(执行SQL语句的对象)。
- 作用:用于向数据库发送sql语句
ResultSet(接口):结果集对象(一张虚拟表)
- 作用:sql查询语句的执行结果会封装在ResultSet中
3、过程分析
我们可以看到原始的JDBC程序操作数据库操作步骤如下:
- 注册驱动
- 获取连接对象
- 执行SQL语句,返回执行结果
- 处理执行结果
- 释放资源
接下来我们将更详细的去分析这些步骤其中的逻辑!
3.1注册驱动
加载MySql的驱动类 :
//加载和注册 MySQL 数据库驱动程序
Class.forName("com.mysql.jdbc.Driver");
Class.forName
方法在运行时会查找并加载指定的类。它通过查找类路径(classpath)上的类文件或者通过其他类加载器加载,使得该类的字节码可以在运行时被 JVM 加载到内存中。当类加载器成功加载指定的类后,会触发该类的静态初始化过程。在这个例子中,com.mysql.jdbc.Driver
类的静态初始化过程会执行一些特定的代码,其中包括将驱动注册到 Java 的数据库驱动管理器中。
注:从 Java 6 开始,使用 Class.forName
加载 JDBC 驱动已经不再是必需的。根据 JDBC 4.0 规范,可以通过简单地在类路径中包含相应的驱动 JAR 文件,或者使用 JDBC 4.0+ 的自动驱动加载功能,无需显式调用 Class.forName
。
我们安装好数据库之后,我们的应用程序是不能直接使用数据库的,必须要通过相应的数据库驱动程序,通过驱动程序去和数据库打交道。即对Connection等接口的实现类的jar文件。
这个jar文件就是我们上面导入的依赖。
驱动实现
java.sql.Driver
接口是提供给数据库厂商实现的,需要依赖对应的jar包。
以下是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 {
}
}
定义了一个名为 Driver
的类,该类扩展了 NonRegisteringDriver
类并实现了 java.sql.Driver
接口。下面对代码进行分析:
- 静态代码块: 在静态代码块中,通过调用
java.sql.DriverManager.registerDriver(new Driver())
注册了一个驱动。registerDriver
方法用于向DriverManager
注册数据库驱动程序,这样应用程序就可以使用该驱动来连接和操作数据库。 - 构造方法:
Driver
类的构造方法没有任何参数,并且声明了一个throws SQLException
异常。
这段代码其实就是定义了一个自定义的驱动程序类 Driver
,它继承了 NonRegisteringDriver
并实现了 java.sql.Driver
接口。
实现内容是在静态代码块中,注册自身为数据库驱动程序,以便在应用程序中可以使用该驱动来连接和操作数据库。
注:这种注册驱动的方式在旧版的 JDBC 中是常见的做法,而在较新的 JDBC 版本中,通常不再需要显式地注册驱动,可以直接使用相应的数据库驱动库。
DriverManager是rt.jar包(rt=runtime)下的类。registerDriver静态方法是 DriverManager
类中的一个方法,用于注册数据库驱动程序。
//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);
}
下面对代码进行分析:
registerDriver
方法签名: 方法签名为public static synchronized void registerDriver(java.sql.Driver driver, DriverAction da) throws SQLException
,表示该方法是一个公共静态方法,可以通过类名直接调用,方法带有两个参数:java.sql.Driver driver
和DriverAction da
,并声明了可能抛出SQLException
异常。- 注册驱动: 在方法内部,首先检查传入的
driver
参数是否为null
,如果不为null
,则将driver
添加到registeredDrivers
列表中。registeredDrivers
是一个并发安全的列表,用于存储已注册的驱动程序信息。DriverInfo
对象封装了驱动程序和驱动操作(DriverAction
)的信息。 - 异常处理: 如果
driver
参数为null
,则抛出NullPointerException
,这是为了与原始的DriverManager
兼容,因为原始的DriverManager
对null
参数的处理方式是抛出NullPointerException
。 - 打印信息: 最后,通过
println
方法打印注册的驱动程序信息。
总结: 该代码片段是 DriverManager
类中的一个方法,用于注册数据库驱动程序。它接受一个驱动程序对象和驱动操作对象作为参数,并将驱动程序添加到已注册驱动的列表中。如果传入的驱动程序对象为 null
,则会抛出 NullPointerException
。最后,会打印注册的驱动程序信息。
3.2获取连接对象
我们来看一下这一行代码:
String url="jdbc:mysql://127.0.0.1:3306/mybatis";
String username = "root";
String password = "1234";
Connection connection = DriverManager.getConnection(url, username, 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(
//该方法是一个私有静态方法,用于获取数据库连接。方法接受三个参数:url 表示数据库连接 URL,info 表示连接所需的属性信息,caller 表示调用者的类。
String url, java.util.Properties info, Class<?> caller) throws SQLException {
//获取调用者的类加载器:根据传入的 caller 类,获取对应的类加载器 callerCL。如果 caller 不为 null,则通过 caller.getClassLoader() 获取其类加载器;如果 caller 为 null,则将 callerCL 设为 null。
ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;
SQLException reason = null;
//遍历注册的数据库驱动
for(DriverInfo aDriver : registeredDrivers) {
try {
//获取连接
Connection con = aDriver.driver.connect(url, info);
//判断连接是否成功:如果连接对象 con 不为 null,表示连接成功,打印日志信息,并返回该连接对象。
if (con != null) {
// Success!
println("getConnection returning " + aDriver.driver.getClass().getName());
return (con);
}
//当遍历完所有注册的驱动程序后,如果没有成功获取到连接,即所有驱动程序的 connect 方法都返回 null,则会抛出 SQLException 异常。
} 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。
//表示该方法是一个公共方法,用于建立数据库连接。方法接受多个参数,包括主机信息(HostInfo)、用户名、密码、数据库名、登录超时时间、事务事件处理器等。
public void connect(HostInfo hi, String user, String password, String database, int loginTimeout, TransactionEventHandler transactionManager)
throws IOException {
//创建 Socket 连接:首先创建一个 SocketConnection 对象,具体实现为 NativeSocketConnection,用于与数据库建立底层的 Socket 连接。
SocketConnection socketConnection = new NativeSocketConnection();
//连接 Socket:调用 socketConnection.connect() 方法,传入主机信息、属性设置、异常拦截器、日志记录器和登录超时时间等参数,建立与数据库服务器的 Socket 连接。
socketConnection.connect(this.hostInfo.getHost(), this.hostInfo.getPort(), this.propertySet, getExceptionInterceptor(), this.log, loginTimeout);
//连接协议:调用 protocol.connect() 方法,传入用户名、密码和数据库名等参数,通过建立的 Socket 连接与数据库服务器进行协议级别的连接。
this.protocol.connect(user, password, database); //设置错误消息编码:通过 protocol.getServerSession().setErrorMessageEncoding() 方法,设置连接使用的错误消息的编码方式。这里根据握手过程中的认证提供程序获取编码方式。
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进行连接数据库的。
3.3执行SQL语句,返回执行结果
获取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(? , ?)}") ;
这里思考两个问题:
1、常用方法Statement和PreparedStatement有什么区别?优缺点是什么?
相同点:两者都是用来执SQL语句的
区别:PreparedStatement需要根据SQL语句来创建,它能够通过设置参数,指定相应的值,而Statement使用字符串拼接的方式。
PreparedStatement的优点:
1、其使用参数设置,可读性好,不易记错。在statement中使用字符串拼接,可读性和维护性比较差。
2、其具有预编译机制,性能比statement更快。
3、其能够有效防止SQL注入攻击。
2、execute和executeUpdate的区别是什么?
相同点:二者都能够执行增加、删除、修改等操作。
区别:
-
execute可以执行查询语句,然后通过getResult把结果取出来。executeUpdate不能执行查询语句。
-
execute返回Boolean类型,true表示执行的是查询语句,false表示执行的insert、delete、update等。executeUpdate的返回值是int,表示有多少条数据受到了影响。
3.4处理执行结果
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的最后面。
3.5释放资源
statement.close();
connection.close();
rs.close();
释放资源,主要是针对一些资源进行关闭,免得一直持有资源。我们处理的资源关闭一般都是在finally中处理。
3.6小结
-
什么是JBDC?
JBDC(Java Database Connectivity),就是使用Java语言操作关系型数据库的一套API。
-
JBDC的底层的主要接口对象是什么?
JBDC的底层主要是三个接口对象,Connection、Statement、ResultSet。Connection用于建立与数据库的连接,Statement用于向数据库发送sql语句,ResultSet用于封装sql查询语句的结果。
-
JDBC操作数据库的步骤有哪些?
- 注册驱动
- 获取连接对象
- 执行SQL语句,返回执行结果
- 处理执行结果
- 释放资源
-
数据库驱动的加载和注册是如何处理的?
使用
Class.forName()
方法加载数据库驱动程序类。通过
DriverManager.registerDriver()
方法注册数据库驱动程序注册完驱动程序后,可以通过
DriverManager.getConnection()
方法建立数据库连接 -
JBDC底层是怎么连接数据库的?
JDBC的底层就是使用Socket进行连接数据库的
-
结果集处理的常用方法有哪些?
这里就不一一举例了。
-
释放资源一般在那个地方处理?有哪些方式?有什么用?
- 在
finally
块中释放资源:
使用try-catch-finally
结构,在finally
块中处理资源的释放。这样无论是否发生异常,都会执行finally
块中的代码,确保资源得到释放。 - 使用 try-with-resources 语句:
在 Java 7 及以上版本中,可以使用 try-with-resources 语句来自动关闭实现了AutoCloseable
接口的资源,无需显式地在finally
块中进行资源释放。 - 在合适的方法或对象生命周期结束时释放资源:
在合适的方法或对象生命周期结束时手动调用资源的释放方法,例如在一个方法的最后或对象的close()
方法中释放相关资源。
作用:避免资源泄漏和不必要的资源占用
- 在
二、JBDC存在的问题及解决方案
1、JBDC的缺点
原始的JDBC程序,存在以下几点问题:
- 数据库链接的四要素(驱动、链接、用户名、密码)全部硬编码在java代码中
- 查询结果的解析及封装非常繁琐
- 每一次查询数据库都需要获取连接,操作完毕后释放连接, 资源浪费, 性能降低
我们现在是怎么解决的呢?
2、与mybatis的技术对比
分析了JDBC的缺点之后,我们再来看一下在mybatis中,是如何解决这些问题的:
- 数据库连接四要素(驱动、链接、用户名、密码),都配置在springboot默认的配置文件 application.properties中
- 查询结果的解析及封装,由mybatis自动完成映射封装,我们无需关注
- 在mybatis中使用了数据库连接池技术,从而避免了频繁的创建连接、销毁连接而带来的资源浪费。
使用SpringBoot+Mybatis的方式操作数据库,能够提升开发效率、降低资源浪费
而对于Mybatis来说,我们在开发持久层程序操作数据库时,需要重点关注以下两个方面:
-
application.properties
#驱动类名称 spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver #数据库连接的url spring.datasource.url=jdbc:mysql://localhost:3306/mybatis #连接数据库的用户名 spring.datasource.username=root #连接数据库的密码 spring.datasource.password=1234
-
Mapper接口(编写SQL语句)
@Mapper public interface UserMapper { @Select("select id, name, age, gender, phone from user") public List<User> list(); }
通过上述代码,我们看到直接基于JDBC程序来操作数据库,代码实现非常繁琐,所以在项目开发中,我们很少使用。 在项目开发中,通常会使用Mybatis这类的高级技术来操作数据库,从而简化数据库操作、提高开发效率。
三、总结
1、什么是JBDC?
JBDC(Java Database Connectivity),就是使用Java语言操作关系型数据库的一套API。
2、JBDC的底层的主要接口对象是什么?
JBDC的底层主要是三个接口对象,Connection、Statement、ResultSet。Connection用于建立与数据库的连接,Statement用于向数据库发送sql语句,ResultSet用于封装sql查询语句的结果。
3、JDBC操作数据库的步骤有哪些?
- 注册驱动
- 获取连接对象
- 执行SQL语句,返回执行结果
- 处理执行结果
- 释放资源
4、数据库驱动的加载和注册是如何处理的?
使用 Class.forName()
方法加载数据库驱动程序类。
通过 DriverManager.registerDriver()
方法注册数据库驱动程序
注册完驱动程序后,可以通过 DriverManager.getConnection()
方法建立数据库连接
5、JBDC底层是怎么连接数据库的?
JDBC的底层就是使用Socket进行连接数据库的
6、结果集处理的常用方法有哪些?
太多了,这里就不一一举例了。
7、释放资源一般在那个地方处理?有哪些方式?有什么用?
- 在
finally
块中释放资源:
使用try-catch-finally
结构,在finally
块中处理资源的释放。这样无论是否发生异常,都会执行finally
块中的代码,确保资源得到释放。 - 使用 try-with-resources 语句:
在 Java 7 及以上版本中,可以使用 try-with-resources 语句来自动关闭实现了AutoCloseable
接口的资源,无需显式地在finally
块中进行资源释放。 - 在合适的方法或对象生命周期结束时释放资源:
在合适的方法或对象生命周期结束时手动调用资源的释放方法,例如在一个方法的最后或对象的close()
方法中释放相关资源。
作用:避免资源泄漏和不必要的资源占用
8、JDBC有哪些问题?
-
数据库链接的四要素(驱动、链接、用户名、密码)全部硬编码在java代码中
-
查询结果的解析及封装非常繁琐
-
每一次查询数据库都需要获取连接,操作完毕后释放连接, 资源浪费, 性能降低
9、Mybatis如何解决的?
-
数据库连接四要素(驱动、链接、用户名、密码),都配置在springboot默认的配置文件 application.properties中
-
查询结果的解析及封装,由mybatis自动完成映射封装,我们无需关注
-
在mybatis中使用了数据库连接池技术,从而避免了频繁的创建连接、销毁连接而带来的资源浪费。
经过以上的层层剖析与总结,是不是对JBDC有了更深刻的了解?后面我将持续更新Mybatis的入门以及Mybatis的执行原理,感兴趣的伙伴记得持续关注动态哦!