JBDC底层原理解析

文章详细解析了JDBC操作数据库的步骤,包括注册驱动、获取数据库连接、执行SQL和处理结果集,以及资源释放。提到了原始JDBC程序的不足,如资源浪费和繁琐的手动处理,并对比了MyBatis如何通过配置和自动映射简化数据库操作。
摘要由CSDN通过智能技术生成

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(类):数据库驱动管理类。

  • 作用:
    1. 注册驱动
    2. 创建java代码和数据库之间的连接,即获取Connection对象

Connection(接口):建立数据库连接的对象

  • 作用:用于建立java程序和数据库之间的连接

Statement(接口): 数据库操作对象(执行SQL语句的对象)。

  • 作用:用于向数据库发送sql语句

ResultSet(接口):结果集对象(一张虚拟表)

  • 作用:sql查询语句的执行结果会封装在ResultSet中

3、过程分析

我们可以看到原始的JDBC程序操作数据库操作步骤如下:

  1. 注册驱动
  2. 获取连接对象
  3. 执行SQL语句,返回执行结果
  4. 处理执行结果
  5. 释放资源

接下来我们将更详细的去分析这些步骤其中的逻辑!

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 接口。下面对代码进行分析:

  1. 静态代码块: 在静态代码块中,通过调用 java.sql.DriverManager.registerDriver(new Driver()) 注册了一个驱动。registerDriver 方法用于向 DriverManager 注册数据库驱动程序,这样应用程序就可以使用该驱动来连接和操作数据库。
  2. 构造方法: 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);
}

下面对代码进行分析:

  1. registerDriver 方法签名: 方法签名为 public static synchronized void registerDriver(java.sql.Driver driver, DriverAction da) throws SQLException,表示该方法是一个公共静态方法,可以通过类名直接调用,方法带有两个参数:java.sql.Driver driverDriverAction da,并声明了可能抛出 SQLException 异常。
  2. 注册驱动: 在方法内部,首先检查传入的 driver 参数是否为 null,如果不为 null,则将 driver 添加到 registeredDrivers 列表中。registeredDrivers 是一个并发安全的列表,用于存储已注册的驱动程序信息。DriverInfo 对象封装了驱动程序和驱动操作(DriverAction)的信息。
  3. 异常处理: 如果 driver 参数为 null,则抛出 NullPointerException,这是为了与原始的 DriverManager 兼容,因为原始的 DriverManagernull 参数的处理方式是抛出 NullPointerException
  4. 打印信息: 最后,通过 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操作数据库的步骤有哪些?

    1. 注册驱动
    2. 获取连接对象
    3. 执行SQL语句,返回执行结果
    4. 处理执行结果
    5. 释放资源
  • 数据库驱动的加载和注册是如何处理的?

    使用 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程序,存在以下几点问题:

  1. 数据库链接的四要素(驱动、链接、用户名、密码)全部硬编码在java代码中
  2. 查询结果的解析及封装非常繁琐
  3. 每一次查询数据库都需要获取连接,操作完毕后释放连接, 资源浪费, 性能降低

在这里插入图片描述

我们现在是怎么解决的呢?

2、与mybatis的技术对比

分析了JDBC的缺点之后,我们再来看一下在mybatis中,是如何解决这些问题的:

  1. 数据库连接四要素(驱动、链接、用户名、密码),都配置在springboot默认的配置文件 application.properties中
  2. 查询结果的解析及封装,由mybatis自动完成映射封装,我们无需关注
  3. 在mybatis中使用了数据库连接池技术,从而避免了频繁的创建连接、销毁连接而带来的资源浪费。

在这里插入图片描述

使用SpringBoot+Mybatis的方式操作数据库,能够提升开发效率、降低资源浪费

而对于Mybatis来说,我们在开发持久层程序操作数据库时,需要重点关注以下两个方面:

  1. 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
    
  2. 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操作数据库的步骤有哪些?

  1. 注册驱动
  2. 获取连接对象
  3. 执行SQL语句,返回执行结果
  4. 处理执行结果
  5. 释放资源

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的执行原理,感兴趣的伙伴记得持续关注动态哦!

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值