简介

什么是JDBC

JDBC是一套连接和操作数据库的标准、规范。通过提供DriverManagerConnectionStatementResultSet等接口将开发人员与数据库提供商隔离,开发人员只需要面对JDBC接口,无需关心怎么跟数据库交互。

几个重要的类

类名 作用
DriverManager 驱动管理器,用于注册驱动,是获取 Connection对象的入口
Driver 数据库驱动,用于获取Connection对象
Connection 数据库连接,用于获取Statement对象、管理事务
Statement sql执行器,用于执行sql
ResultSet 结果集,用于封装和操作查询结果
prepareCall 用于调用存储过程

使用中的注意事项

  1. 记得释放资源。另外,ResultSetStatement的关闭都不会导致Connection的关闭。
  2. maven要引入oracle的驱动包,要把jar包安装在本地仓库或私服才行。
  3. 使用PreparedStatement而不是Statement。可以避免SQL注入,并且利用预编译的特点可以提高效率。

使用例子

需求

使用JDBC对mysql数据库的用户表进行增删改查。

工程环境

JDK:1.8

maven:3.6.1

IDE:sts4

mysql driver:8.0.15

mysql:5.7

主要步骤

一个完整的JDBC保存操作主要包括以下步骤:

  1. 注册驱动(JDK6后会自动注册,可忽略该步骤);
  2. 通过DriverManager获得Connection对象;
  3. 开启事务;
  4. 通过Connection获得PreparedStatement对象;
  5. 设置PreparedStatement的参数;
  6. 执行保存操作;
  7. 保存成功提交事务,保存失败回滚事务;
  8. 释放资源,包括ConnectionPreparedStatement

创建表

