jdk 的 URL 源码浅析

缘起

工作中遇到了ldap使用. 后来自学了一下,不可避免的遇到了jndi这个老朋友. 后来仔细一想,jndi只需要初始化一个context就可以想拿啥就拿啥了,觉得和jdk的spi机制以及java.net.URL的构造器很像. 你想啊,URL背后其实是URLConnection, 而它本身是一个抽象类, 可以根据你传入的url的scheme自动选择使用的URLConnection子类. 这本质上都是类的动态加载和服务发现. 关于JDK的spi机制可以参见我写的一篇文章【1】. 关于jndi我会再写一篇文章. 最后你会发现, jndi、spi、URL 这三个东西其实用的核心思想是完全一样的.

本质上都是为了打破父委托机制而引入的线程上下文类加载器啊!

分析

首先是架构图

这里要回答的问题是: 为什么URL可以根据传入的url的scheme,譬如我们最常用的

1
URL url = new URL("https://www.baidu.com")

自动选择不同的URLStreamHandler?

例如有这么多URLStreamHandler的实现类

有处理http协议的, 有处理https协议的, 有处理本地文件系统的.

我们跟踪 URL的构造器,不难发现最终调用的构造器是

1
2
public URL(URL context, String spec, URLStreamHandler handler)
throws MalformedURLException

这个方法的逻辑是首先从传入的url利用字符串手段解析得到url的scheme. 如果字符串手段处理失败的话,

1
throw new MalformedURLException("no protocol: "+url);

然后得到scheme之后,譬如http、https、file之类的. 最后来到了我们核心的方法

1
java.net.URL.getURLStreamHandler(String)

源码如下

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
static URLStreamHandler getURLStreamHandler(String protocol) {
URLStreamHandler handler = handlers.get(protocol);
if (handler == null) {
boolean checkedWithFactory = false;
// Use the factory (if any)
if (factory != null) {
handler = factory.createURLStreamHandler(protocol);
checkedWithFactory = true;
}
// Try java protocol handler
if (handler == null) {
String packagePrefixList = null;

packagePrefixList
= java.security.AccessController.doPrivileged(
new sun.security.action.GetPropertyAction(
protocolPathProp,""));
if (packagePrefixList != "") {
packagePrefixList += "|";
}

// REMIND: decide whether to allow the "null" class prefix
// or not.
packagePrefixList += "sun.net.www.protocol";
StringTokenizer packagePrefixIter =
new StringTokenizer(packagePrefixList, "|");
while (handler == null &&
packagePrefixIter.hasMoreTokens()) {

String packagePrefix =
packagePrefixIter.nextToken().trim();
try {
String clsName = packagePrefix + "." + protocol +
".Handler";
Class<?> cls = null;
try {
cls = Class.forName(clsName);
} catch (ClassNotFoundException e) {
ClassLoader cl = ClassLoader.getSystemClassLoader();
if (cl != null) {
cls = cl.loadClass(clsName);
}
}
if (cls != null) {
handler =
(URLStreamHandler)cls.newInstance();
}
} catch (Exception e) {
// any number of exceptions can get thrown here
}
}
}
synchronized (streamHandlerLock) {
URLStreamHandler handler2 = null;
// Check again with hashtable just in case another
// thread created a handler since we last checked
handler2 = handlers.get(protocol);

if (handler2 != null) {
return handler2;
}

// Check with factory if another thread set a
// factory since our last check
if (!checkedWithFactory && factory != null) {
handler2 = factory.createURLStreamHandler(protocol);
}

if (handler2 != null) {
// The handler from the factory must be given more
// importance. Discard the default handler that
// this thread created.
handler = handler2;
}

// Insert this handler into the hashtable
if (handler != null) {
handlers.put(protocol, handler);
}

}
}
return handler;
}

我们关注最终handler是怎么得到的.

  1. 如果缓存(handlers)中已经有了protocol对应的handler的话, 就直接返回(line 2)

  2. 如果程序已经通过 java.net.URL.setURLStreamHandlerFactory(URLStreamHandlerFactory) 方法为URL设置了URLStreamHandlerFactory接口的实现类的话, 就直接由该URLStreamHandlerFactory创建URLStreamHandler,值得注意的是, setURLStreamHandlerFactory在一个jvm进程中只能被调用一次, 第二次就会抛Error,并且标记 checkedWithFactory 为true,表示当前已经由URLStreamHandlerFactory创建过了URLStreamHandler实例.(line 6-9)

  3. 如果1和2两步之后handler依旧是null的话, 则通过常量 protocolPathProp=java.protocol.handler.pkgs 去通过AccessController.doPrivileged获取系统变量(为了防止权限检查). 其中 GetPropertyAction的源码是

    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
    26
    27
    package sun.security.action;

    import java.security.PrivilegedAction;

    public class GetPropertyAction
    implements PrivilegedAction<String>
    {
    private String theProp;
    private String defaultVal;

    public GetPropertyAction(String paramString)
    {
    this.theProp = paramString;
    }

    public GetPropertyAction(String paramString1, String paramString2)
    {
    this.theProp = paramString1;
    this.defaultVal = paramString2;
    }

    public String run()
    {
    String str = System.getProperty(this.theProp);
    return str == null ? this.defaultVal : str;
    }
    }

    可见,其实就是获取系统变量java.protocol.handler.pkgs. 所以如果我们使用了 -Djava.protocol.handler.pkgs 这样的jvm参数启动进程的话, 就会使用我们自己的URLStreamHandler实现类. 如果我们没写或者我们写了但是都加载失败的话, 则就会加载sun公司提供的sun.net.www.protocol.协议名称.Handler 所以,如果我们想加载自己的URLStreamHandler实现类的话, 我们也需要注意命名规范, 譬如我们自定义的用于http协议处理的URLStreamHandler就应该命名为 xxx.http.Handler, 其中xxx是自定义包名.其中这里最值得注意的是下面一段代码

    1
    2
    3
    4
    5
    6
    7
    8
    try {
    cls = Class.forName(clsName);
    } catch (ClassNotFoundException e) {
    ClassLoader cl = ClassLoader.getSystemClassLoader();
    if (cl != null) {
    cls = cl.loadClass(clsName);
    }
    }

    这段代码中第二行使用的类加载器是加载java.net.URL 的类加载器,即BootstrapClassloader. 但是这个类加载器是无法加载我们自定义的URLStreamHandler实现类的. 所以才会有4-6行. ClassLoader.getSystemClassLoader得到的是线程上下文类加载器,也就是AppClassLoader. 这一点可以参考【2】

    用它就可以加载我们自定义的URLStreamHandler的实现类了. 这本质上也是JNDI、SPI机制的玩法. 其实就是线程上下文类加载器打破类加载父委托机制. 说穿了就不值钱了.

    (line 11-52)

  4. 最后因为jdk设计者认为由URLStreamHandlerFactory创建的URLStreamHandler更加重要,所以最后返回handler并缓存起来之前再一次使用互斥锁检查了一遍能否使用工厂创建handler.

参考

【1】 https://yfsyfs.github.io/2019/02/28/JDK%E4%B9%8BSPI%E6%9C%BA%E5%88%B6%E7%AE%80%E6%98%8E%E5%88%86%E6%9E%90/

【2】《Java网络编程精解》 孙卫琴 Chpt6

【3】 https://zy19982004.iteye.com/blog/1983236