夯實 Java 基礎 - 注解

cbz2w-v6k6p.png

夯實 Java 基礎 - 注解

不知道大家有沒有一種感覺,當你想要了解某個知識點的時候,就會發現好多技術類 APP 或者公眾號在推一些關于這個知識點的文章。也許這就是大數據的作用,這也說明總有人比你搶先一步。學習不能停滯,要不你就會被別人越落越遠。

本文接著來回顧和總結 Java 基礎中注解的知識點和簡單的使用,同樣本文將從以下幾個方面來回顧注解知識:

  1. 注解的定義
  2. 注解的語法
  3. 源碼級別的注解的使用
  4. 運行時注解的使用
  5. 編譯時注解的使用
  6. Android 預置的注解

注解的定義

注解(Annotation),也叫元數據。一種代碼級別的說明。它是 JDK 1.5 以后版本引入的一個特性,與類、接口、枚舉是在同一個層次。它可以聲明在包、類、字段、方法、局部變量、方法參數等元素上。它提供數據用來解釋程序代碼,但是注解并非是所解釋的代碼本身的一部分。注解對于代碼的運行效果沒有直接影響

注解有許多用處,主要如下:

  • 提供信息給編譯器: 編譯器可以利用注解來探測錯誤和警告信息
  • 編譯階段時的處理: 軟件工具可以用來利用注解信息來生成代碼、Html 文檔或者做其它相應處理。
  • 運行時的處理: 某些注解可以在程序運行的時候接受代碼的提取

如我們所熟知的依賴注入框架 ButterKnife 就是在編譯階段來生成 findViewById 的代碼(文件)的,而我們所見過的 @Deprecated 就是提供信息給編輯器的RetentionPolicy.SOURCE類型注解,說明這個屬性已經過時的,對于運行時的注解在反射的文章的最后我們也舉了個小例子,說明了它的作用。

在自定義了一個編譯或者運行階段的注解后,需要一個開發者編寫相應的代碼來解釋這些注解,從而來發揮注解的作用。這些用來解釋注解的代碼被統稱為是 APT(Annotation Processing Tool)。換句話說注解其實是給 APT 或者編輯器來使用的,而對于非框架開發人員的我們我們只需要關注注解的使用,并遵守規則即可,從而我們節省了很多代碼提高了效率。

但是凡事如果只滿足于用上,就不算是一個合 (tong) 格 (guo) 程 (mian)序 (shi) 員 (de)! 但是不要慌,當你打開這篇文章的時候你已經離 offer 又進了一步。

注解的語法

注解的聲明

注解的聲明和聲明一個接口十分類似,沒錯只是名字很類似~ 我們使用@interface 來聲明一個注解,如我們最常見的Override 注解的聲明

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}

注解聲明的修飾符,可以是 private,public, protected 或者默認 default 這一點跟定義一個類或者接口相同。

在聲明一個注解的時候我們常常需要一些其他注解來修飾和限制該定義注解的使用和運行方式。上述的 @Target@Retention 就是如此,我們稱之為元注解,詳細的元注解在下邊說明。

注解成員

注解跟一個類相似,它們并不是都是像上面的 @Override一樣只有聲明。一個類大概可以包含構造函數,成員變量,成員函數等,而一個注解只能包含注解成員,注解成員的聲明格式為:

類型 參數名() default 默認值;

注解成員可以是:

  1. 基本類型 byte,short,int,long,float,double,boolean 八種基本類型及這些類型的數組, 注意這里沒對應基本數據類型的包裝類。

  2. String,Enum,Class,annotations 及這些類型的數組

  3. 注解的成員修飾符只能是 public 或默認(default)

  4. 注解元素必須有確定的值,可以在注解中定義默認值,也可以使用注解時指定。即我們在定義注解的時候聲明的成員,可以不賦值,但是就跟抽象函數一樣,在使用的時候就必須指定。

如:

public @interface TestAnnotation {

   String value() default "";

   String[] values();

   int id() default -1;

   int[] ids();

