2009年12月14日 星期一

[Java]URLClassLoader運用上的一點筆記

這一陣子接了一個case必須要撈一些網站的資料
由於這是必須多人maintain 的一個系統,要防止對方網頁改版而撈不到資料
因此我想到了Java的dynamic binding功能

在Java原生支援dynamic binding,而他主要又分成Implicit(隱式)explicit(顯式)兩大類

參考資料:http://www.yuloo.com/news/2008-08-27/112873.html 



 Implicit(隱式連結)

Java在宣告的時候類別本身並不會真正加載類別,只有在產生實體的時候會用ClassLoader去載入Class
考慮下面程式碼

class MyClass{
    static{
        System.out.println("類別載入了");
    }
}
public class Test {

   
    public static void main(String[] args) {
        MyClass myclass;
    }

}




上面程式碼執行的時候他不會去做類別載入的動作,因為他在這個case下並沒有用到
必須使用new關鍵字產生實體後才會真正去作載入的動作
考慮下方程式




class MyClass{
    static{
        System.out.println("類別載入了");
    }
}
public class Test {

   
    public static void main(String[] args) {
        MyClass myclass;
        myclass=new MyClass();
    }

}
 

//output: 類別載入了

Explicit(顯式連結)


Implicit的方式可以幫助我們用比較彈性的方式去呼叫一個類別,但是這還不能完全展現動態連結的優勢
為了可以做到類似Plug-in的功能,讓程式可以在run-time期間去改變甚至更新他的行為,就要依靠顯式連結
Java有提供Class.forName以及ClassLoader.loadClass兩個solution去替我們達到這個目的
在Java共有三種主要ClassLoader

參考資料:
http://caterpillar.onlyfun.net/Gossip/JavaEssence/ClassLoader.html
http://godleon.blogspot.com/2007/09/class-class-java-class-class-jvm-class.html

Bootstrap Loader→Extended Loader→System Loader

  1. Bootstrap Loader
    由 C++ 開發,會去搜尋 JRE 目錄($JAVA_HOME/jre/)中的 class 以及 lib 資料夾中的 *.jar 檔,檢查是否有指定的 class 需要載入。
  2. ExtClassLoader
    會去搜尋 JRE 目錄($JAVA_HOME/jre/)中的 lib/ext 資料夾,檢查其中的 class 與 *.jat 檔案,是否有指定的 class 需要載入。
  3. AppClassLoader
    搜尋 classpath 中是否有指定的 class 需要載入。
上面那三句是copy&paste,詳細的去請去看參考資料

當我們自定一些型別的時候,都會由AppClassLoader去幫我們載入
 考慮下面這段程式

class MyClass{
    static{
        System.out.println("類別載入了");
    }
}
public class Test {

   
    public static void main(String[] args) {
        MyClass myclass;
        myclass=new MyClass();
        System.out.println(myclass.getClass().getClassLoader().getClass());
    }

}
//output: 類別載入了
//output:class sun.misc.Launcher$AppClassLoader


而像這種類別載入器就是幫我們做到用字串去得到類別的好幫手,根據字串就能指定類別
這可以替我們的程式更有彈性更易擴充
下面先簡單舉一段Class.forName的例子

public interface CLI2 {

    public String say();
}

/**********************Interface Definition****************************/

 public class CLTest implements CLI2 {

    static{
        System.out.println("Now!Class Load!");
    }

    @Override
    public String say() {
        System.out.println("Hello I am a very happy boy!");
        return "Hello";
    }

}

/**********************Implement Definition****************************/
.......
 
Class c=Class.forName("CLTest");
  Object o=c.newInstance(); 

  CLI2 c2=(CLI2)o;
  System.out.println(c2.say());

.......
/**********************Main Function****************************/

/*output:
Now!Class Load!
Hello I am a very happy boy!
Hello

*/


上面的程式碼必須要注意一件是就是 CLTest產生的.class要跟執行main function的程式放到同一個目錄下才抓的到,不然要出現ClassNotFoundException這個例外,因為他會去classpath找,所以要方到classpath裡有指定的目錄

接下來就是今天筆記的重點URLClassLoader啦,因為他可以指定要去哪個Path或是jar檔去抓我們要的class,所以今天就在研究這個類別

來說說今天的心路歷程,原本我想說可以針對Interface寫程式的話可以讓程式更自由一點
所以開了兩個Project一個放實作的plug-in,CLTest跟介面CLI2,一個則是放實際要跑的main function跟同樣的介面CLI2

第一個Project內容

public interface CLI2 {

    public String say();
}

/**********************Interface Definition****************************/

 public class CLTest implements CLI2 {

    static{
        System.out.println("Now!Class Load!");
    }

    @Override
    public String say() {
        System.out.println("Hello I am a very happy boy!");
        return "Hello";
    }

}

/**********************Implement Definition****************************/ 



第二個Project內容

 public interface CLI2 {

    public String say();
}
/**********************Interface Definition****************************/

