JavaWeb的内容大致包括:

  • Tomcat(Tomcat外传)
  • Servlet、JSP(浅谈JSP)
  • Session、Cookie(Cookie与Session)
  • JDBC
  • Filter/Listener
  • AJAX、JSON(AJAX与JSON)

我记得18年6月开始JavaWeb系列的写作,截止目前,还剩下JDBC和Filter/Listener。速度有点超乎预期地慢...不过随著自己开发经验的增长以及对知识点的理解,相信后面出活会越来越快。

JDBC本身是非常简单的,就是一组API介面,只要导入对应的驱动包(JDBC实现类)就可以对资料库进行增删改查。但是其中涉及到很多问题,比如:

  • Connection的管理(连接池)
  • 冗余代码的抽取(获取、释放连接)
  • 结果集的封装(映射Pojo栏位)

等等。不论培训班视频还是书籍,讲到JDBC时都会把上面的内容串联起来,这使得它的难度瞬时翻了几番。

主要内容:

  • JDBC简介
  • 基本环境搭建
  • Driver
  • DriverManager
  • 代码优化

JDBC简介

在说JDBC之前,必须先聊聊数据持久化。

持久化

把数据保存到可掉电式存储设备中以供之后使用。

大多数情况下,数据持久化意味著将内存中的数据保存到磁碟中加以「固化」。而持久化的实现过程大多通过各种关系资料库完成。当然,也可以存入磁碟文件或者XML数据文件(崔老师JavaWeb day14练习中,使用了XML充当「资料库」)。

图片来自尚矽谷JDBC

JDBC

资料库是实现持久化的一种途径,而JDBC则是通向资料库的桥梁。

通俗地讲,JDBC就是一组API(包括少量类),为访问不同资料库提供了统一的途径,为开发者屏蔽了一些细节问题。比如,我们都知道浏览器发送HTTP请求访问伺服器,但其实请求底层仍是TCP协议。同样的,访问资料库底层也通过TCP协议。你知道怎么与资料库建立TCP连接吗?一部分科班读者可能对计算机网路非常熟悉,但是大部分像我这样的野生程序员可能压根没想过这个问题。所幸,这些具体的实现,各大资料库产商已经替我们做了。

驱动

JDBC是Java制定的介面,资料库产商依照该介面编写与自家资料库配套的实现类。比如MySQL、Oracle、SqlServer等都有自己的不同实现,这些实现类的集合既是我们笼统意义上的「驱动」。

面向介面编程

在代码中直接new具体的驱动类,会使程序高度耦合。比如,后期如果要切换资料库(虽然很少),就要临时调换驱动类,需要修改源码,不符合开闭原则。而面向介面编程,实际上就是一种「多态」。屏蔽具体的实现,只需调用介面方法,传入规定的参数即可得到预期的返回值。切换资料库驱动并不影响程序运行结果。


基本环境搭建

上面说过,JDBC只是一组介面,具体实现交给驱动。所以,要使用JDBC完成CRUD,必须先导入具体的资料库驱动。本次我们以MySQL为例,所以导入MySQL的资料库驱动。

pom.xml

<dependencies>
<!--MySQL资料库驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.46</version>
</dependency>
<!--Junit-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>compile</scope>
</dependency>
</dependencies>

代码结构

DriverDemo

public class DriverDemo {
@Test
public void testDriver() throws SQLException {
//1. 创建一个 Driver 实现类的对象
Driver driver = new com.mysql.jdbc.Driver();

//2. 准备连接资料库的基本信息: url, user, password
String url = "jdbc:mysql://192.168.136.128:3306/test";
Properties info = new Properties();
info.put("user", "root");
info.put("password", "root");

//3. 调用 Driver 介面的 connect(url, info) 获取资料库连接
Connection connection = driver.connect(url, info);
System.out.println(connection);
}
}

运行结果

使用JDBC有三个大步骤:

  • 连接资料库
  • 执行SQL语句
  • 获得结果

我们上面的代码,仅完成了第一步。


Driver

上面testDriver()方法中的第一句:

//1. 创建一个 Driver 实现类的对象
Driver driver = new com.mysql.jdbc.Driver();

这是一个典型的面向介面编程。

我们先看左边的Driver介面:

介面的方法并不多,我们在上面用到了其中的connect()方法

接著我们再来看看右边MySQL的Driver实现类:

怎么回事?!

只有一个静态代码块和无参构造。不对啊,我们明明在程序中调用了driver.connect(),怎么连connect方法都没了?!