   // 錯誤的不能使用包裝類 以及自定義類型
   // Integer idInt();
   // Apple apple();

   enum Color {BULE, RED, GREEN}
   Color testEnum() default Color.BULE;
   Color[] testEnums();
   
   //注解類型成員 注解元素必須有確定的值,可以在注解中定義默認值,也可以使用注解時指定
   FruitName fruitName() default @FruitName("apple");
}
    
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
protected @interface FruitName {
   String value();
   String alias() default "no alias";
}

元注解

我們在 Override 注解的聲明中可以看到還有注解修飾著如@Target(ElementType.METHOD),我們講元注解理解為修飾注解定義的注解。換句話說元注解為 JDK 提供給我們的一些基本注解,我們使用元注解來定義一個注解是如何工作的。

JDK 1.8 中存在的元注解有以下 5 種:

@Target, @Retention、@Documented、@Inherited、@Repeatable

下面我們依次來說明這幾種類型的注解是如何使用的。

@Target 元注解

@Target 指定了被修飾的注解運用的地方,這些 "地方" 定義在 ElementType 類中,包括:

  1. ElementType.ANNOTATION_TYPE 可以給一個注解進行注解
  2. ElementType.CONSTRUCTOR 可以給構造方法進行注解
  3. ElementType.FIELD 可以給屬性進行注解
  4. ElementType.LOCAL_VARIABLE 可以給局部變量進行注解
  5. ElementType.METHOD 可以給方法進行注解
  6. ElementType.PACKAGE 可以給一個包進行注解
  7. ElementType.PARAMETER 可以給一個方法內的參數進行注解
  8. ElementType.TYPE 可以給一個類型進行注解,比如類、接口、枚舉

其中 METHODPARAMETERFIELD 最為常見,如 Override 注解被 @Target(ElementType.METHOD) 修飾,如果我們想要標記一個參數不能為空則可以使用 @NonNull 去修飾一個 param, FIELD 用來指定注解只能用來修飾成員變量如我們經常使用的 @BindView

值得注意的是 @Target 元注解定義如下,

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Target {
    ElementType[] value();
}

它內部的成員為ElementType[] 數組也就是說,我們可以同時指定一個注解可以用于很多地方。如 @ColorRes 的注解的元注解為@Target({METHOD, PARAMETER, FIELD, LOCAL_VARIABLE})

@Retention 元注解

Retention 翻譯過來是保留期的意思。當@Retention 用于修飾一個注解上的時候,它規定了了被修飾的注解應用的時期,或者存活的時期

它可以有如下 3 種取值:

  • RetentionPolicy.SOURCE 注解只在源碼階段保留,在編譯器進行編譯時它將被丟棄。
  • RetentionPolicy.RUNTIME 注解可以保留到程序運行的時候,它會被加載進入到 JVM 中,所以在程序運行時通過反射獲取到它們,并解釋他們。
  • RetentionPolicy.CLASS 注解只被保留到編譯進行的時候,它并不會被加載到 JVM 中。

源碼級別注解 RetentionPolicy.SOURCE

對于第一種 RetentionPolicy.SOURCE 注解只在源碼階段保留,更多的效果時做一些編譯檢查,在 Android 中有個為 @IntDef 的注解,他可以和常量組合一起 代替枚舉 enum 做參數限制作用,來優化內存使用。

這里說只是替代了參數限制作用,而 JDK 1.5 為我們帶來的 enum 的作用不只是簡單的參數限制作用作用,對于 Enum 更多優雅使用可以參考 《Effective Java》。

@IntDef 的注解定義如下:

@Retention(SOURCE)
@Target({ANNOTATION_TYPE})
public @interface IntDef {
    long[] value() default {};
    boolean flag() default false;
}

如我們常用的設置一個 View 的可見屬性就使用了 @IntDef 注解來保證使用者傳入的參數是對的,如下:

@IntDef({VISIBLE, INVISIBLE, GONE})
@Retention(RetentionPolicy.SOURCE)
public @interface Visibility {}

