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()方法
}
}
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命令去运行该应用时,会报如下错误:
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()的源代码如下:
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。那么可以尝试修改该方法的源代码,以突破这一限制。
从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中有如下语句:
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将不会再对包名有任何限制了。
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()方法,如下所示:
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后的期望输出应该如下所示:
put - key=Key, value=Value
Value
但运行SyncMapTest后的实际输出却为如下:
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中加载到的。但理论上,可以通过自定义类加载器去打破委托模式,然而这就是另一个话题了。