一、概述
本文旨在传递更多JDBC的细节和原理。环境是Mysql,后续会继续介绍事务、数据源和获取数据库元信息。
本文涉及问题概览:
1. Class.forName这句代码做了什么?
2. Class.forName和ClassLoader.loadClass区别?
3. DriverManager介绍;
二、详解
简单的JDBC操作,有以下几个步骤:
1. 注册驱动;
2. 获取链接;
3. 创建执行对象;
4. 执行SQL语句获得结果集;
5. 处理结果集;
6. 关闭资源;
一个完整的小例子,旨在引出下文。
package cn.wxy.jdbc;
import java.io.Serializable;
/**
* domain对象,用于封装数据
* @author wxy51
*/
public class User implements Serializable {
private static final long serialVersionUID = 7823619631737327735L;
private Integer id;
private String username;
private String password;
private String gender;
public User() {
}
public User(Integer id, String username, String password, String gender) {
super();
this.id = id;
this.username = username;
this.password = password;
this.gender = gender;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getGender() {
return gender;
}
public void setGender(String gender) {
this.gender = gender;
}
public String toString() {
return "User [id=" + id + ", username=" + username + ", password="
+ password + ", gender=" + gender + "]";
}
}
package cn.wxy.jdbc;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
public class SimpleJdbcOperation {
/**
* jdbc查询简单示例
* @param id 用户id
* @return 数据库中该用户id对应记录
*/
public User queryOneUser(Integer id) {
if(id == null)
return null;
Connection conn = null;
PreparedStatement pstmt = null;
ResultSet rSet = null;
User user = null;
try {
Class.forName("com.mysql.jdbc.Driver");
conn = DriverManager
.getConnection("jdbc:mysql://localhost:3306/demo", "用户名",
"密码");
pstmt = conn.prepareStatement("select * from user where id=?");
pstmt.setInt(1, id);
rSet = pstmt.executeQuery();
if (rSet.next())
user = new User(rSet.getInt(1), rSet.getString(2),
rSet.getString(3), rSet.getString(4));
} catch (Exception e) {
e.printStackTrace();
} finally {
if (conn != null)
try {
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
if (pstmt != null)
try {
pstmt.close();
} catch (SQLException e) {
e.printStackTrace();
}
if (rSet != null)
try {
rSet.close();
} catch (SQLException e) {
e.printStackTrace();
}
conn = null;
pstmt = null;
rSet = null;
}
return user;
}
}
1. 注册驱动
在JDK API中,注册驱动的方式(具体在java.sql.Driver接口的API中)已经明确指出“When a Driver class is loaded, it should create an instance of itself and register it with the DriverManager. This means that a user can load and register a driver by calling Class.forName(your driver path)”。
以mysql环境为例,推荐的驱动注册方式为Class.forName("com.mysql.jdbc.Driver");,参数“com.mysql.jdbc.Driver”是mysql jdbc驱动包中实现了java.sql.Driver接口规范的驱动实现类,先不看com.mysql.jdbc.Driver的源码,Class.forName这句代码做了什么?
@CallerSensitive
public static Class<?> forName(String className)
throws ClassNotFoundException {
Class<?> caller = Reflection.getCallerClass();
return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
}
private static native Class<?> forName0(String name, boolean initialize, ClassLoader loader,
Class<?> caller) throws ClassNotFoundException;
查看Class的源码,这句话是根据一个类的全量路径获得这个类的Class对象,但Class.forName这句代码所做的事情绝不仅仅只是获取Class对象这么一点。
类加载器是JDK中一个模块,这个模块描述的是将一个类的字节码文件加载到内存,并为之生成java.lang.Class对象作为其对外访问的接口,他涉及三个环节五个步骤,加载、连接(验证、准备、解析)和初始化,java的初始化知识涉及类初始化和对象初始化,那么在类加载过程中,准备和初始化两个步骤则描述了类初始化的过程中static修饰的变量和代码块的默认初始化和语句执行,其实也就是对应着生成类构造器<clinit>和执行类构造器的过程,从代码层面上来讲,就是static修饰的属性赋值语句和静态代码块中的语句将会被执行。
和类加载器ClassLoader的loadClass相同的是,这两个方法都是根据一个类的全量路径加载这个类的字节码并生成java.lang.Class对象返回,但是loadClass只涉及了五个步骤中的第一个“加载”,而Class.forName则完整的经历了五个步骤(可尝试用两种方式去加载一个含有静态代码块的类,你会发现loadCalss的加载方式将不会执行静态代码块中的内容),也就意味着loadClass不会使得类中的静态属性或静态代码块的语句被执行。
铺垫够了,那么回过头来看com.mysql.jdbc.Driver的源码,除了一个空构造方法之外,就只有一个静态代码块,代码块的内容就是注册驱动及其过程。而根据JDBC的规范,代码Class.forName("com.mysql.jdbc.Driver")语义将会使得这个静态代码块被执行。
package com.mysql.jdbc;
import java.sql.SQLException;
public class Driver extends NonRegisteringDriver implements java.sql.Driver {
static {
try {
java.sql.DriverManager.registerDriver(new Driver());
} catch (SQLException E) {
throw new RuntimeException("Can't register driver!");
}
}
public Driver() throws SQLException {
// Required for Class.forName().newInstance()
}
}
因此可以得出结论:
1. Class.forName这句代码通过java类加载的知识可知其实现了驱动注册,而根据JDBC规范可知,数据库厂商如果支持JDBC技术,那么无论是mysql还是oracle等等厂商,都可以通过Class.forName来实现驱动的注册;因此可以预见的是,实现JDBC的驱动包中,几乎都会有这样的代码出现;
2. 不应该通过DriverManager.registerDriver(new com.mysql.jdbc.Driver())这样的代码来实现驱动注册,通过对象实例化的知识可知这样会导致驱动重复注册;
3. 是否会导致同一个数据库被重复注册多个驱动?答案是不会的,因为static代码块只会被执行一次,如果要实现连接池一类的引用,那么应该显式的调用DriverManager.registerDriver去注册多个驱动;
2. DriverManager
java.sql.DriverManager是JDBC中提供的少有的不是接口或抽象类的类,它里面全是静态方法,是JDBC规范中对外提供的一个工具类,用来注册驱动以及管理成功注册的驱动,还有就是从驱动获取数据库连接。驱动源码下一小结在看,先看看注册驱动的过程。
private static Connection getConnection( String url, java.util.Properties info, Class<?> caller) throws SQLException {
ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;
synchronized(DriverManager.class) {
// synchronize loading of the correct classloader.
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;
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());
}
}
// 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");
}
}
既然是通过驱动来获取连接对象,那么首先要清楚的问题是如何从ArrayList中获取正确的驱动对象。源码已经透露很多信息了,通过isDriverAllowed方法来判定该驱动是否适配改数据库,其实就是遍历存放已注册驱动的ArrayList,return第一个适配的驱动,通过比较该结构中的每一个驱动对象的Class和类加载器对象是否相同,如果相同则表示该驱动适配改数据库,接着就进行获取链接。
3. 获得连接
未完待续
三、补充
持久层技术,JDBC只是开篇,后续进阶apache commons DButils轻量级JDBC框架,再进一步spring JDBC,届时将接触spring独家的事务的传播特性,最后是Mybatis,不完全ORM的持久层框架,最后面是逐渐沦为鸡肋的完全ORM框架hibernate,至于hibernate,有时间就看吧,依然鸡肋。
但只看JDBC,除了基本操作之外,还有事务和数据源已经获取数据库元信息,本文只介绍了基本操作,后续还会继续讲解其他部分。