@RemotableViewMethod
public void setVisibility(@Visibility int visibility) {
        setFlags(visibility, VISIBILITY_MASK);
}

//設置一個 View 的屬性:
...
toolbar.setVisibility(View.VISIBLE);// it is Ok

//toolbar.setVisibility(1000);// 如果我們隨便寫一個數值 那么編輯器將會報錯 

運行期時的注解 RetentionPolicy.RUNTIME

源碼級別的注解對我們的編碼約束,運行期注解與之不同的是,如果要是讓該注解生效,我們必須要編寫一定的代碼去將定義好的注解,在運行中"注入"應用中,看到運行時注入就可以應該能想得起反射,是的注入這個操作就是需要開發人員自己編寫的。

另外,我們也都了解,在運行反射的時候效率是無法保證的。因為反射將遍歷對應類的 Class 文件來獲取相應的信息。所以運行時注解,并不是那么廣泛被運用,而稍后我們要說明的編譯期注解則不會對程序的運行造成效率的影響,因此應用更廣泛一些。

我們來試著寫一個 Dota 英雄名稱的運行期注解來了解下他的運作方式:

/**
* 定義一個注解表示英雄的名字
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
private @interface HeroName {
   String value();

   String alias();
}

/**
* 定義一個類包含英雄名稱的屬性
*/
public class Hero {
    // 定義注解的時候沒有 deflaut 屬性名稱所以在使用的時候必須賦值
   @HeroName(value = "Spirit Walker", alias = "SB")
   private String heroName;

   public void setHeroName(String heroName) {
       this.heroName = heroName;
   }

   public String getHeroName() {
       return heroName;
   }
}

ok 聲明就是這么簡單,那么如何讓一個屬性生效呢,這時候我們就需要一個注解處理方法。為了方便觀察運行注解的結果,所以我們這個處理方法選擇傳遞一個 Hero 對象,不過你為了更通用也可以不用這么做。

/** 運行時注解處理方法*/
public static void getHeroNameInfo(Hero hero) {
   try {
       Class<? extends Hero> clazz = hero.getClass();
       Field field = clazz.getDeclaredField("heroName");
       // Field isAnnotationPresent 判斷一個屬性是否被對應的注解修飾
       if (field.isAnnotationPresent(HeroName.class)) {
           //field.getAnnotation 獲取屬性的注解
           HeroName fruitNameAnno = field.getAnnotation(HeroName.class);
           hero.setHeroName("name = " +fruitNameAnno.value() +" alias = " + fruitNameAnno.alias());
       }
   } catch (NoSuchFieldException e) {
       e.printStackTrace();
   }
}

下面我們來運行下程序測試下:

public static void main(String[] args) {
   Hero hero = new Hero();
   getHeroNameInfo(hero);
   System.out.println("hero = " + hero);
}

運行結果:

hero = Hero{heroName='name = Spirit Walker alias = SB'}

通過上述的例子,可以了解運行時注解就是這樣聲明和運用的。相信 SB 這個別名更容易讓大家記得這個例子(白牛這個英雄其實很好玩,只是別名...)。

編譯時期的注解 RetentionPolicy.CLASS

經過運行時注解的了解,相比對于注解應該都有一個大概的了解了。接下來到了編譯時注解,這個注解類型,便是眾多工具庫中應用的注解類型,它不會影響運行時的效率問題,而是在編譯期,或者打包過程中就生成了對應的代碼,在運行時將會生效。如我們常見的 ButterKnifeEventBus

編譯時注解與運行時注解不同,編譯時注解主要是幫助我們在編譯器編譯期使用注解處理器生成相應的代碼,幫我們解放勞動力。

我們知道運行時注解是通過反射來解釋對應注解并使注解生效的,那么編譯時如何解釋對應的注解呢?這里就需要用到注解處理器的知識了。

注解處理器(Annotation Processor)是javac的一個工具,它用來在編譯時掃描和處理注解(Annotation)。你可以自定義注解,并注冊相應的注解處理器(自定義的注解處理器需繼承自AbstractProcessor)。