仔细一看,原来MySQL的Driver类还继承了NonRegisteringDriver,它实现了Driver介面的全部方法:

connect()的核心代码就一句:

Connection newConn = com.mysql.jdbc.ConnectionImpl.getInstance(host(props), port(props), props, database(props), url);

总之,就是根据给定的url和用户名密码,返回一个与资料库关联的Connection。JDBC相当于是程序与资料库之间的桥梁。得到Connect代表桥已经建好,此刻已经可以通车了。

由于com.mysql.jdbc.Driver继承了NonRegisteringDriver,所以它也可以调用connect方法(继承父类方法),上面我们已经用过,确实可行。

现在我们把注意力集中到com.mysql.jdbc.Driver中独有的代码:静态代码块+无参构造。

public class Driver extends NonRegisteringDriver implements java.sql.Driver {
//
// Register ourselves with the DriverManager
//
static {
try {
java.sql.DriverManager.registerDriver(new Driver());
} catch (SQLException E) {
throw new RuntimeException("Cant register driver!");
}
}

/**
* Construct a new driver and register it with DriverManager
*
* @throws SQLException
* if a database error occurs.
*/
public Driver() throws SQLException {
// Required for Class.forName().newInstance()
}
}

空参构造的作用,仅仅是为了能够new com.mysql.jdbc.Driver()。但重点不在于new本身,而是new这个Driver的时候,JVM会去载入这个Driver实现类。此时会自动执行静态代码块中的代码,也就是:

java.sql.DriverManager.registerDriver(new Driver());

注释已经说得很明白,就是把这个Driver注册到DriverManager。

所以,我们使用Driver获取连接的整体逻辑是:

  • new com.mysql.jdbc.Driver()
  • 类被载入进内存
  • 静态代码块执行:java.sql.DriverManager.registerDriver(newDriver());
  • com.mysql.jdbc.driver实例被注册进DriverManager
  • 调用com.mysql.jdbc.driver实例的connect()方法获得Connection

new com.mysql.jdbc.driver();最终目的是为了driver.connect()获得Connection,这一点很合理。但是为啥还要在类中搞一个静态代码块去注册驱动?没有这一步,我不照样获得Connection吗?


DriverManager

其实,之所以com.mysql.jdbc.Driver类中的静态代码块要去注册驱动,是因为获取Connection通常有两种办法:

  • 根据具体的Driver实现类直接获取
  • 通过驱动管理器获取

通常后者更常用。而所谓的驱动管理器,就是DriverManager。通过DriverManager.getConnection()即可获取。

此刻,我们发现脑中瞬间多了两个疑问:

  1. DriverManager是个啥呀?咋还能getConnection()了呢?
  2. 还有,将Driver注册进DriverManager又能咋地?driver.connect()不也好使吗,照样获得了Connection,为啥要整出一个DriverManager?

不急,先上代码,看看DriverManager如何获得Connection。

@Test
public void testDriverManager() throws Exception{
//1. 驱动的全类名
String driverClass = "com.mysql.jdbc.Driver";
//2. 准备连接资料库的基本信息: url, user, password
String url = "jdbc:mysql://192.168.136.128:3306/test";
String user = "root";
String password = "root";

//2. 载入资料库驱动程序(对应的 Driver 实现类中有注册驱动的静态代码块)
Class.forName(driverClass);

//3. 通过 DriverManager 的 getConnection() 方法获取资料库连接
Connection connection =
DriverManager.getConnection(url, user, password);
System.out.println(connection);

}

上面示例中,最关键的一句代码是:

//2. 载入资料库驱动程序(对应的 Driver 实现类中有注册驱动的静态代码块)
Class.forName(driverClass);

JVM何时会载入一个类呢?

大体可以归为两种情况:

被动载入(Driver方式)

//JVM发现要new一个com.mysql.jdbc.Driver实例,而此时内存中没有com.mysql.jdbc.Driver位元组码对象,就会去载入该类
Driver driver = new com.mysql.jdbc.Driver();

主动载入(DriverManager方式)

//通过Class对象主动触发JVM载入com.mysql.jdbc.Driver类
Class.forName("com.mysql.jdbc.Driver");

我们已经分析过,不论是主动还是被动,只要类被载入,静态代码块都会执行执行。也就是说,com.mysql.jdbc.Driver都会被注册进DriverManager。

Driver方式获取Connection确实和「注册」操作没有直接关系,但是DriverManager方式与「注册」关系密切。我们来分析一下源码。