URL url1 = new URL("file:c:/TC/"); 

URLClassLoader urlClassLoader1 =new URLClassLoader(new URL[] {url1});

Class c2 = urlClassLoader1.loadClass("CLTest");




Object o=c2.newInstance();
         

CLI2 ci=(CLI2)o;
 

ci.say();

/**********************Main Function****************************/


上面是我程式最一開始的長相(非常糟糕 我知道),這程式跑出來會一堆錯誤
先看一下URLClassLoader的reference
http://java.sun.com/j2se/1.4.2/docs/api/java/net/URLClassLoader.html
他可以藉由URL類別去指定要查找指定路徑下或是指定jar內的.class

他有幾種指定方法
  • 指定一個絕對位置目錄:URL url1 = new URL("file:c:/TC/"); 以/符號結尾
  • 指定一個絕對位置Jar檔:URL url1 = new URL("file:c:/Lib.jar"); 
  • 指定一個相對位置目錄:URL url1 = new URL("file:TC/");  以/符號結尾
  • 指定一個相對位置Jar檔:URL url1 = new URL("file:Lib.jar");
如果是要一起打包放出去的話,可以選擇相對位置,或是用ClassLoader.getSystemResource("")及System.getProperty("user.dir")之類的函式去抓取當前目錄


或許有人會問為什麼我兩個Project都要放Interface CLI2,主要是當初考量寫程式方便,CLTest能夠順利參考到他的Interface,不過後來經人指證這是不好的做法,之後再談談改進的方法。


而我在此狀態下執行的結果IllegalAccessError這樣一個錯誤

Exception in thread "main" java.lang.IllegalAccessError:class CLTest cannot access its superinterface CLI2


Why?找不到Interface,難道不能用繼承嗎?後來查了一下這跟ClassLoader的屬性有關而造成無法存取Default package的東西。所以我採用了URLClassLoader的另一個建構子


URLClassLoader urlClassLoader1 = new URLClassLoader(new URL[] {url1}
                                        ,Thread.currentThread().getContextClassLoader());
              
 Class c2 = urlClassLoader1.loadClass("CLTest");
               
Object o=c2.newInstance();
       
CLI2 ci=(CLI2)o

ci.say();


使用getComtectClassLoader取得當前的類別下載器,不過很可惜他並沒有解決我的問題
因此我很天真的乾脆將他設為null

URLClassLoader urlClassLoader1 = new URLClassLoader(new URL[] {url1}
                                        ,null);
              
 Class c2 = urlClassLoader1.loadClass("CLTest");
               
Object o=c2.newInstance();
       
CLI2 ci=(CLI2)o

ci.say();


可以跑了,但是卻出現轉型錯誤的exception

Exception in thread "main" java.lang.ClassCastException:CLTest cannot be cast to CLI2


後來上網查了一下資料,Java在比對Type的時候除了Class Name跟Package Name之外,還必須要同一個ClassLoader載入才會當做同一個。也就是(ClassName,PackageName,ClassLoader)三個比對
在這支程式CLI2是由AppClassLoader所載入,而CLTest則是由URLClassLoader所載入,他不能當作同一個家族

後來查找了一些資料,才發現必須設定package而不能用default package。
因為我那堆程式都放在default package之下才會出現這問題,因此我就替他們修改到特定Package之下

第一個Project內容

package org.inter;
public interface CLI2 {

    public String say();
}

/**********************Interface Definition****************************/

import org.inter.*; 

 public class CLTest implements CLI2 {

    static{
        System.out.println("Now!Class Load!");
    }

    @Override
    public String say() {
        System.out.println("Hello I am a very happy boy!");
        return "Hello";
    }

}

/**********************Implement Definition****************************/ 


第二個Project內容

package org.inter;

 public interface CLI2 {

    public String say();
}
/**********************Interface Definition****************************/

import org.inter.CLI2; 
..................

URL url1 = new URL("file:c:/TC/"); 

URLClassLoader urlClassLoader1 =new URLClassLoader(new URL[] {url1},Thread.currentThread().getContextClassLoader());

Class c2 = urlClassLoader1.loadClass("CLTest");




Object o=c2.newInstance();
         

CLI2 ci=(CLI2)o;
 

ci.say();

/**********************Main Function****************************/



如此一來就解決了問題,用package設定去解IllegalAccessErrorThread.currentThread().getContextClassLoader()去解的問題

--------------------------------------------------------
但是上面存在著一個問題,就是兩個Project都維護著一份Interface CLI2
這個是比較不好的體系,而Eclipse有提一個解決方案,就是可以參照Project

這樣一來我只需maintain一個inteface就好

首先我先修改架構
第一個Project

CLTest.java //interface implement



第二個Project

CLI2.java //interface

Main.java



之後Eclipse對第一個Project按右見選[properties],再來選[Java Build Path],選裡面的[Projects]按下[Add]按鈕,把第二個Project加入參照就可以大功告成



沒有留言: