John Jiang

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

             :: 首頁 ::  :: 聯(lián)系 :: 聚合  :: 管理 ::
            131 隨筆 :: 1 文章 :: 530 評論 :: 0 Trackbacks
          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()方法
              }
          }

          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)用時,會報如下錯誤:
          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方法以去除相關(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中有如下語句:
          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將不會再對包名有任何限制了。

          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()方法,如下所示:
          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環(huán)境中,SynchronizedHashMap使用的java.util.HashMap被認(rèn)為是上述新創(chuàng)建的HashMap類。那么運行應(yīng)用程序SyncMapTest后的期望輸出應(yīng)該如下所示:
          put - key=Key, value=Value
          Value
          但運行SyncMapTest后的實際輸出卻為如下:
          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中加載到的。但理論上,可以通過自定義類加載器去打破委托模式,然而這就是另一個話題了。
          posted on 2015-11-01 20:06 John Jiang 閱讀(3828) 評論(0)  編輯  收藏 所屬分類: JavaSEJava原創(chuàng)OpenJDK
          主站蜘蛛池模板: 柘城县| 武功县| 二连浩特市| 策勒县| 涿州市| 宜兰市| 兴山县| 名山县| 清苑县| 大埔区| 桦南县| 威海市| 汉中市| 大渡口区| 顺昌县| 绍兴县| 图们市| 阿拉善盟| 林西县| 嘉峪关市| 岑溪市| 罗平县| 黄平县| 西安市| 兴山县| 调兵山市| 龙里县| 昌邑市| 牟定县| 九台市| 阳东县| 凤山市| 开化县| 宜都市| 阿瓦提县| 秦皇岛市| 萍乡市| 溧阳市| 湘潭县| 扶绥县| 松潘县|