JDBC为什么不需要Class.forName()

# DriverManager

在初学JDBC时,总是记得开头就要来这么一句

1
Class.forName("com.mysql.jdbc.Driver");

然而今天学习类加载机制时偶然知道不需要这句也同样可以正常运行,测试的确如此,看了一眼源码,看似原因很简单,因为无论写不写那段Class.forName,在触发加载DriverManager时,会运行

1
2
3
4
5
6
7
8
    /**
     * 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");
    }

都会扫描到这个Driver,注册到registeredDrivers中。


如果只要回答这个问题,可能上面的答案就已经足够了。

然而兴趣来了,总想把源码继续看下去到底扫描了啥,咋扫描的,明明

1
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);

这里是 java.sql.Driver 接口,上哪找出来的实现类?

我们就一层层的往下看吧。

# ServiceLoader

主要做了两点

  1. 设置ClassLoader为当前线程的。(破坏双亲委派,让它rt.jar包中的类可以通过AppClassLoader获取到外部的类,很多讲双亲委派机制的文章已经大书特书过了)
  2. 初始化了一个懒加载的迭代器 lookupIterator = new LazyIterator(service, loader);。正是这个迭代器中最终帮我们找到了各种driver注册进去。

# LazyIterator

既然是迭代器,最重要的当然是hasNext()和next()方法。

在next()方法中,已经拿到了nextName完整的"com.mysql.cj.jdbc.Driver"类名,说明还是hasNext()方法干了真正的扫描工作。

hasNext()方法的主要工作也都放在了hasNextService()中

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
        private boolean hasNextService() {
            if (nextName != null) {
                return true;
            }
            if (configs == null) {
                try {
                    //此处拼出了全名为 META-INF/services/java.sql.Driver
                    String fullName = PREFIX + service.getName();
                    if (loader == null)
                        configs = ClassLoader.getSystemResources(fullName);
                    else
                        configs = loader.getResources(fullName);
                } catch (IOException x) {
                    fail(service, "Error locating configuration files", x);
                }
            }
            while ((pending == null) || !pending.hasNext()) {
                if (!configs.hasMoreElements()) {
                    return false;
                }
                pending = parse(service, configs.nextElement());
            }
            nextName = pending.next();
            return true;
        }

看起来不算复杂,我们拿着"META-INF/services/java.sql.Driver"去AppClassLoader找个configs。

但是debug的时候并不直观,因为debug的时候看到config似乎包含了各层类加载器扫出来的几十个jar包,然而到configs.nextElement()时突然就直接拿到了jar:file:/C:/Users/dell/.m2/repository/mysql/mysql-connector-java/8.0.13/mysql-connector-java-8.0.13.jar!/META-INF/services/java.sql.Driver的完整路径。最后发现实在是源码作者太喜欢实现hasNext()和next()方法(hasMoreElementsnextElement)了,一层又一层的把人看晕了。

我们先跟进这段代码 configs = loader.getResources(fullName);,看configs是如何拿到的吧。

# ClassLoader

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
public Enumeration<URL> getResources(String name) throws IOException {
    @SuppressWarnings("unchecked")
    Enumeration<URL>[] tmp = (Enumeration<URL>[]) new Enumeration<?>[2];
    if (parent != null) {
        tmp[0] = parent.getResources(name);
    } else {
        tmp[0] = getBootstrapResources(name);
    }
    //AppClassLoader和ExtClassLoader都是调用父类URLClassLoader的findResources方法
    tmp[1] = findResources(name);

    return new CompoundEnumeration<>(tmp);
}

遵循了双亲委派机制,先向上找parent,一直找到顶部的Bootstrap启动类加载器。再向下一直找到自身类加载器。 当然此处由于可以返回多层结果,因此返回的最终是CompoundEnumeration 组合的结果,而不是单条数据。

后面我们会看到,不同的实现的Enumeration的hasMoreElementsnextElement真是花里胡哨。用Enumeration<URL> 这个看似最简单的只有两个方法的Interface作为返回类型,真是给作者玩出花来了。

三层找的结果如下图。

debug看完三层扫描的结果,三层类加载器一共扫了一百多个jar包出来,貌似没毛病,但是明明我传了name进去找resource的啊,如果你要返回全部jar包,还要我传name干啥?要是遍历一百多个jar包的话,pending = parse(service, configs.nextElement());这要遍历一百多次去找java.sql.Driver的实现类?

但是debug到configs.nextElement()时突然就直接拿到了jar:file:/C:/Users/dell/.m2/repository/mysql/mysql-connector-java/8.0.13/mysql-connector-java-8.0.13.jar!/META-INF/services/java.sql.Driver的完整路径。我们先看一眼这个jar包中的内容,

的确这就是我们要找的jar包和配置文件,里面写明了我们要注册的Driver的实现类就是"com.mysql.cj.jdbc.Driver"。

那这到底是什么时候遍历出来的呢?看来返回的这个configs中间大有玄机,我们继续跟进它的迭代的方法。

# URLClassLoader

AppClassLoader和ExtClassLoader都是继承于URLClassLoader,且它们的findResources方法都是使用父类的。

而URLClassLoader的这个方法的核心又是调用URLClassPath的findResources方法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
   public Enumeration<URL> findResources(final String name)
        throws IOException
    {
        final Enumeration<URL> e = ucp.findResources(name, true);
        //这里还是在URLClassPath返回的Enumeration上再包了一次,又实现了一遍Enumeration
        return new Enumeration<URL>() {
            private URL url = null;

            private boolean next() {
            ...
                //这里额外加上的checkURL方法,连name都没带,肯定也不是校验我们java.sql.Driver的,只是简单校验下路径合法或者存在。
                url = ucp.checkURL(u);
            ...
            }
            ...
         }
   }

# URLClassPath

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public Enumeration<URL> findResources(final String var1, final boolean var2) {
    return new Enumeration<URL>() {
        private int index = 0;
        private int[] cache = URLClassPath.this.getLookupCache(var1);
        private URL url = null;

        private boolean next() {
            if (this.url != null) {
                return true;
            } else {
                do {
                    URLClassPath.Loader var1x;
                    //终于找到遍历jar包的地方,此处的var1x就是每个jar包
                    if ((var1x = URLClassPath.this.getNextLoader(this.cache, this.index++)) == null) {
                        return false;
                    }
                    //findResource就是从这个jar包中找出我们要的META-INF/services/java.sql.Driver
                    this.url = var1x.findResource(var1, var2);
                } while(this.url == null);

                return true;
            }
        }
        ...
    }

在一个jar包中,又有很多的配置文件路径,如何找到我们要的那个的呢?

这里var1x就是JarLoader,用了其中的getResource方法。

# JarLoader

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
Resource getResource(String var1, boolean var2) {
    //关键就在这个metaIndex中,mayContain方法去查了jar包中是否包含我们要找的目标文件
    if (this.metaIndex != null && !this.metaIndex.mayContain(var1)) {
        return null;
    } else {
        try {
            this.ensureOpen();
        } catch (IOException var5) {
            throw new InternalError(var5);
        }

        JarEntry var3 = this.jar.getJarEntry(var1);
        if (var3 != null) {
            return this.checkResource(var1, var2, var3);
        } else if (this.index == null) {
            return null;
        } else {
            HashSet var4 = new HashSet();
            return this.getResource(var1, var2, var4);
        }
    }
}

至此,我们已经找到了全部遍历的地方,URLClassLoader加载了全部jar包之后,在开始迭代时,URLClassPath 遍历每个jar包,JarLoader查找每个jar包中是否有目标文件META-INF/services/java.sql.Driver。