1、直接编写测试代码,重点关注这个代码是怎么执行的DriverManager.getConnection()
//这行代码注释也是可以的
//Driver driver = Class.forName("com.mysql.jdbc.Driver");
connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/gwmdb?useSSL=false&useUnicode=true&characterEncoding=UTF-8","root", "itsme999");
2、开启debug
模式跟踪,最先都会先执行DriverManager
类的静态代码块
static {
loadInitialDrivers();
println("JDBC DriverManager initialized");
}
3、进入loadInitialDrivers()
方法,把无关代码都省去
private static void loadInitialDrivers() {
/**....省去上部分代码*/
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
//需要关注的代码及方法load()
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
//需要关注的代码及方法hasNext(),next()
while(driversIterator.hasNext()) {
driversIterator.next();
}
}
});
/**....省去下部分代码*/
}
4、然后我们先来分析上述代码的其中这一行代码
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
5、ServiceLoader
没有静态代码块,所以直接看调用的静态方法load(Driver.class)
就行,我们进入load(Driver.class)
方法
public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
进入可以看到这里Thread.currentThread().getContextClassLoader()
这行代码,这个是获取当前线程上下文类加载器,这个线程上下文类加载可以用来设置自己想要的类加载器,默认是系统类加载器,现在我们需要加载Classpath
路径下Clazz
,当然需要应用类加载器
,所以使用默认获取到就可以了
Java默认有三种类加载器(此父类只是说明层次关系):
BootstrapClassLoader
是嵌在JVM内核加载器,用C++语言编写,主要负载加载JAVA_HOME/lib
下的类库,启动类加载器无法被应用程序直接使用。ExtensionClassLoader
是用Java编写,且它的父类加载器是BootStrap
,是由sun.misc.Launcher$ExtClassLoader
实现的,主要加载JAVA_HOME/lib/ext
目录中的类库。我们知道Java中系统属性java.ext.dirs
指定的目录由ExtClassLoader
加载器加载,如果程序中没有指定该系统属性(-Djava.ext.dirs=sss/lib
)那么该加载器默认加载$JAVA_HOME/lib/ext
目录下的所有jar
文件,通过程序来看下系统变量java.ext.dirs
所指定的路径AppClassLoader
是应用程序类加载器,负责加载应用程序Classpath
目录下的所有jar
和class
文件。它的父加载器为ExtClassLoader
6、通过线程上下文类加载器帮我们拿到应用类类加载器,这样我们就可以去加载第三方提供的jar
包文件,然后进入ServiceLoader.load(service, cl)
里面,这里把java.sql.Driver
、cl(应用类加载器)两个参数传入进去,然后再跟进ServiceLoader
构造方法里面
public static <S> ServiceLoader<S> load(Class<S> service, ClassLoader loader) {
return new ServiceLoader<>(service, loader);
}
//ServiceLoader构造方法
private ServiceLoader(Class<S> svc, ClassLoader cl) {
/**省去无关代码*/
reload();
}
//进入reload()方法
//生成LazyIterator迭代器对象
public void reload() {
/**省去无关代码*/
lookupIterator = new LazyIterator(service, loader);
}
//进入LazyIterator构造方法
//赋值类加载器,接口servier(java.sql.Driver.class)
private LazyIterator(Class<S> service, ClassLoader loader) {
this.service = service;
this.loader = loader;
}
从上述代码可以总结出ServiceLoader.load()
就是在帮我们做了三件事情:
⑴通过线程上下文类加载器设置获取到应用类类加载器
⑵生成LazyIterator
迭代器对象,后续准备需要使用(hasNext()、next()方法)做铺垫
⑶赋值service=java.sql.Driver.class以及类加载器loader=应用类类累加器,后续准备需要使用
7、这样这行代码ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
就分析完了,然后在回到第3步的第二行代码,开始要执行迭代器了,代码虽短却蕴藏着东西
while(driversIterator.hasNext()) {
driversIterator.next();
}
8、跟踪进入hasNext()
方法,代码如下,发现内部其实是调用hasNextService()
方法,然后我们在看hasNextService()
内部执行逻辑,可以发现该hasNextService()
方法其实主要是用来查找META-INF/services/
文件夹下面配置的实现类包全路径的内容,如果有信息就读取进去,然后就可以调用next()
(其实是nextService()方法),next()
方后面会讲到用来干啥的,其实是用来加载Class
类及创建Class
的对象
ServiceLoader
的成员变量PREFIX
private static final String PREFIX = "META-INF/services/";
此图片发现在mysql
包里面配置确实存在写死了这个配置文件,到了这就里,其实明白为什么我们可以省略Class.forName("com.mysql.jdbc.Driver")
这个步骤,还是可以成功加载该类
hasNext()
方法里面具体调用的是hasNextService
,hasNextService
就是用来查找配置信息,代码如下,具体只需要关注hasNextService
方法
public boolean hasNext() {
//省去无关代码..
return hasNextService();
//省去无关代码...
}
private boolean hasNextService() {
/**
* PREFIX = "META-INF/services/"
* service.getName()=java.sql.Driver
* 拼接全路径能够找到这个java.sql.Driver的文件配置的内容
*/
String fullName = PREFIX + service.getName();
//通过路径URL更全面信息(不用在意这里)
configs = loader.getResources(fullName);
while ((pending == null) || !pending.hasNext()) {
//通过IO流读取配置信息 com.mysql.jdbc.Driver
pending = parse(service, configs.nextElement());
}
//这里如果上面发现有配置信息,就说明找到了包路径了,接下来就要开始加载这个Clazz了,
//那么我们就开始要调用next方法,来实例化我们的Clazz,那么next里到底是怎么做的呢?(反射)
nextName = pending.next();
}
经过hasNext()
方法之后最终得到了下面截图的结果,获取到了两个第三方的全路径信息,接下来就是要遍历这个ArrayList
里面的两个值,开始通过反射机制生成Clazz
实例,集合遍历可以使用迭代器,那么执行遍历pending.next()
代码,得到的第一个肯定是com.msql.jdbc.Driver
包路径,所以代此时变量nextName
被赋值为com.msql.jdbc.Driver
,因为next()
方法里面需要使用这个全局变量
9、开始进入pending.next();
代码,执行迭代器遍历上述的ArrayList
,发现其实是调用的nextService()
方法,然后在进入nextService()
方法,其实就是拿到了包路径(nextName
),开始通过反射在进行Clazz
实例化并创建其对象
nextName = pending.next();
public S next() {
//省去无关代码...
return nextService();
//省去无关代码...
}
在进入nextService()
方法,这个方法很重要很重要,每一行代码都有东西
private S nextService() {
//省去无关代码...
String cn = nextName;
nextName = null;
Class<?> c = null;
//class类实例化,这里因为设置了false,不会调用类的clinit()方法
c = Class.forName(cn, false, loader);
//创建class的对象,这里创建对象会调用clinit()方法
S p = service.cast(c.newInstance());
//省去无关代码...
}
首先分析这一行Class.forName()
代码,我们知道主动类加载,会触发clinit()
方法执行,也就是静态代码块首先会被执行(但是这里没有执行,是因为第二个参数设置为false,关闭了静态代码初始化,所以就不会去调用clinit()
),
此时我们知道cn
就是上文的nextName=com.mysql.jdbc.Driver
全路径,loader
就是一开始的ServiceLoader
帮我们准备好的应用类类加载器,这样我们就可以加载到这个com.mysql.jdbc.Driver
类
c = Class.forName(cn, false, loader);
这里我们会发现com.mysql.jdbc.Driver
是有静态代码块的,那么这个静态代码块什么时候执行呢?其实就是在创建Driver
类的对象的时候执行的,我们看看这一行代码,看到newInstance
就应该很明白了,就是在创建对象了,同时也这里也会执行静态代码块
S p = service.cast(c.newInstance());
c.newInstance()
会调用下面的static代码块
方法下面我们一起看看这个静态代码块做了啥事?就是干了一件事情,就是将我们的从第三方jar
包中加载到的Driver
类添加到registeredDrivers
集合中,后面调用getConnection()
方法需要使用
public class Driver extends NonRegisteringDriver implements java.sql.Driver {
static {
DriverManager.registerDriver(new Driver());
}
}
public class DriverManager {
private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>();
public static synchronized void registerDriver(java.sql.Driver driver, DriverAction da) {
registeredDrivers.addIfAbsent(new DriverInfo(driver, da));
}
}
到此为止,我们在DriverManager
的静态代码块中的loadInitialDrivers()
方法的事情就全部做完了,总结下来其实就是设置应用类加载器,加载第三方jar
包(META-INF/services/java.sql.Driver
),然后注册Driver
实例即可
connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/gwmdb?useSSL=false&useUnicode=true&characterEncoding=UTF-8","root", "itsme999");
static {
loadInitialDrivers();
println("JDBC DriverManager initialized");
}
10、最后在看看getConnection()
方法,上面驱动类已经有了,获取连接就简单多了,通过驱动获取到Connection
即可
for(DriverInfo aDriver : registeredDrivers) {
//url: jdbc:mysql://localhost:3306/gwmdb?useSSL=false&useUnicode=true&characterEncoding=UTF-8
//info就是封装的用户名、密码
Connection con = aDriver.driver.connect(url, info);
}
这里我们使用到了一个很有用的东西就是线程上线文类加载,默认是应用类加载器,该加载器还允许我们自己设置需要的类加载,然后就可以直接通过该类加载器进行加载clazz
,jar
包文件,打破了传统的双亲委派机制