1. JDBC
1.1 什么是 JDBC?
JDBC 是什么?来看看文档说明:
The Java Database Connectivity (JDBC) API provides universal data access from the Java programming language. Using the JDBC API, you can access virtually any data source, from relational databases to spreadsheets and flat files. JDBC technology also provides a common base on which tools and alternate interfaces can be built.
大致意思:Java数据库连接(JDBC) API提供了来自Java编程语言的通用数据访问。使用JDBC API,您几乎可以访问任何数据源,从关系数据库到电子表格和平面文件。JDBC技术还提供了构建工具和替代接口的公共基础。
简单来说,JDBC 就是通过 Java 语言来操作数据库(执行 SQL 语句)
1.2 JDBC 原理
来看看文档怎么说?
To use the JDBC API with a particular database management system, you need a JDBC technology-based driver to mediate between JDBC technology and the database. Depending on various factors, a driver might be written purely in the Java programming language or in a mixture of the Java programming language and Java Native Interface (JNI) native methods. To obtain a JDBC driver for a particular database management system, see JDBC Data Access API.
大致意思:要在特定的数据库管理系统中使用JDBC API,您需要一个基于JDBC技术的驱动程序来充当JDBC技术和数据库之间的中介。根据各种因素,驱动程序可以纯用Java编程语言编写,也可以混合使用Java编程语言和Java本机接口(JNI)本机方法编写。
早期SUN公司的天才们想编写一套可以连接天下所有数据库的API,但是当他们刚刚开始时就发现这是不可完成的任务,因为各个厂商的数据库服务器(mysql 数据库服务器、oracle 数据库服务器)差异太大了。后来SUN开始与数据库厂商们讨论,最终得出的结论是:由SUN提供一套访问数据库的规范(一组接口),并提供连接数据库的协议标准,然后各个数据库厂商会遵循SUN的规范提供一套访问自己公司的数据库服务器的API实现。SUN提供的规范命名为 JDBC,而各个厂商提供的,遵循了JDBC规范的,可以访问自己数据库的API被称之为驱动!
JDBC 只是一套规范接口,真正实现的是下面的各种驱动:
没有驱动无法完成数据库连接!每个数据库厂商都有自己的驱动,用来连接自己公司的数据库。
文档还说明:
The JDBC API is comprised of two packages:
- java.sql
- javax.sql
You automatically get both packages when you download the Java Platform Standard Edition (Java SE) 8.
JDBC API 位于 jdk 8 中的 java.sql、javax.sql 包下
1.3 JDBC 核心类/接口介绍
JDBC 中的核心类/接口有:DriverManager、Connection、Statement/PreparedStatement、ResultSet
DriverManager:驱动管理器。负责加载各种不同驱动程序(Driver)
- 注册驱动:让 JDBC 知道要使用的是哪个驱动
- 获取连接对象:根据不同的请求,向调用者返回相应的数据库连接(Connection)
Connection:数据库连接对象。与数据库的通讯都是通过这个对象展开的
- 获取Statement对象
Statement:数据库操作对象。向数据库发送 SQL 语句(静态)。这样数据库就会执行发送过来的 SQL 语句
- void executeUpdate(String sql):执行更新操作(insert、update、delete等)
- ResultSet executeQuery(String sql):执行查询操作。查询结果就是ResultSet
PreparedStatement:预编译声明。它是 Statement 的子接口
-
prepareStatement(String sql):创建它时就让它与一条SQL模板绑定
-
调用PreparedStatement的setXXX()系列方法为问号设置值
-
调用executeUpdate()或executeQuery()方法,但要注意,调用没有参数的方法
ResultSet:对象表示查询结果集。只有在执行查询操作后才会有结果集的产生。结果集是一个二维的表格,有行有列。操作结果集要学习移动ResultSet内部的“行光标”,以及获取当前行上的每一列上的数据。
- boolean next():使“行光标”移动到下一行,并返回移动后的行是否存在
- XXX getXXX(int col):获取当前行指定列上的值,参数就是列数,列数从1开始,而不是0
1.4 JDBC 案例
这里使用 mysql 数据库服务器来做一个查询操作。
public class DBUtil {
private static final String JDBC_URL = "jdbc:mysql://127.0.0.1:3306/zzc?characterEncoding=UTF-8";
private static final String USERNAME = "root";
private static final String PASSWORD = "root";
// 获取数据库连接
public static Connection getConnection() {
Connection conn = null;
try {
// 1.注册驱动
Class.forName("com.mysql.jdbc.Driver");
try {
conn = DriverManager.getConnection(JDBC_URL, USERNAME, PASSWORD);
} catch (SQLException e) {
System.out.println("数据库连接失败");
}
} catch (ClassNotFoundException e) {
System.out.println("驱动包不存在");
}
return conn;
}
// 关闭连接
public static void close(Connection conn, Statement stst, ResultSet rs) {
if (null != conn) {
try {
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (null != stst) {
try {
stst.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (null != rs) {
try {
rs.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
注册驱动
上面的代码中,注册驱动时使用的是 “Class.forName(“com.mysql.jdbc.Driver”);”,那么要为什么是这样的写法?
先查看 DriverManager.registerDriver(java.sql.Driver) 方法,java.sql.Driver 是一个接口,其实现类是由 mysql 驱动提供。其实现类为 “com.mysql.jdbc.Driver”。那么注册驱动的代码本应该为:
DriverManager.registerDriver(new com.mysql.jdbc.Driver());
再来看看 com.mysql.jdbc.Driver 类:
public class Driver extends NonRegisteringDriver implements java.sql.Driver {
public Driver() throws SQLException {
}
static {
try {
DriverManager.registerDriver(new Driver());
} catch (SQLException var1) {
throw new RuntimeException("Can't register driver!");
}
}
}
上述代码确实可以注册驱动,但是会出现以下问题:
- 出现硬编码,以致应用程序与驱动包无法解耦。如果以后修改为 oracle 数据库,那么需要修改源代码
- 这种注册驱动方式会将 com.mysql.jdbc.Driver() 对象创建两次,完全没必要
JDBC 中规定,驱动类在被加载时,需要自己“主动”把自己注册到 DriverManger 中。如上的 Driver 类的源码:
static 代码块中,会创建 Driver 对象,并且注册到 DriverManager 中。
这说明只要去加载 com.mysql.jdbc.Driver 类,那么就会执行这个 static 块,从而也就会把com.mysql.jdbc.Driver 注册到 DriverManager 中,所以可以把注册驱动类的代码修改为加载驱动类。即:
Class.forName("com.mysql.jdbc.Driver");
public class DBTest {
public void selectAll() {
String sql = "SELECT * FROM user";
Connection conn = null;
PreparedStatement prst = null;
ResultSet rs = null;
try {
conn = DBUtil.getConnection();
prst = conn.prepareStatement(sql);
rs = prst.executeQuery();
int row = 0;
while (rs.next()) {
row++;
int id = rs.getInt(1);
String name = rs.getString(2);
int sex = rs.getInt(3);
System.out.println("================第" + row + "行");
System.out.print(id + ",");
System.out.print(name + ",");
System.out.println(sex);
}
} catch (SQLException e) {
e.printStackTrace();
} finally {
DBUtil.close(conn, prst, rs);
}
}
public static void main(String[] args) {
DBTest dbTest = new DBTest();
dbTest.selectAll();
}
}
2. JDBC 中的 SPI 机制
那么,在 JDBC 中,SPI 机制是如何使用的呢?
如果还不了解 SPI 机制,请移步 Java 中的 SPI 机制
我们知道,在 JDBC 使用过程中,是有“注册驱动”这一步骤的,代码如下:
Class.forName("com.mysql.jdbc.Driver");
现在,我们将那个案例中的这行代码给注释/删除掉。即 getConnection() 方法代码如下:
public static Connection getConnection() {
Connection conn = null;
//try {
//Class.forName("com.mysql.jdbc.Driver");
try {
conn = DriverManager.getConnection(JDBC_URL, USERNAME, PASSWORD);
System.out.println(conn);
} catch (SQLException e) {
System.out.println("数据库连接失败");
}
//} catch (ClassNotFoundException e) {
//System.out.println("驱动包不存在");
//}
return conn;
}
发现,也能查询数据库成功。
问题:为什么 JDBC 中不注册驱动还能连接数据库成功?
答案就在 SPI机制中。
2.1 DriverManager 类
查看 DriverManager 类源码中的静态代码块:
public class DriverManager {
/* Prevent the DriverManager class from being instantiated. */
private DriverManager(){}
/**
* Load the initial JDBC drivers by checking the System property
* jdbc.properties and then use the {@code ServiceLoader} mechanism
*/
static {
loadInitialDrivers();
println("JDBC DriverManager initialized");
}
查看 loadInitialDrivers() 方法:它在里面查找的是 Driver 接口的服务类,所以它的文件路径就是:META-INF/services/java.sql.Driver。并在调用 next() 方法时,会创建文件里面的实现类
private static void loadInitialDrivers() {
String drivers;
try {
drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
public String run() {
return System.getProperty("jdbc.drivers");
}
});
} catch (Exception ex) {
drivers = null;
}
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
// 很明显,它要加载 Driver 接口的服务类,Driver接口的包为:java.sql.Driver
// 所以它要找的就是 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;
}
});
...
}
文件 META-INF/services/java.sql.Driver
位置:
其内容为:
com.mysql.jdbc.Driver
com.mysql.fabric.jdbc.FabricMySQLDriver
所以,会创建 com.mysql.jdbc.Driver 类、com.mysql.fabric.jdbc.FabricMySQLDriver 类
2.2 Driver 类
查看 com.mysql.jdbc.Driver 类源码:
public class Driver extends NonRegisteringDriver implements java.sql.Driver {
public Driver() throws SQLException {
}
static {
try {
DriverManager.registerDriver(new Driver());
} catch (SQLException var1) {
throw new RuntimeException("Can't register driver!");
}
}
}
在实例化 com.mysql.jdbc.Driver 对象时,会调用 Driver 类中的 static 代码块----向 DriverManager 注册自身的实例(Driver)。
好了,再回过头来看看 Java API 中对 DriverManager 类的说明:
…
The
DriverManager
methodsgetConnection
andgetDrivers
have been enhanced to support the Java Standard EditionService Provider mechanism. JDBC 4.0 Drivers must include the file
META-INF/services/java.sql.Driver
. This file contains the name of the JDBC drivers implementation ofjava.sql.Driver
. For example, to load themy.sql.Driver
class,
theMETA-INF/services/java.sql.Driver
file would contain the entry:my.sql.Driver
.Applications no longer need to explicitly load JDBC drivers using
Class.forName()
. Existing programs
which currently load JDBC drivers usingClass.forName()
will continue to work without
modification.…
大致意思:JDBC 4.0驱动程序必须包含文件META-INF/services/java.sql.Driver 。 该文件包含java.sql.Driver的JDBC驱动程序实现的名称。应用程序不再需要使用Class.forName() 显式加载 JDBC 驱动程序。 当前使用 Class.forName() 加载 JDBC 驱动程序的现有程序将继续运行,而无需进行修改。
所以,现在明白了使用 JDBC 为什么可以不需要注册驱动了吧。
2.3 创建 Connection 类
在DriverManager.getConnection() 方法就是创建连接的地方:它通过循环已注册的数据库驱动程序,调用其 connect() 方法,获取连接并返回。
private static Connection getConnection(String url, java.util.Properties info, Class<?> caller) throws SQLException {
...
SQLException reason = null;
// 遍历已注册的数据库驱动 Driver
for(DriverInfo aDriver : registeredDrivers) {
if(isDriverAllowed(aDriver.driver, callerCL)) {
try {
println(" trying " + aDriver.driver.getClass().getName());
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;
}
}
} else {
println(" skipping: " + aDriver.getClass().getName());
}
}
...
}
2.4 自定义驱动类 MyDriver
既然我们已经明白了数据库创建的过程,那么,我们可以尝试一下,自己创建一个驱动类 MyDriver。
按照之前的概念,总结出服务接口、服务实现:
服务接口(JDK):java.sql.Driver
服务实现:com.tinady.jdbc.MyDriver
按照 Java 中的 SPI 机制规范,需要做出以下准备工作:
- 需要在类路径下添加一个文件,文件名为:META-INF/services/java.sql.Driver
- 内容为:com.tinady.jdbc.MyDriver(包名 + 类名)
MyDriver 继承自 MySQL 中 的NonRegisteringDriver,还要实现 java.sql.Driver 接口。这样,在调用 connect() 方法的时候,就会调用到此类,但实际创建的过程还靠 MySQL 中的 NonRegisteringDriver 类完成。
public class MyDriver extends NonRegisteringDriver implements Driver {
public MyDriver() throws SQLException {
}
static {
try {
DriverManager.registerDriver(new MyDriver());
} catch (SQLException var1) {
throw new RuntimeException("Can't register driver!");
}
}
@Override
public Connection connect(String url, Properties info) throws SQLException {
System.out.println("准备创建数据库连接.url:" + url);
System.out.println("JDBC配置信息:" + info);
info.setProperty("user", "root");
Connection connection = super.connect(url, info);
System.out.println("数据库连接创建完成!" + connection.toString());
return connection;
}
@Override
public Logger getParentLogger() throws SQLFeatureNotSupportedException {
return null;
}
}