初见问题
在学习注解的时候,我遇到了一个自定义注解模拟DBUtil的案例。
package util;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
public class DBUtil {
static String ip = "127.0.0.1";
static int port = 3306;
static String database = "test";
static String encoding = "UTF-8";
static String loginName = "root";
static String password = "admin";
static{
try {
Class.forName("com.mysql.jdbc.Driver");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
public static Connection getConnection() throws SQLException {
String url = String.format("jdbc:mysql://%s:%d/%s?characterEncoding=%s", ip, port, database, encoding);
return DriverManager.getConnection(url, loginName, password);
}
public static void main(String[] args) throws SQLException {
System.out.println(getConnection());
}
}
其中Class.forName("com.mysql.jdbc.Driver"),这句话引起了我的注意。为什么这里为什么会平白无故的创建一个Class呢,而且为什么只是创建对象,而没有实例化呢?这个问题一下子带出了很多有意思的内容。
对象的创建
首先我们回顾一下创建类的两种方式:new以及反射。
通过new的方式创建一个类,这是我们最常见的一种方式。通过这种方式,我们可以调用任何是public的构造方法,它相对反射创建可以很高效的创建一个类。
而通过反射的方式,我们同样可以创建一个类。我们可以通过Class.forName()的方式获取一个类对象。然后通过将这个类newInstance()实例化,完成一个对象的创建。这里返回的对象为Object,所以我们通过需要做一个类型转化,来完成真正意义上的对象创建。还有另一种方式,就是通过ClassLoader.loadCLass()的方式来加载类,随后再实例化。
而问题的答案其实就存在于通过反射创建对象的过程当中。联系到JVM的知识,我们回忆一下,JVM是如何加载类的。
类的加载
首先我们要知道,JVM运行字节码需要将代码加载到内存中去。将class文件加载到内存中需要以下三个步骤:加载、链接、初始化。这个问题的核心在于初始化,我们不妨再大概回顾一下前两步的内容。
加载的意义在于查询字节流,并据此创建类。其中涉及到的知识点还有boot classloader启动类加载器,这玩意儿是C++写的,Java肯定访问不了的,但是我们必须有它才可以把一些顶级类加载器加载到JVM中,再进行将其他类加载进来。说到这里就又要提双亲委派模型了:低级类加载器应该先让它的上级类加载器查询后才确认是否干活。再需要说的就是类的唯一性了,我们可以借助不同的类加载器,来实现一个类的不同实现。这是由类加载器提供的命名空间实现的。
说完加载,链接的过程相对简单一点了。链接的主要作用就是把我们上一步中加载的类链接起来。它具体又有三个步骤:验证、准备、解析。但是解析阶段是非必须的。
最后回到我们问题的答案上,初始化阶段干了什么?给常量赋值以及执行静态代码块。我们看一下什么情况下会触发初始化。
- 当虚拟机启动时,初始化用户指定的主类;
- 当遇到用以新建目标类实例的 new 指令时,初始化 new 指令的目标类;
- 当遇到调用静态方法的指令时,初始化该静态方法所在的类;
- 当遇到访问静态字段的指令时,初始化该静态字段所在的类;
- 子类的初始化会触发父类的初始化;
- 如果一个接口定义了 default 方法,那么直接实现或者间接实现该接口的类的初始化,会触发该接口的初始化;
- 使用反射 API 对某个类进行反射调用时,初始化这个类;
- 当初次调用 MethodHandle 实例时,初始化该 MethodHandle 指向的方法所在的类。
通过上面的事例,我们大概可以猜测到之所以用Class.forName()的原因是在于要执行某些静态代码块或者为某些常量赋值。我们先解答一下为什么要用Class.forName(),而不是new和ClassLoader.loadClass()的问题。
- new开销太大,我们其实只需要执行某个静态代码块而已,并不需要将整个对象创建
- ClassLoader.loadClass()并没有将对象初始化,也就是没有执行我们需要的静态代码块
DriverManager
最后我们来揭晓一下这个神秘的静态代码块到底是什么。
public class Driver extends NonRegisteringDriver implements java.sql.Driver {
public Driver() throws SQLException {
}
static {
try {
DriverManager.registerDriver(new Driver());//首先new一个Driver对象,并将它注册到DriverManage中
} catch (SQLException var1) {
throw new RuntimeException("Can't register driver!");
}
}
}
在JDBC规范中明确要求这个Driver类必须向DriverManager注册自己。之所以使用Class.forName("com.mysql.jdbc.Driver")的原因,就是为了执行这个静态代码块,使Driver在DriverManager中注册。
而在高版本的JDK中,我们已经不需要在手动的调用这种方法,在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()的作用。
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;
}
// If the driver is packaged as a Service Provider, load it.
// Get all the drivers through the classloader
// exposed as a java.sql.Driver.class service.
// ServiceLoader.load() replaces the sun.misc.Providers()
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
/* Load these drivers, so that they can be instantiated.
* It may be the case that the driver class may not be there
* i.e. there may be a packaged driver with the service class
* as implementation of java.sql.Driver but the actual class
* may be missing. In that case a java.util.ServiceConfigurationError
* will be thrown at runtime by the VM trying to locate
* and load the service.
*
* Adding a try catch block to catch those runtime errors
* if driver not available in classpath but it's
* packaged as service and that service is there in classpath.
*/
try{
while(driversIterator.hasNext()) {
driversIterator.next();
}
} catch(Throwable t) {
// Do nothing
}
return null;
}
});
重点是ServiceLoader.load(Driver.class)
这行代码。
上面这行代码可以把类路径下所有jar包中META-INF/services/java.sql.Driver文件中定义的类加载上来,此类必须继承自java.sql.Driver。
最后我们看一下Iterator的next()方法做了什么就完全懂了,通过next()方法调用了:
private S nextService() {
if (!hasNextService())
throw new NoSuchElementException();
String cn = nextName;
nextName = null;
Class<?> c = null;
try {
c = Class.forName(cn, false, loader); //看这里,Class.forName()
} catch (ClassNotFoundException x) {
fail(service,
"Provider " + cn + " not found");
}
if (!service.isAssignableFrom(c)) {
fail(service,
"Provider " + cn + " not a subtype");
}
try {
S p = service.cast(c.newInstance());
providers.put(cn, p);
return p;
} catch (Throwable x) {
fail(service,
"Provider " + cn + " could not be instantiated",
x);
}
throw new Error(); // This cannot happen
}
说白了就是把原来我们手动的方式变成了框架自动的方式。这种方式也叫做SPI。关于SPI的知识我们可以自行去查询学习一下,也是挺有意思的。