Java 中提供給我們了注解處理器實現方法,主要是通過實現一個名為 AbstractProcessor 的注解處理器基類。該抽象類要求我們必須實現 process 方法來定義處理邏輯。下邊我們來看下注解處理器中的幾個方法的作用:

public class NameProcessor extends AbstractProcessor {
    
    //會被注解處理工具調用,并輸入ProcessingEnviroment參數。ProcessingEnviroment提供很多有用的工具類如Elements, Types和Filer等
    @Override
    public synchronized void init(ProcessingEnvironment env){ }
    
   //返回最高所支持的java版本, 如返回 SourceVersion.latestSupported();
    @Override
    public SourceVersion getSupportedSourceVersion() { }
    
    //一個注解處理器可能會處理多個注解邏輯,這個方法將返回待處理的注解類型集合,返回值作為參數傳遞給 process 方法。
    @Override
    public Set<String> getSupportedAnnotationTypes() { }

    //process 函數就是我們處理待處理注解的地方了,我們需要在這里編寫生成 java 文件的具體邏輯。 方法返回布爾值類型,表示注解是否已經處理完成。一般情況下我們返回 true 即可。
    @Override
    public boolean process(Set<? extends TypeElement> annoations, RoundEnvironment env) { }
}

注解處理器的處理步驟主要有以下:

  1. 編譯器開始執行注解處理器
  2. process 方法中循環處理注解元素(Element),找到被該注解所修飾的類,方法,或者屬性
  3. 拿上一步得到的備注注解修飾的類或者屬性,方法,生成對應的輔助類,并寫入 java 文件
  4. 生成 java 文件后就可以在運行時,在程序中獲取并調用對應的輔助方法,如 ButterKnife.bind(this); 方法就是獲取對應 Activity 的注解處理器生成的java 文件,并執行了構造函數。

自定義一個編譯時注解

自定義編譯時注解要比運行時注解要繁瑣一些。下面我們來舉一個簡單的例子,意在說明編譯時注解是如何工作的。

在 Android 中為了實現一個編譯時注解我們一般需要借助兩個三方庫:

  1. com.google.auto.service:auto-service:1.0-rc2 這是谷歌官方提供的一個注解處理注冊插件可以幫助我們更方便的注冊注解處理器,只需要在自定義的 Processor 類上方添加@AutoService(Processor.class)即可,不用自己動手執行注解處理器的注冊工作(即編寫 resource/META-INF/services/javax.annotation.processing.Processor文件)。

  2. 為了更方便的在 process 文件中生成 Java 類,需要依賴一個 Square 公司開源的 javapoet 庫,com.squareup:javapoet:1.9.0 這個庫中包裝提供了一些好用的 API 幫助我們更快更準確的構建 .java 文件。當然你也可以自己手寫拼接字符串然后寫入文件(如果你能保證正確)。

仿照 ButterKnife 的實現,我們建立一個新的 Android project ,然后創建兩個 Java Moudle,其中 processor 用來存放注解處理器,processor-lib 用來存放對應的注解,如下圖所示:

WX20180513-110801@2x.png

在注解處理器存在的lib的 build.gradle 中添加依賴關系:

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    compile project(':processor-lib')
    compile 'com.squareup:javapoet:1.9.0'
    compile 'com.google.auto.service:auto-service:1.0-rc3'
}

主 moudle 中也需要添加對 processor 和 processor -lib 的依賴:

dependencies {
    ....
   implementation project(':processor-lib')
   // 注意這里的注解處理器的依賴方式
   annotationProcessor project(':processor')
}

好了經過上述的準備我們終于能夠編寫我們的編譯時注解了:

  1. 在 processor-lib 定義一個 Name 注解如下:
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.CLASS)
public @interface Name {
    String name();
    String alias();
}
  1. 編寫兩個類使用我們定義的注解:
public class SBHero {
    @Name(name = "Spirit Walker", alias = "SB")
    private String heroName;
}

public class PAHero {
    @Name(name = "Phantom Assassin", alias = "PA")
    private String heroName;
}
  1. 在 processor 注解處理lib 下定義一個 NamePorcessor