我们之前已经查看过com.mysql.jdbc.Driver的源码,知道了connect()方法的具体实现(继承自NonRegisteringDriver):

这一句代码,底层屏蔽了好多细节,直接连上资料库返回了Connection

也知道了com.mysql.jdbc.Driver会通过静态代码块,将自己注册到了DriverManager。

那么,问题就来了:driver.connect()和DriverManager.getConnection()都可以获得连接,它们之间有什么联系吗?

1.DriverManager内部维护一个容器,该容器存储所有已注册的Driver
2.通过DriverManager也可以得到连接

来看一下这个同名的getConnection()方法具体做了啥:

//私有方法,只能内部调用:获取资料库连接
private static Connection getConnection(
String url, java.util.Properties info, Class<?> caller) throws SQLException {
//callerCl是类载入器
ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;
//同步代码块
synchronized(DriverManager.class) {
if (callerCL == null) {
//如果没有传入类载入器,使用当前线程的类载入器
callerCL = Thread.currentThread().getContextClassLoader();
}
}

if(url == null) {
throw new SQLException("The url cannot be null", "08001");
}

println("DriverManager.getConnection("" + url + "")");

SQLException reason = null;

//循环遍历容器中所有已注册的Driver
for(DriverInfo aDriver : registeredDrivers) {
//isDriverAllowed()方法会使用callerCl尝试载入每一个Driver,载入成功返回true
if(isDriverAllowed(aDriver.driver, callerCL)) {
try {
println(" trying " + aDriver.driver.getClass().getName());
//底层还是使用了Driver本身的connect方法,获取Connection
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());
}

}

// if we got here nobody could connect.
if (reason != null) {
println("getConnection failed: " + reason);
throw reason;
}

println("getConnection: no suitable driver found for "+ url);
throw new SQLException("No suitable driver found for "+ url, "08001");
}

//载入驱动类,载入失败返回false
private static boolean isDriverAllowed(Driver driver, ClassLoader classLoader) {
boolean result = false;
if(driver != null) {
Class<?> aClass = null;
try {
aClass = Class.forName(driver.getClass().getName(), true, classLoader);
} catch (Exception ex) {
result = false;
}

result = ( aClass == driver.getClass() ) ? true : false;
}

return result;
}

小结

DriverManager是个啥呀?咋还能getConnection()了呢?

DriverManager是java.sql包下的一个类,内部维护著一个CopyOnWriteArrayList用于存放已经注册的驱动实例。之所以能getConnection(),是因为底层会循环遍历所有驱动,找到当前注册的驱动后调用driver.connect()获得Connection。

还有,将Driver注册进DriverManager又能咋地?driver.connect()不也好使吗,照样获得了Connection,为啥要整出一个DriverManager?

其实也没为啥,人家设计的初衷就是为了支持注册多个(多种)驱动,通过DriverManager可以管理多个驱动程序。所以它叫「驱动管理器」(DriverManager)。

静态代码块注册驱动到DriverManager对于Driver方式获取Connection是多余的,但是对于DriverManager获取Connection是必须的。


代码优化

把url、user、password、driver移到jdbc.properties文件中,做成可配置。

@Test
public void getConnection() throws Exception{
//1. 准备连接资料库的 4 个字元串.
//1). 创建 Properties 对象
Properties properties = new Properties();

//2). 获取 jdbc.properties 对应的输入流
InputStream in =
this.getClass().getClassLoader().getResourceAsStream("jdbc.properties");

//3). 载入 2) 对应的输入流
properties.load(in);

//4). 具体决定 user, password 等4 个字元串.
String user = properties.getProperty("user");
String password = properties.getProperty("password");
String jdbcUrl = properties.getProperty("jdbcUrl");
String driver = properties.getProperty("driver");

//2. 载入资料库驱动程序(对应的 Driver 实现类中有注册驱动的静态代码块)
Class.forName(driver);

//3. 通过 DriverManager 的 getConnection() 方法获取资料库连接
Connection connection = DriverManager.getConnection(jdbcUrl, user, password);

System.out.println(connection);
}

JDBC的三大步骤:

  • 连接资料库
  • 执行SQL语句
  • 获得结果

这一篇只涉及如何获取Connection,下篇才真正开始CRUD以及代码抽取。

最后,我自己看源码时,发现一个问题:Driver介面有个getParentLogger(),但是NonRegisteringDriver并没有实现这个方法(其他方法都实现了)。

这是为啥呢?

打开源码,IDEA提示错误...

2019-5-5 20:42:45

推荐阅读:

相关文章