Play OpenJDK: 允許你的包名以"java."開頭
本文是Play OpenJDK的第二篇,介紹了如何突破JDK不允許自定義的包名以"java."開頭這一限制。這一技巧對于基于已有的JDK向java.*中添加新類還是有所幫助的。(2015.11.02最后更新)
無論是經(jīng)驗豐富的Java程序員,還是Java的初學(xué)者,總會有一些人或有意或無意地創(chuàng)建一個包名為"java"的類。但出于安全方面的考慮,JDK不允許應(yīng)用程序類的包名以"java"開頭,即不允許java,java.foo這樣的包名。但javax,javaex這樣的包名是允許的。
1. 例子
比如,以O(shè)penJDK 8為基礎(chǔ),臆造這樣一個例子。筆者想向OpenJDK貢獻(xiàn)一個同步的HashMap,即類SynchronizedHashMap,而該類的包名就為java.util。SynchronizedHashMap是HashMap的同步代理,由于這兩個類是在同一包內(nèi),SynchronizedHashMap不僅可以訪問HashMap的public方法與變量,還可以訪問HashMap的protected和default方法與變量。SynchronizedHashMap看起來可能像下面這樣:
package java.util;
public class SynchronizedHashMap<K, V> {
private HashMap<K, V> hashMap = null;
public SynchronizedHashMap(HashMap<K, V> hashMap) {
this.hashMap = hashMap;
}
public SynchronizedHashMap() {
this(new HashMap<>());
}
public synchronized V put(K key, V value) {
return hashMap.put(key, value);
}
public synchronized V get(K key) {
return hashMap.get(key);
}
public synchronized V remove(K key) {
return hashMap.remove(key);
}
public synchronized int size() {
return hashMap.size; // 直接調(diào)用HashMap.size變量,而非HashMap.size()方法
}
}
public class SynchronizedHashMap<K, V> {
private HashMap<K, V> hashMap = null;
public SynchronizedHashMap(HashMap<K, V> hashMap) {
this.hashMap = hashMap;
}
public SynchronizedHashMap() {
this(new HashMap<>());
}
public synchronized V put(K key, V value) {
return hashMap.put(key, value);
}
public synchronized V get(K key) {
return hashMap.get(key);
}
public synchronized V remove(K key) {
return hashMap.remove(key);
}
public synchronized int size() {
return hashMap.size; // 直接調(diào)用HashMap.size變量,而非HashMap.size()方法
}
}
2. ClassLoader的限制
使用javac去編譯源文件SynchronizedHashMap.java并沒有問題,但在使用編譯后的SynchronizedHashMap.class時,JDK的ClassLoader則會拒絕加載java.util.SynchronizedHashMap。
設(shè)想有如下的應(yīng)用程序:
import java.util.SynchronizedHashMap;
public class SyncMapTest {
public static void main(String[] args) {
SynchronizedHashMap<String, String> syncMap = new SynchronizedHashMap<>();
syncMap.put("Key", "Value");
System.out.println(syncMap.get("Key"));
}
}
使用java命令去運行該應(yīng)用時,會報如下錯誤:public class SyncMapTest {
public static void main(String[] args) {
SynchronizedHashMap<String, String> syncMap = new SynchronizedHashMap<>();
syncMap.put("Key", "Value");
System.out.println(syncMap.get("Key"));
}
}
Exception in thread "main" java.lang.SecurityException: Prohibited package name: java.util
at java.lang.ClassLoader.preDefineClass(ClassLoader.java:659)
at java.lang.ClassLoader.defineClass(ClassLoader.java:758)
at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142)
at java.net.URLClassLoader.defineClass(URLClassLoader.java:467)
at java.net.URLClassLoader.access$100(URLClassLoader.java:73)
at java.net.URLClassLoader$1.run(URLClassLoader.java:368)
at java.net.URLClassLoader$1.run(URLClassLoader.java:362)
at java.security.AccessController.doPrivileged(Native Method)
at java.net.URLClassLoader.findClass(URLClassLoader.java:361)
at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:331)
at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
at SyncMapTest.main(SyncMapTest.java:6)
方法ClassLoader.preDefineClass()的源代碼如下:at java.lang.ClassLoader.preDefineClass(ClassLoader.java:659)
at java.lang.ClassLoader.defineClass(ClassLoader.java:758)
at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142)
at java.net.URLClassLoader.defineClass(URLClassLoader.java:467)
at java.net.URLClassLoader.access$100(URLClassLoader.java:73)
at java.net.URLClassLoader$1.run(URLClassLoader.java:368)
at java.net.URLClassLoader$1.run(URLClassLoader.java:362)
at java.security.AccessController.doPrivileged(Native Method)
at java.net.URLClassLoader.findClass(URLClassLoader.java:361)
at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:331)
at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
at SyncMapTest.main(SyncMapTest.java:6)
private ProtectionDomain preDefineClass(String name,
ProtectionDomain pd)
{
if (!checkName(name))
throw new NoClassDefFoundError("IllegalName: " + name);
if ((name != null) && name.startsWith("java.")) {
throw new SecurityException
("Prohibited package name: " +
name.substring(0, name.lastIndexOf('.')));
}
if (pd == null) {
pd = defaultDomain;
}
if (name != null) checkCerts(name, pd.getCodeSource());
return pd;
}
很清楚地,該方法會先檢查待加載的類全名(即包名+類名)是否以"java."開頭,如是,則拋出SecurityException。那么可以嘗試修改該方法的源代碼,以突破這一限制。ProtectionDomain pd)
{
if (!checkName(name))
throw new NoClassDefFoundError("IllegalName: " + name);
if ((name != null) && name.startsWith("java.")) {
throw new SecurityException
("Prohibited package name: " +
name.substring(0, name.lastIndexOf('.')));
}
if (pd == null) {
pd = defaultDomain;
}
if (name != null) checkCerts(name, pd.getCodeSource());
return pd;
}
從JDK中的src.zip中拿出java/lang/ClassLoader.java文件,修改其中的preDefineClass方法以去除相關(guān)限制。重新編譯ClassLoader.java,將生成的ClassLoader.class,ClassLoader$1.class,ClassLoader$2.class,ClassLoader$3.class,ClassLoader$NativeLibrary.class,ClassLoader$ParallelLoaders.class和SystemClassLoaderAction.class去替換JDK/jre/lib/rt.jar中對應(yīng)的類。
再次運行SyncMapTest,卻仍然會拋出相同的SecurityException,如下所示:
Exception in thread "main" java.lang.SecurityException: Prohibited package name: java.util
at java.lang.ClassLoader.defineClass1(Native Method)
at java.lang.ClassLoader.defineClass(ClassLoader.java:760)
at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142)
at java.net.URLClassLoader.defineClass(URLClassLoader.java:467)
at java.net.URLClassLoader.access$100(URLClassLoader.java:73)
at java.net.URLClassLoader$1.run(URLClassLoader.java:368)
at java.net.URLClassLoader$1.run(URLClassLoader.java:362)
at java.security.AccessController.doPrivileged(Native Method)
at java.net.URLClassLoader.findClass(URLClassLoader.java:361)
at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:331)
at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
at SyncMapTest.main(SyncMapTest.java:6)
此時是由方法ClassLoader.defineClass1()拋出的SecurityException。但這是一個native方法,那么僅通過修改Java代碼是無法解決這個問題的(JDK真是層層設(shè)防啊)。原來在Hotspot的C++源文件hotspot/src/share/vm/classfile/systemDictionary.cpp中有如下語句:at java.lang.ClassLoader.defineClass1(Native Method)
at java.lang.ClassLoader.defineClass(ClassLoader.java:760)
at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142)
at java.net.URLClassLoader.defineClass(URLClassLoader.java:467)
at java.net.URLClassLoader.access$100(URLClassLoader.java:73)
at java.net.URLClassLoader$1.run(URLClassLoader.java:368)
at java.net.URLClassLoader$1.run(URLClassLoader.java:362)
at java.security.AccessController.doPrivileged(Native Method)
at java.net.URLClassLoader.findClass(URLClassLoader.java:361)
at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:331)
at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
at SyncMapTest.main(SyncMapTest.java:6)
const char* pkg = "java/";
if (!HAS_PENDING_EXCEPTION &&
!class_loader.is_null() &&
parsed_name != NULL &&
!strncmp((const char*)parsed_name->bytes(), pkg, strlen(pkg))) {
// It is illegal to define classes in the "java." package from
// JVM_DefineClass or jni_DefineClass unless you're the bootclassloader
ResourceMark rm(THREAD);
char* name = parsed_name->as_C_string();
char* index = strrchr(name, '/');
*index = '\0'; // chop to just the package name
while ((index = strchr(name, '/')) != NULL) {
*index = '.'; // replace '/' with '.' in package name
}
const char* fmt = "Prohibited package name: %s";
size_t len = strlen(fmt) + strlen(name);
char* message = NEW_RESOURCE_ARRAY(char, len);
jio_snprintf(message, len, fmt, name);
Exceptions::_throw_msg(THREAD_AND_LOCATION,
vmSymbols::java_lang_SecurityException(), message);
}
修改該文件以去除掉相關(guān)限制,并按照本系列的第一篇文章中介紹的方法去重新構(gòu)建一個OpenJDK。那么,這個新的JDK將不會再對包名有任何限制了。if (!HAS_PENDING_EXCEPTION &&
!class_loader.is_null() &&
parsed_name != NULL &&
!strncmp((const char*)parsed_name->bytes(), pkg, strlen(pkg))) {
// It is illegal to define classes in the "java." package from
// JVM_DefineClass or jni_DefineClass unless you're the bootclassloader
ResourceMark rm(THREAD);
char* name = parsed_name->as_C_string();
char* index = strrchr(name, '/');
*index = '\0'; // chop to just the package name
while ((index = strchr(name, '/')) != NULL) {
*index = '.'; // replace '/' with '.' in package name
}
const char* fmt = "Prohibited package name: %s";
size_t len = strlen(fmt) + strlen(name);
char* message = NEW_RESOURCE_ARRAY(char, len);
jio_snprintf(message, len, fmt, name);
Exceptions::_throw_msg(THREAD_AND_LOCATION,
vmSymbols::java_lang_SecurityException(), message);
}
3. 覆蓋Java核心API?
開發(fā)者們在使用主流IDE時會發(fā)現(xiàn),如果工程有多個jar文件或源文件目錄中包含相同的類,這些IDE會根據(jù)用戶指定的優(yōu)先級順序來加載這些類。比如,在Eclipse中,右鍵點擊某個Java工程-->屬性-->Java Build Path-->Order and Export,在這里調(diào)整各個類庫或源文件目錄的位置,即可指定加載類的優(yōu)先級。
當(dāng)開發(fā)者在使用某個開源類庫(jar文件)時,想對其中某個類進(jìn)行修改,那么就可以將該類的源代碼復(fù)制出來,并在Java工程中創(chuàng)建一個同名類,然后指定Eclipse優(yōu)先加息自己創(chuàng)建的類。即,在編譯時與運行時用自己創(chuàng)建的類去覆蓋類庫中的同名類。那么,是否可以如法炮制去覆蓋Java核心API中的類呢?
考慮去覆蓋類java.util.HashMap,只是簡單在它的put()方法添加一條打印語。那么就需要將src.zip中的java/util/HashMap.java復(fù)制出來,并在當(dāng)前Java工程中創(chuàng)建一個同名類java.util.HashMap,并修改put()方法,如下所示:
Java類加載器由下至上分為三個層次:引導(dǎo)類加載器(Bootstrap Class Loader),擴(kuò)展類加載器(Extension Class Loader)和應(yīng)用程序類加載器(Application Class Loader)。其中引導(dǎo)類加載器用于加載rt.jar這樣的核心類庫。并且引導(dǎo)類加載器為擴(kuò)展類加載器的父加載器,而擴(kuò)展類加載器又為應(yīng)用程序類加載器的父加載器。同時JVM在加載類時實行委托模式。即,當(dāng)前類加載器在加載類時,會首先委托自己的父加載器去進(jìn)行加載。如果父加載器已經(jīng)加載了某個類,那么子加載器將不會再次加載。
由上可知,當(dāng)應(yīng)用程序試圖加載java.util.Map時,它會首先逐級向上委托父加載器去加載該類,直到引導(dǎo)類加載器加載到rt.jar中的java.util.HashMap。由于該類已經(jīng)被加載了,我們自己創(chuàng)建的java.util.HashMap就不會被重復(fù)加載。
使用java命令運行SyncMapTest程序時加上VM參數(shù)-verbose:class,會在窗口中打印出形式如下的語句:
開發(fā)者們在使用主流IDE時會發(fā)現(xiàn),如果工程有多個jar文件或源文件目錄中包含相同的類,這些IDE會根據(jù)用戶指定的優(yōu)先級順序來加載這些類。比如,在Eclipse中,右鍵點擊某個Java工程-->屬性-->Java Build Path-->Order and Export,在這里調(diào)整各個類庫或源文件目錄的位置,即可指定加載類的優(yōu)先級。
當(dāng)開發(fā)者在使用某個開源類庫(jar文件)時,想對其中某個類進(jìn)行修改,那么就可以將該類的源代碼復(fù)制出來,并在Java工程中創(chuàng)建一個同名類,然后指定Eclipse優(yōu)先加息自己創(chuàng)建的類。即,在編譯時與運行時用自己創(chuàng)建的類去覆蓋類庫中的同名類。那么,是否可以如法炮制去覆蓋Java核心API中的類呢?
考慮去覆蓋類java.util.HashMap,只是簡單在它的put()方法添加一條打印語。那么就需要將src.zip中的java/util/HashMap.java復(fù)制出來,并在當(dāng)前Java工程中創(chuàng)建一個同名類java.util.HashMap,并修改put()方法,如下所示:
package java.util;
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable {
.
public V put(K key, V value) {
System.out.printf("put - key=%s, value=%s%n", key, value);
return putVal(hash(key), key, value, false, true);
}

}
此時,在Eclipse環(huán)境中,SynchronizedHashMap使用的java.util.HashMap被認(rèn)為是上述新創(chuàng)建的HashMap類。那么運行應(yīng)用程序SyncMapTest后的期望輸出應(yīng)該如下所示:public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable {

public V put(K key, V value) {
System.out.printf("put - key=%s, value=%s%n", key, value);
return putVal(hash(key), key, value, false, true);
}

}
put - key=Key, value=Value
Value
但運行SyncMapTest后的實際輸出卻為如下:Value
Value
看起來,新創(chuàng)建的java.util.HashMap并沒有被使用上。這是為什么呢?能夠"想像"到的原因還是類加載器。關(guān)于Java類加載器的討論超出了本文的范圍,而且關(guān)于該主題的文章已是汗牛充棟,但本文仍會簡述其要點。Java類加載器由下至上分為三個層次:引導(dǎo)類加載器(Bootstrap Class Loader),擴(kuò)展類加載器(Extension Class Loader)和應(yīng)用程序類加載器(Application Class Loader)。其中引導(dǎo)類加載器用于加載rt.jar這樣的核心類庫。并且引導(dǎo)類加載器為擴(kuò)展類加載器的父加載器,而擴(kuò)展類加載器又為應(yīng)用程序類加載器的父加載器。同時JVM在加載類時實行委托模式。即,當(dāng)前類加載器在加載類時,會首先委托自己的父加載器去進(jìn)行加載。如果父加載器已經(jīng)加載了某個類,那么子加載器將不會再次加載。
由上可知,當(dāng)應(yīng)用程序試圖加載java.util.Map時,它會首先逐級向上委托父加載器去加載該類,直到引導(dǎo)類加載器加載到rt.jar中的java.util.HashMap。由于該類已經(jīng)被加載了,我們自己創(chuàng)建的java.util.HashMap就不會被重復(fù)加載。
使用java命令運行SyncMapTest程序時加上VM參數(shù)-verbose:class,會在窗口中打印出形式如下的語句:
[Opened /home/ubuntu/jdk1.8.0_custom/jre/lib/rt.jar]
[Loaded java.lang.Object from /home/ubuntu/jdk1.8.0_custom/jre/lib/rt.jar]