// @AutoService(Processor.class) 幫助我們生成對應的注解處理器配置
@AutoService(Processor.class)
@SupportedSourceVersion(SourceVersion.RELEASE_8)
@SupportedAnnotationTypes("com.wangshijia.www.processor.Name")
public class NamePorcessor extends AbstractProcessor {
    //文件寫入工具類
    private Filer filer;
    //可以幫助我們在 gradle 控制臺打印信息的類
    private Messager messager;
    // 元素操作的輔助類
    private Elements elementUtils;
    //自定義文件名的后綴
    private static final String SUFFIX = "AutoGenerate";

    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
        filer = processingEnv.getFiler();
        messager = processingEnv.getMessager();
        elementUtils = processingEnv.getElementUtils();
    }

    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }

    /**
     * @return 你所需要處理的所有注解,該方法的返回值會被 process()方法所接收, 這里其實只有Name 注解,
     */
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        HashSet<String> set = new HashSet<>();
        set.add(Name.class.getCanonicalName());
        return set;
    }
    ... 
}
  1. 最后我們要編輯我們的 process 方法了,process 方法中一共進行了下面這幾件事:

    • 遍歷程序中所有被該注解修飾器處理注解修飾的元素 存放進創建的Map集合
    • 依次取出map 中的元素構建對應的類和方法
    • 構建對應的方法內容
    • 生成.java 文件 位置在 ~/app/build/generated/source/apt 目錄下
 @Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {

   String packageName= "";
   // 獲得被該注解聲明的元素
   Set<? extends Element> elememts = roundEnv.getElementsAnnotatedWith(Name.class);
   // 聲明一個存放成員變量的列表
   List<VariableElement> fields;
   //key 對應包含注解修飾元素的類的全類名 vaule 代表所有被注解修飾的變量
   Map<String, List<VariableElement>> maps = new HashMap<>();
   
   // 遍歷程序中所有被該注解修飾器處理注解修飾的元素
   for (Element ele : elememts) {
       //  ele.getKind() 獲取注解修飾的成員的類型,判斷該元素是否為成員變量
       if (ele.getKind() == ElementKind.FIELD) {
           VariableElement varELe = (VariableElement) ele;
           // 獲取該元素封裝類型
           TypeElement enclosingElement = (TypeElement) varELe.getEnclosingElement();
           // 拿到包含 enclosingElement 元素的類的名稱 樣式如 com.wangshijia.www.annotationapplication.Hero
           String key = enclosingElement.getQualifiedName().toString();
           messager.printMessage(Diagnostic.Kind.NOTE, "Printing: " + key);
           packageName = elementUtils.getPackageOf(enclosingElement).getQualifiedName().toString();
           fields = maps.get(key);
           if (fields == null) {
               maps.put(key, fields = new ArrayList<>());
           }
           fields.add(varELe);
       }
   }

   /*
    * maps 包含有所有被 @Name 修飾的類
    */
   for (String key : maps.keySet()) {
       List<VariableElement> elementFileds = maps.get(key);
       messager.printMessage(Diagnostic.Kind.NOTE, "Printing: " + key);
       messager.printMessage(Diagnostic.Kind.NOTE, "Printing: " + elementFileds);

       String className = key.substring(key.lastIndexOf(".") + 1);
       className += SUFFIX;
       // 創建 className 類
       TypeSpec.Builder classBuilder = TypeSpec.classBuilder(className)
               .addModifiers(Modifier.PUBLIC, Modifier.FINAL);
       // 創建方法
       MethodSpec.Builder methodBuild = MethodSpec.methodBuilder("printNameAnnotation")
               .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
               .returns(void.class);
       //創建方法中的打印語句
       for (VariableElement e : elementFileds) {
           Name annotation = e.getAnnotation(Name.class);
           // 創建 printNameAnnotation 方法
           methodBuild
                   .addStatement("$T.out.println($S)", System.class, e.getSimpleName() + " = " + annotation.name())
                   .addStatement("$T.out.println($S)", System.class, e.getSimpleName() + " = " + annotation.alias());

       }

       //將方法中添加到類中
       MethodSpec printNameMethodSpec = methodBuild.build();
       TypeSpec classTypeSpec = classBuilder.addMethod(printNameMethodSpec).build();

       try {
           //構造的 java 文件 參數一 包名,參數二 上述構建的類描述 TypeSpec
           JavaFile javaFile = JavaFile.builder(packageName, classTypeSpec)
                   .addFileComment(" This codes are generated automatically. Do not modify!")
                   .build();
           javaFile.writeTo(filer);
       } catch (IOException exception) {
           exception.printStackTrace();
       }
   }

上述注釋寫的很詳細了,這里希望不熟悉的朋友,自己動手實現下,才能更好的理解是如何構建對應的文件的。生成的文件位于指定目錄下:

WX20180513-122856@2x.png
  1. 使用我們定義好的注解生成文件

    使用注解生成器生成的 java 文件和普通的類沒什么區別,通過編譯后就放在上述文件夾中,我們可以正常調用我們構造類的方法,ButterKnife.bind(this) 實際上就是調用生成類的方法的過程。我們是一個簡單的 demo 就不這么復雜的調用了。直接在 App 目錄下的任意文件調用,如在一個 Activity 中:

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        
        PAHeroAutoGenerate.printNameAnnotation();
        SBHeroAutoGenerate.printNameAnnotation();
    }
}

