jdbc-mysql测试例子和源码详解
简介
什么是JDBC
JDBC是一套连接和操作数据库的标准、规范。通过提供DriverManager、Connection、Statement、ResultSet等接口将开发人员与数据库提供商隔离,开发人员只需要面对JDBC接口,无需关心怎么跟数据库交互。
几个重要的类
| 类名 | 作用 |
|---|---|
DriverManager |
驱动管理器,用于注册驱动,是获取 Connection对象的入口 |
Driver |
数据库驱动,用于获取Connection对象 |
Connection |
数据库连接,用于获取Statement对象、管理事务 |
Statement |
sql执行器,用于执行sql |
ResultSet |
结果集,用于封装和操作查询结果 |
prepareCall |
用于调用存储过程 |
使用中的注意事项
- 记得释放资源。另外,
ResultSet和Statement的关闭都不会导致Connection的关闭。 - maven要引入oracle的驱动包,要把jar包安装在本地仓库或私服才行。
- 使用
PreparedStatement而不是Statement。可以避免SQL注入,并且利用预编译的特点可以提高效率。
使用例子
需求
使用JDBC对mysql数据库的用户表进行增删改查。
工程环境
JDK:1.8
maven:3.6.1
IDE:sts4
mysql driver:8.0.15
mysql:5.7
主要步骤
一个完整的JDBC保存操作主要包括以下步骤:
- 注册驱动(JDK6后会自动注册,可忽略该步骤);
- 通过
DriverManager获得Connection对象; - 开启事务;
- 通过
Connection获得PreparedStatement对象; - 设置
PreparedStatement的参数; - 执行保存操作;
- 保存成功提交事务,保存失败回滚事务;
- 释放资源,包括
Connection、PreparedStatement。
创建表
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配置是&采用&替代
如果是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对象呢?
通过
DriverInfo的源码可知,当我们调用equals方法比较两个DriverInfo对象是否相等时,实际上比较的是Driver对象的地址,也就是说,我可以在DriverManager中注册多个MYSQL驱动。而如果直接存放的是Driver对象,就不能达到这种效果(因为没有遇到需要注册多个同类驱动的场景,所以我暂时理解不了这样做的好处)。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://www.cnblogs.com/ZhangZiSheng001/p/11917307.html
jdbc-mysql测试例子和源码详解的更多相关文章
- dom4j的测试例子和源码详解(重点对比和DOM、SAX的区别)
目录 简介 DOM.SAX.JAXP和DOM4J xerces解释器 SAX DOM JAXP DOM解析器 获取SAX解析器 DOM4j 项目环境 工程环境 创建项目 引入依赖 使用例子--生成xm ...
- cglib测试例子和源码详解
目录 简介 为什么会有动态代理? 常见的动态代理有哪些? 什么是cglib 使用例子 需求 工程环境 主要步骤 创建项目 引入依赖 编写被代理类 编写MethodInterceptor接口实现类 编写 ...
- DBCP2的使用例子和源码详解(不包括JNDI和JTA支持的使用)
目录 简介 使用例子 需求 工程环境 主要步骤 创建项目 引入依赖 编写jdbc.prperties 获取连接池和获取连接 编写测试类 配置文件详解 数据库连接参数 连接池数据基本参数 连接检查参数 ...
- [Spark内核] 第40课:CacheManager彻底解密:CacheManager运行原理流程图和源码详解
本课主题 CacheManager 运行原理图 CacheManager 源码解析 CacheManager 运行原理图 [下图是CacheManager的运行原理图] 首先 RDD 是通过 iter ...
- [Qt Creator 快速入门] 第2章 Qt程序编译和源码详解
一.编写 Hello World Gui程序 Hello World程序就是让应用程序显示"Hello World"字符串.这是最简单的应用,但却包含了一个应用程序的基本要素,所以 ...
- mysql测试和sysbench工具详解
前言 作为一名后台开发,对数据库进行基准测试,以掌握数据库的性能情况是非常必要的.本文介绍了MySQL基准测试的基本概念,以及使用sysbench对MySQL进行基准测试的详细方法. 文章有疏漏之处, ...
- Spark Sort-Based Shuffle具体实现内幕和源码详解
为什么讲解Sorted-Based shuffle?2方面的原因:一,可能有些朋友看到Sorted-Based Shuffle的时候,会有一个误解,认为Spark基于Sorted-Based Shuf ...
- Arouter核心思路和源码详解
前言 阅读本文之前,建议读者: 对Arouter的使用有一定的了解. 对Apt技术有所了解. Arouter是一款Alibaba出品的优秀的路由框架,本文不对其进行全面的分析,只对其最重要的功能进行源 ...
- go map数据结构和源码详解
目录 1. 前言 2. go map的数据结构 2.1 核心结体体 2.2 数据结构图 3. go map的常用操作 3.1 创建 3.2 插入或更新 3.3 删除 3.4 查找 3.5 range迭 ...
随机推荐
- 自己动手破解Z.EntityFramework.Extensions 4.0.11.0的方法
因为项目中使用到Z.EntityFramework.Extensions 和 Z.EntityFramework.Plus(免费开源)两个类库,但是Z.EntityFramework.Extensio ...
- 处理 Could not find a 'KafkaClient' entry in the JAAS configuration. System property 'java.security.auth.login.config' is
场景 某监控进程需要访问多个集群的Kafka INFO - org.apache.kafka.common.KafkaException: Failed to construct kafka cons ...
- int和string的相互装换 (c++)
int和string的相互装换 (c++) int转换为string 第一种方法 to_string函数,这是c++11新增的函数 string to_string (int val); string ...
- .NET webAPI中集成swagger
最近做的项目使用winform三层+webapi,对于webAPI路由文档管理一直觉得单独做一些管理比较麻烦,并且测试的时候项目内的代码测试运行起来也比较麻烦,所以在网上开始检索相关办法,发现热度比较 ...
- Mycat分布式数据库架构解决方案--Mycat实现读写分离
echo编辑整理,欢迎转载,转载请声明文章来源.欢迎添加echo微信(微信号:t2421499075)交流学习. 百战不败,依不自称常胜,百败不颓,依能奋力前行.--这才是真正的堪称强大!!! 安装完 ...
- java和python对比
一:解释性和编译型 梳理 编译型:源代码经过编译直接变为二进制的机器语言,每次都可以直接重新运行不需要翻译.典型的就是c语言. 解释性:java和python都是解释型,源代码经过编译变为字节码文件, ...
- JVM参数及调优
## 3.2.1 JVM参数及调优 ### 调优基本概念 在调整JVM性能时,通常有三个组件需要考虑:1. 堆大小调整2. 垃圾收集器调整3. JIT编译器 大多数调优选项都与调整堆大小和选择合适的垃 ...
- Python3爬虫(1)_使用Urllib进行网络爬取
网络爬虫 又被称为网页蜘蛛,网络机器人,在FOAF社区中间,更经常的称为网页追逐者,是一种按照一定的规则,自动地抓取万维网信息的程序或者脚本.另外一些不常使用的名字还有蚂蚁.自动索引.模拟程序或者蠕虫 ...
- 记一次C#调用C++踩过的坑
一般来说,C#调用C++生产的dll,如下: C++的项目要设置为"导出dll的项目",而且导出的函数,一般为: extern "C" __declspec(d ...
- gedit一些小的新发现
写应该还有一些人正在像我一样用gedit呢. 现在vim,gedit,guide三党还是互相瞧不起呢. 我写这一篇是想稍微交流一下gedit的一些乱七八糟的玩意,非gedit党勿喷. 有些人连一些比较 ...