day23-單元測試、反射
恭喜同學們,Java主要的知識我們其實已經學習得差不多了。今天同學們再把單元測試、反射、注解、動態代理學習完。Java的基礎知識就算全齊活了。
首先,我們進入單元測試的學習。
一、單元測試
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{@Testpublic void testPrintNumber(){StringUtil.printNumber("admin");StringUtil.printNumber(null);}
}
寫完代碼之后,我們會發現測試方法左邊,會有一個綠色的三角形按鈕。點擊這個按鈕,就可以運行測試方法。
1.2 單元測試斷言
接下來,我們學習一個單元測試的斷言機制。所謂斷言:意思是程序員可以預測程序的運行結果,檢查程序的運行結果是否與預期一致。
我們在StringUtil類中新增一個測試方法
public static int getMaxIndex(String data){if(data == null){return -1;}return data.length();}
接下來,我們在StringUtilTest類中寫一個測試方法
public class StringUtilTest{@Testpublic 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{@Beforepublic void test1(){System.out.println("--> test1 Before 執行了");}@BeforeClasspublic static void test11(){System.out.println("--> test11 BeforeClass 執行了");}@Afterpublic void test2(){System.out.println("--> test2 After 執行了");}@AfterCalsspublic 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;@Beforepublic void test1(){System.out.println("--> test1 Before 執行了");}@BeforeClasspublic static void test11(){System.out.println("--> test11 BeforeClass 執行了");//初始化Socket對象socket = new Socket();}@Afterpublic void test2(){System.out.println("--> test2 After 執行了");}@AfterCalsspublic static void test22(){System.out.println("--> test22 AfterCalss 執行了");//關閉Socketsocket.close();}
}
最后,我們再補充一點。前面的注解是基于Junit4版本的,再Junit5版本中對注解作了更新,但是作用是一樣的。所以這里就不做演示了
二、反射
各位小伙伴,接下來我們要學習反射技術。在學習反射之前,有幾個點需要給同學們提前交代一下,接下來我們學習的反射、動態代理、注解等知識點,在以后開發中極少用到,這些技術都是以后學習框架、或者做框架的底層源碼。給同學們講這些技術的目的,是為了以后我們理解框架、或者自己開發框架給別人用作鋪墊的。同時由于這些技術非常抽象,所以按照國際慣例,我們都會采用先帶著大家充分的認識它們,然后再了解其作用和應用場景。
接下來,我們就需要帶著同學們認識一下什么是反射。其實API文檔中對反射有詳細的說明,我們去了解一下。在java.lang.reflect包中對反射的解釋如下圖所示
翻譯成人話就是:反射技術,指的是加載類的字節碼到內存,并以編程的方法解刨出類中的各個成分(成員變量、方法、構造器等)。
反射有啥用呢?其實反射是用來寫框架用的,但是現階段同學們對框架還沒有太多感覺。為了方便理解,我給同學們看一個我們見過的例子:平時我們用IDEA開發程序時,用對象調用方法,IDEA會有代碼提示,idea會將這個對象能調用的方法都給你列舉出來,供你選擇,如果下圖所示
問題是IDEA怎么知道這個對象有這些方法可以調用呢? 原因是對象能調用的方法全都來自于類,IDEA通過反射技術就可以獲取到類中有哪些方法,并且把方法的名稱以提示框的形式顯示出來,所以你能看到這些提示了。
那記事本寫代碼為什么沒有提示呢? 因為技術本軟件沒有利用反射技術開發這種代碼提示的功能,哈哈!!
好了,認識了反射是什么之后,接下來我還想給同學們介紹一下反射具體學什么?
因為反射獲取的是類的信息,那么反射的第一步首先獲取到類才行。由于Java的設計原則是萬物皆對象,獲取到的類其實也是以對象的形式體現的,叫字節碼對象,用Class類來表示。獲取到字節碼對象之后,再通過字節碼對象就可以獲取到類的組成成分了,這些組成成分其實也是對象,其中每一個成員變量用Field類的對象來表示、每一個成員方法用Method類的對象來表示,每一個構造器用Constructor類的對象來表示。
如下圖所示:
1.1 獲取類的字節碼
反射的第一步:是將字節碼加載到內存,我們需要獲取到的字節碼對象。
比如有一個Student類,獲取Student類的字節碼代碼有三種寫法。不管用哪一種方式,獲取到的字節碼對象其實是同一個。
public class Test1Class{public static void main(String[] args){Class c1 = Student.class;System.out.println(c1.getName()); //獲取全類名System.out.println(c1.getSimpleName()); //獲取簡單類名Class c2 = Class.forName("com.zhushanglin.d2_reflect.Student");System.out.println(c1 == c2); //trueStudent s = new Student();Class c3 = s.getClass();System.out.println(c2 == c3); //true}
}
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){}
}
-
- 接下來,我們寫一個測試方法,來測試獲取類中所有的構造器
public class Test2Constructor(){@Testpublic void testGetConstructors(){//1、反射第一步:必須先得到這個類的Class對象Class c = Cat.class;//2、獲取類的全部構造器Constructor[] constructors = c.getDeclaredConstructors();//3、遍歷數組中的每一個構造器對象。for(Constructor constructor: constructors){System.out.println(constructor.getName()+"---> 參數個數:"+constructor.getParameterCount());}}
}
運行測試方法查看打印結果(一定要親自嘗試喲!)
-
- 剛才演示的是獲取Cat類中所有的構造器,接下來,我們演示單個構造器試一試
public class Test2Constructor(){@Testpublic void testGetConstructor(){//1、反射第一步:必須先得到這個類的Class對象Class c = Cat.class;//2、獲取類public修飾的空參數構造器Constructor constructor1 = c.getConstructor();System.out.println(constructor1.getName()+"---> 參數個數:"+constructor1.getParameterCount());//3、獲取private修飾的有兩個參數的構造器,第一個參數String類型,第二個參數int類型Constructor constructor2 = c.getDeclaredConstructor(String.class,int.class);System.out.println(constructor2.getName()+"---> 參數個數:"+constructor1.getParameterCount());}
}
查看打印結果(一定要親自嘗試喲!)
1.3 反射獲取構造器的作用
同學們,剛才上一節我們已經獲取到了Cat類中的構造器。獲取到構造器后,有什么作用呢?
其實構造器的作用:初始化對象并返回。
這里我們需要用到如下的兩個方法,注意:這兩個方法時屬于Constructor的,需要用Constructor對象來調用。
如下圖所示,constructor1和constructor2分別表示Cat類中的兩個構造器。現在我要把這兩個構造器執行起來
由于構造器是private修飾的,先需要調用setAccessible(true)
表示禁止檢查訪問控制,然后再調用newInstance(實參列表)
就可以執行構造器,完成對象的初始化了。
代碼如下:為了看到構造器真的執行, 故意在兩個構造器中分別加了兩個打印語句
查看代碼的執行結果(一定要親自嘗試喲!)
1.4 反射獲取成員變量&使用
同學們,上一節我們已經學習了獲取類的構造方法并使用。接下來,我們再學習獲取類的成員變量,并使用。
其實套路是一樣的,在Class類中提供了獲取成員變量的方法,如下圖所示。
這些方法的記憶規則,如下
get:獲取
Declared: 有這個單詞表示可以獲取任意一個,沒有這個單詞表示只能獲取一個public修飾的
Field: 成員變量的意思
后綴s: 表示可以獲取多個,沒有后綴s只能獲取一個
- 假設有一個Cat類它有若干個成員變量,用Class類提供 的方法將成員變量的對象獲取出來。
執行完上面的代碼之后,我們可以看到控制臺上打印輸出了,每一個成員變量的名稱和它的類型。
- 獲取到成員變量的對象之后該如何使用呢?
在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對象
public class Test3Method{public static void main(String[] args){//1、反射第一步:先獲取到Class對象Class c = Cat.class;//2、獲取類中的全部成員方法Method[] methods = c.getDecalaredMethods();//3、遍歷這個數組中的每一個方法對象for(Method method : methods){System.out.println(method.getName()+"-->"+method.getParameterCount()+"-->"+method.getReturnType());}}
}
執行上面的代碼,運行結果如下圖所示:打印輸出每一個成員方法的名稱、參數格式、返回值類型
也能獲取單個指定的成員方法,如下圖所示
獲取到成員方法之后,有什么作用呢?
在Method類中提供了方法,可以將方法自己執行起來。
下面我們演示一下,把run()
方法和eat(String name)
方法執行起來。看分割線之下的代碼
public class Test3Method{public static void main(String[] args){//1、反射第一步:先獲取到Class對象Class c = Cat.class;//2、獲取類中的全部成員方法Method[] methods = c.getDecalaredMethods();//3、遍歷這個數組中的每一個方法對象for(Method method : methods){System.out.println(method.getName()+"-->"+method.getParameterCount()+"-->"+method.getReturnType());}System.out.println("-----------------------");//4、獲取private修飾的run方法,得到Method對象Method run = c.getDecalaredMethod("run");//執行run方法,在執行前需要取消權限檢查Cat cat = new Cat();run.setAccessible(true);Object rs1 = run.invoke(cat);System.out.println(rs1)//5、獲取private 修飾的eat(String name)方法,得到Method對象Method eat = c.getDeclaredMethod("eat",String.class);eat.setAccessible(true);Object rs2 = eat.invoke(cat,"魚兒");System.out.println(rs2)}
}
打印結果如下圖所示: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{public static void saveObject(Object obj) throws Exception{PrintStream ps = new PrintStream(new FileOutputStream("模塊名\\src\\data.txt",true));//1)參數obj對象中有哪些屬性,屬性名是什么實現值是什么,中有對象自己最清楚。//2)接著就通過反射獲取類的成員變量信息了(變量名、變量值)Class c = obj.getClass(); //獲取字節碼ps.println("---------"+class.getSimpleName()+"---------");Field[] fields = c.getDeclaredFields(); //獲取所有成員變量//3)把變量名和變量值寫到文件中去for(Field field : fields){String name = field.getName();Object value = field.get(obj)+"";ps.println(name);}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{@Testpublic 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 MyTest{String aaa();boolean bbb() default true; //default true 表示默認值為true,使用時可以不賦值。String[] ccc();
}
定義好MyTest注解之后,我們可以使用MyTest注解在類上、方法上等位置做標記。注意使用注解時需要加@符號,如下
@MyTest1(aaa="牛魔王",ccc={"HTML","Java"})
public class AnnotationTest1{@MyTest(aaa="鐵扇公主",bbb=false, ccc={"Python","前端","Java"})public void test1(){}
}
注意:注解的屬性名如何是value的話,并且只有value沒有默認值,使用注解時value名稱可以省略。比如現在重新定義一個MyTest2注解
public @interface MyTest2{String value(); //特殊屬性int age() default 10;
}
定義好MyTest2注解后,再將@MyTest2標記在類上,此時value屬性名可以省略,代碼如下
@MyTest2("孫悟空") //等價于 @MyTest2(value="孫悟空")
@MyTest1(aaa="牛魔王",ccc={"HTML","Java"})
public class AnnotationTest1{@MyTest(aaa="鐵扇公主",bbb=false, ccc={"Python","前端","Java"})public void test1(){}
}
到這里關于定義注解的格式、以及使用注解的格式就學習完了。
注解本質是什么呢?
想要搞清楚注解本質是什么東西,我們可以把注解的字節碼進行反編譯,使用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="蜘蛛俠",aaa=99.9, bbb={"至尊寶","清華"})
public class Demo{@MyTest4(value="孫悟空",aaa=199.9, bbb={"紫霞","牛夫人"})public void test1(){}
}
③ 寫一個測試類AnnotationTest3解析Demo類上的MyTest4注解
public class AnnotationTest3{@Testpublic void parseClass(){//1.先獲取Class對象Class c = Demo.class;//2.解析Demo類上的注解if(c.isAnnotationPresent(MyTest4.class)){//獲取類上的MyTest4注解MyTest4 myTest4 = (MyTest4)c.getDeclaredAnnotation(MyTest4.class);//獲取MyTests4注解的屬性值System.out.println(myTest4.value());System.out.println(myTest4.aaa());System.out.println(myTest4.bbb());}}@Testpublic void parseMethods(){//1.先獲取Class對象Class c = Demo.class;//2.解析Demo類中test1方法上的注解MyTest4注解Method m = c.getDeclaredMethod("test1");if(m.isAnnotationPresent(MyTest4.class)){//獲取方法上的MyTest4注解MyTest4 myTest4 = (MyTest4)m.getDeclaredAnnotation(MyTest4.class);//獲取MyTests4注解的屬性值System.out.println(myTest4.value());System.out.println(myTest4.aaa());System.out.println(myTest4.bbb());}}
}
3.4 注解的應用場景
各位同學,關于注解的定義、使用、解析注解就已經學習完了。接下來,我們再學習一下注解的應用場景,注解是用來寫框架的,比如現在我們要模擬Junit寫一個測試框架,要求有@MyTest注解的方法可以被框架執行,沒有@MyTest注解的方法不能被框架執行。
第一步:先定義一個MyTest注解
@Target(ElementType.METHOD)
@Retetion(RetetionPloicy.RUNTIME)
public @interface MyTest{}
第二步:寫一個測試類AnnotationTest4,在類中定義幾個被@MyTest注解標記的方法
public class AnnotationTest4{@MyTestpublic void test1(){System.out.println("=====test1====");}@MyTestpublic 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就可以通過這個接口為楊超越生成一個代理對象,只要接口中有的方法代理對象也會有。
接下來我們就先把有唱歌和跳舞功能的接口,和實現接口的大明星類定義出來。
4.2 生成動態代理對象
下面我們寫一個為BigStar生成動態代理對象的工具類。這里需要用Java為開發者提供的一個生成代理對象的類叫Proxy類。
通過Proxy類的newInstance(…)方法可以為實現了同一接口的類生成代理對象。 調用方法時需要傳遞三個參數,該方法的參數解釋可以查閱API文檔,如下。
public class ProxyUtil {public static Star createProxy(BigStar bigStar){/* newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h)參數1:用于指定一個類加載器參數2:指定生成的代理長什么樣子,也就是有哪些方法參數3:用來指定生成的代理對象要干什么事情*/// Star starProxy = ProxyUtil.createProxy(s);// starProxy.sing("好日子") starProxy.dance()Star starProxy = (Star) Proxy.newProxyInstance(ProxyUtil.class.getClassLoader(),new Class[]{Star.class}, new InvocationHandler() {@Override // 回調方法public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {// 代理對象要做的事情,會在這里寫代碼if(method.getName().equals("sing")){System.out.println("準備話筒,收錢20萬");}else if(method.getName().equals("dance")){System.out.println("準備場地,收錢1000萬");}return method.invoke(bigStar, args);}});return starProxy;}
}
調用我們寫好的ProxyUtil工具類,為BigStar對象生成代理對象
public class Test {public static void main(String[] args) {BigStar s = new BigStar("楊超越");Star starProxy = ProxyUtil.createProxy(s);String rs = starProxy.sing("好日子");System.out.println(rs);starProxy.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{@Overridepublic 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));}@Overridepublic 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));}@Overridepublic 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{@Overridepublic 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);}@Overridepublic void deleteUsers() throws Exception{System.out.println("成功刪除了1萬個用戶~");Thread.sleep(1500);}@Overridepublic 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() {@Overridepublic 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方法。
恭喜同學們,動態代理我們已經學習完了。到此整個JavaSE的課程也學習完了。