Play OpenJDK: 允许你的包名以"java."开头(原)
Play OpenJDK: 允许你的包名以"java."开头
本文是Play OpenJDK的第二篇,介绍了如何突破JDK不允许自定义的包名以"java."开头这一限制。这一技巧对于基于已有的JDK向java.*中添加新类还是有所帮助的。(2015.11.02最后更新)
无论是经验丰富的Java程序员,还是Java的初学者,总会有一些人或有意或无意地创建一个包名为"java"的类。但出于安全方面的考虑,JDK不允许应用程序类的包名以"java"开头,即不允许java,java.foo这样的包名。但javax,javaex这样的包名是允许的。
1. 例子
比如,以OpenJDK 8为基础,臆造这样一个例子。笔者想向OpenJDK贡献一个同步的HashMap,即类SynchronizedHashMap,而该类的包名就为java.util。SynchronizedHashMap是HashMap的同步代理,由于这两个类是在同一包内,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; // 直接调用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; // 直接调用HashMap.size变量,而非HashMap.size()方法
}
}
2. ClassLoader的限制
使用javac去编译源文件SynchronizedHashMap.java并没有问题,但在使用编译后的SynchronizedHashMap.class时,JDK的ClassLoader则会拒绝加载java.util.SynchronizedHashMap。
设想有如下的应用程序:
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命令去运行该应用时,会报如下错误:
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方法以去除相关限制。重新编译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中对应的类。
再次运行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真是层层设防啊)。原来在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) ;
}
修改该文件以去除掉相关限制,并按照本系列的 第一篇文章中介绍的方法去重新构建一个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?
开发者们在使用主流IDE时会发现,如果工程有多个jar文件或源文件目录中包含相同的类,这些IDE会根据用户指定的优先级顺序来加载这些类。比如,在Eclipse中,右键点击某个Java工程-->属性-->Java Build Path-->Order and Export,在这里调整各个类库或源文件目录的位置,即可指定加载类的优先级。
当开发者在使用某个开源类库(jar文件)时,想对其中某个类进行修改,那么就可以将该类的源代码复制出来,并在Java工程中创建一个同名类,然后指定Eclipse优先加息自己创建的类。即,在编译时与运行时用自己创建的类去覆盖类库中的同名类。那么,是否可以如法炮制去覆盖Java核心API中的类呢?
考虑去覆盖类java.util.HashMap,只是简单在它的put()方法添加一条打印语。那么就需要将src.zip中的java/util/HashMap.java复制出来,并在当前Java工程中创建一个同名类java.util.HashMap,并修改put()方法,如下所示:
Java类加载器由下至上分为三个层次:引导类加载器(Bootstrap Class Loader),扩展类加载器(Extension Class Loader)和应用程序类加载器(Application Class Loader)。其中引导类加载器用于加载rt.jar这样的核心类库。并且引导类加载器为扩展类加载器的父加载器,而扩展类加载器又为应用程序类加载器的父加载器。同时JVM在加载类时实行委托模式。即,当前类加载器在加载类时,会首先委托自己的父加载器去进行加载。如果父加载器已经加载了某个类,那么子加载器将不会再次加载。
由上可知,当应用程序试图加载java.util.Map时,它会首先逐级向上委托父加载器去加载该类,直到引导类加载器加载到rt.jar中的java.util.HashMap。由于该类已经被加载了,我们自己创建的java.util.HashMap就不会被重复加载。
使用java命令运行SyncMapTest程序时加上VM参数-verbose:class,会在窗口中打印出形式如下的语句:
开发者们在使用主流IDE时会发现,如果工程有多个jar文件或源文件目录中包含相同的类,这些IDE会根据用户指定的优先级顺序来加载这些类。比如,在Eclipse中,右键点击某个Java工程-->属性-->Java Build Path-->Order and Export,在这里调整各个类库或源文件目录的位置,即可指定加载类的优先级。
当开发者在使用某个开源类库(jar文件)时,想对其中某个类进行修改,那么就可以将该类的源代码复制出来,并在Java工程中创建一个同名类,然后指定Eclipse优先加息自己创建的类。即,在编译时与运行时用自己创建的类去覆盖类库中的同名类。那么,是否可以如法炮制去覆盖Java核心API中的类呢?
考虑去覆盖类java.util.HashMap,只是简单在它的put()方法添加一条打印语。那么就需要将src.zip中的java/util/HashMap.java复制出来,并在当前Java工程中创建一个同名类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环境中,SynchronizedHashMap使用的java.util.HashMap被认为是上述新创建的HashMap类。那么运行应用程序SyncMapTest后的期望输出应该如下所示:
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
看起来,新创建的java.util.HashMap并没有被使用上。这是为什么呢?能够"想像"到的原因还是类加载器。关于Java类加载器的讨论超出了本文的范围,而且关于该主题的文章已是汗牛充栋,但本文仍会简述其要点。
Java类加载器由下至上分为三个层次:引导类加载器(Bootstrap Class Loader),扩展类加载器(Extension Class Loader)和应用程序类加载器(Application Class Loader)。其中引导类加载器用于加载rt.jar这样的核心类库。并且引导类加载器为扩展类加载器的父加载器,而扩展类加载器又为应用程序类加载器的父加载器。同时JVM在加载类时实行委托模式。即,当前类加载器在加载类时,会首先委托自己的父加载器去进行加载。如果父加载器已经加载了某个类,那么子加载器将不会再次加载。
由上可知,当应用程序试图加载java.util.Map时,它会首先逐级向上委托父加载器去加载该类,直到引导类加载器加载到rt.jar中的java.util.HashMap。由于该类已经被加载了,我们自己创建的java.util.HashMap就不会被重复加载。
使用java命令运行SyncMapTest程序时加上VM参数-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 ]