Play OpenJDK: 允许你的包名以"java."开头(原)

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()方法
    }
}

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中加载到的。但理论上,可以通过自定义类加载器去打破委托模式,然而这就是另一个话题了。

你可能感兴趣的:(Play OpenJDK: 允许你的包名以"java."开头(原))