代码已上传至Github,有兴趣的同学可以下载来看看:https://github.com/ylw-github/Java-DesignMode
引言
使用过JDBC来连接数据库的同学知道连接数据库时,会有一段Class.forName
的代码,会提出很多疑问,比如:
- 为什么没有返回值?
- 为什么要必须加上这一段代码?
- 不加这段代码会怎么样?
下面来看看这一段代码是怎么写的:
// 1.加载驱动类
Class.forName("com.mysql.jdbc.Driver");
// 2.通过DriverManager获取数据库连接
String url = "jdbc:mysql://192.168.1.150/test";
String user = "ylw";
String password = "123456";
Connection connection = (Connection) DriverManager.getConnection(url, user, password);
首先我们来了解一下类加载机制。
1. 类加载机制
类加载的原理:JVM将class文件字节码文件加载到内存中, 并将这些静态数据转换成方法区中的运行时数据结构,在堆(并不一定在堆中,HotSpot在方法区中)中生成一个代表这个类的java.lang.Class 对象,作为方法区类数据的访问入口。
JVM类加载机制分为五个部分:加载
,验证
,准备
,解析
,初始化
。
1.1 加载
加载过程主要完成三件事情:
- 通过类的全限定名来获取定义此类的二进制字节流
- 将这个类字节流代表的静态存储结构转为方法区的运行时数据结构
- 在堆中生成一个代表此类的java.lang.Class对象,作为访问方法区这些数据结构的入口。
1.2 校验
此阶段主要确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机的自身安全。
- 文件格式验证:基于字节流验证。
- 元数据验证:基于方法区的存储结构验证。
- 字节码验证:基于方法区的存储结构验证。
- 符号引用验证:基于方法区的存储结构验证。
1.3 准备
为类变量分配内存,并将其初始化为默认值。(此时为默认值,在初始化的时候才会给变量赋值)即在方法区中分配这些变量所使用的内存空间。例如:
public static int value = 123;
此时在准备阶段过后的初始值为0而不是123;将value赋值为123的putstatic指令是程序被编译后,存放于类构造器<client>
方法之中.特例:
public static final int value = 123;
此时value的值在准备阶段过后就是123。
1.4 解析
把类型中的符号引用转换为直接引用。
符号引用与虚拟机实现的布局无关,引用的目标并不一定要已经加载到内存中。各种虚拟机实现的内存布局可以各不相同,但是它们能接受的符号引用必须是一致的,因为符号引用的字面量形式明确定义在Java虚拟机规范的Class文件格式中。
直接引用可以是指向目标的指针,相对偏移量或是一个能间接定位到目标的句柄。如果有了直接引用,那引用的目标必定已经在内存中存在
主要有以下四种:
- 类或接口的解析
- 字段解析
- 类方法解析
- 接口方法解析
1.5 初始化
初始化阶段是执行类构造器<client>
方法的过程。<client>
方法是由编译器自动收集类中的类变量的赋值操作和静态语句块中的语句合并而成的。虚拟机会保证<client>
方法执行之前,父类的<client>
方法已经执行完毕。如果一个类中没有对静态变量赋值也没有静态语句块,那么编译器可以不为这个类生成<client>()
方法。
java中,对于初始化阶段,有且只有以下五种情况才会对要求类立刻“初始化”(加载,验证,准备,自然需要在此之前开始):
- 使用new关键字实例化对象、访问或者设置一个类的静态字段(被final修饰、编译器优化时已经放入常量池的例外)、调用类方法,都会初始化该静态字段或者静态方法所在的类。
- 初始化类的时候,如果其父类没有被初始化过,则要先触发其父类初始化。
- 使用java.lang.reflect包的方法进行反射调用的时候,如果类没有被初始化,则要先初始化。
- 虚拟机启动时,用户会先初始化要执行的主类(含有main)
- jdk 1.7后,如果java.lang.invoke.MethodHandle的实例最后对应的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic方法句柄,并且这个方法所在类没有初始化,则先初始化。
2. Class.forName
在Java官方文档中对Class.forName的解释为在运行时动态的加载一个类,返回值为生成的Class对象。
那么很明显在jdbc中使用Class.forName(“com.mysql.jdbc.Driver”);仅仅就是将com.mysql.jdbc.Driver类加载到Jvm中了,这个原因大多数人应该都知道。
但是我们要知道Class.forName貌似只是对类进行了加载,我们甚至都没有对返回的Class对象做任何操作,那么为什么后面就可以直接用了呢?
首先看Class.forName
方法:
调用了native方法forName0(...)
:
要注意forName0中有一个关键的参数boolean initialize,;该参数用来标识在将该类加载后是否进行初始化操作。可以看到代码中是true,就表示会进行初始化操作。
初始化过程实际上就是对变量赋值(不是赋初值,不会调用构造函数)的过程。包含所有类变量的赋值以及静态代码语句块的执行代码,包括对父类的初始化。
3. Driver驱动类
再看com.mysql.jdbc.Driver驱动类:
该类中定义了一个静态代码块,静态代码快中创建了一个驱动类实例注册给了DriverManager
,而静态代码块的内容会在初始化的过程中执行,所以才能通过DriverManager.getConnection
直接获取一个连接。这就是为什么必须使用使用Class.forName()
的原因了。
那么既然知道了原理是Driver静态代码块初始化 的原因后,那还有其它的办法来解决吗?当然是有的。
4. 使用New的关键字实现
在使用new关键字时会查看该类是否已经被加载,如果没有被加载的话则会进行加载操作。所以我们的类中也可以这样写:
public static Connection getConnection() throws ClassNotFoundException, SQLException {
if(connection == null){
new Driver();//会自动调用静态代码块
connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/xxx?serverTimezone=UTC", "root", "xxxx");
}
return connection;
}
但是实际上因为在驱动类的静态代码快中实际上已经有了实例化对象并注册到DriverMananger中的操作。所以这里根本就没有在实例化一个对象的过程。使用Class.forName即可,这也算是一个优化的过程吧。
5. 可以不使用Class.forName
在测试的过程中发现即使不显示的使用Class.forName("com.mysql.jdbc.Driver")
也能够连接到数据库,一时间觉得很奇怪。
深入跟踪代码后发现实际上只要我们引入了mysql的驱动包,那么在使用时会根据驱动包下提供的配置文件默认的创建一个类。
所以实际上只要引入了该驱动包,那么使用jdbc是可以直接通过DriverManage来获取连接。
public static Connection getConnection() SQLException {
return DriverManager.getConnection("jdbc:mysql://localhost:3306/xxx?serverTimezone=UTC", "root", "xxxxxx");
}