John Jiang

a cup of Java, cheers!
https://github.com/johnshajiang/blog

   :: 首页 ::  :: 联系 :: 聚合  :: 管理 ::
  131 随笔 :: 1 文章 :: 530 评论 :: 0 Trackbacks
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, falsetrue);
    }
    
}
此时,在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中加载到的。但理论上,可以通过自定义类加载器去打破委托模式,然而这就是另一个话题了。
posted on 2015-11-01 20:06 John Jiang 阅读(3778) 评论(0)  编辑  收藏 所属分类: JavaSEJava原创OpenJDK

只有注册用户登录后才能发表评论。


网站导航: