JavaWeb的内容大致包括:
我记得18年6月开始JavaWeb系列的写作,截止目前,还剩下JDBC和Filter/Listener。速度有点超乎预期地慢...不过随著自己开发经验的增长以及对知识点的理解,相信后面出活会越来越快。
JDBC本身是非常简单的,就是一组API介面,只要导入对应的驱动包(JDBC实现类)就可以对资料库进行增删改查。但是其中涉及到很多问题,比如:
等等。不论培训班视频还是书籍,讲到JDBC时都会把上面的内容串联起来,这使得它的难度瞬时翻了几番。
主要内容:
在说JDBC之前,必须先聊聊数据持久化。
持久化
把数据保存到可掉电式存储设备中以供之后使用。
大多数情况下,数据持久化意味著将内存中的数据保存到磁碟中加以「固化」。而持久化的实现过程大多通过各种关系资料库完成。当然,也可以存入磁碟文件或者XML数据文件(崔老师JavaWeb day14练习中,使用了XML充当「资料库」)。
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有三个大步骤:
我们上面的代码,仅完成了第一步。
上面testDriver()方法中的第一句:
//1. 创建一个 Driver 实现类的对象 Driver driver = new com.mysql.jdbc.Driver();
这是一个典型的面向介面编程。
我们先看左边的Driver介面:
接著我们再来看看右边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();最终目的是为了driver.connect()获得Connection,这一点很合理。但是为啥还要在类中搞一个静态代码块去注册驱动?没有这一步,我不照样获得Connection吗?
DriverManager
其实,之所以com.mysql.jdbc.Driver类中的静态代码块要去注册驱动,是因为获取Connection通常有两种办法:
通常后者更常用。而所谓的驱动管理器,就是DriverManager。通过DriverManager.getConnection()即可获取。
此刻,我们发现脑中瞬间多了两个疑问:
不急,先上代码,看看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);
}
上面示例中,最关键的一句代码是:
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):
也知道了com.mysql.jdbc.Driver会通过静态代码块,将自己注册到了DriverManager。
那么,问题就来了:driver.connect()和DriverManager.getConnection()都可以获得连接,它们之间有什么联系吗?
来看一下这个同名的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的三大步骤:
这一篇只涉及如何获取Connection,下篇才真正开始CRUD以及代码抽取。
最后,我自己看源码时,发现一个问题:Driver介面有个getParentLogger(),但是NonRegisteringDriver并没有实现这个方法(其他方法都实现了)。
这是为啥呢?
2019-5-5 20:42:45
推荐阅读: