單元測驗、反射
一、單元測驗
1.1 單元測驗快速入門
所謂單元測驗,就是針對最小的功能單元,撰寫測驗代碼對其進行正確性測驗,
我們想想,咱們之前是怎么進行測驗的呢?
比如說我們寫了一個學生管理系統,有添加學生、修改學生、洗掉學生、查詢學生等這些功能,要對這些功能這幾個功能進行測驗,我們是在main方法中撰寫代碼來測驗的,
但是在main方法中寫測驗代碼有如下的幾個問題,如下圖所示:

為了測驗更加方便,有一些第三方的公司或者組織提供了很好用的測驗框架,給開發者使用,這里給同學們介紹一種Junit測驗框架,
Junit是第三方公司開源出來的,用于對代碼進行單元測驗的工具(IDEA已經集成了junit框架),相比于在main方法中測驗有如下幾個優點,

我們知道單元測驗是什么之后,接下來帶領同學們使用一下,由于Junit是第三方提供的,所以我們需要把jar包匯入到我們的專案中,才能使用,具體步驟如下圖所示:

接下來,我們就按照上面的步驟,來使用一下.
先準備一個類,假設寫了一個StringUtil工具類,代碼如下
public class StringUtil{ public static void printNumber(String name){ System.out.println("名字長度:"+name.length()); } }
接下來,寫一個測驗類,測驗StringUtil工具類中的方法能否正常使用,
public class StringUtilTest{ @Test public void testPrintNumber(){ StringUtil.printNumber("admin"); StringUtil.printNumber(null); } }
寫完代碼之后,我們會發現測驗方法左邊,會有一個綠色的三角形按鈕,點擊這個按鈕,就可以運行測驗方法,