對于 JavaPoet 生成 java 文件的過程如果想深入了解的話可以查看該博客:JavaPoet - 優雅地生成代碼

Android 預置的注解

日常開發中,注解能夠幫助我們寫出更好更優秀的代碼,為了更好地支持 Android 開發,在已有的 android.annotation 基礎上,Google 開發了 android.support.annotation 擴展包,共計50個注解,幫助開發者們寫出更優秀的程序,這五十多種注解得以應用場景各不相同,常見的如 @IntDef @ColorInt @Nullable。

對于這些注解的用途這里不再詳細說明,感興趣的可以去查看下一個朋友寫的關于 Android 中注解的作用的文章: Android 注解指南

總結

這篇文章寫的時候遇到很多的困難,因為本人對于注解之前了解情況和大多數人一樣,只停留在很少的使用階段,在文章的構成方面也是一改再改。但是功夫不負有心人,在查閱了大量的資料后,學習到了很多注解的使用和原理的知識。也發現自己的知識掌握程度已經落下不少,比如鴻洋大神寫的 Android 打造編譯時注解解析框架 這只是一個開始 這篇文章在15年的時候就有了,想想當時剛畢業,與大神的距離整整拉開了進3年,讓我去哭一會。但是個人認為這是件好事。總比一直停留在用上好一些,每次深一步了解,就感覺我跟大神之間的差距少了一些。

參考

?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,428評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,024評論 3 413
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,285評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,548評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,328評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,878評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,971評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,098評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,616評論 1 331
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,554評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,725評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,243評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,971評論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,361評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,613評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,339評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,695評論 2 370

推薦閱讀更多精彩內容

  • 本文章涉及代碼已放到github上annotation-study 1.Annotation為何而來 What:A...
    zlcook閱讀 29,216評論 15 116
  • 什么是注解(Annotation):Annotation(注解)就是Java提供了一種元程序中的元素關聯任何信息和...
    九尾喵的薛定諤閱讀 3,199評論 0 2
  • Spring Cloud為開發人員提供了快速構建分布式系統中一些常見模式的工具(例如配置管理,服務發現,斷路器,智...
    卡卡羅2017閱讀 134,775評論 18 139
  • 文章開頭先引入一處圖片。 這處圖片引自老羅的博客。為了避免不必要的麻煩,首先聲明我個人比較尊敬老羅的。至于為什么放...
    小乖心塞閱讀 360評論 1 0
  • 整體Retrofit內容如下: 1、Retrofit解析1之前哨站——理解RESTful 2、Retrofit解析...
    隔壁老李頭閱讀 6,599評論 4 31