CREATE TABLE `demo_user` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '用户id',
`name` varchar(16) COLLATE utf8_unicode_ci NOT NULL COMMENT '用户名',
`age` int(3) unsigned DEFAULT NULL COMMENT '用户年龄',
`gmt_create` datetime DEFAULT NULL COMMENT '记录创建时间',
`gmt_modified` datetime DEFAULT NULL COMMENT '记录最近修改时间',
`deleted` bit(1) DEFAULT b'0' COMMENT '是否删除',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_name` (`name`),
KEY `index_age` (`age`)
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci

创建项目

项目类型Maven Project,打包方式jar

引入依赖

<!-- junit -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<!-- mysql驱动的jar包 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.15</version>
</dependency>
<!-- oracle驱动的jar包 -->
<!-- <dependency>
<groupId>com.oracle</groupId>
<artifactId>ojdbc6</artifactId>
<version>11.2.0.2.0</version>
</dependency> -->

注意:由于oracle商业版权问题,maven并不提供Oracle JDBC driver,需要将驱动包手动添加到本地仓库或私服。

编写jdbc.prperties

下面的url拼接了好几个参数,主要为了避免乱码和时区报错的异常。

路径:resources目录下

driver=com.mysql.cj.jdbc.Driver
url=jdbc:mysql://localhost:3306/github_demo?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8&useSSL=true
#这里指定了字符编码和解码格式,时区,是否加密传输
username=root
password=root
#注意,xml配置是&采用&amp;替代

如果是oracle数据库,配置如下:

driver=oracle.jdbc.driver.OracleDriver
url=jdbc:oracle:thin:@//localhost:1521/xe
username=system
password=root

获得Connection对象

    private static Connection createConnection() throws Exception {
// 导入配置文件
Properties pro = new Properties();
InputStream in = JDBCUtil.class.getClassLoader().getResourceAsStream( "jdbc.properties" );
Connection conn = null;
pro.load( in );
// 获取配置文件的信息
String driver = pro.getProperty( "driver" );
String url = pro.getProperty( "url" );
String username = pro.getProperty( "username" );
String password = pro.getProperty( "password" );
// 注册驱动,JDK6后不需要再手动注册,DirverManager的静态代码块会帮我们注册
// Class.forName(driver);
// 获得连接
conn = DriverManager.getConnection( url, username, password );
return conn;
}

使用Connection对象完成保存操作

这里简单地模拟实际业务层调用持久层,并开启事务。另外,获取连接、释放资源可以通过自定义的工具类 JDBCUtil 来实现,具体见源码。

    @Test
public void save() throws Exception {
UserDao userDao = new UserDaoImpl();
// 创建用户
User user = new User("zzf002", 18, new Date(), new Date());
try (Connection connection = JDBCUtil.getConnection()) {
// 开启事务
connection.setAutoCommit(false);
// 保存用户
userDao.insert(user);
// 提交事务
connection.commit();
}
}

接下来看看具体的保存操作,即DAO层方法。

    public void insert(User user) throws Exception {
String sql = "insert into demo_user (name,age,gmt_create,gmt_modified) values(?,?,?,?)";
Connection connection = JDBCUtil.getConnection();
// 获取PreparedStatement对象
PreparedStatement prepareStatement = connection.prepareStatement(sql);
// 设置参数
prepareStatement.setString(1, user.getName());
prepareStatement.setInt(2, user.getAge());
prepareStatement.setDate(3, new java.sql.Date(user.getGmt_create().getTime()));
prepareStatement.setDate(4, new java.sql.Date(user.getGmt_modified().getTime()));
// 执行保存
prepareStatement.executeUpdate();
// 释放资源
JDBCUtil.release(prepareStatement, null, null);
}

源码分析

驱动注册

DriverManager.registerDriver

DriverManager主要用于管理数据库驱动,并为我们提供了获取连接对象的接口。其中,它有一个重要的成员属性registeredDrivers,是一个CopyOnWriteArrayList集合(通过ReentrantLock实现线程安全),存放的是元素是DriverInfo对象。

    //存放数据库驱动包装类的集合(线程安全)
private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>();
public static synchronized void registerDriver(java.sql.Driver driver)
throws SQLException {
//调用重载方法,传入的DriverAction对象为null
registerDriver(driver, null);
}
public static synchronized void registerDriver(java.sql.Driver driver,
DriverAction da)
throws SQLException {
if(driver != null) {
//当列表中没有这个DriverInfo对象时,加入列表。
//注意,这里判断对象是否已经存在,最终比较的是driver地址是否相等。
registeredDrivers.addIfAbsent(new DriverInfo(driver, da));
} else {
throw new NullPointerException();
} println("registerDriver: " + driver); }

为什么集合存放的是Driver的包装类DriverInfo对象,而不是Driver对象呢?

  1. 通过DriverInfo的源码可知,当我们调用equals方法比较两个DriverInfo对象是否相等时,实际上比较的是Driver对象的地址,也就是说,我可以在DriverManager中注册多个MYSQL驱动。而如果直接存放的是Driver对象,就不能达到这种效果(因为没有遇到需要注册多个同类驱动的场景,所以我暂时理解不了这样做的好处)。

  2. DriverInfo中还包含了另一个成员属性DriverAction,当我们注销驱动时,必须调用它的deregister方法后才能将驱动从注册列表中移除,该方法决定注销驱动时应该如何处理活动连接等(其实一般在构造DriverInfo进行注册时,传入的DriverAction对象为空,根本不会去使用到这个对象,除非一开始注册就传入非空DriverAction对象)。

综上,集合中元素不是Driver对象而DriverInfo对象,主要考虑的是扩展某些功能,虽然这些功能几乎不会用到。

注意:考虑篇幅,以下代码经过修改,仅保留所需部分。

class DriverInfo {

    final Driver driver;
DriverAction da;
DriverInfo(Driver driver, DriverAction action) {
this.driver = driver;
da = action;
} @Override
public boolean equals(Object other) {
//这里对比的是地址
return (other instanceof DriverInfo)
&& this.driver == ((DriverInfo) other).driver;
} }

为什么Class.forName(com.mysql.cj.jdbc.Driver) 可以注册驱动?

当加载com.mysql.cj.jdbc.Driver这个类时,静态代码块中会执行注册驱动的方法。

    static {
try {
//静态代码块中注册当前驱动
java.sql.DriverManager.registerDriver(new Driver());
} catch (SQLException E) {
throw new RuntimeException("Can't register driver!");
}
}

为什么JDK6后不需要Class.forName也能注册驱动?

因为从JDK6开始,DriverManager增加了以下静态代码块,当类被加载时会执行static代码块的loadInitialDrivers方法。

而这个方法会通过查询系统参数(jdbc.drivers)SPI机制两种方式去加载数据库驱动。

注意:考虑篇幅,以下代码经过修改,仅保留所需部分。

    static {
loadInitialDrivers();
}
//这个方法通过两个渠道加载所有数据库驱动:
//1. 查询系统参数jdbc.drivers获得数据驱动类名
//2. SPI机制
private static void loadInitialDrivers() {
//通过系统参数jdbc.drivers读取数据库驱动的全路径名。该参数可以通过启动参数来设置,其实引入SPI机制后这一步好像没什么意义了。
String drivers;
try {
drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
public String run() {
return System.getProperty("jdbc.drivers");
}
});
} catch (Exception ex) {
drivers = null;
}
//使用SPI机制加载驱动
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
//读取META-INF/services/java.sql.Driver文件的类全路径名。
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
//加载并初始化类
try{
while(driversIterator.hasNext()) {
driversIterator.next();
}
} catch(Throwable t) {
// Do nothing
}
return null;
}
}); if (drivers == null || drivers.equals("")) {
return;
}
//加载jdbc.drivers参数配置的实现类
String[] driversList = drivers.split(":");
for (String aDriver : driversList) {
try {
Class.forName(aDriver, true,
ClassLoader.getSystemClassLoader());
} catch (Exception ex) {
println("DriverManager.Initialize: load failed: " + ex);
}
}
}

补充:SPI机制本质上提供了一种服务发现机制,通过配置文件的方式,实现服务的自动装载,有利于解耦和面向接口编程。具体实现过程为:在项目的META-INF/services文件夹下放入以接口全路径名命名的文件,并在文件中加入实现类的全限定名,接着就可以通过ServiceLoder动态地加载实现类。

打开mysql的驱动包就可以看到一个java.sql.Driver文件,里面就是mysql驱动的全路径名。

获得连接对象

DriverManager.getConnection

获取连接对象的入口是DriverManager.getConnection,调用时需要传入url、username和password。

获取连接对象需要调用java.sql.Driver实现类(即数据库驱动)的方法,而具体调用哪个实现类呢?

正如前面讲到的,注册的数据库驱动被存放在registeredDrivers中,所以只有从这个集合中获取就可以了。

注意:考虑篇幅,以下代码经过修改,仅保留所需部分。

    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);
}
//传入url、包含username和password的信息类、当前调用类
return (getConnection(url, info, Reflection.getCallerClass()));
}
private static Connection getConnection(String url, java.util.Properties info, Class<?> caller) throws SQLException {
ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;
//遍历所有注册的数据库驱动
for(DriverInfo aDriver : registeredDrivers) {
//先检查这当前类加载器是否有权限加载这个驱动,如果是才进入
if(isDriverAllowed(aDriver.driver, callerCL)) {
//这一步是关键,会去调用Driver的connect方法
Connection con = aDriver.driver.connect(url, info);
if (con != null) {
return con;
}
} else {
println(" skipping: " + aDriver.getClass().getName());
}
}
}

com.mysql.cj.jdbc.Driver.connection

由于使用的是mysql的数据驱动,这里实际调用的是com.mysql.cj.jdbc.Driver的方法。

从以下代码可以看出,mysql支持支持多节点部署的策略,本文仅对单机版进行扩展。

注意:考虑篇幅,以下代码经过修改,仅保留所需部分。

    //mysql支持多节点部署的策略,根据架构不同,url格式也有所区别。
private static final String REPLICATION_URL_PREFIX = "jdbc:mysql:replication://";
private static final String URL_PREFIX = "jdbc:mysql://";
private static final String MXJ_URL_PREFIX = "jdbc:mysql:mxj://";
public static final String LOADBALANCE_URL_PREFIX = "jdbc:mysql:loadbalance://";
public java.sql.Connection connect(String url, Properties info) throws SQLException {
//根据url的类型来返回不同的连接对象,这里仅考虑单机版
ConnectionUrl conStr = ConnectionUrl.getConnectionUrlInstance(url, info);
switch (conStr.getType()) {
case SINGLE_CONNECTION:
//调用ConnectionImpl.getInstance获取连接对象
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;
}
}

ConnectionImpl.getInstance

这个类有个比较重要的字段session,可以把它看成一个会话,和我们平时浏览器访问服务器的会话差不多,后续我们进行数据库操作就是基于这个会话来实现的。

注意:考虑篇幅,以下代码经过修改,仅保留所需部分。

    private NativeSession session = null;
public static JdbcConnection getInstance(HostInfo hostInfo) throws SQLException {
//调用构造
return new ConnectionImpl(hostInfo);
}
public ConnectionImpl(HostInfo hostInfo) throws SQLException {
//先根据hostInfo初始化成员属性,包括数据库主机名、端口、用户名、密码、数据库及其他参数设置等等,这里省略不放入。
//最主要看下这句代码
createNewIO(false);
}
public void createNewIO(boolean isForReconnect) {
if (!this.autoReconnect.getValue()) {
//这里只看不重试的方法
connectOneTryOnly(isForReconnect);
return;
} connectWithRetries(isForReconnect);
}
private void connectOneTryOnly(boolean isForReconnect) throws SQLException { JdbcConnection c = getProxy();
//调用NativeSession对象的connect方法建立和数据库的连接
this.session.connect(this.origHostInfo, this.user, this.password, this.database, DriverManager.getLoginTimeout() * 1000, c);
return;
}

NativeSession.connect

接下来的代码主要是建立会话的过程,首先时建立物理连接,然后根据协议建立会话。

注意:考虑篇幅,以下代码经过修改,仅保留所需部分。

    public void connect(HostInfo hi, String user, String password, String database, int loginTimeout, TransactionEventHandler transactionManager)
throws IOException {
//首先获得TCP/IP连接
SocketConnection socketConnection = new NativeSocketConnection();
socketConnection.connect(this.hostInfo.getHost(), this.hostInfo.getPort(), this.propertySet, getExceptionInterceptor(), this.log, loginTimeout); // 对TCP/IP连接进行协议包装
if (this.protocol == null) {
this.protocol = NativeProtocol.getInstance(this, socketConnection, this.propertySet, this.log, transactionManager);
} else {
this.protocol.init(this, socketConnection, this.propertySet, transactionManager);
} // 通过用户名和密码连接指定数据库,并创建会话
this.protocol.connect(user, password, database);
}

针对数据库的连接,暂时点到为止,另外还有涉及数据库操作的源码分析,后续再完善补充。

相关源码请移步:https://github.com/ZhangZiSheng001/jdbc-demo

本文为原创文章,转载请附上原文出处链接:https://www.cnblogs.com/ZhangZiSheng001/p/11917307.html

jdbc-mysql测试例子和源码详解的更多相关文章

  1. dom4j的测试例子和源码详解(重点对比和DOM、SAX的区别)

    目录 简介 DOM.SAX.JAXP和DOM4J xerces解释器 SAX DOM JAXP DOM解析器 获取SAX解析器 DOM4j 项目环境 工程环境 创建项目 引入依赖 使用例子--生成xm ...

  2. cglib测试例子和源码详解

    目录 简介 为什么会有动态代理? 常见的动态代理有哪些? 什么是cglib 使用例子 需求 工程环境 主要步骤 创建项目 引入依赖 编写被代理类 编写MethodInterceptor接口实现类 编写 ...

  3. DBCP2的使用例子和源码详解(不包括JNDI和JTA支持的使用)

    目录 简介 使用例子 需求 工程环境 主要步骤 创建项目 引入依赖 编写jdbc.prperties 获取连接池和获取连接 编写测试类 配置文件详解 数据库连接参数 连接池数据基本参数 连接检查参数 ...

  4. [Spark内核] 第40课:CacheManager彻底解密:CacheManager运行原理流程图和源码详解

    本课主题 CacheManager 运行原理图 CacheManager 源码解析 CacheManager 运行原理图 [下图是CacheManager的运行原理图] 首先 RDD 是通过 iter ...

  5. [Qt Creator 快速入门] 第2章 Qt程序编译和源码详解

    一.编写 Hello World Gui程序 Hello World程序就是让应用程序显示"Hello World"字符串.这是最简单的应用,但却包含了一个应用程序的基本要素,所以 ...

  6. mysql测试和sysbench工具详解

    前言 作为一名后台开发,对数据库进行基准测试,以掌握数据库的性能情况是非常必要的.本文介绍了MySQL基准测试的基本概念,以及使用sysbench对MySQL进行基准测试的详细方法. 文章有疏漏之处, ...

  7. Spark Sort-Based Shuffle具体实现内幕和源码详解

    为什么讲解Sorted-Based shuffle?2方面的原因:一,可能有些朋友看到Sorted-Based Shuffle的时候,会有一个误解,认为Spark基于Sorted-Based Shuf ...

  8. Arouter核心思路和源码详解

    前言 阅读本文之前,建议读者: 对Arouter的使用有一定的了解. 对Apt技术有所了解. Arouter是一款Alibaba出品的优秀的路由框架,本文不对其进行全面的分析,只对其最重要的功能进行源 ...

  9. go map数据结构和源码详解

    目录 1. 前言 2. go map的数据结构 2.1 核心结体体 2.2 数据结构图 3. go map的常用操作 3.1 创建 3.2 插入或更新 3.3 删除 3.4 查找 3.5 range迭 ...

随机推荐

  1. HTTP协议详解(一)——初识HTTP及请求

    何为HTTP(超文本传输协议)? 协议,即约定俗成的规范.HTTP则是浏览器与服务器之间交流的一种规范.HTTP基于TCP/IP协议,属于应用层的协议. 为什么Web应用不使用TCP或者UDP协议呢? ...

  2. django-模板之comment标签(六)

    index.html <!DOCTYPE html> <html lang="en"> <head> <meta charset=&quo ...

  3. fenby C语言 P17

    for姐姐 dowhile妹妹 while for(循环变量赋初值,循环条件,循环变量自加) #include <stdio.h> int main(){ int sum=0,i; for ...

  4. Oracle ADG环境搭建

    部署 环境介绍 1,软件安装前基础部署 (两台做同样操作) 1.1,关闭selinux和防火墙 因为centos7里面没有/etc/sysconfig/iptables这个配置文件所以我们首先用yum ...

  5. vue页面首次加载缓慢原因及解决方案

    第一次打包vue的项目部署到服务器下时,发现初次加载特别的缓慢,将近20s页面才加载出来,完全没有开发环境上的那么流畅.主要原因是页面在打包后如果不进行相关配置会导致资源文件特别的大,一次想要全部加载 ...

  6. 20191107-配置 pyqt5+pycharm 环境

    因公司需要,今天配置了 pyqt5+pycharm 环境,准备试着写些 UI 界面. 参考资源: 1. Python3+PyQt5+PyCharm 桌面GUI开发环境搭建 https://www.cn ...

  7. [Java] 生成二维码源码,可以在二维码中间加logo,底部可以加文字介绍

    链接:https://pan.baidu.com/s/1bc1h-ix-No-2o9Ysd4_B3Q提取码:0ad4

  8. 学习笔记64_k邻近算法

    1 .假定已知数据的各个属性值,以及其类型,例如: 电影名称 打斗镜头 接吻镜头 电影类别 m1 3 104 爱情片 m2 2 100 爱情片 m3 1 81 爱情片 m4 2 90 爱情片 w1 1 ...

  9. 模拟80(a)

    其实隔壁的那套题比这套难的多....一道都不会.. T1 题目中已经给出了递推公式,那么这题就没什么了,直接矩阵乘就完了. 然而考场上并没有看出矩阵,主要是用了好久发明crt,我知道原理,但是不会打了 ...

  10. vue-router动态添加路由报错

    [报错] Uncaught Error: [vue-router] route config "component" for path: /home cannot be a str ...