# 二十、反射(完結)
二十、反射
20.1 類的載入
20.1.1 類的載入概述
程式執行後,某個類在第一次使用時,會將該類的 class
檔案讀取到記憶體,並將此類的所有資訊儲存到一個Class
物件中
20.1.2 類載入的時機
- 建立類的例項
- 訪問類的成員
- 使用反射方式來強制建立某個類或介面對應的
java.lang.Class
物件。 - 初始化某個類的子類,父類會先載入。
- 直接使用
java.exe
命令來執行某個主類。
總結:用到就載入,不用就不載入
20.1.3 類載入的過程介紹
當一個**Java**
** 檔案要被載入到記憶體中使用執行的過程**
- 需要把當前的
Java
檔案通過Javac
編譯成位元組碼檔案(.class
- 位元組碼檔案需要進行 載入 , 連線 , 初始化 三個動作
- 位元組碼資料載入到
jvm
方法區記憶體中, 在堆記憶體中建立此類的物件 ,Java
程式進行使用
注意 : 把一個位元組碼檔案載入到記憶體過程 , 就需要用到類載入進行完成
20.2 類的載入過程各階段的任務
當
Java
程式中需要使用到某個類時,虛擬機器會保證這個類已經被載入、連線和初始化。而連線又包含驗證、準備和解析這三個子過程,這個過程必須嚴格按照順序執行
20.2.3 載入
- 通過類的全類名(包名和類名) , 查詢此類的位元組碼檔案,把類的
.class
檔案中的二進位制資料流讀入到記憶體中,並存放在執行時資料區的方法區內,然後利用位元組碼檔案建立一個Class
- 簡單來說 : 就是把硬碟中的位元組碼檔案 , 載入到
jvm
中的方法區以位元組碼物件的形式存在 , 並在堆記憶體中建立此類物件
20.2.2 連線
- 驗證 : 確保被載入類的正確性。
class
檔案的位元組流中包含的資訊符合當前虛擬機器要求,不會危害虛擬機器自身的安全。 - 準備 : 為類的靜態變數分配記憶體,並將其設定為預設值。此階段僅僅只為靜態類變數(即
static
修飾的欄位變數)分配記憶體,並且設定該變數的初始值。(比如static int num = 5
,這裡只將num
初始化為0,5的值將會在初始化時賦值)。對於final static
- 解析 : 把類中的符號引用轉換為直接引用。符號引用就是一組符號來描述目標,而直接引用就是直接指向目標在記憶體的位置,即地址,如果引用的這個類沒載入進記憶體就會先載入,載入了就直接替換。
- 簡單理解就是如果當前類中用到了其他類, 就他符號引用替換成其他類
20.2.3 初始化
- 類載入最後階段,為靜態變數賦值,靜態程式碼塊的程式碼也將被初始化
- 若該類具有父類,則先對父類進行初始化
- 底層的初始化方法加了鎖,做了執行緒同步校驗:如果多個執行緒同時對一個類繼續初始化,一次只有一個執行緒會執行,其他執行緒會阻塞等待。
20.3 類載入器
20.3.1 Java虛擬機器自帶的類載入器 (瞭解)
類載入器:是負責將磁碟上的某個class檔案讀取到記憶體並生成Class的物件。
Java
中有三種類載入器,它們分別用於載入不同種類的 class
:
- 啟動類載入器(
Bootstrap ClassLoader
):用於載入系統類庫<JAVA_HOME>\bin目錄下的class
,例如:rt.jar
- 擴充套件類載入器(
Extension ClassLoader
):用於載入擴充套件類庫<JAVA_HOME>\lib\ext
目錄下的class
- 應用程式類載入器(
Application ClassLoader
):用於載入我們自定義類的載入器。
public class Test{
public static void main(String[] args){
//通過Class物件,獲取類載入器的方法 --> public ClassLoader getClassLoader() : 返回該類的類載入器
//如果該類由啟動類載入器載入(例如核心類庫中的類String 等),則將返回 null。
System.out.println(Test.class.getClassLoader());//sun.misc.Launcher$AppClassLoader
System.out.println(String.class.getClassLoader());//null
}
}
總結:
- 在程式開發中,類的載入幾乎是由上述3種類載入器相互配合執行的,同時我們還可以自定義類載入器
- 需要注意的是,
java
虛擬機器對class
檔案採用的是按需載入的方式,也就是說當需要使用該類時才會將它的class
檔案載入到記憶體中生成class
物件 - 而且載入某個類的
class
檔案時,Java
虛擬機器採用的是雙親委派模式,即把載入類的請求交由父載入器處理,它是一種任務委派模式
20.3.2 類載入器--雙親委派機制介紹
上圖展示了"類載入器"的層次關係,這種關係稱為類載入器的 "雙親委派模型":
- "雙親委派模型"中,除了頂層的啟動類載入器外,其餘的類載入器都應當有自己的"父級類載入器"。
- 這種關係不是通過"繼承"實現的,通常是通過"組合"實現的。通過"組合"來表示父級類載入器。
- "雙親委派模型"的工作過程:
- 某個"類載入器"收到類載入的請求,它首先不會嘗試自己去載入這個類,而是把請求交給父級類載入器。
- 因此,所有的類載入的請求最終都會傳送到頂層的"啟動類載入器"中。
- 如果"父級類載入器"無法載入這個類,然後子級類載入器再去載入。
public class ClassLoaderDemo4 {
/*
當前是根據自己定義的類進行獲取的類載入器物件
getParent方法獲取父類載入器
第一條輸出語句列印的是 : 系統類載入器
第二條輸出語句列印的是 : 擴充套件類載入器
第三條輸出語句列印的是 : null是根類載入器
*/
public static void main(String[] args) {
ClassLoader classLoader = ClassLoaderDemo4.class.getClassLoader();
System.out.println(classLoader);// sun.misc.Launcher$AppClassLoader@b4aac2
System.out.println(classLoader.getParent());// sun.misc.Launcher$ExtClassLoader@16d3586
System.out.println(classLoader.getParent().getParent());// null
}
20.3.3 雙親委派模型的優點
- 避免類的重複載入,
- 當父類載入器已經載入了該類時,就沒有必要子
ClassLoader
再載入一次 , 位元組碼檔案只加載一次
- 當父類載入器已經載入了該類時,就沒有必要子
- 考慮到安全因素,
java
核心api
中定義型別不會被隨意替換,假設通過網路傳遞一個名為java.lang.Object
的類,通過雙親委託模式傳遞到啟動類載入器,而啟動類載入器在核心Java API
發現這個名字的類,發現該類已被載入,並不會重新載入網路傳遞過來的java.lang.Object
,而直接返回已載入過的Object.class
,這樣便可以防止核心**API**
庫被隨意篡改!!!
package java.lang;
/**
* @author: Carl Zhang
* @create: 2022-01-07 13:59
* 舉例 : 如果我們自己定義一個包名字叫做java.lang , 在當前這個包下建立一個類叫做MyObject類
* 因為java.lang包屬於核心包,只能由根類載入器進行載入,而根據類載入的雙親委派機制,根類載入不到這個MyObject類的(自定義的)**
* 所以只能由AppClassLoader進行載入,而這又是不允許的,因為java.lang下的類需要使用根載入器進行載入**
* 所以會報出"Prohilbited package name:java.lang"(禁止的包名)異常"
*/
public class MyObject {
public static void main(String[] args) {
System.out.println(MyObject.class.getClassLoader()); //java.lang.SecurityException: Prohibited package name: java.lang
}
}
20.4 反射的介紹
20.4.1 反射的引入
- 需求:如何根據配置檔案
re.properties
裡的不同的資訊,建立指定的物件,呼叫各自的方法 - 以前的方式:讀取配置檔案裡的資訊,通過
switch
格式判斷class
鍵對應的值,建立不同的物件,呼叫方法 - 問題:程式碼冗餘,不方便
- 引入反射:實現動態建立物件,動態呼叫方法,這樣的需求在學習框架時特別多,即通過外部檔案配置,在不修改原始碼情況下,來控制程式,也符合設計模式的
ocp
原則(開閉原則:不修改原始碼,擴容功能)
20.4.2 反射的概念
- 反射是一種機制,利用該機制可以在程式執行過程中,對於任意一個類,都能夠知道這個類的所有屬性和方法;對於任意一個物件,都能夠呼叫它的任意方法和屬性
- 利用反射可以無視修飾符獲取類裡面所有的屬性和方法。先獲取配置檔案中的資訊,動態獲取資訊並建立物件和呼叫方法
20.4.3 反射的使用前提和場景
- 使用反射操作類成員的前提:
- 要獲得該類位元組碼檔案物件,就是
Class
物件
- 要獲得該類位元組碼檔案物件,就是
- 反射在實際開發中的應用:
- 開發
IDE
(整合開發環境),比如IDEA
,Eclipse
- 各種框架的設計和學習 比如
Spring
,Hibernate
,Struct
,Mybaits
....
- 開發
20.5 Class物件的獲取方式
20.5.1 三種獲取方法
-
方式1:
類名.class
獲得 -
方式2:
物件名.getClass()
方法獲得,該方法是繼承Object
類的 -
方式3:
Class
類的靜態方法獲得:static Class forName("類全名")
- 每一個類的
Class
物件都只有一個。
- 每一個類的
-
示例程式碼
package com.itheima._03反射;
public class Student{
}
public class ReflectDemo01 {
public static void main(String[] args) throws ClassNotFoundException {
// 獲得Student類對應的Class物件
Class c1 = Student.class;
// 建立學生物件
Student stu = new Student();
// 通過getClass方法
Class c2 = stu.getClass();
System.out.println(c1 == c2);
// 通過Class類的靜態方法獲得: static Class forName("類全名")
Class c3 = Class.forName("com.itheima._03反射.Student");
System.out.println(c1 == c3); //true
System.out.println(c2 == c3); //true
}
}
20.5.2 Class類常用方法
String getSimpleName();
獲得類名字串:類名String getName();
獲得類全名:包名+類名T newInstance() ;
建立Class
物件關聯類的物件
示例程式碼
public class ReflectDemo02 {
public static void main(String[] args) throws Exception {
// 獲得Class物件
Class c = Student.class;
// 獲得類名字串:類名
System.out.println(c.getSimpleName());
// 獲得類全名:包名+類名
System.out.println(c.getName());
// 建立物件
//此方式相當於通過類的空參構造建立物件,如果目標類沒有空參構造,會報錯 InstantiationException
Student stu = (Student) c.newInstance();
System.out.println(stu);
}
}
20.6 反射之操作構造方法
20.6.1 Constructor類概述
- 反射之操作構造方法的目的
- 獲得
Constructor
物件來建立類的物件。
- 獲得
Constructor
類概述- 類中的每一個構造方法都是一個
Constructor
類的物件
- 類中的每一個構造方法都是一個
20.6.2 Class類中與Constructor相關的方法
Constructor getConstructor(Class... parameterTypes)
:返回單個公共構造方法物件Constructor getDeclaredConstructor(Class... parameterTypes)
:返回單個構造方法物件,不受修飾符影響Constructor[] getConstructors()
:返回所有公共構造方法物件的陣列,只能獲得public
的Constructor[] getDeclaredConstructors()
:返回所有構造方法物件的陣列,不受修飾符影響
20.6.3 Constructor物件常用方法
T newInstance(Object... initargs)
—— 根據指定的引數建立物件void setAccessible(true)
:設定"暴力反射"——是否取消許可權檢查,true
取消許可權檢查,false
表示不取消
示例程式碼
public class Student{
private String name;
private String sex;
private int age;
//公有構造方法
public Student(String name,String sex,int age){
this.name = name;
this.sex = sex;
this.age = age;
}
//私有構造方法
private Student(String name,String sex){
this.name = name;
this.sex = sex;
}
}
public class ReflectDemo03 {
/*
Constructor[] getConstructors()
獲得類中的所有構造方法物件,只能獲得public的
Constructor[] getDeclaredConstructors()
獲得類中的所有構造方法物件,包括private修飾的
*/
@Test
public void test03() throws Exception {
// 獲得Class物件
Class c = Student.class;
// 獲得類中的所有構造方法物件,只能獲得public的
// Constructor[] cons = c.getConstructors();
// 獲取類中所有的構造方法,包括public、protected、(預設)、private的
Constructor[] cons = c.getDeclaredConstructors();
for (Constructor con:cons) {
System.out.println(con);
}
}
/*
Constructor getDeclaredConstructor(Class... parameterTypes)
根據引數型別獲得對應的Constructor物件
*/
@Test
public void test02() throws Exception {
// 獲得Class物件
Class c = Student.class;
// 獲得兩個引數構造方法物件
Constructor con = c.getDeclaredConstructor(String.class,String.class);
// 取消許可權檢查(暴力反射)
con.setAccessible(true);
// 根據構造方法建立物件
Object obj = con.newInstance("rose","女");
System.out.println(obj);
}
/*
Constructor getConstructor(Class... parameterTypes)
根據引數型別獲得對應的Constructor物件
*/
@Test
public void test01() throws Exception {
// 獲得Class物件
Class c = Student.class;
// 獲得無引數構造方法物件
Constructor con = c.getConstructor();
// 根據構造方法建立物件
Object obj = con.newInstance();
System.out.println(obj);
// 獲得有引數的構造方法物件
Constructor con2 = c.getConstructor(String.class, String.class,int.class);
// 建立物件
Object obj2 = con2.newInstance("jack", "男",18);
System.out.println(obj2);
}
}
20.7 反射之操作成員方法
20.7.1 Method類概述
- 反射之操作成員方法的目的
- 操作
Method
物件來呼叫成員方法
- 操作
Method
類概述- 每一個成員方法都是一個
Method
類的物件。
- 每一個成員方法都是一個
20.7.2 Class類中與Method相關的方法
Method getMethod(String name,Class...args);
返回單個公共的成員方法物件Method getDeclaredMethod(String name,Class...args);
返回單個的成員方法物件,不受訪問修飾符限制Method[] getMethods();
返回所有公共的成員方法物件,包括繼承的Method[] getDeclaredMethods();
返回所有的成員方法物件,不包括繼承的
20.7.3 Method物件常用方法
Object invoke(Object obj, Object... args)
- 呼叫指定物件
obj
的該方法 args:
呼叫方法時傳遞的引數- 沒有返回值,則返回
null
- 呼叫指定物件
void setAccessible(true)
設定"暴力訪問"——是否取消許可權檢查,true
取消許可權檢查,false
表示不取消
示例程式碼
public class Student{
private void eat(String str){
System.out.println("我吃:" + str);
}
private void sleep(){
System.out.println("我睡覺...");
}
public void study(int a){
System.out.println("我學習Java,引數a = " + a);
}
}
public class ReflectDemo04 {
// 反射操作靜態方法
@Test
public void test04() throws Exception {
// 獲得Class物件
Class c = Student.class;
// 根據方法名獲得對應的公有成員方法物件
Method method = c.getDeclaredMethod("eat",String.class);
// 通過method執行對應的方法
method.invoke(null,"蛋炒飯");
}
/*
* Method[] getMethods();
* 獲得類中的所有成員方法物件,返回陣列,只能獲得public修飾的且包含父類的
* Method[] getDeclaredMethods();
* 獲得類中的所有成員方法物件,返回陣列,只獲得本類的,包含private修飾的
*/
@Test
public void test03() throws Exception {
// 獲得Class物件
Class c = Student.class;
// 獲得類中的所有成員方法物件,返回陣列,只能獲得public修飾的且包含父類的
// Method[] methods = c.getMethods();
// 獲得類中的所有成員方法物件,返回陣列,只獲得本類的,包含private修飾的
Method[] methods = c.getDeclaredMethods();
for (Method m: methods) {
System.out.println(m);
}
}
/*
Method getDeclaredMethod(String name,Class...args);
* 根據方法名和引數型別獲得對應的構造方法物件,
*/
@Test
public void test02() throws Exception {
// 獲得Class物件
Class c = Student.class;
// 根據Class物件建立學生物件
Student stu = (Student) c.newInstance();
// 獲得sleep方法對應的Method物件
Method m = c.getDeclaredMethod("sleep");
// 暴力反射
m.setAccessible(true);
// 通過m物件執行stuy方法
m.invoke(stu);
}
/*
Method getMethod(String name,Class...args);
* 根據方法名和引數型別獲得對應的構造方法物件,
*/
@Test
public void test01() throws Exception {
// 獲得Class物件
Class c = Student.class;
// 根據Class物件建立學生物件
Student stu = (Student) c.newInstance();
// 獲得study方法對應的Method物件
Method m = c.getMethod("study");
// 通過m物件執行stuy方法
m.invoke(stu);
/// 獲得study方法對應的Method物件
Method m2 = c.getMethod("study", int.class);
// 通過m2物件執行stuy方法
m2.invoke(stu,8);
}
}
20.8 反射之操作成員變數
20.8.1 Field類概述
- 反射之操作成員變數的目的
- 通過
Field
物件給對應的成員變數賦值和取值
- 通過
Field
類概述- 每一個成員變數都是一個
Field
類的物件。
- 每一個成員變數都是一個
20.8.2 Class類中與Field相關的方法
Field getField(String name);
返回單個公共的成員變數物件Field getDeclaredField(String name);
返回單個成員變數,不受修飾符限制Field[] getFields();
返回所有的公共的成員變數物件Field[] getDeclaredFields();
返回所有的成員變數物件
20.8.3 Field物件常用方法
void set(Object obj, Object value)
:給物件obj
的屬性設定值Object get(Object obj)
:獲取物件obj
對應的屬性值void setAccessible(true);
暴力反射,設定為可以直接訪問私有型別的屬性。Class getType();
獲取屬性的型別,返回Class
物件。
示例程式碼
public class Student{
public String name;
private String gender;
public String toString(){
return "Student [name = " + name + " , gender = " + gender + "]";
}
}
public class ReflectDemo05 {
/*
Field[] getFields();
* 獲得所有的成員變數對應的Field物件,只能獲得public的
Field[] getDeclaredFields();
* 獲得所有的成員變數對應的Field物件,包含private的
*/
@Test
public void test02() throws Exception {
// 獲得Class物件
Class c = Student.class;
// 獲得所有的成員變數對應的Field物件
// Field[] fields = c.getFields();
// 獲得所有的成員變數對應的Field物件,包括private
Field[] fields = c.getDeclaredFields();
for (Field f: fields) {
System.out.println(f);
}
}
/*
Field getField(String name);
根據成員變數名獲得對應Field物件,只能獲得public修飾
Field getDeclaredField(String name);
* 根據成員變數名獲得對應Field物件,包含private修飾的
*/
@Test
public void test01() throws Exception {
// 獲得Class物件
Class c = Student.class;
// 建立物件
Object obj = c.newInstance();
// 獲得成員變數name對應的Field物件
Field f = c.getField("name");
// 給成員變數name賦值
// 給指定物件obj的name屬性賦值為jack
f.set(obj,"jack");
// 獲得指定物件obj成員變數name的值
System.out.println(f.get(obj)); // jack
// 獲得成員變數的名字
System.out.println(f.getName()); // name
// 給成員變數gender賦值
// 獲得成員變數gender對應的Field物件
Field f1 = c.getDeclaredField("gender");
// 暴力反射
f1.setAccessible(true);
// 給指定物件obj的gender屬性賦值為男
f1.set(obj,"男");
System.out.println(obj);
}
}
20.9 使用反射解析註解
注:註解的基本介紹見 11.6 註解介紹
20.9.1 AnnotatedElement介面介紹
AnnotatedElement
: 是一個介面,定義瞭解析註解的方法
1. boolean isAnnotationPresent(Class<Annotation> annotationClass)
引數 : 註解的位元組碼物件
作用 : 判斷當前物件是否使用了指定的註解,如果使用了則返回true,否則false
2. T getAnnotation(Class<T> annotationClass)
引數 : 註解的位元組碼物件
返回值 : 根據註解型別獲得對應註解物件 , 有了註解物件就可以呼叫屬性(抽象方法),獲取屬性值
20.9.2 註解的解析原理
前提:Class,Constructor,Method,Field都實現了AnnotatedElement 介面。
解析註解的原理:獲取註解作用位置的物件,來呼叫方法解析註解
- 解析類上的註解:藉助位元組碼物件(Class物件)
- 解析構造方法上的註解 :藉助構造器物件(Constructor物件)
- 解析方法上的註解 :藉助方法物件(Method物件)
- 解析欄位上的註解 :藉助欄位物件(Field物件)
相關方法:
isAnnotationPresent()
:判斷是否存在註解getAnnotation()
:如果存在獲取註解物件
20.9.3 註解的解析案例
需求如下:
1. 定義註解 `Book,要求如下:
- 包含屬性:String value() 書名
- 包含屬性:double price() 價格,預設值為 100
- 包含屬性:String[] authors() 多位作者
- 限制註解使用的位置 :類和成員方法上 【Target】
- 指定註解的有效範圍 :RUNTIME 【Retention】
2. 定義BookStore類,在類和成員方法上使用Book註解
3. 定義TestAnnotation測試類獲取Book註解上的資料
給成員注入四大名著資訊 :
西遊記 --- 施耐庵
水滸傳 --- 吳承恩
三國演義 --- 羅貫中
紅樓夢 --- 曹雪芹 , 高鶚
Book註解 :
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
//定義一個書的註解,包含屬性書名,價格(預設100),作者。作者要求可以有多名作者
@Target({ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Book {
String value();
double price() default 100;
String[] author();
}
BookStore類 :
@Book(value = "水滸傳", author = "施耐庵")
public class BookStore {
@Book(value = "三國演義", author = {"羅貫中"})
public void buy() {
}
}
**TestAnnotation**
** 類:**
import java.lang.reflect.Method;
import java.util.Arrays;
/**
思路:
1. 型別上的註解,使用Class物件解析
2. 方法上的註解,使用Method物件解析
*/
public class TestAnnotation {
public static void main(String[] args) throws NoSuchMethodException {
//解析型別上的註解
Class<BookStore> cls = BookStore.class;
//判斷是否有Book註解,如果有進行解析
if (cls.isAnnotationPresent(Book.class)) {
//有
Book book = cls.getAnnotation(Book.class);
String name = book.value();
double price = book.price();
String[] author = book.author();
System.out.println(name);
System.out.println(price);
System.out.println(Arrays.toString(author));
}
//解析方法上面的註解
Method buyMethod = cls.getMethod("buy");
if (buyMethod.isAnnotationPresent(Book.class)) {
Book book = buyMethod.getAnnotation(Book.class);
String name = book.value();
double price = book.price();
String[] author = book.author();
System.out.println(name);
System.out.println(price);
System.out.println(Arrays.toString(author));
}
}
}
20.10 設計模式 - 代理模式
20.10.1 代理模式介紹
為什麼要有 “代理” ?
- 生活中就有很多例子,比如委託業務,黃牛(票販子)等等
- 代理就是被代理者沒有能力或者不願意去完成某件事情,需要找個人代替自己去完成這件事,這才是“代理”存在的原因。
- 例如要租房子,房產中介可以在我們住房前代理我們找房子。中介就是代理,而自己就是被代理了。
在程式碼設計中,代理模式作用主要就是讓 "被代理物件" 的某個方法執行之前或者執行之後加入其他增強邏輯。
前增強 : 例如獲取當前時間
被代理物件呼叫方法
後增強 : 例如獲取當前時間
計算方法執行的時間
20.10.2 代理的前提條件
- 抽象角色 :宣告功能 ,相當於父介面
- 代理角色 :實現抽象功能 , 完成代理邏輯,相當於介面的實現類
- 被代理角色 :實現抽象功能,相當於介面的實現類
意味著被代理角色和代理角色有著共同的父型別(既抽象角色) , 例如我要租房子, 我只能找房產中介, 不能找票販子
20.10.3 代理模式的兩種實現方式
- 靜態代理 (瞭解 , 用於對動態代理做鋪墊)
- 動態代理 (為後面學習的框架做鋪墊)
20.11 靜態代理模式
20.11.1 靜態代理模式的介紹
- 靜態代理是由程式設計師建立 或 工具生成代理類的原始碼,再編譯代理類。在程式執行前就已經存在代理類的位元組碼檔案,代理類和被代理類的關係在執行前就確定了。
- 簡單理解 : 在程式執行之前 , 代理類就存在了,這就是靜態代理 ; 動態代理是程式執行時動態生成代理類
20.11.2 靜態代理實現的步驟
- 存在一個抽象角色
- 定義被代理角色
- 定義代理,增強被代理角色的功能
20.11.3 靜態代理案例
案例:以現實中經紀人代理明星
已知存在介面:
// 1.抽象角色
interface Star {
// 真人秀方法
double liveShow(double money);
void sleep();
}
定義被代理類:
- 定義王寶強類,實現Star方法
// 定義被代理角色(寶強)
class BaoQiang implements Star {
@Override
public double liveShow(double money) {
System.out.println("參加了真人秀節目 , 賺了" + money + "元");
return money;
}
@Override
public void sleep() {
System.out.println("寶強累了 , 睡覺了...");
}
}
定義代理類:
- 定義宋喆經紀人類
// 定義代理角色(宋喆),增強被代理角色的功能
class SongZhe implements Star {
BaoQiang baoQiang = new BaoQiang();
@Override
public double liveShow(double money) {// 1000
// 前增強
System.out.println("宋喆幫寶強接了一個真人秀的活動,獲取佣金" + money * 0.8 + "元");
double v = baoQiang.liveShow(money * 0.2);
// 後增強
System.out.println("宋喆幫寶強把賺的錢存起來...");
return v;
}
@Override
public void sleep() {
System.out.println("宋喆幫寶強找了一家五星級大酒店...");
baoQiang.sleep();
System.out.println("宋喆幫寶強退房..");
}
}
定義測試類進行測試
public class StaticAgentDemo {
public static void main(String[] args) {
// 被代理角色
BaoQiang baoQiang = new BaoQiang();
double v = baoQiang.liveShow(1000);
System.out.println(v);
baoQiang.sleep();
System.out.println("===========================");
SongZhe songZhe = new SongZhe();
double v1 = songZhe.liveShow(1000);
System.out.println(v1);
songZhe.sleep();
}
}
關係圖 :宋喆和寶強都有共同的父型別。他們的業務方法都是一樣。
20.11.4 靜態代理和裝飾模式的區別
相同:
- 都要實現與目標類相同的業務介面
- 在倆個類中都要宣告目標物件
不同:
- 目標不同 :
- 裝飾者模式考慮的是物件某個功能的擴充套件,是在原有功能基礎上增加
- 靜態代理模式考慮的是物件某個功能的呼叫,對這個功能的流程把控和輔助
- 物件獲取方式不同
- 裝飾者模式是通過構造方法的傳參來獲取被裝飾的物件
- 靜態代理模式是內部直接建立被裝飾的物件
注意:設計模式本身是為了提升程式碼的可擴充套件性,靈活應用即可,不必生搬硬套,非要分出個所以然來,裝飾器模式和代理模式的區別也是如此
20.12 動態代理模式
20.12.1 動態代理模式介紹
- 在實際開發過程中往往我們自己不會去建立代理類而是通過
JDK
提供的Proxy
類在程式執行時,運用反射機制動態建立而成這就是我們所謂的動態代理。 - 與靜態代理之間的區別,在於不用自己寫代理類
- 雖然我們不需要自己定義代理類建立代理物件,但是我們要定義對被代理物件直接訪問方法的攔截,原因就是對攔截的方法做增強。
- 動態代理技術在框架中使用居多,例如:很快要學到的資料庫框架
MyBatis
框架等後期學的一些主流框架技術(Spring
,SpringMVC
)中都使用了動態代理技術。
20.12.2 動態代理相關API
- Proxy類
java.lang.reflect.Proxy
類提供了用於建立動態代理類和物件的靜態方法- 它還是由這些方法建立的所有動態代理類的超類(代理類的父類是
Proxy
)。
//獲取代理物件的方法
public static Object newProxyInstance (ClassLoader loader, Class<?>[] interfaces, InvocationHandler h )
/**解析:
- 返回值:該方法返回就是動態生成的代理物件
- 引數列表說明:
1. ClassLoader loader - 定義代理類的類載入器 = 被代理物件.getClass().getClassLoader();
2. Class<?>[] interfaces - 代理類要實現的介面列表,要求與被代理類的介面一樣。 = 被代理物件.getClass().getInterfaces();
3. InvocationHandler h - 呼叫處理器:就是具體實現代理邏輯的介面
*/
**InvocationHandler**
** 介面**java.lang.reflect.InvocationHandler
是代理物件實際處理代理邏輯的介面,具體代理實現邏輯在其invoke
方法中。- 所有代理物件呼叫的方法,執行是都會經過
**invoke**
。因此如果要對某個方法進行代理增強,就可以在這個invoke
方法中進行定義。
interface InvocationHandler{
public Object invoke(Object proxy, Method method, Object[] args); //代理邏輯
1. 返回值:方法被代理後執行的結果。
2. 引數列表:
1. proxy - 就是代理物件
2. method -
3. args - 代理物件呼叫方法傳入引數值的物件陣列.
}
20.12.3 動態代理案例
需求:將經紀人代理明星的案例使用動態代理實現
分析:
- 把父介面定義
- 定義被代理類:寶強
- 動態生成代理類物件
- 建立執行代理邏輯的呼叫處理器
- 通過代理物件執行代理方法
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
/**
* @author Carl Zhang
* @description 動態代理模式對BaoQiang的show方法進行處理
* @date 2022/1/8 14:37
*/
public class DynamicProxy {
public static void main(String[] args) throws ClassNotFoundException {
//1.獲取被代理的物件
BaoQiang baoQiang = new BaoQiang();
Class<?> aClass = aClass = baoQiang.getClass(); //被代理類的位元組碼物件
ClassLoader classLoader = aClass.getClassLoader(); //被代理類的類載入器
Class<?>[] interfaces = aClass.getInterfaces(); //被代理類實現的所以的介面的陣列
//獲取自定義的呼叫處理器物件 -- 即真正執行代理邏輯的物件
MyInvocationHandler myInvocationHandler = new MyInvocationHandler(baoQiang);
//2. 獲取被代理類的代理物件
Star songZhe = (Star) Proxy.newProxyInstance(classLoader,
interfaces, myInvocationHandler);
//3. 通過代理物件執行要代理的方法 -- 此處會執行呼叫處理器的invoke方法
//double v = songZhe.liveShow(1000); -- 如果方法執行完返回null,會報空指標異常
songZhe.liveShow(1000);
songZhe.sleep();
}
}
/**
* 建立代理物件的呼叫處理器 -- 用來執行代理的邏輯
*/
@SuppressWarnings("ALL")
class MyInvocationHandler implements InvocationHandler {
BaoQiang baoQiang; //被代理的物件
public MyInvocationHandler(BaoQiang baoQiang) {
this.baoQiang = baoQiang;
}
/**
* 代理行為 - 代理物件的所有方法都會執行此處
* @param proxy 代理物件,即songZhe
* @param method 被代理的方法 , 即BaoQiang的show方法的Method物件
* @param args 被代理方法的引數
* @return 代理方法執行後的結果
* @throws Throwable 異常
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object invoke = null; //用來儲存被代理的方法執行的結果
//如果是show方法就執行代理邏輯
if (method.getName().equals("liveShow")) {
double money = (double) args[0];
//前增強
System.out.println("宋喆幫寶強接真人秀,賺了" + money * 0.2);
//檢視proxy的執行時型別 -- com.heima.agent.$Proxy0 ,匿名內部類 即動態建立的代理類
//System.out.println(proxy.getClass().getName());
//被代理物件執行被代理方法 -- 即baoqiang執行liveShow方法
//Object money = method.invoke(proxy, args[0]);
invoke = method.invoke(baoQiang, money * 0.8);
//後增強
System.out.println("宋哲幫寶強存錢");
return invoke;
}
invoke = method.invoke(baoQiang, args);
return invoke;
}
}
//1.抽象角色
interface Star {
double liveShow(double money);
void sleep();
}
//2.被代理角色
class BaoQiang implements Star {
@Override
public double liveShow(double money) {
System.out.println("寶強參加了一個真人秀節目,賺了" + money + "元");
return money;
}
@Override
public void sleep() {
System.out.println("寶強累了,睡覺了!!");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
20.12.4 動態代理呼叫流程
20.12.5 動態代理的缺點
只能針對介面的實現類做代理物件,普通類是不能做代理物件的。後面框架學習的時候會接觸到 CGLib
(Code Genneration Library
)可以實現對類的代理