類的載入、連線和初始化(系統可能在第一次使用某個類時載入該類, 也可能採用預載入機制 機製來載入某個類)動態代理實現
1、JVM和類
當呼叫 java 命令執行某個 Java 程式時, 該命令將會啓動一個 Java 虛擬機器進程, 不管該 Java 程式有多麼複雜, 該程式啓動了多少個執行緒, 它們都處於該 Java 虛擬機器進程裡。 同一個 JVM的所有執行緒、 所有變數都處於同一個進程裡, 它們都使用該 JVM 進程的記憶體區。
JVM執行時數據區
當系統出現以下幾種情況時, JVM 進程將被終止:
小結:當 Java 程式執行結束時, JVM 進程結束, 該進程在記憶體中的狀態將會丟失。
範例:
1.1、定義一個包含類變數的類:A
public class A
{
// 定義該類的類變數
public static int a = 6;
}
1.2、定義一個類建立 A 類的範例, 並存取 A 物件的類變數 a:
public class ATest1
{
public static void main(String[] args)
{
// 建立A類的範例
A a = new A();
// 讓a範例的類變數a的值自加
a.a ++;
System.out.println(a.a);
}
}
1.3、建立 A 物件, 並存取其類變數 a 的值:
public class ATest2
{
public static void main(String[] args)
{
// 建立A類的範例
A b = new A();
// 輸出b範例的類變數a的值
System.out.println(b.a);
}
}
分析:在 ATestl.java 程式中建立了 A 類的範例, 並讓該範例的類變數 a 的值自加, 程式輸出該範例的類變數 a 的值將看到 7。 執行第二個程式 ATest2 時, 程式再次建立了 A 物件, 並輸出 A 物件類變數的 a 的值, 此時 a 的值是多少呢? 結果依然是 6, 並不是 7。 這是因爲執行 ATestl 和 ATest2 是兩次執行 JVM 進程, 第一次執行 JVM 結束後, 它對 A 類所做的修改將全部丟失——第二次執行 JVM 時將再次初始化 A 類。
2、類的載入
Java類的生命週期
2.1、當程式主動使用某個類時, 如果該類還未被載入到記憶體中, 則系統會通過載入、 連線、 初始化三個步驟來對該類進行初始化。 如果沒有意外, JVM 將會連續完 成這三個步驟, 所以有時也把這三個步驟統稱爲類載入或類初始化。
類載入指的是將類的 class 檔案讀入記憶體, 併爲之建立一個 java.lang.Class 物件, 也就是說, 當程式中使用任何類時, 系統都會爲之建立一個java.lang.Class 物件。
2.2、類的載入由類載入器完成, 類載入器通常由 JVM 提供, 這些類載入器也是前面所有程式執行的基礎, JVM 提供的這些類載入器通常被稱爲系統類載入器。 除此之外, 開發者可以通過繼承 ClassLoader基礎類別來建立自己的類載入器。
2.3、通過使用不同的類載入器, 可以從不同來源載入類的二進制數據, 通常有如下幾種來源。
類載入器通常無須等到「 首次使用」 該類時才載入該類, Java 虛擬機器規範允許系統預先載入某些類。
3、類的連線
3.1、當類被載入之後, 系統爲之生成一個對應的 Class 物件, 接着將會進入連線階段, 連線階段負責把類的二進制數據合併到 JRE 中。 類連線又可分爲如下三個階段:
(1) 驗證: 驗證階段用於檢驗被載入的類是否有正確的內部結構, 並和其他類協調一致。
(2) 準備: 類準備階段則負責爲類的類變數分配記憶體, 並設定預設初始值。
(3 ) 解析: 將類的二進制數據中的符號參照替換成直接參照。
4、類的初始化
在類的初始化階段, 虛擬機器負責對類進行初始化, 主要就是對類變數進行初始化。
4.1、在 Java 類中對類變數指定初始值有兩種方式:① 宣告類變數時指定初始值;② 使用靜態初始化塊爲類變數指定初始值。
例:
public class Test{
// 宣告變數 a 時指定初始值
static int a = 5
static int b;
static int c;
static{
// 使用靜態初始化塊爲變數 b 指定初始值
b=6;
}
}
分析:對於上面程式碼, 程式爲類變數 a、 b 都顯式指定了初始值, 所以這兩個類變數的值分別爲 5、 6, 但類變數 c 則沒有指定初始值, 它將採用預設初始值 0。
4.2、宣告變數時指定初始值, 靜態初始化塊都將被當成類的初始化語句, JVM 會按這些語句在程式中的排列順序依次執行它們
public class Test
{
static
{
// 使用靜態初始化塊爲變數b指定出初始值
b = 6;
System.out.println("----------");
}
// 宣告變數a時指定初始值
static int a = 5;
static int b = 9; // ①
static int c;
public static void main(String[] args)
{
System.out.println(Test.b);
}
}
分析:上面程式碼先在靜態初始化塊中爲 b 變數賦值, 此時類變數 b 的值爲 6; 接着程式向下執行, 執行到①號程式碼處, 這行程式碼也屬於該類的初始化語句, 所以程式再次爲類變數 b 賦值。 也就是說, 當 Test類初始化結束後, 該類的類變數 b 的值爲 9。
4.3、JVM 初始化一個類包含如下幾個步驟:
4.4、類初始時機
當 Java 程式首次通過下面 下麪 6 種方式來使用某個類或介面時, 系統就會初始化該類或介面:
建立類的範例。 爲某個類建立範例的方式包括: 使用 new 操作符來建立範例, 通過反射來建立範例, 通過反序列化的方式來建立範例。
呼叫某個類的類方法(靜態方法)。
存取某個類或介面的類變數, 或爲該類變數賦值。
使用反射方式來強制建立某個類或介面對應 的 java.lang.Class 對 象 。 例 如 代 碼 : Class.forName(MPerson"), 如果系統還未初始化 Person 類, 則這行程式碼將會導致該 Person 類被初始化, 並返回 Person 類對應的 java.lang.Class 物件。
初始化某個類的子類。 當初始化某個類的子類時, 該子類的所有父類別都會被初始化。
直 接 使 用 java.exe 命令來執行某個主類。 當執行某個主類時, 程式會先初始化該主類。
5、類載入器
類載入器負責將.class 檔案( 可能在磁碟上, 也可能在網路上) 載入到記憶體中, 併爲之生成對應的java.lang.Class 物件。
6、類載入機制 機製
類載入器負責載入所有的類, 系統爲所有被載入記憶體中的類生成java.lang.Class 範例。
一個類被載入 JVM 中, 同 一個類就不會被再次載入了——正如一個物件有一個唯一的標識一樣, 一個載入 JVM 中的類也有一個唯一的標識。 在 Java 中, 一
個類用其全限定類名( 包括包名和類名) 作爲標識; 但在 JVM 中, 一個類用其全限定類名和其類載入器作爲唯一標識。 例如, 如果在 pg 的包中有一個名爲 Person 的類, 被類載入器 ClassLoader 的範例 kl負責載入, 則該 Person 類對應的 Class 物件在 JVM 中表示爲 Person、pg、kl ) 這意味着兩個類載入器載入的同名類: (Person 、pg、 kl ) 和( Person、 pg、 kl2) 是不同的, 它們所載入的類也是完全不同、互不相容的。
當 JVM 啓動時, 會形成由三個類載入器組成的初始類載入器層次結構。
除了可以使用 Java 提供的類載入器之外, 開發者也可以實現自己的類載入器, 自定義的類載入器通過繼承 ClassLoader 來實現。
類載入器的層次
**雙親委派模型:如上圖所示的類載入器之間的這種層次關係,就稱爲類載入器的雙親委派模型(Parent Delegation Model)。該模型要求除了頂層的啓動類載入器外,其餘的類載入器都應當有自己的父類別載入器。子類載入器和父類別載入器不是以繼承(Inheritance)的關係來實現,而是通過組合(Composition)關係來複用父載入器的程式碼。
雙親委派模型的工作過程爲:如果一個類載入器收到了類載入的請求,它首先不會自己去嘗試載入這個類,而是把這個請求委派給父類別載入器去完成,每一個層次的載入器都是如此,因此所有的類載入請求都會傳給頂層的啓動類載入器,只有當父載入器反饋自己無法完成該載入請求(該載入器的搜尋範圍中沒有找到對應的類)時,子載入器纔會嘗試自己去載入。
**
JVM 的類載入機制 機製主要有如下三種:
程式存取 JVM 的類載入器:
import java.util.*;
import java.net.*;
import java.io.*;
public class ClassLoaderPropTest {
public static void main(String[] args) throws IOException {
// 獲取系統類載入器
ClassLoader systemLoader = ClassLoader.getSystemClassLoader();
System.out.println("系統類載入器:" + systemLoader);
/*
* 獲取系統類載入器的載入路徑——通常由CLASSPATH環境變數指定 如果操作系統沒有指定CLASSPATH環境變數,預設以當前路徑作爲
* 系統類載入器的載入路徑
*/
Enumeration<URL> em1 = systemLoader.getResources("");
while (em1.hasMoreElements()) {
System.out.println(em1.nextElement());
}
// 獲取系統類載入器的父類別載入器:得到擴充套件類載入器
ClassLoader extensionLader = systemLoader.getParent();
System.out.println("擴充套件類載入器:" + extensionLader);
System.out.println("擴充套件類載入器的載入路徑:" + System.getProperty("java.ext.dirs"));
System.out.println("擴充套件類載入器的parent: " + extensionLader.getParent());
}
}
執行結果:
系統類載入器是 AppClassLoader 的範例, 擴充套件類載入器 PlatformClassLoader
的範例。 實際上, 這兩個類都是 URLClassLoader 類的範例。
根類載入器並不是Java實現的,而且由於程式通常須存取根載入器,因此存取擴充套件類載入器的父類別載入器時返回NULL。
類載入器載入 Class 大致要經過如下 8 個步驟(對應上面的雙親委派模型):
其中,第5、6步允許重寫ClassLoader的findClass()方法來實現自己的載入策略,甚至重寫loadClass() 方法來實現自己的載入過程。
JVM 中除根類載入器之外的所有類載入器都是 ClassLoader 子類的範例, 開發者可以通過擴充套件ClassLoader 的子類, 並重寫該 ClassLoader 所包含的方法來實現自定義的類載入器。 查閱 API 文件中關於 ClassLoader的方法不難發現, ClassLoader 中包含了大量的 protected 方法 這些方法都可被子類重寫。
ClassLoader 類有如下兩個關鍵方法:
loadClass()方法的執行步驟如下:
從上面步驟中可以看出,重寫findClass()方法可以避免覆蓋預設類載入器的父類別委託、緩衝機制 機製兩 種策略;如果重寫loadClass()方法,則實現邏輯更爲複雜。
在 ClassLoader 裡還有一個核心方法:Class defineClass(String name, byte[] b, int off, int len),該方法 負責將指定類的位元組碼檔案(即Class檔案,如Hello.class)讀入位元組陣列byte[] b內,並把它轉換爲 Class物件,該位元組碼檔案可以來源於檔案、網路等。
defileClass()方法管理JVM的許多複雜的實現,它負責將位元組碼分析成執行時數據結構,並校驗有效性等。
除此之外,ClassLoader裡還包含如下一些普通方法。
自定義的ClassLoader,該ClassLoader通過重寫findClass()方法來實現自定義 的類載入機制 機製。這個ClassLoader可以在載入類之前先編譯該類的原始檔,從而實現執行Java之前先編 譯該程式的目標,這樣即可通過該ClassLoader直接執行Java原始檔。
public class MyClassLoader extends ClassLoader {
private String root;
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] classData = loadClassData(name);
if (classData == null) {
throw new ClassNotFoundException();
} else {
return defineClass(name, classData, 0, classData.length);
}
}
private byte[] loadClassData(String className) {
String fileName = root + File.separatorChar
+ className.replace('.', File.separatorChar) + ".class";
try {
InputStream ins = new FileInputStream(fileName);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int bufferSize = 1024;
byte[] buffer = new byte[bufferSize];
int length = 0;
while ((length = ins.read(buffer)) != -1) {
baos.write(buffer, 0, length);
}
return baos.toByteArray();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
public String getRoot() {
return root;
}
public void setRoot(String root) {
this.root = root;
}
public static void main(String[] args) {
MyClassLoader classLoader = new MyClassLoader();
classLoader.setRoot("E:\\temp");
Class<?> testClass = null;
try {
testClass = classLoader.loadClass("com.neo.classloader.Test2");
Object object = testClass.newInstance();
System.out.println(object.getClass().getClassLoader());
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
自定義類載入器的核心在於對位元組碼檔案的獲取,如果是加密的位元組碼則需要在該類中對檔案進行解密。由於這裏只是演示,我並未對class檔案進行加密,因此沒有解密的過程。這裏有幾點需要注意:
Java 爲ClassLoader 提供了一個 URLClassLoader 實現類, 該類也是系統類載入器和擴充套件類載入器的父類別( 此處的父類別, 就是指類與類之間的繼承關係)。 URLClassLoader 功能比較強大, 它既可以從本地檔案系統獲取二進制檔案來載入類, 也可以從遠端主機獲取二進制檔案來載入類。
在應用程式中可以直接使用 URLClassLoader 載入類, URLClassLoader 類提供瞭如下兩個構造器:
一旦得到了 URLClassLoader 物件之後, 就可以呼叫該物件的 loadClass()方法來載入指定類。 下面 下麪程式示範瞭如何直接從檔案系統中載入 MySQL 驅動, 並使用該驅動來獲取數據庫連線。 通過這種方式來獲取數據庫連線, 可以無須將 MySQL 驅動新增到 CLASSPATH 環境變數中。
import java.sql.*;
import java.util.*;
import java.net.*;
public class URLClassLoaderTest {
private static Connection conn;
// 定義一個獲取數據庫連線方法
public static Connection getConn(String url, String user, String pass) throws Exception {
if (conn == null) {
// 建立一個URL陣列
URL[] urls = { new URL("file:mysql-connector-java-5.1.30-bin.jar") };
// 以預設的ClassLoader作爲父ClassLoader,建立URLClassLoader
URLClassLoader myClassLoader = new URLClassLoader(urls);
// 載入MySQL的JDBC驅動,並建立預設範例
Driver driver = (Driver) myClassLoader.loadClass("com.mysql.jdbc.Driver").getConstructor().newInstance();
// 建立一個設定JDBC連線屬性的Properties物件
Properties props = new Properties();
// 至少需要爲該物件傳入user和password兩個屬性
props.setProperty("user", user);
props.setProperty("password", pass);
// 呼叫Driver物件的connect方法來取得數據庫連線
conn = driver.connect(url, props);
}
return conn;
}
public static void main(String[] args) throws Exception {
System.out.println(getConn("jdbc:mysql://localhost:3306/mysql", "root", "32147"));
}
}
通過反射檢視類資訊
Java 程式中的許多物件在執行時都會出現兩種型別: 編譯時型別和執行時型別, 例如程式碼: Person p=new Student();,這行程式碼將會生成一個 p 變數, 該變數的編譯時型別爲 Person,執行時型別爲 Student;除此之外, 還有更極端的情形, 程式在執行時接收到外部傳入的一個物件, 該物件的編譯時型別是 Object,但程式又需要呼叫該物件執行時型別的方法。
獲得 Class 物件
在 Java 程式中獲得 Class 物件通常有如下三種方式:
對於第一種方式和第二種方式都是直接根據類來取得該類的 Class 物件, 相比之下, 第二種方式有如下兩種優勢:
也就是說, 大部分時候都應該使用第二種方式來獲取指定類的 Class 物件。 但如果程式只能獲得一個字串, 例如」java.lang.String」, 若需要獲取該字串對應的 Class 物件, 則只能使用第一種方式, 使用Class 的 forName(String clazzName)方法獲取 Class 物件時, 該方法可能拋出一個 ClassNotFoundException異常。
一旦獲得了某個類所對應的 Class 物件之後, 程式就可以呼叫 Class 物件的方法來獲得該物件和該類的資訊了。
從 Class 中獲取資訊
Class 類提供了大量的實體方法來獲取該 Class 物件所對應類的詳細資訊,Class 類大致包含如下方法, 下面 下麪每個方法都可能包括多個過載的版本, 應該參照官方API。
下面 下麪 4 個方法用於獲取 Class 對應類所包含的構造器:
下面 下麪 4 個方法用於獲取 Class 對應類所包含的方法:
下 4 個方法用於存取 Class 對應類所包含的成員變數:
如下幾個方法用於存取 Class 對應類上所包含的 Annotation:
如下方法用於存取該 Class 物件對應類包含的內部類:
如下方法用於存取該 Class 物件對應類所在的外部類:
如下方法用於存取該 Class 物件對應類所實現的介面:
如下幾個方法用於存取該 Class 物件對應類所繼承的父類別:
如下方法用於獲取 Class 物件對應類的修飾符、 所在包、 類名等基本資訊:
除此之外, Class 物件還可呼叫如下幾個判斷方法來判斷該類是否爲介面、 列舉、 註解型別等:
上面的多個 getMethod()方法和 getConstructor()方法中, 都需要傳入多個型別爲 Class<?〉的參數, 用於獲取指定的方法或指定的構造器。 關於這個參數的作用, 假設某個類內包含如下三個 info 方法簽名:
在程式中獲取該方法使用如下程式碼:
// 前一個參數指定方法名, 後面的個數可變的 Class 參數指定形參型別列表
clazz.getMethod("info" , String.class)
// 前一個參數指定方法名, 後面的個數可變的 Class 參數指定形參型別列表
clazz.getMethod("info" , String.class, Integer.class)
import java.util.*;
import java.lang.reflect.*;
import java.lang.annotation.*;
// 定義可重複註解
@Repeatable(Annos.class)
@interface Anno {
}
@Retention(value = RetentionPolicy.RUNTIME)
@interface Annos {
Anno[] value();
}
// 使用4個註解修飾該類
@SuppressWarnings(value = "unchecked")
@Deprecated
// 使用重複註解修飾該類
@Anno
@Anno
public class ClassTest {
// 爲該類定義一個私有的構造器
private ClassTest() {
}
// 定義一個有參數的構造器
public ClassTest(String name) {
System.out.println("執行有參數的構造器");
}
// 定義一個無參數的info方法
public void info() {
System.out.println("執行無參數的info方法");
}
// 定義一個有參數的info方法
public void info(String str) {
System.out.println("執行有參數的info方法" + ",其str參數值:" + str);
}
// 定義一個測試用的內部類
class Inner {
}
public static void main(String[] args) throws Exception {
// 下面 下麪程式碼可以獲取ClassTest對應的Class
Class<ClassTest> clazz = ClassTest.class;
// 獲取該Class物件所對應類的全部構造器
Constructor[] ctors = clazz.getDeclaredConstructors();
System.out.println("ClassTest的全部構造器如下:");
for (Constructor c : ctors) {
System.out.println(c);
}
// 獲取該Class物件所對應類的全部public構造器
Constructor[] publicCtors = clazz.getConstructors();
System.out.println("ClassTest的全部public構造器如下:");
for (Constructor c : publicCtors) {
System.out.println(c);
}
// 獲取該Class物件所對應類的全部public方法
Method[] mtds = clazz.getMethods();
System.out.println("ClassTest的全部public方法如下:");
for (Method md : mtds) {
System.out.println(md);
}
// 獲取該Class物件所對應類的指定方法
System.out.println("ClassTest裏帶一個字串參數的info()方法爲:" + clazz.getMethod("info", String.class));
// 獲取該Class物件所對應類的上的全部註解
Annotation[] anns = clazz.getAnnotations();
System.out.println("ClassTest的全部Annotation如下:");
for (Annotation an : anns) {
System.out.println(an);
}
System.out.println("該Class元素上的@SuppressWarnings註解爲:"
+ Arrays.toString(clazz.getAnnotationsByType(SuppressWarnings.class)));
System.out.println("該Class元素上的@Anno註解爲:" + Arrays.toString(clazz.getAnnotationsByType(Anno.class)));
// 獲取該Class物件所對應類的全部內部類
Class<?>[] inners = clazz.getDeclaredClasses();
System.out.println("ClassTest的全部內部類如下:");
for (Class c : inners) {
System.out.println(c);
}
// 使用Class.forName方法載入ClassTest的Inner內部類
Class inClazz = Class.forName("ClassTest$Inner");
// 通過getDeclaringClass()存取該類所在的外部類
System.out.println("inClazz對應類的外部類爲:" + inClazz.getDeclaringClass());
System.out.println("ClassTest的包爲:" + clazz.getPackage());
System.out.println("ClassTest的父類別爲:" + clazz.getSuperclass());
}
}
API:java.lang.Class
使用反射生成並操作物件
Class 物件可以獲得該類裡的方法( 由 Method 物件表示)、 構造器( 由 Constructor 物件表示)、 成員變數( 由 Field 物件表示), 這三個類都位於 java.lang.reflect 包下, 並實現了 java.lang.reflect.Member介面。 程式可以通過 Method 物件來執行對應的方法, 通過 Constructor 物件來呼叫對應的構造器建立範例, 能通過 Field 物件直接存取並修改物件的成員變數值。
建立物件
通過反射來生成物件需要先使用 Class 物件獲取指定的 Constructor 物件, 再呼叫 Constructor 物件的 newlnstance()方法來建立該 Class 物件對應類的範例。 通過這種方式可以選擇使用指定的構造器來建立範例。
在很多 Java EE (例如Spring)框架中都需要根據組態檔資訊來建立 Java 物件,從組態檔讀取的只是某個類的字串類名, 程式需要根據該字串來建立對應的範例, 就必須使用反射。
下面 下麪程式就實現了一個簡單的物件池, 該物件池會根據組態檔讀取 key-value 對, 然後建立這些物件, 並將這些物件放入一個 HashMap 中:
import java.util.*;
import java.io.*;
public class ObjectPoolFactory {
// 定義一個物件池,前面是物件名,後面是實際物件
private Map<String, Object> objectPool = new HashMap<>();
// 定義一個建立物件的方法
// 該方法只要傳入一個字串類名,程式可以根據該類名生成Java物件
private Object createObject(String clazzName) throws Exception, IllegalAccessException, ClassNotFoundException {
// 根據字串來獲取對應的Class物件
Class<?> clazz = Class.forName(clazzName);
// 使用clazz對應類的預設構造器建立範例
return clazz.getConstructor().newInstance();
}
// 該方法根據指定檔案來初始化物件池
// 它會根據組態檔來建立物件
public void initPool(String fileName)
throws InstantiationException, IllegalAccessException, ClassNotFoundException {
try (FileInputStream fis = new FileInputStream(fileName)) {
Properties props = new Properties();
props.load(fis);
for (String name : props.stringPropertyNames()) {
// 每取出一對key-value對,就根據value建立一個物件
// 呼叫createObject()建立物件,並將物件新增到物件池中
objectPool.put(name, createObject(props.getProperty(name)));
}
} catch (Exception ex) {
System.out.println("讀取" + fileName + "異常");
}
}
public Object getObject(String name) {
// 從objectPool中取出指定name對應的物件
return objectPool.get(name);
}
public static void main(String[] args) throws Exception {
ObjectPoolFactory pf = new ObjectPoolFactory();
pf.initPool("obj.txt");
System.out.println(pf.getObject("a")); // ①
System.out.println(pf.getObject("b")); // ②
}
}
程式呼叫 Class 物件的 newlnstance()方法即可建立一個 Java 物件。 程式中的 initPool()方法會讀取屬性檔案, 對屬性檔案中每個 key-value 對建立一個 Java 物件, 其中 value 是該 Java 物件的實現類, 而 key 是該 Java 物件放入物件池中的名字。 爲該程式提供如下屬性組態檔:
obj.txt
a=java.util.Date
b=javax.swing.JFrame
如果不想利用預設構造器來建立 Java 物件, 而想利用指定的構造器來建立 Java 物件, 則需要利用Constructor 物件, 每個 Constructor 對應一個構造器。 爲了利用指定的構造器來建立 Java 物件, 需要如下三個步驟:
下面 下麪程式利用反射來建立一個 JFrame 物件, 而且使用指定的構造器:
import java.lang.reflect.*;
public class CreateJFrame {
public static void main(String[] args) throws Exception {
// 獲取JFrame對應的Class物件
Class<?> jframeClazz = Class.forName("javax.swing.JFrame");
// 獲取JFrame中帶一個字串參數的構造器
Constructor ctor = jframeClazz.getConstructor(String.class);
// 呼叫Constructor的newInstance方法建立物件
Object obj = ctor.newInstance("測試視窗");
// 輸出JFrame物件
System.out.println(obj);
}
}
呼叫方法
當獲得某個類對應的 Class 物件後, 就可以通過該 Class 物件的 getMethods()方法或者 getMethod()方法來獲取全部方法或指定方法—這兩個方法的返回值是 Method 陣列, 或者 Method 物件。每個 Method 物件對應一個方法, 獲得 Method 物件後, 程式就可通過該 Method 來呼叫它對應的方法。 在 Method 裡包含一個 invoke()方法, 該方法的簽名如下:
下面 下麪程式對前面的物件池工廠進行加強, 允許在組態檔中增加設定物件的成員變數的值, 物件池工廠會讀取爲該物件設定的成員變數值, 並利用該物件對應的 setter 方法設定成員變數的值:
import java.util.*;
import java.io.*;
import java.lang.reflect.*;
public class ExtendedObjectPoolFactory {
// 定義一個物件池,前面是物件名,後面是實際物件
private Map<String, Object> objectPool = new HashMap<>();
private Properties config = new Properties();
// 從指定屬性檔案中初始化Properties物件
public void init(String fileName) {
try (FileInputStream fis = new FileInputStream(fileName)) {
config.load(fis);
} catch (IOException ex) {
System.out.println("讀取" + fileName + "異常");
}
}
// 定義一個建立物件的方法
// 該方法只要傳入一個字串類名,程式可以根據該類名生成Java物件
private Object createObject(String clazzName) throws Exception {
// 根據字串來獲取對應的Class物件
Class<?> clazz = Class.forName(clazzName);
// 使用clazz對應類的預設構造器建立範例
return clazz.getConstructor().newInstance();
}
// 該方法根據指定檔案來初始化物件池
// 它會根據組態檔來建立物件
public void initPool() throws Exception {
for (String name : config.stringPropertyNames()) {
// 每取出一個key-value對,如果key中不包含百分號(%)
// 這就表明是根據value來建立一個物件
// 呼叫createObject建立物件,並將物件新增到物件池中
if (!name.contains("%")) {
objectPool.put(name, createObject(config.getProperty(name)));
}
}
}
// 該方法將會根據屬性檔案來呼叫指定物件的setter方法
public void initProperty() throws InvocationTargetException, IllegalAccessException, NoSuchMethodException {
for (String name : config.stringPropertyNames()) {
// 每取出一對key-value對,如果key中包含百分號(%)
// 即可認爲該key用於控制呼叫物件的setter方法設定值
// %前半爲物件名字,後半控制setter方法名
if (name.contains("%")) {
// 將組態檔中的key按%分割
String[] objAndProp = name.split("%");
// 取出調用setter方法的參數值
Object target = getObject(objAndProp[0]);
// 獲取setter方法名:set + "首字母大寫" + 剩下部分
String mtdName = "set" + objAndProp[1].substring(0, 1).toUpperCase() + objAndProp[1].substring(1);
// 通過target的getClass()獲取它的實現類所對應的Class物件
Class<?> targetClass = target.getClass();
// 獲取希望呼叫的setter方法
Method mtd = targetClass.getMethod(mtdName, String.class);
// 通過Method的invoke方法執行setter方法
// 將config.getProperty(name)的值作爲呼叫setter方法的參數
mtd.invoke(target, config.getProperty(name));
}
}
}
public Object getObject(String name) {
// 從objectPool中取出指定name對應的物件
return objectPool.get(name);
}
public static void main(String[] args) throws Exception {
ExtendedObjectPoolFactory epf = new ExtendedObjectPoolFactory();
epf.init("extObj.txt");
epf.initPool();
epf.initProperty();
System.out.println(epf.getObject("a"));
}
}
78
爲上面程式提供如下組態檔:
ext0bj.text
a=javax.swing.JFrame
b=javax.swing.JLabel
#set the title of a
a%title=Test Title
Spring 框架就是通過這種方式將成員變數值以及依賴物件等都放在組態檔中進行 ,管理的, 從而實現了較好的解耦。 這也是 Spring 框架的 IOC 的原理。
當通過Method的invoke()方法來呼叫對應的方法時,Java會要求程式必須有呼叫該方法的許可權。 如果程式確實需要呼叫某個物件的private方法,則可以先呼叫Method物件的如下方法: * setAccessible(boolean flag):將Method物件的accessible設定爲指定的布爾值。值爲true,指示 該Method在使用時應該取消Java語言的存取許可權檢査;值爲false,則指示該Method在使用時 要實施Java語言的存取許可權檢查。
存取成員變數值
通過 Class 物件的 getFields()或 getField()方法可以獲取該類所包括的全部成員變數或指定成員變數。
Field 提供瞭如下兩組方法來讀取或設定成員變數值:
使用這兩個方法可以隨意地存取指定物件的所有成員變數, 包括 private 修飾的成員變數。
import java.lang.reflect.*;
class Person {
private String name;
private int age;
public String toString() {
return "Person[name:" + name + " , age:" + age + " ]";
}
}
public class FieldTest {
public static void main(String[] args) throws Exception {
// 建立一個Person物件
Person p = new Person();
// 獲取Person類對應的Class物件
Class<Person> personClazz = Person.class;
// 獲取Person的名爲name的成員變數
// 使用getDeclaredField()方法表明可獲取各種存取控制符的成員變數
Field nameField = personClazz.getDeclaredField("name");
// 設定通過反射存取該成員變數時取消存取許可權檢查
nameField.setAccessible(true);
// 呼叫set()方法爲p物件的name成員變數設定值
nameField.set(p, "Yeeku.H.Lee");
// 獲取Person類名爲age的成員變數
Field ageField = personClazz.getDeclaredField("age");
// 設定通過反射存取該成員變數時取消存取許可權檢查
ageField.setAccessible(true);
// 呼叫setInt()方法爲p物件的age成員變數設定值
ageField.setInt(p, 30);
System.out.println(p);
}
}
運算元組
在 java.lang.reflect 包下還提供了一個 Array 類, Array 物件可以代表所有的陣列。 程式可以通過使用 Array 來動態地建立陣列, 運算元組元素等。
Array 提供瞭如下幾類方法:
下面 下麪程式示範瞭如何使用 Array 來生成陣列, 爲指定陣列元素賦值, 並獲取指定陣列元素的方式:
import java.lang.reflect.*;
public class ArrayTest1 {
public static void main(String args[]) {
try {
// 建立一個元素型別爲String ,長度爲10的陣列
Object arr = Array.newInstance(String.class, 10);
// 依次爲arr陣列中index爲5、6的元素賦值
Array.set(arr, 5, "瘋狂Java講義");
Array.set(arr, 6, "輕量級Java EE企業應用實戰");
// 依次取出arr陣列中index爲5、6的元素的值
Object book1 = Array.get(arr, 5);
Object book2 = Array.get(arr, 6);
// 輸出arr陣列中index爲5、6的元素
System.out.println(book1);
System.out.println(book2);
} catch (Throwable e) {
System.err.println(e);
}
}
}
123456789101112131415161718192021
使用反射生成 JDK 動態代理
代理分爲靜態代理和動態代理,靜態代理是在編譯時就將介面、實現類、代理類一股腦兒全部手動完成,但如果我們需要很多的代理,每一個都這麼手動的去建立實屬浪費時間,而且會有大量的重複程式碼,此時我們就可以採用動態代理,動態代理可以在程式執行期間根據需要動態的建立代理類及其範例,來完成具體的功能。
使用 Proxy 和 InvocationHandler 建立動態代理
代理(Proxy)是一種設計模式,提供了對目標物件另外的存取方式;即通過代理物件存取目標物件.這樣做的好處是:可以在目標物件實現的基礎上,增強額外的功能操作,即擴充套件目標物件的功能。 這裏用到程式設計中的一個思想:不要隨意去修改別人已經寫好的程式碼或者方法,如果需改修改,可以通過代理的方式來擴充套件該方法。 舉個例子來說明代理的作用:假設我們想邀請一位明星,那麼並不是直接連線明星,而是聯繫明星的經紀人,來達到同樣的目的.明星就是一個目標物件,他只要負責活動中的節目,而其他瑣碎的事情就交給他的代理人(經紀人)來解決.這就是代理思想在現實中的一個例子。
Proxy 提供了用於建立動態代理類和代理物件的靜態方法, 它也是所有動態代理類的父類別。 如果在程式中爲一個或多個介面動態地生成實現類, 就可以使用 Proxy 來建立動態代理類; 如果需要爲一個或多個介面動態地建立範例, 也可以使用 Proxy 來建立動態代理範例。
Java動態代理UML圖
Proxy 提供瞭如下兩個方法來建立動態代理類和動態代理範例:
實際上, 即使採用第一個方法生成動態代理類之後, 如果程式需要通過該代理類來建立物件, 依然需要傳入一個 InvocationHandler 物件。 也就是說, 系統生成的每個代理物件都有一個與之關聯的InvocationHandler 物件。
程式中可以採用先生成一個動態代理類, 然後通過動態代理類來建立代理物件的方式生成一個動態代理物件。 程式碼片段如下:
程式中可以採用先生成一個動態代理類, 然後通過動態代理類來建立代理物件的方式生成一個動態代理物件。 程式碼片段如下:
// 建立一個 InvocationHandler 物件
InvocationHandler handler = new MylnvocationHandler ( ...);
// 使 用 Proxy 生成一個動態代理類 proxyClass
Class proxyClass = Proxy.getProxyClass(Foo.class.getClassLoader()
, new Class[] { Foo.class });
// 會取 proxyClass_中帶一個
Constructor ctor = proxyClass. getConstructor(new Class[]
InvocationHandler 參數的構造器
{ InvocationHandler•class });
// 呼叫 ctor 的 newlnstance 方法來建立動態範例
Foo f (Foo)ctor.newlnstance(new Object[]{handler});
1234567891011
上面程式碼也可以簡化成如下程式碼:
// 建立一個 InvocationHandler 物件
InvocationHandler handler = new MyInvocationHandler(...);
// 使用 Proxy 直接生成一個動態代理物件
Foo f = (Foo)Proxy.newProxyInstance(Foo.class.getClassLoader()
, new Class[]{Foo.class} , handler);
12345
下面 下麪程式示範了使用 Proxy 和 InvocationHandler 來生成動態代理物件:
import java.lang.reflect.*;
interface Person {
void walk();
void sayHello(String name);
}
class MyInvokationHandler implements InvocationHandler {
/*
* 執行動態代理物件的所有方法時,都會被替換成執行如下的invoke方法 其中: proxy:代表動態代理物件 method:代表正在執行的方法
* args:代表呼叫目標方法時傳入的實參。
*/
public Object invoke(Object proxy, Method method, Object[] args) {
System.out.println("----正在執行的方法:" + method);
if (args != null) {
System.out.println("下面 下麪是執行該方法時傳入的實參爲:");
for (Object val : args) {
System.out.println(val);
}
} else {
System.out.println("呼叫該方法沒有實參!");
}
return null;
}
}
public class ProxyTest {
public static void main(String[] args) throws Exception {
// 建立一個InvocationHandler物件
InvocationHandler handler = new MyInvokationHandler();
// 使用指定的InvocationHandler來生成一個動態代理物件
Person p = (Person) Proxy.newProxyInstance(Person.class.getClassLoader(), new Class[] { Person.class },
handler);
// 呼叫動態代理物件的walk()和sayHello()方法
p.walk();
p.sayHello("孫悟空");
}
}
12345678910111213141516171819202122232425262728293031323334353637383940
上面程式首先提供了一個 Person 介面, 該介面中包含了 walk()和 sayHello()兩個抽象方法, 接着定義了一個簡單的 InvocationHandler 實現類, 定義該實現類時需要重寫 invoke()方法—呼叫代理物件的所有方法時都會被替換成呼叫該 invoke()方法。 該 invoke()方法中的三個參數解釋如下。
API:java.lang.reflect.Proxy
API:java.lang.reflect.InvocationHandler
動態代理和 AOP
根據前面介紹的 Proxy 和 InvocationHandler, 實在很難看出這種動態代理的優勢。 下面 下麪介紹一種更實用的動態代理機制 機製。
開發實際應用的軟件系統時, 通常會存在相同程式碼段重複出現的情況, 在這種情況下, 對於許多剛開始從事軟件開發的人而言, 他們的做法是: 選中那些程式碼, 一路「 複製「 、「 貼上」, 立即實現了系統功能, 如果僅僅從軟體功能上來看, 他們確實己經完成了軟件開發。通過這種「 複製」、 「 貼上」 方式開發出來的軟體如圖a所示。
圖 a:多個地方包含相同程式碼的軟體
採用圖 a所示結構實現的軟件系統, 在軟件開發期間可能會覺得無所謂, 但如果有一天需要修改程式的深色程式碼的實現, 則意味着開啓三份原始碼進行修改。 如果有 100 個地方甚至 1000 個地方使用了這段深色程式碼段, 那麼修改、 維護這段程式碼的工作量將變成噩夢。
這種情況下, 大部分稍有經驗的開發者都會將這段深色程式碼段定義成一個方法, 然後讓另外三段程式碼段直接呼叫該方法即可。 在這種方式下, 軟件系統的結構如圖 b 所示。
圖 b:通過方法呼叫實現程式碼複用
對於如圖 b 所示的軟件系統, 如果需要修改深色部分的程式碼, 則只要修改一個地方即可, 而呼叫該方法的程式碼段, 不管有多少個地方呼叫了該方法, 都完全無須任何修改, 只要被呼叫方法被修改了,所有呼叫該方法的地方就會自然改變——通過這種方式, 大大降低了軟體後期維護的複雜度。
但採用這種方式來實現程式碼複用依然產生一個重要問題: 程式碼段 1、 程式碼段 2、 程式碼段 3 和深色程式碼段分離開了, 但程式碼段 1、 程式碼段 2 和程式碼段 3 又和一個特定方法耦合了! 最理想的效果是: 程式碼塊1、 程式碼塊 2 和程式碼塊 3 既可以執行深色程式碼部分, 又無須在程式中以寫死方式直接呼叫深色程式碼的方法, 這時就可以通過動態代理來達到這種效果。
由於 JDK 動態代理只能爲介面建立動態代理, 所以下面 下麪先提供一個 Dog 介面, 該介面程式碼非常簡單, 僅僅在該介面裏定義了兩個方法。
public interface Dog {
// info方法宣告
void info();
// run方法宣告
void run();
}
1234567
上面介面裏只是簡單地定義了兩個方法, 並未提供方法實現。 如果直接使用 Proxy 爲該介面建立動態代理物件, 則動態代理物件的所有方法的執行效果又將完全一樣。 實際情況通常是, 軟件系統會爲該Dog 介面提供一個或多個實現類。 此處先提供一個簡單的實現類: GunDog:
public class GunDog implements Dog {
// 實現info()方法,僅僅列印一個字串
public void info() {
System.out.println("我是一隻獵狗");
}
// 實現run()方法,僅僅列印一個字串
public void run() {
System.out.println("我奔跑迅速");
}
}
1234567891011
Dog 的實現類爲每個方法提供了一個簡單實現。 再看需要實現的功能: 讓程式碼段 1、 程式碼段 2 和程式碼段 3 既可以執行深色程式碼部分, 又無須在程式中以寫死方式直接呼叫深色程式碼的方法。 此處假設 info()、 run()兩個方法代表程式碼段 1、 程式碼段 2, 那麼要求: 程式執行 info()、 nm()方法時能呼叫某個通用方法, 但又不想以寫死方式呼叫該方法。
下面 下麪提供一個 DogUtil類, 該類裡包含兩個通用方法:
public class DogUtil {
// 第一個攔截器方法
public void method1() {
System.out.println("=====模擬第一個通用方法=====");
}
// 第二個攔截器方法
public void method2() {
System.out.println("=====模擬通用方法二=====");
}
}
123456789101112
藉助於 Proxy 和 InvocationHandler 就可以實現 當程式呼叫 info()方法和 run()方法時, 系統可以「自動」 將 methodl()和 method2()兩個通用方法插入 info()和 run()方法中執行。
這個程式的關鍵在於下面 下麪的 MylnvokationHandler 類, 該類是一個 InvocationHandler 實現類, 該實現類的 invoke()方法將會作爲代理物件的方法實現:
import java.lang.reflect.*;
public class MyInvokationHandler implements InvocationHandler {
// 需要被代理的物件
private Object target;
public void setTarget(Object target) {
this.target = target;
}
// 執行動態代理物件的所有方法時,都會被替換成執行如下的invoke方法
public Object invoke(Object proxy, Method method, Object[] args) throws Exception {
DogUtil du = new DogUtil();
// 執行DogUtil物件中的method1。
du.method1();
// 以target作爲主調來執行method方法
Object result = method.invoke(target, args);
// 執行DogUtil物件中的method2。
du.method2();
return result;
}
}
12345678910111213141516171819202122
上面程式實現 invoke()方法時包含了一行關鍵程式碼, 這行程式碼通過反射以 target作爲主調來執行 method 方法, 這就是回調了 target 物件的原有方法。 在粗體字程式碼之前呼叫 DogUtil物件的 methodl()方法, 在粗體字程式碼之後呼叫 DogUtil 物件的 method2()方法。
下面 下麪再爲程式提供一個 MyProxyFactory 類, 該物件專爲指定的 target 生成動態代理範例:
import java.lang.reflect.*;
public class MyProxyFactory {
// 爲指定target生成動態代理物件
public static Object getProxy(Object target) throws Exception {
// 建立一個MyInvokationHandler物件
MyInvokationHandler handler = new MyInvokationHandler();
// 爲MyInvokationHandler設定target物件
handler.setTarget(target);
// 建立、並返回一個動態代理
return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), handler);
}
}
1234567891011121314
上面的動態代理工廠類提供了一個 getProxy()方法, 該方法爲 target 物件生成一個動態代理物件,這個動態代理物件與 target 實現了相同的介面, 所以具有相同的 public 方法—從這個意義上來看, 動態代理物件可以當成 target 物件使用。 當程式呼叫動態代理物件的指定方法時,實際上將變爲執行MylnvokationH andler 物件的 invoke()方法。
例如, 呼叫動態代理物件的 info()方法, 程式將幵始執行invoke()方法, 其執行步驟如下:
通過上面的執行過程可以發現: 當使用動態代理物件來代替 target 物件時, 代理物件的方法就實現了前面的要求—程式執行 info()、 nm()方法時既能「 插入」 methodl()、 method2()通用方法,但 GunDog 的方法中又沒有以寫死方式呼叫 methodl()和 method2()方法。
以一個主程式來測試這種動態代理的效果。
public class Test {
public static void main(String[] args) throws Exception {
// 建立一個原始的GunDog物件,作爲target
Dog target = new GunDog();
// 以指定的target來建立動態代理
Dog dog = (Dog) MyProxyFactory.getProxy(target);
dog.info();
dog.run();
}
}
12345678910
上面程式中的 dog 物件實際上是動態代理物件, 只是該動態代理物件也實現了 Dog 介面, 所以也可以當成 Dog 物件使用。 程式執行 dog 的 info()和 run()方法時, 實際上會先執行 DogUtil 的 method1()方法, 再執行 target 物件的 info()和 run()方法, 最後執行 DogUtil 的 method2()方法。
執行上面程式, 會看到如圖 c 所示的執行結果:
圖 c:執行結果
通過圖 c 所示的執行結果來看, 可以發現採用動態代理可以非常靈活地實現解耦。 通常而言,使用 Proxy 生成一個動態代理時, 往往並不會憑空產生一個動態代理, 這樣沒有太大的實際意義。 通常都是爲指定的目標物件生成動態代理。
這種動態代理在 AOP ( Aspect Orient Programming, 面向切面程式設計) 中被稱爲 AOP 代理, AOP 代理可代替目標物件, AOP 代理包含了目標物件的全部方法。 但 AOP 代理中的方法與目標物件的方法存在差異: AOP 代理裡的方法可以在執行目標方法之前、 之後插入一些通用處理。
AOP 代理包含的方法與目標物件包含的方法示意圖如圖 d所示。
圖d AOP 代理的方法與目標物件的方法示意圖
CGLIB動態代理:
JDK中提供的生成動態代理類的機制 機製有個鮮明的特點是:
某個類必須有實現的介面,而生成的代理類也只能代理某個類介面定義的方法。
比如:如果上面例子的GunDog實現了繼承自Dog介面的方法外,另外實現了方法eat(),則在產生的動態代理類中不會有這個方法了!更極端的情況是:如果某個類沒有實現介面,那麼這個類就不能用JDK產生動態代理了!
這時候就應該引入CGLIB動態代理了,「CGLIB(Code Generation Library),是一個強大的,高效能,高品質的Code生成類庫,它可以在執行期擴充套件Java類與實現Java介面。」
CGLIB建立某個類A的動態代理類的模式是:
1.查詢A上的所有非final的public型別的方法定義;
2.將這些方法的定義轉換成位元組碼;
3.將組成的位元組碼轉換成相應的代理的class物件;
4.實現 MethodInterceptor介面,用來處理對代理類上所有方法的請求(這個介面和JDK動態代理InvocationHandler的功能和角色是一樣的)