[Loaded java.util.HashMap from /home/ubuntu/jdk1.8.0_custom/jre/lib/rt.jar]
[Loaded java.util.HashMap$Node from /home/ubuntu/jdk1.8.0_custom/jre/lib/rt.jar]

[Loaded java.util.SynchronizedHashMap from file:/home/ubuntu/projects/test/classes/]
Value
[Loaded java.lang.Shutdown from /home/ubuntu/jdk1.8.0_custom/jre/lib/rt.jar]
[Loaded java.lang.Shutdown$Lock from /home/ubuntu/jdk1.8.0_custom/jre/lib/rt.jar]
從中可以看出,類java.util.HashMap確實是從rt.jar中加載到的。但理論上,可以通過自定義類加載器去打破委托模式,然而這就是另一個話題了。[Loaded java.lang.Object from /home/ubuntu/jdk1.8.0_custom/jre/lib/rt.jar]

[Loaded java.util.HashMap from /home/ubuntu/jdk1.8.0_custom/jre/lib/rt.jar]
[Loaded java.util.HashMap$Node from /home/ubuntu/jdk1.8.0_custom/jre/lib/rt.jar]

[Loaded java.util.SynchronizedHashMap from file:/home/ubuntu/projects/test/classes/]
Value
[Loaded java.lang.Shutdown from /home/ubuntu/jdk1.8.0_custom/jre/lib/rt.jar]
[Loaded java.lang.Shutdown$Lock from /home/ubuntu/jdk1.8.0_custom/jre/lib/rt.jar]