什麼是註解
註解是一種後設資料, 可以新增到java程式碼中. 類、方法、變數、引數、包都可以被註解,註解對註解的程式碼沒有直接影響.
定義註解用的關鍵字是 @interface
為什麼要引入註解
在Annotation之前,XML被廣泛的應用於描述後設資料。但是XML是鬆耦合的而且維護比較麻煩。 有時使用一些和程式碼緊耦合的東西更加合適(比如一些服務),Annotation應運而生,而且它更加方便維護。
目前,許多框架將XML和Annotation兩種方式結合使用,平衡兩者之間的利弊。例如ButterKnife, EventBus, Retrofit, Dagger等
註解是如何工作的
Annotations僅僅是後設資料,和業務邏輯無關。也就是說Annotations只是指定了業務邏輯,它的使用者來 完成其業務邏輯,JVM便是它的使用者,它工作在位元組碼層面.
當然,前端編譯生成位元組碼階段,編譯器針對註釋做了處理,如果有註解錯誤等,無法正常編譯成位元組碼.只有成功編譯生成位元組碼後.在執行期JVM就可以進行業務邏輯處理.
元註解
java內建的註解有Override, Deprecated, SuppressWarnings等, 作用相信大家都知道.
元註解就是用來定義註解的註解.其作用就是定義註解的作用範圍, 使用在什麼元素上等等
JDK5.0版本開始提供註解支援: @Documented、@Retention、@Target、@Inherited
@Documented : 是否會儲存到 Javadoc 文件中。
@Retention : 定義該註解的生命週期。 它有三個列舉型別: RetentionPolicy.SOURCE(只在原始碼中可用)、 RetentionPolicy.CLASS(在原始碼和位元組碼中可用,註解預設使用這種方式)、 RetentionPolicy.RUNTIME(在原始碼,位元組碼,執行時均可用,我們自定義的註解通常使用這種方式) Tips : RetentionPolicy.SOURCE – 在編譯階段丟棄。這些註解在編譯結束之後就不再有任何意義,所以它們不會寫入位元組碼。 @Override, @SuppressWarnings都屬於這類註解
@Target : 表示該註解用於什麼地方。如果不明確指出,該註解可以放在任何地方。以下是一些可用的引數。 Tips : 屬性的註解是相容的,你可以新增多個屬性。 ElementType.TYPE:用於描述類、介面或enum宣告 ElementType.FIELD:用於描述例項變數 ElementType.METHOD:方法 ElementType.PARAMETER引數 ElementType.CONSTRUCTOR構造器 ElementType.LOCAL_VARIABLE本地變數 ElementType.ANNOTATION_TYPE 另一個註釋 ElementType.PACKAGE 用於記錄java檔案的package資訊
@Inherited : 是否可以被繼承,預設為false
以下程式碼全部通過Idea開發
一個簡單的例子
建立一個註解類
@Retention(RetentionPolicy.RUNTIME)
public @interface SingleAnno {
String value() default "shy";
}
複製程式碼
引用它
public class MyClass {
@SingleAnno("single")
public void run(){ }
}
複製程式碼
通過反射獲取值
public class TestDemo {
@Test
public void test(){
Class<MyClass> myClass = MyClass.class;
for (Method method : myClass.getDeclaredMethods()){
SingleAnno anno = method.getAnnotation(SingleAnno.class);
if(anno != null){
System.out.println(method.getName());//列印方法名
System.out.println(anno.value());//列印註解值
}
}
}
}
複製程式碼
控制檯可以看到,輸出的是single
run
single
複製程式碼
註解定義規則
Annotations只支援基本型別、String及列舉型別。註釋中所有的屬性被定義成方法,並允許提供預設值。
自定義註解以及使用
①定義註解型別(稱為A),最好給A加上執行Retention的RUNTIME註解.預設應該是SOURCE型別. ②定義屬性,其實是方法表示.提供預設值. ③在其他類方法(稱為M)等新增A註解,並給A指定屬性值. ④可以在其他地方獲取M方法,然後獲取M的註解,並獲取註解值等.
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MultiAnno {
enum Priority{HIGH,MID,LOW}
enum Status {START,PAUSE,STOP}
String name() default "TheShy";
Priority priority() default Priority.HIGH;
Status status() default Status.START;
}
複製程式碼
關於代理模式:
靜態代理:
核心: 通過聚合來實現,讓代理類持有委託類的引用即可.
一個小例子: 我們用一個隨機睡眠時間模擬火車執行的時間。如果我要計算執行時間,並且這個類無法改動.
public interface Runnable {
void running();
}
public class Train implements Runnable {
public void running() {
System.out.println("Train is running......");
int ranTime = new Random().nextInt(1000);
try {
Thread.sleep(ranTime);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
複製程式碼
這裡有很多解決方案: 例如在呼叫方法地方的前後記錄,繼承(繼承Train呼叫父類方法),聚合(新建Train2,構造方法傳入Train物件,然後呼叫running).
但是如果再增加需求:在running方法前後列印日誌,並控制執行順序,當然是用繼承還是可以實現,但是要繼續建立新的子類,導致無限擴充套件......
這時候修改聚合,使其成為靜態代理就可以完美解決這個問題: 將構造方法改傳入Runnable介面:
//代理-在方法前後列印日誌
public class TrainLogProxy implements Runnable {
private Runnable runnable;
public TrainLogProxy(Runnable runnable) {
this.runnable = runnable;
}
public void running() {
System.out.println("Train running start...");
runnable.running();
System.out.println("Train running end...");
}
}
複製程式碼
//代理-計算執行時間
public class TrainTimeProxy implements Runnable {
private Runnable runnable;
public TrainTimeProxy(Runnable runnable) {
this.runnable = runnable;
}
public void running() {
long start = System.currentTimeMillis();
runnable.running();
long end = System.currentTimeMillis();
System.out.println("run time = " + (end - start));
}
}
複製程式碼
接下來:
Train train = new Train();
//想先計算執行時間,後列印log
// TrainTimeProxy trainTimeProxy = new TrainTimeProxy(train);
// TrainLogProxy trainLogProxy = new TrainLogProxy(trainTimeProxy);
// trainLogProxy.running();
//想先列印log,後計算執行時間
TrainLogProxy trainLogProxy = new TrainLogProxy(train);
TrainTimeProxy trainTimeProxy = new TrainTimeProxy(trainLogProxy);
trainTimeProxy.running();
複製程式碼
繼承和聚合的區別:
接下來,觀察上面的類TimeProxy,在它的fly方法中我們直接呼叫了Runable->run()方法。換而言之,TrainTimeProxy其實代理了傳入的Runnable物件,這就是典型的靜態代理實現。 從表面上看,靜態代理已經完美解決了我們的問題。可是,試想一下,如果我們需要計算SDK中100個方法的執行時間,同樣的程式碼至少需要重複100次,並且建立至少100個代理類。往小了說,如果Train類有多個方法,我們需要知道其他方法的執行時間,同樣的程式碼也至少需要重複多次。因此,靜態代理至少有以下兩個侷限性問題:
- 如果同時代理多個類,依然會導致類無限制擴充套件
- 如果類中有多個方法,同樣的邏輯需要反覆實現
那麼,我們是否可以使用同一個代理類來代理任意物件呢?我們以獲取方法執行時間為例,是否可以使用同一個類(例如:TrainProxy)來計算任意物件的任一方法的執行時間呢?甚至再大膽一點,代理的邏輯也可以自己指定。比如,獲取方法的執行時間,列印日誌,這類邏輯都可以自己指定。
動態代理
核心原理 :
首先通過Proxy.newProxyInstance方法獲取代理類例項,而後可以通過這個代理類例項呼叫代理類的方法,對代理類的方法的呼叫實際上都會呼叫中介類(呼叫處理器)的invoke方法,在invoke方法中我們呼叫委託類的相應方法,並且可以新增自己的處理邏輯。
- 委託類:委託類必須實現某個介面,這裡我們實現的是Runnable介面.
- 代理類:動態生成,呼叫Proxy類的newProxyInstance方法來獲取一個代理類例項.
- 中介類:中介類必須實現InvocationHandler介面,作為呼叫處理器”攔截“對代理類方法的呼叫 步驟 :
- Proxy->newProxyInstance(infs, handler) 用於生成代理物件
- InvocationHandler:這個介面主要用於自定義代理邏輯處理
- 為了完成對被代理物件的方法攔截,我們需要在InvocationHandler物件中傳入被代理物件例項。
Runnable runnable = (Runnable) Proxy.newProxyInstance(Runnable.class.getClassLoader(), new Class[]{Runnable.class}, new InvocationHandler() {
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("before");
Object invoke = method.invoke(new Train(), args);
System.out.println("after");
return invoke;
}
});
runnable.running();
複製程式碼
以上我們就成功的通過不修改Train類在執行running()前後列印了日誌.
代理模式
代理模式最大的特點就是代理類和實際業務類實現同一個介面(或繼承同一父類),代理物件持有一個實際物件的引用,外部呼叫時操作的是代理物件,而在代理物件的內部實現中又會去呼叫實際物件的操作
Java動態代理其實內部也是通過Java反射機制來實現的,即已知的一個物件,然後在執行時動態呼叫其方法,這樣在呼叫前後作一些相應的處理
仿寫Retrofit
通過動態代理+註解,完成類似retrofit效果,在InvocationHandler的Invoke方法處獲取方法、方法註解、方法引數註解、方法引數等資訊,根據情況設定adapter,完成業務邏輯. (當然,這裡省略了adapter的動作)
- 總體思路 :通過註解中使用的引數,動態的生成Request然後由OKHttp去呼叫
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Get {
public String value();
}
複製程式碼
public interface ServerAPI {
@Get("https://www.baidu.com/")
public String getBaiduHome(@Query("type") String type);
@Post("https://www.baidu.com/update")
public String getBaiduUser(@Field("name") String name, @Field("age") String age);
}
複製程式碼
public class APICreater {
public static ServerAPI create(Class<ServerAPI> api){
ServerAPI serverAPI = (ServerAPI) Proxy.newProxyInstance(api.getClassLoader(), new Class[]{api}, new InvocationHandler() {
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
getMethodMsg(method, args);
if ("getBaiduHome".equals(method.getName())) {
return "I am getBaiduHome return by proxy";
}
if ("getBaiduUser".equals(method.getName())) {
return "I am getBaiduUser return by proxy";
}
ServerAPI obj = getAPI();
return method.invoke(obj, args);
}
});
return serverAPI;
}
private static ServerAPI getAPI() {
return new ServerAPI() {
@Override
public String getBaiduHome(String type) {
return null;
}
@Override
public String getBaiduUser(String name, String age) {
return null;
}
};
}
// 獲取了註解資訊和引數資訊,結合起來就可以實現自己的自定義方法.
private static void getMethodMsg(Method method, Object[] args) {
AnnoBean bean = new AnnoBean();
bean.setMethodName(method.getName());
Annotation[] annotations = method.getAnnotations();
for (Annotation annotation : annotations) {
if (annotation instanceof Get) {
Get getAnni = (Get) annotation;
String value = getAnni.value();
bean.setMethodAnniType("Get");
bean.setMethodAnniValue(value);
}
if (annotation instanceof Post) {
Post getAnni = (Post) annotation;
String value = getAnni.value();
bean.setMethodAnniType("Post");
bean.setMethodAnniValue(value);
}
}
bean.setMethodArgs(Arrays.asList(args));
Annotation[][] parameterAnnotations = method.getParameterAnnotations();
for (Annotation[] annotation : parameterAnnotations) {
for (Annotation annotation1 : annotation) {
if (annotation1 instanceof Field) {
List<String> list = bean.getParamAnniList();
if (list == null) {
list = new ArrayList<String>();
}
list.add("paramAnniType: field " + " value: " + ((Field) annotation1).value());
bean.setParamAnniList(list);
}
if (annotation1 instanceof Query) {
List<String> list = bean.getParamAnniList();
if (list == null) {
list = new ArrayList<String>();
}
list.add("paramAnniType: query " + " value: " + ((Query) annotation1).value());
bean.setParamAnniList(list);
}
}
}
System.out.println(bean.toString());
}
}
複製程式碼
public class TestRetrofitDemo {
@Test
public void testRetrofit(){
ServerAPI serverAPI = APICreater.create(ServerAPI.class);
String homeeeeee = serverAPI.getBaiduHome("Homeeeeee");
System.out.println("-----" + homeeeeee);
}
}
複製程式碼
最後測試一下輸出結果:
AnniBean{methodName='getBaiduHome', methodArgs=[Homeeeeee], methodAnniType='Get', methodAnniValue='https://www.baidu.com/', paramAnniList=[paramAnniType: query value: type]}
-----I am getBaiduHome return by proxy
複製程式碼
github地址 : github.com/saurylip/An…