1.2 單元測驗斷言
接下來,我們學習一個單元測驗的斷言機制,所謂斷言:意思是程式員可以預測程式的運行結果,檢查程式的運行結果是否與預期一致,
我們在StringUtil類中新增一個測驗方法
public static int getMaxIndex(String data){ if(data =https://www.cnblogs.com/yaomagician/p/= null){ return -1; } return data.length(); }
接下來,我們在StringUtilTest類中寫一個測驗方法
public class StringUtilTest{ @Test public void testGetMaxIndex(){ int index1 = StringUtil.getMaxIndex(null); System.out.println(index1); int index2 = StringUtil.getMaxIndex("admin"); System.out.println(index2); //斷言機制:預測index2的結果 Assert.assertEquals("方法內部有Bug",4,index2); } }
運行測驗方法,結果如下圖所示,表示我們預期值與實際值不一致

1.3 Junit框架的常用注解
同學們,剛才我們以及學習了@Test注解,可以用來標記一個方法為測驗方法,測驗才能啟動執行,
除了@Test注解,還有一些其他的注解,我們要知道其他注解標記的方法什么時候執行,以及其他注解在什么場景下可以使用,

接下來,我們演示一下其他注解的使用,我們在StringUtilTest測驗類中,再新增幾個測驗方法,代碼如下
public class StringUtilTest{ @Before public void test1(){ System.out.println("--> test1 Before 執行了"); } @BeforeClass public static void test11(){ System.out.println("--> test11 BeforeClass 執行了"); } @After public void test2(){ System.out.println("--> test2 After 執行了"); } @AfterClass public static void test22(){ System.out.println("--> test22 AfterCalss 執行了"); } }
執行上面的測驗類,結果如下圖所示,觀察執行結果特點如下
1.被@BeforeClass標記的方法,執行在所有方法之前
2.被@AfterCalss標記的方法,執行在所有方法之后
3.被@Before標記的方法,執行在每一個@Test方法之前
4.被@After標記的方法,執行在每一個@Test方法之后

我們現在已經知道每一個注解的作用了,那他們有什么用呢?應用場景在哪里?
我們來看一個例子,假設我想在每個測驗方法中使用Socket物件,并且用完之后,需要把Socket關閉,代碼就可以按照下面的結構來設計
public class StringUtilTest{ private static Socket socket; @Before public void test1(){ System.out.println("--> test1 Before 執行了"); } @BeforeClass public static void test11(){ System.out.println("--> test11 BeforeClass 執行了"); //初始化Socket物件 socket = new Socket(); } @After public void test2(){ System.out.println("--> test2 After 執行了"); } @AfterCalss public static void test22(){ System.out.println("--> test22 AfterCalss 執行了"); //關閉Socket socket.close(); } }
二、反射
什么是反射,其實API檔案中對反射有詳細的說明,我們去了解一下,在java.lang.reflect包中對反射的解釋如下圖所示

翻譯成人話就是:反射技術,指的是加載類的位元組碼到記憶體,并以編程的方法解刨出類中的各個成分(成員變數、方法、構造器等),
反射有啥用呢?其實反射是用來寫框架用的,但是現階段同學們對框架還沒有太多感覺,為了方便理解,我給同學們看一個我們見過的例子:平時我們用IDEA開發程式時,用物件呼叫方法,IDEA會有代碼提示,idea會將這個物件能呼叫的方法都給你列舉出來,供你選擇,如果下圖所示

問題是IDEA怎么知道這個物件有這些方法可以呼叫呢? 原因是物件能呼叫的方法全都來自于類,IDEA通過反射技術就可以獲取到類中有哪些方法,并且把方法的名稱以提示框的形式顯示出來,所以你能看到這些提示了,
為反射獲取的是類的資訊,那么反射的第一步首先獲取到類才行,由于Java的設計原則是萬物皆物件,獲取到的類其實也是以物件的形式體現的,叫位元組碼物件,用Class類來表示,獲取到位元組碼物件之后,再通過位元組碼物件就可以獲取到類的組成成分了,這些組成成分其實也是物件,其中每一個成員變數用Field類的物件來表示、每一個成員方法用Method類的物件來表示,每一個構造器用Constructor類的物件來表示,
如下圖所示:

1.1 獲取類的位元組碼
反射的第一步:是將位元組碼加載到記憶體,我們需要獲取到的位元組碼物件,

比如有一個Student類,獲取Student類的位元組碼代碼有三種寫法,不管用哪一種方式,獲取到的位元組碼物件其實是同一個,
public class Test01_Class { ? public static void main(String[] args) throws ClassNotFoundException { /* 位元組碼檔案物件獲取三種方式 */ //1:根據類名.class Class c1 = Student.class; System.out.println(c1); System.out.println(c1.getName());// 獲取全類名 包名+類名 System.out.println(c1.getSimpleName());// 獲取簡單類名 ? //2:根據類的全路徑 (包名+類名)獲取 Class c2 = Class.forName("com.itheima.d2_reflect.Student"); // c1 c2 是不是一個物件? 類只加載一次!! System.out.println(c1==c2); ? //3:得到物件之后,通過物件的方法獲取 運行時類 Student stu = new Student(); Class c3= stu.getClass(); System.out.println(c1==c3); } } ?
1.2 獲取類的構造器
同學們,上一節我們已經可以獲取到類的位元組碼物件了,接下來,我們學習一下通過位元組碼物件獲取構造器,并使用構造器創建物件,
獲取構造器,需要用到Class類提供的幾個方法,如下圖所示:

想要快速記住這個方法的區別,給同學們說一下這些方法的命名規律,按照規律來記就很方便了,
get:獲取
Declared: 有這個單詞表示可以獲取任意一個,沒有這個單詞表示只能獲取一個public修飾的
Constructor: 構造方法的意思
后綴s: 表示可以獲取多個,沒有后綴s只能獲取一個
話不多少,上代碼,假設現在有一個Cat類,里面有幾個構造方法,代碼如下
public class Cat{ private String name; private int age; public Cat(){ } private Cat(String name, int age){ } }
-
接下來,我們寫一個測驗方法,來測驗獲取類中所有的構造器
-
@Test public void testGetConstructors(){ // 反射第一步 獲取位元組碼物件 Class c= Cat.class; ? // 獲取 所有的 public 修飾的 構造器 // Constructor[] constructors = c.getConstructors(); // System.out.println(constructors.length); // for (Constructor constructor : constructors) { // System.out.println("構造器全稱:"+constructor); // System.out.println("構造器名稱:"+constructor.getName());//獲取構造器的名字 // System.out.println("引數個數:"+constructor.getParameterCount()); // } // 獲取 所有存在的 構造器 (無所謂修飾符) Constructor[] constructors = c.getDeclaredConstructors(); System.out.println("總共:"+constructors.length+"個構造器"); for (Constructor constructor : constructors) { System.out.println("構造器全稱:"+constructor); System.out.println("構造器名稱:"+constructor.getName());//獲取構造器的名字 System.out.println("引數個數:"+constructor.getParameterCount()); System.out.println("=============================="); } ? }
運行測驗方法列印結果如下

-
剛才演示的是獲取Cat類中所有的構造器,接下來,我們演示單個構造器試一試
-
//獲取指定的構造器 @Test public void testContructor() throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException { // 獲取構造器(方法 ) 是不需要指定名字的 因為構造方法名字是類名!!! ? //第一步 獲取 位元組碼物件 Class c = Cat.class; //獲取 無參這個構造器 public Constructor constructor1 = c.getConstructor(); //列印構造器 相關資訊 System.out.println("構造器全稱:"+constructor1); System.out.println("構造器名稱:"+constructor1.getName());//獲取構造器的名字 System.out.println("引數個數:"+constructor1.getParameterCount()); ? Cat cat1 = (Cat)constructor1.newInstance(); System.out.println("查看物件:"+cat1); System.out.println("=============================="); // String型別?String.class ? //獲取兩個引數的構造器 忽略修飾符 Constructor constructor2 = c.getDeclaredConstructor(String.class, int.class); //() 引數的順序和型別來匹配指定的構造器 System.out.println("構造器全稱:"+constructor2); System.out.println("構造器名稱:"+constructor2.getName());//獲取構造器的名字 System.out.println("引數個數:"+constructor2.getParameterCount()); //針對私有的成員 需要繞過權限訪問 constructor2.setAccessible(true); Cat cat2 =(Cat)constructor2.newInstance("小花",3);//引數 倆 第一個引數String,第二個是int System.out.println(cat2); }
列印結果如下

1.3 反射獲取構造器的作用
同學們,剛才上一節我們已經獲取到了Cat類中的構造器,獲取到構造器后,有什么作用呢?
其實構造器的作用:初始化物件并回傳,
這里我們需要用到如下的兩個方法,注意:這兩個方法時屬于Constructor的,需要用Constructor物件來呼叫,

如下圖所示,constructor1和constructor2分別表示Cat類中的兩個構造器,現在我要把這兩個構造器執行起來

由于構造器是private修飾的,先需要呼叫setAccessible(true) 表示禁止檢查訪問控制,然后再呼叫newInstance(實參串列) 就可以執行構造器,完成物件的初始化了,
代碼如下:為了看到構造器真的執行, 故意在兩個構造器中分別加了兩個列印陳述句

代碼的執行結果如下圖所示:

1.4 反射獲取成員變數&使用
同學們,上一節我們已經學習了獲取類的構造方法并使用,接下來,我們再學習獲取類的成員變數,并使用,
其實套路是一樣的,在Class類中提供了獲取成員變數的方法,如下圖所示,

這些方法的記憶規則,如下
get:獲取
Declared: 有這個單詞表示可以獲取任意一個,沒有這個單詞表示只能獲取一個public修飾的
Field: 成員變數的意思
后綴s: 表示可以獲取多個,沒有后綴s只能獲取一個
-
假設有一個Cat類它有若干個成員變數,用Class類提供 的方法將成員變數的物件獲取出來,

執行完上面的代碼之后,我們可以看到控制臺上列印輸出了,每一個成員變數的名稱和它的型別,

public class Test03_Field { ? @Test public void getFields() throws NoSuchFieldException, IllegalAccessException { //獲取所有的屬性 // 1:獲取位元組碼物件 Class c = Cat.class; //2:獲取所有的成員屬性 Field[] fields = c.getDeclaredFields(); //3:遍歷得到每個成員屬性的 物件形式 for (Field field : fields) { System.out.println("全稱:"+field+" 查小名:"+field.getName()+" 屬性的型別:"+field.getType()); } System.out.println("========================="); // 需求 對某個物件的某個屬性完成 賦值 取值 Cat cat = new Cat(); // 給cat物件的 name 屬性 賦值 "加菲貓" // 給cat物件的 age 屬性 賦值 3 // 通過反射方式 找到指定的屬性物件 Field fName = c.getDeclaredField("name"); Field fAge= c.getDeclaredField("age"); ? // 只要是在反射形式呼叫 可以繞過權限檢查 fName.setAccessible(true); fAge.setAccessible(true); // 完成 賦值 // fName fAge 屬性的物件形式 fName.set(cat,"加菲貓"); fAge.set(cat,3); ? System.out.println(cat); // 獲取到 指定物件的指定屬性的值? cat.getName() cat.name //屬性物件反向呼叫 String name = (String) fName.get(cat);//cat.name System.out.println(name); } } ?
-
獲取到成員變數的物件之后該如何使用呢?
在Filed類中提供給給成員變數賦值和獲取值的方法,如下圖所示,

再次強調一下設定值、獲取值的方法時Filed類的需要用Filed類的物件來呼叫,而且不管是設定值、還是獲取值,都需要依賴于該變數所屬的物件,代碼如下

執行代碼,控制臺會有如下的列印

1.5 反射獲取成員方法
各位同學,上面幾節我們已經學習了反射獲取構造方法、反射獲取成員變數,還剩下最后一個就是反射獲取成員方法并使用了,
在Java中反射包中,每一個成員方法用Method物件來表示,通過Class類提供的方法可以獲取類中的成員方法物件,如下下圖所示

接下來我們還是用代碼演示一下:假設有一個Cat類,在Cat類中紅有若干個成員方法
public class Cat{ private String name; private int age; public Cat(){ System.out.println("空引數構造方法執行了"); } private Cat(String name, int age){ System.out.println("有引數構造方法執行了"); this.name=name; this.age=age; } private void run(){ System.out.println("(>^ω^<)喵跑得賊快~~"); } public void eat(){ System.out.println("(>^ω^<)喵愛吃貓糧~"); } private String eat(String name){ return "(>^ω^<)喵愛吃:"+name; } public void setName(String name){ this.name=name; } public String getName(){ return name; } public void setAge(int age){ this.age=age; } public int getAge(){ return age; } }
接下來,通過反射獲取Cat類中所有的成員方法,每一個成員方法都是一個Method物件
package com.itheima.d2_reflect; ? import org.junit.Test; ? import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; ? public class Test04_Method { ? @Test public void testGetMethod() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { //1:獲取位元組碼物件 Class c = Cat.class; //2:獲取所有的方法 Method[] methods = c.getDeclaredMethods(); //3:遍歷 for (Method method : methods) { System.out.println("全稱:"+method+" 簡稱:"+method.getName()+" 回傳值型別:"+method.getReturnType() +" 引數個數:"+method.getParameterCount()); } System.out.println("=============="); //獲取指定的方法 // run Method run = c.getDeclaredMethod("run");// 因為類中 是這么區分方法的 方法名和引數串列 System.out.println(" 簡稱:"+run.getName()+" 回傳值型別:"+run.getReturnType() +" 引數個數:"+run.getParameterCount()); ? ? Method eat = c.getDeclaredMethod("eat",String.class);// 因為類中 是這么區分方法的 方法名和引數串列 System.out.println(" 簡稱:"+eat.getName()+" 回傳值型別:"+eat.getReturnType() +" 引數個數:"+eat.getParameterCount()); } }
?
執行上面的代碼,運行結果如下圖所示:列印輸出每一個成員方法的名稱、引數格式、回傳值型別

也能獲取單個指定的成員方法,如下圖所示

獲取到成員方法之后,有什么作用呢?
在Method類中提供了方法,可以將方法自己執行起來,

下面我們演示一下,把run()方法和eat(String name)方法執行起來,看分割線之下的代碼
package com.itheima.d2_reflect; ? import org.junit.Test; ? import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; ? public class Test04_Method { ? @Test public void testGetMethod() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { //1:獲取位元組碼物件 Class c = Cat.class; //2:獲取所有的方法 Method[] methods = c.getDeclaredMethods(); //3:遍歷 for (Method method : methods) { System.out.println("全稱:"+method+" 簡稱:"+method.getName()+" 回傳值型別:"+method.getReturnType() +" 引數個數:"+method.getParameterCount()); } System.out.println("=============="); //獲取指定的方法 // run Method run = c.getDeclaredMethod("run");// 因為類中 是這么區分方法的 方法名和引數串列 System.out.println(" 簡稱:"+run.getName()+" 回傳值型別:"+run.getReturnType() +" 引數個數:"+run.getParameterCount()); ? ? Method eat = c.getDeclaredMethod("eat",String.class);// 因為類中 是這么區分方法的 方法名和引數串列 System.out.println(" 簡稱:"+eat.getName()+" 回傳值型別:"+eat.getReturnType() +" 引數個數:"+eat.getParameterCount()); ? Cat cat = new Cat(); // 方法物件是主體 由方法物件完成呼叫 // run.invoke(cat,實際引數); run.setAccessible(true); //繞過權限檢查 Object returnValue1 = run.invoke(cat);//run方法執行 // run方法被 cat 物件呼叫執行 System.out.println("run方法呼叫之后的回傳值:"+returnValue1); eat.setAccessible(true); Object returnValue2 = eat.invoke(cat,"魚兒");//eat方法執行 System.out.println("eat方法呼叫之后的回傳值:"+returnValue2); } } ?
列印結果如下圖所示:run()方法執行后列印貓跑得賊快~~,回傳null; eat()方法執行完,直接回傳貓最愛吃:魚兒

1.6 反射的應用
各位小伙伴,按照前面我們學習反射的套路,我們已經充分認識了什么是反射,以及反射的核心作用是用來獲取類的各個組成部分并執行他們,但是由于同學們的經驗有限,對于反射的具體應用場景還是很難感受到的(這個目前沒有太好的辦法,只能慢慢積累,等經驗積累到一定程度,就會豁然開朗了),
我們一直說反射使用來寫框架的,接下來,我們就寫一個簡易的框架,簡單窺探一下反射的應用,反射其實是非常強大的,這個案例也僅僅值小試牛刀,

需求是讓我們寫一個框架,能夠將任意一個物件的屬性名和屬性值寫到檔案中去,不管這個物件有多少個屬性,也不管這個物件的屬性名是否相同,
分析一下該怎么做
1.先寫好兩個類,一個Student類和Teacher類
2.寫一個ObjectFrame類代表框本架
在ObjectFrame類中定義一個saveObject(Object obj)方法,用于將任意物件存到檔案中去
引數:Object obj: 就表示要存入檔案中的物件
3.撰寫方法內部的代碼,往檔案中存盤物件的屬性名和屬性值
1)引數obj物件中有哪些屬性,屬性名是什么實作值是什么,中有物件自己最清楚,
2)接著就通過反射獲取類的成員變數資訊了(變數名、變數值)
3)把變數名和變數值寫到檔案中去
寫一個ObjectFrame表示自己設計的框架,代碼如下圖所示
public class ObjectFrame { ? /** * 告訴物件 我幫你解剖 * 屬性 屬性值決議出來 并寫到指定檔案中, * 1: 根據傳遞的物件 即系 屬性名 和屬性值 * 2: 通過流的形式把內容寫到檔案中 * 方法三要素: 方法名 引數串列 回傳值型別 */ public static void saveTxt(Object obj) throws IllegalAccessException, FileNotFoundException { //已知條件 obj物件 PrintStream ps = new PrintStream(new FileOutputStream("day14\\src\\data.txt",true)); //1:根據obj物件獲取其對應的位元組碼物件, Class c = obj.getClass(); //獲取類名 String className= c.getSimpleName();//獲取類名 小名 ps.println("-----------"+className+"------------"); System.out.println("-----------"+className+"------------"); //2:根據位元組碼物件獲取所有的屬性物件, Field[] fields = c.getDeclaredFields(); //3:遍歷每個屬性物件,獲取屬性的名字 以及 其對應的值 if(fields!=null && fields.length>0){ //健壯性判斷 for (Field field : fields) { //取消權限檢查 field.setAccessible(true); // 獲取屬性名字 String name = field.getName(); Object value = field.get(obj); ps.println(name+"="+value); System.out.println(name+"="+value); } } //4: 再去思考用流完成 資料的列印, ps.close(); } }
使用自己設計的框架,往檔案中寫入Student物件的資訊和Teacher物件的資訊,
先準備好Student類和Teacher類
public class Student{
private String name;
private int age;
private char sex;
private double height;
private String hobby;
}
public class Teacher{
private String name;
private double salary;
}
創建一個測驗類,在測驗中類創建一個Student物件,創建一個Teacher物件,用ObjectFrame的方法把這兩個物件所有的屬性名和屬性值寫到檔案中去,
public class Test5Frame{ @Test public void save() throws Exception{ Student s1 = new Student("黑馬吳彥祖",45, '男', 185.3, "籃球,冰球,閱讀"); Teacher s2 = new Teacher("播妞",999.9); ObjectFrame.save(s1); ObjectFrame.save(s2); } }
打開data.txt檔案,內容如下圖所示,就說明我們這個框架的功能已經實作了

三、注解
3.1 認識注解&定義注解
注解和反射一樣,都是用來做框架的,我們這里學習注解的目的其實是為了以后學習框架或者做框架做鋪墊的,
那注解該怎么學呢?和反射的學習套路一樣,我們先充分的認識注解,掌握注解的定義和使用格式,然后再學習它的應用場景,
先來認識一下什么是注解?
Java注解是代碼中的特殊標記,比如@Override、@Test等,作用是:讓其他程式根據注解資訊決定怎么執行該程式,
比如:Junit框架的@Test注解可以用在方法上,用來標記這個方法是測驗方法,被@Test標記的方法能夠被Junit框架執行,
再比如:@Override注解可以用在方法上,用來標記這個方法是重寫方法,被@Override注解標記的方法能夠被IDEA識別進行語法檢查,

-
注解不光可以用在方法上,還可以用在類上、變數上、構造器上等位置,
上面我們說的@Test注解、@Overide注解是別人定義好給我們用的,將來如果需要自己去開發框架,就需要我們自己定義注解,
接著我們學習自定義注解
自定義注解的格式如下圖所示

比如:現在我們自定義一個MyTest注解
public @interface MyTest1{ String aaa(); boolean bbb() default true; //default true 表示默認值為true,使用時可以不賦值, String[] ccc(); }
定義好MyTest注解之后,我們可以使用MyTest注解在類上、方法上等位置做標記,注意使用注解時需要加@符號,如下
@MyTest1(aaa="牛魔王",ccc={"HTML","Java"})
public class AnnotationTest1{
@MyTest1(aaa="鐵扇公主",bbb=false, ccc={"Python","前端","Java"})
public void test1(){
}
}
注意:注解的屬性名如何是value的話,并且只有value沒有默認值,使用注解時value名稱可以省略,比如現在重新定義一個MyTest2注解
public @interface MyTest2 { String value(); String name() default "太上老君"; ? // value可以省略屬性 前提 其他的屬性不用賦值 }
定義好MyTest2注解后,再將@MyTest2標記在類上,此時value屬性名可以省略,代碼如下
@MyTest1(aaa="小黑子",ccc = {"雞你太美","打籃球rap"})
public class AnnotationTest1 { //Class
?
@MyTest1(aaa ="ikun" ,bbb = false,ccc="跳")
@MyTest2("紅孩兒")
public void test1(){} //Method
@MyTest2("鐵扇公主")
private String name;// Field物件
}
?
到這里關于定義注解的格式、以及使用注解的格式就學習完了,
注解本質是什么呢?
想要搞清楚注解本質是什么東西,我們可以把注解的位元組碼進行反編譯,使用XJad工具進行反編譯,經過對MyTest1注解位元組碼反編譯我們會發現:
1.MyTest1注解本質上是介面,每一個注解介面都繼承子Annotation介面
2.MyTest1注解中的屬性本質上是抽象方法
3.@MyTest1實際上是作為MyTest介面的實作類物件
4.@MyTest1(aaa="孫悟空",bbb=false,ccc={"Python","前端","Java"})里面的屬性值,可以通過呼叫aaa()、bbb()、ccc()方法獲取到, 【別著急,繼續往下看,再決議注解時會用到】

3.2 元注解
各位小伙伴,剛才我們已經認識了注解以及注解的基本使用,接下來我們還需要學習幾種特殊的注解,叫做元注解,
什么是元注解?
元注解是修飾注解的注解,這句話雖然有一點饒,但是非常準確,我們看一個例子

別一下@Target注解和@Retention注解有什么作用,如下圖所示
@Target是用來宣告注解只能用在那些位置,比如:類上、方法上、成員變數上等
@Retetion是用來宣告注解保留周期,比如:源代碼時期、位元組碼時期、運行時期

-
@Target元注解的使用:比如定義一個MyTest3注解,并添加@Target注解用來宣告MyTest3的使用位置
@Target(ElementType.TYPE) //宣告@MyTest3注解只能用在類上
public @interface MyTest3{
}
接下來,我們把@MyTest3用來類上觀察是否有錯,再把@MyTest3用在方法上、變數上再觀察是否有錯

如果我們定義MyTest3注解時,使用@Target注解屬性值寫成下面樣子
//宣告@MyTest3注解只能用在類上和方法上
@Target({ElementType.TYPE,ElementType.METHOD})
public @interface MyTest3{
}
此時再觀察,@MyTest用在類上、方法上、變數上是否有錯

到這里@Target元注解的使用就演示完畢了,
-
@Retetion元注解的使用:定義MyTest3注解時,給MyTest3注解添加@Retetion注解來宣告MyTest3注解保留的時期
@Retetion是用來宣告注解保留周期,比如:源代碼時期、位元組碼時期、運行時期 @Retetion(RetetionPloicy.SOURCE): 注解保留到源代碼時期、位元組碼中就沒有了 @Retetion(RetetionPloicy.CLASS): 注解保留到位元組碼中、運行時注解就沒有了 @Retetion(RetetionPloicy.RUNTIME):注解保留到運行時期 【自己寫代碼時,比較常用的是保留到運行時期】 //宣告@MyTest3注解只能用在類上和方法上 @Target({ElementType.TYPE,ElementType.METHOD}) //控制使用了@MyTest3注解的代碼中,@MyTest3保留到運行時期 @Retetion(RetetionPloicy.RUNTIME) public @interface MyTest3{ }
3.3 決議注解
各位小伙伴,通過前面的學習我們能夠自己定義注解,也能夠把自己定義的注解標記在類上或者方法上等位置,但是總感覺有點別扭,給類、方法、變數等加上注解后,我們也沒有干什么呀!!!
接下來,我們就要做點什么,我們可以通過反射技術把類上、方法上、變數上的注解物件獲取出來,然后通過呼叫方法就可以獲取注解上的屬性值了,我們把獲取類上、方法上、變數上等位置注解及注解屬性值的程序稱為決議注解,
決議注解套路如下
1.如果注解在類上,先獲取類的位元組碼物件,再獲取類上的注解
2.如果注解在方法上,先獲取方法物件,再獲取方法上的注解
3.如果注解在成員變數上,先獲取成員變數物件,再獲取變數上的注解
總之:注解在誰身上,就先獲取誰,再用誰獲取誰身上的注解

決議來看一個案例,來演示決議注解的代碼撰寫

按照需求要求一步一步完成
① 先定義一個MyTest4注解
//宣告@MyTest4注解只能用在類上和方法上 @Target({ElementType.TYPE,ElementType.METHOD}) //控制使用了@MyTest4注解的代碼中,@MyTest4保留到運行時期 @Retetion(RetetionPloicy.RUNTIME) public @interface MyTest4{ String value(); double aaa() default 100; String[] bbb(); }
② 定義有一個類Demo
@MyTest4(value = "https://www.cnblogs.com/yaomagician/p/杉木",aaa =1000 ,bbb={"喝水","曬太陽"})
public class Demo {
?
@MyTest4(value = "熏悟空",bbb = {"喝牛奶","吃香蕉"})
public void test1(){
?
}
}
③ 寫一個測驗類AnnotationTest3決議Demo類上的MyTest4注解
package com.itheima.d3_annotation; ? import org.junit.Test; ? import java.lang.annotation.Annotation; import java.lang.reflect.Method; public class AnnotationTest3 { ? @Test public void parseClassAnnotation() { // 決議 Demo類上的 MyTest4注解 //1:獲取Demo位元組碼檔案物件 Class c = Demo.class; //2: 判斷該位元組碼物件上 有沒有使用MyTest4型別的 注解 boolean b = c.isAnnotationPresent(MyTest4.class); ? if(b){ //3: 如果有就獲取出來 注解物件 // Annotation annotation = c.getAnnotation(); // MyTest4 extends Annotation 向下轉型 MyTest4 myTest4 =(MyTest4) c.getAnnotation(MyTest4.class); //4: 決議它的屬性 double aaa = myTest4.aaa(); System.out.println(aaa); System.out.println(myTest4.bbb()); System.out.println(myTest4.value()); ? } ? } ? @Test public void parseMethodAnnotation() throws NoSuchMethodException { // 決議 Demo類上的 MyTest4注解 //1:獲取Demo位元組碼檔案物件 Class c = Demo.class; //2: 根據位元組碼物件獲取 指定的方法物件 Method method = c.getDeclaredMethod("test1"); ? //2: 判斷該位元組碼物件上 有沒有使用MyTest4型別的 注解 boolean b = method.isAnnotationPresent(MyTest4.class); ? if(b){ //3: 如果有就獲取出來 注解物件 // Annotation annotation = c.getAnnotation(); // MyTest4 extends Annotation 向下轉型 MyTest4 myTest4 =(MyTest4) method.getAnnotation(MyTest4.class); //4: 決議它的屬性 double aaa = myTest4.aaa(); System.out.println(aaa); System.out.println(myTest4.bbb()); System.out.println(myTest4.value()); ? } } } ?
3.4 注解的應用場景
各位同學,關于注解的定義、使用、決議注解就已經學習完了,接下來,我們再學習一下注解的應用場景,注解是用來寫框架的,比如現在我們要模擬Junit寫一個測驗框架,要求有@MyTest注解的方法可以被框架執行,沒有@MyTest注解的方法不能被框架執行,
第一步:先定義一個MyTest注解
@Target(ElementType.METHOD)
@Retetion(RetetionPloicy.RUNTIME)
public @interface MyTest{
}
第二步:寫一個測驗類AnnotationTest4,在類中定義幾個被@MyTest注解標記的方法
public class AnnotationTest4{ @MyTest public void test1(){ System.out.println("=====test1===="); } @MyTest public void test2(){ System.out.println("=====test2===="); } ? public void test3(){ System.out.println("=====test2===="); } public static void main(String[] args){ AnnotationTest4 a = new AnnotationTest4(); //1.先獲取Class物件 Class c = AnnotationTest4.class; //2.決議AnnotationTest4類中所有的方法物件 Method[] methods = c.getDeclaredMethods(); for(Method m: methods){ //3.判斷方法上是否有MyTest注解,有就執行該方法 if(m.isAnnotationPresent(MyTest.class)){ m.invoke(a); } } } }
恭喜小伙伴們,學習到這里,關于注解的使用就學會了(^▽^)
四、動態代理
4.1 動態代理介紹、準備功能
各位同學,這節課我們學習一個Java的高級技術叫做動態代理,首先我們認識一下代理長什么樣?我們以大明星“楊超越”例,
假設現在有一個大明星叫楊超越,它有唱歌和跳舞的本領,作為大明星是要用唱歌和跳舞來賺錢的,但是每次位元組目,唱歌的時候要準備話筒、收錢,再唱歌;跳舞的時候也要準備場地、收錢、再唱歌,楊超越越覺得我擅長的做的事情是唱歌,和跳舞,但是每次唱歌和跳舞之前或者之后都要做一些繁瑣的事情,有點煩,于是楊超越就找個一個經濟公司,請了一個代理人,代理楊超越處理這些事情,如果有人想請楊超越演出,直接找代理人就可以了,如下圖所示

我們說楊超越的代理是中介公司派的,那中介公司怎么知道,要派一個有唱歌和跳舞功能的代理呢?
解決這個問題,Java使用的是介面,楊超越想找代理,在Java中需要楊超越實作了一個介面,介面中規定要唱歌和跳舞的方法,Java就可以通過這個介面為楊超越生成一個代理物件,只要介面中有的方法代理物件也會有,

接下來我們就先把有唱歌和跳舞功能的介面,和實作介面的大明星類定義出來,
public interface Star { String sing(String name); void dance(); } /** * @author :石破天 * @date :Created in 2023年02月07日 * @description : * @version: 1.0 */ public class BigStar implements Star{ ? private String name; ? public BigStar(String name){ this.name = name; } ? @Override public String sing(String name) { System.out.println("您點了:"+name+"歌曲,"+this.name+"就唱著歌曲"); return "栓Q!!!"; } ? @Override public void dance() { System.out.println(this.name+"正在跳 freestyle dance!!"); } }

4.2 生成動態代理物件
下面我們寫一個為BigStar生成動態代理物件的工具類,這里需要用Java為開發者提供的一個生成代理物件的類叫Proxy類,
通過Proxy類的newInstance(...)方法可以為實作了同一介面的類生成代理物件, 呼叫方法時需要傳遞三個引數,該方法的引數解釋可以查閱API檔案,如下,

/** * @date :Created in 2023年02月07日 * @description : * 經濟公司 * 給 明星 產生代理物件 幫助明星 完成功能的增強 * @version: 1.0 */ public class ProxyFactory { /* 方法回傳值 寫 Star 接收代理物件 代理物件和 明星 需要具備相同的功能 所以代理物件 也是 Star介面實作類物件 * @param bigStar 明星物件 * @return 代理物件 */ public static Star createProxy(BigStar bigStar){ //JDK 產生代理的方式 是通過 /* public static Object newProxyInstance(ClassLoader loader, 用于指定加載類 到記憶體的一個類加載器 Class<?>[] interfaces, 明星實作的介面 InvocationHandler h) 用于指定代理物件干什么!!! ? */ Star startProxy = (Star) Proxy.newProxyInstance(ProxyFactory.class.getClassLoader(),//獲取了當前類的加載器,后面負責代理類的加載 new Class[]{Star.class},//bigStar.getClass().getInterfaces() new InvocationHandler() { ? /** * 用于攔截 明星的唱歌跳舞 對唱歌跳舞進行增強控制 * @param proxy 代理物件 * @param method 攔截的方法 sing("好日子") dance() * @param args 引數 {"好日子"} * @return 方法的回傳值 執行方法的回傳值 * @throws Throwable */ @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { ? if(method.getName().equals("sing")){// 代理物件攔住 唱歌方法 進行增強 System.out.println("準備話筒,收錢200~"); // 大明星唱歌 method 反射呼叫 }else if(method.getName().equals("dance")){ System.out.println("準備場地,收錢30W~"); // 大明星跳舞 method 反射呼叫 } ? return method.invoke(bigStar,args);//反射執行 被代理人的功能 } }); ? return startProxy; } }
呼叫我們寫好的ProxyUtil工具類,為BigStar物件生成代理物件
? /** * @date :Created in 2023年02月07日 * @description : * 動態代理 * 在不修改原來的基礎上,實作功能的增強 * 誰來誰先增強,是代理物件, * 只不過代理物件的生成非常的麻煩, * @version: 1.0 */ public class Test { public static void main(String[] args) { // 通過經紀公司給 楊超越產生一個代理 Star star = ProxyFactory.createProxy(new BigStar("楊超越")); ? String sing = star.sing("好日子"); System.out.println(sing); ? star.dance(); ? } } ?
運行測驗類,結果如下圖所示

恭喜同學們,當你把上面的案例寫出來,并且理解,那么動態代理的基本使用就學會了,
4.3 動態代理應用
學習完動態代理的基本使用之后,接下來我們再做一個應用案例,

現有如下代碼
/** * 用戶業務介面 */ public interface UserService { // 登錄功能 void login(String loginName,String passWord) throws Exception; // 洗掉用戶 void deleteUsers() throws Exception; // 查詢用戶,回傳陣列的形式, String[] selectUsers() throws Exception; }
下面有一個UserService介面的實作類,下面每一個方法中都有計算方法運行時間的代碼,
/** * 用戶業務實作類(面向介面編程) */ public class UserServiceImpl implements UserService{ @Override public void login(String loginName, String passWord) throws Exception { long time1 = System.currentTimeMillis(); if("admin".equals(loginName) && "123456".equals(passWord)){ System.out.println("您登錄成功,歡迎光臨本系統~"); }else { System.out.println("您登錄失敗,用戶名或密碼錯誤~"); } Thread.sleep(1000); long time2 = System.currentTimeMillis(); System.out.println("login方法耗時:"+(time2-time1)); } ? @Override public void deleteUsers() throws Exception{ long time1 = System.currentTimeMillis(); System.out.println("成功洗掉了1萬個用戶~"); Thread.sleep(1500); long time2 = System.currentTimeMillis(); System.out.println("deleteUsers方法耗時:"+(time2-time1)); } ? @Override public String[] selectUsers() throws Exception{ long time1 = System.currentTimeMillis(); System.out.println("查詢出了3個用戶"); String[] names = {"張全蛋", "李二狗", "牛愛花"}; Thread.sleep(500); long time2 = System.currentTimeMillis(); System.out.println("selectUsers方法耗時:"+(time2-time1)); return names; } }
觀察上面代碼發現有什么問題嗎?
我們會發現每一個方法中計算耗時的代碼都是重復的,我們可是學習了動態代理的高級程式員,怎么能忍受在每個方法中寫重復代碼呢!況且這些重復的代碼并不屬于UserSerivce的主要業務代碼,

所以接下來我們打算,把計算每一個方法的耗時操作,交給代理物件來做,
先在UserService類中把計算耗時的代碼洗掉,代碼如下
/** * 用戶業務實作類(面向介面編程) */ public class UserServiceImpl implements UserService{ @Override public void login(String loginName, String passWord) throws Exception { if("admin".equals(loginName) && "123456".equals(passWord)){ System.out.println("您登錄成功,歡迎光臨本系統~"); }else { System.out.println("您登錄失敗,用戶名或密碼錯誤~"); } Thread.sleep(1000); } ? @Override public void deleteUsers() throws Exception{ System.out.println("成功洗掉了1萬個用戶~"); Thread.sleep(1500); } ? @Override public String[] selectUsers() throws Exception{ ? System.out.println("查詢出了3個用戶"); String[] names = {"張全蛋", "李二狗", "牛愛花"}; Thread.sleep(500); ? return names; } }
然后為UserService生成一個動態代理物件,在動態代理中呼叫目標方法,在呼叫目標方法之前和之后記錄毫秒值,并計算方法運行的時間,代碼如下
public class ProxyUtil { public static UserService createProxy(UserService userService){ UserService userServiceProxy = (UserService) Proxy.newProxyInstance( ProxyUtil.class.getClassLoader(), new Class[]{UserService.class}, new InvocationHandler() { @Override public Object invoke( Object proxy, Method method, Object[] args) throws Throwable { if( method.getName().equals("login") || method.getName().equals("deleteUsers")|| method.getName().equals("selectUsers")){ //方法運行前記錄毫秒值 long startTime = System.currentTimeMillis(); //執行方法 Object rs = method.invoke(userService, args); //執行方法后記錄毫秒值 long endTime = System.currentTimeMillis(); ? System.out.println(method.getName() + "方法執行耗時:" + (endTime - startTime)/ 1000.0 + "s"); return rs; }else { Object rs = method.invoke(userService, args); return rs; } } }); //回傳代理物件 return userServiceProxy; } }
在測驗類中為UserService創建代理物件
/** * 目標:使用動態代理解決實際問題,并掌握使用代理的好處, */ public class Test { public static void main(String[] args) throws Exception{ // 1、創建用戶業務物件, UserService userService = ProxyUtil.createProxy(new UserServiceImpl()); ? // 2、呼叫用戶業務的功能, userService.login("admin", "123456"); System.out.println("----------------------------------"); ? userService.deleteUsers(); System.out.println("----------------------------------"); ? String[] names = userService.selectUsers(); System.out.println("查詢到的用戶是:" + Arrays.toString(names)); System.out.println("----------------------------------"); ? } }
執行結果如下圖所示

動態代理物件的執行流程如下圖所示,每次用代理物件呼叫方法時,都會執行InvocationHandler中的invoke方法,

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/546668.html
標籤:Java
