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 閱讀(3828) 評論(0)  編輯  收藏 所屬分類: JavaSEJava原創OpenJDK
          主站蜘蛛池模板: 金湖县| 卓尼县| 那坡县| 宁城县| 长葛市| 治县。| 张家界市| 沁源县| 广水市| 观塘区| 太仆寺旗| 资阳市| 潼关县| 漾濞| 肇州县| 辽中县| 五华县| 万宁市| 新化县| 天台县| 乌恰县| 江孜县| 浏阳市| 大港区| 安宁市| 阿城市| 南开区| 胶州市| 墨竹工卡县| 焦作市| 县级市| 廊坊市| 洛阳市| 库车县| 阜南县| 青阳县| 邵阳市| 乐山市| 城固县| 武平县| 遂昌县|