源碼級注解

請看前言


上一篇文章我們說到運行時框架是在虛擬機(jī)運行程序時使用反射技術(shù)搭建的框架忘朝;而源碼級框架是在javac編譯源碼時,生成框架代碼或文件。源碼級別框架發(fā)生過程是在編譯期間虐块,并不會過多影響到運行效率。因此,Android等對效率性能要求較高的平臺一般使用源碼級別注解來搭建苍柏。

注解處理器


注解處理器是一個在javac中的,用來編譯時掃描和處理的注解的工具姜贡。

一個注解的注解處理器试吁,以Java代碼(或者編譯過的字節(jié)碼)作為輸入,生成文件(通常是.java文件)作為輸出楼咳。這具體的含義什么呢熄捍?你可以生成Java代碼!
生成的Java代碼是在新的.java文件中母怜,所以你并不能修改已存在的Java類治唤,例如向已有的類中添加方法。并且這些生成的Java文件糙申,會同其他普通的手動編寫的Java源代碼一樣被javac編譯宾添。

寫代碼之前


在開始寫代碼之前,我們需要了解一個叫AbstractProcessor的類柜裸。AbstractProcessor(虛處理器)缕陕,是注解處理器核心API。注解處理器需要繼承于AbstractProcessor疙挺,如下所示:

public class MyProcessor extends AbstractProcessor {

    // 這個方法主要是獲取工具類扛邑,有Elements, Types和Filer等。后面會提到
    @Override
    public synchronized void init(ProcessingEnvironment env){ }

    // 相當(dāng)于main(),寫處理的過程
    // annotations是getSupportedAnnotationTypes()的子集
    // env代表這一輪掃描后的結(jié)果,返回true則表示消費完此次掃描铐然,此輪掃描注解結(jié)束
    @Override
    public boolean process(Set<? extends TypeElement> annoations, RoundEnvironment env) { }

    // 在這里定義你的注解處理器注冊到哪些注解上
    @Override
    public Set<String> getSupportedAnnotationTypes() { }

    // 用來指定你使用的Java版本蔬崩。通常這里返回SourceVersion.latestSupported()
    @Override
      public SourceVersion getSupportedSourceVersion() { }

 }

Java 7,我們可以使用注解來代替getSupportedAnnotationTypes()和getSupportedSourceVersion()搀暑。但是因為兼容原因(特別針對Android平臺)沥阳,建議使用重載getSupportedAnnotationTypes()和getSupportedSourceVersion()的方式。

@SupportedSourceVersion(SourceVersion.latestSupported())
@SupportedAnnotationTypes({
   // 合法注解全名的集合
 })
public class MyProcessor extends AbstractProcessor {

    @Override
    public synchronized void init(ProcessingEnvironment env){ }

    @Override
    public boolean process(Set<? extends TypeElement> annoations, RoundEnvironment env) { }

}

值得一提
注解處理器是運行它自己的虛擬機(jī)JVM中自点。javac啟動一個完整Java虛擬機(jī)來運行注解處理器桐罕。這意味著我們可以使用任何你在其他java應(yīng)用中使用的的東西。可以使用依賴注入工具(Dagger等)功炮,或者其他想要的類庫溅潜。

自定義注解處理器


在自定義前,問一個問題薪伏,怎么注冊Processor到j(luò)avac中滚澜?
我們需要編譯產(chǎn)生一個類似這樣的.jar文件。(注解處理器代碼實現(xiàn)+可擴(kuò)展應(yīng)用程序)

MyProcessor.jar
    - com
        - example
            - MyProcessor.class

    - META-INF
        - services
            - javax.annotation.processing.Processor

打包進(jìn)MyProcessor.jar中的javax.annotation.processing.Processor的內(nèi)容是注解處理器的合法的全名列表嫁怀。(不明白可搜索“可擴(kuò)展應(yīng)用程序”)

com.example.MyProcessor

值得為人稱道的是博秫,Google老大哥提供了auto-service.jar,極大簡化了我們的打包操作眶掌。我們可以用它生成META-INF/services/javax.annotation.processing.Processor文件挡育。是的,我們可以在注解處理器中使用注解朴爬,只需要一個@AutoService即寒。后面會使用到。

不說廢話


場景:我們要幫一個動物學(xué)家的朋友完成觀察動物的報告召噩。于是我們寫了一個Animal接口母赵,然后實現(xiàn)了Bird,Dog具滴,F(xiàn)ish凹嘲。因為目前只觀察到了這幾種。學(xué)過設(shè)計模式的朋友會把它們抽取出來构韵,做一個工廠類周蹭,這種思路是很好的。但是疲恢,假設(shè)我們只會簡單工廠模式凶朗,就if..else..那種。那這時候我們添加一個動物显拳,就需要在工廠自動生成對應(yīng)的if語句來生成相應(yīng)實例棚愤。

于是,我們想到說使用注解處理器的方法來在編譯時自動生成有關(guān)代碼杂数。

我們可以自定義注解如下:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.SOURCE)
public @interface Factory {

    // Animal接口實現(xiàn)類
    Class<?> type();

    String id();

}

有了工廠注解后宛畦,我們先不急著放在Animal接口實現(xiàn)類上面。我們先實現(xiàn)注解處理器揍移。

@AutoService(Processor.class)
public class FactoryProcessor extends AbstractProcessor {

}

前面我們提到過@AutoService注解次和,加了此注解,可以自動生成services下的文件羊精。因此斯够,我們不需要手動編寫service,只需要關(guān)注注解處理器代碼邏輯即可喧锦。

我們需要一些工具類读规,在代碼中添加并在init()方法對其進(jìn)行初始化。

private Filer filer;
private Messager messager;

private Types typeUtils;
private Elements elementUtils;

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

接著燃少,重載getSupportedAnnotationTypes(),getSupportedSourceVersion()獲取注解支持束亏,以及源碼編譯版本支持。

@Override
public Set<String> getSupportedAnnotationTypes() {
    Set<String> annotataions = new LinkedHashSet<String>();
    annotataions.add(Factory.class.getCanonicalName());
    return annotataions;
}

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

Elements和TypeMirrors


需要你對工具類的用途大概有個印象阵具。

工具類 用途
Elements 用來處理Element的工具類
Types 用來處理TypeMirror的工具類
Filer 使用Filer你可以創(chuàng)建文件

需要注意
在注解處理的過程中碍遍,我們掃描所有的Java源文件。源代碼的每一個部分都是一個特定類型的Element阳液。它只是結(jié)構(gòu)化的文本怕敬,不是可運行的,可以像你認(rèn)識Xml一樣的方式認(rèn)識Element帐我。例如說:

package com.example;    // PackageElement

public class Foo {        // TypeElement

    private int a;      // VariableElement
    private Foo other;  // VariableElement

    public Foo () {}    // ExecuteableElement

    public void setA (  // ExecuteableElement
                 int newA   // TypeElement
                 ) {}
}

因此认臊,我們可以像Xml那樣定位到某個元素膝捞。
舉例來說,假如你有一個代表public class Foo類的TypeElement元素虽填,你可以遍歷它的孩子,如下:

TypeElement fooClass = ... ;  
for (Element e : fooClass.getEnclosedElements()){ // iterate over children  
    Element parent = e.getEnclosingElement();  // parent == fooClass
}

可以從上面看出來曹动,Element代表的是源代碼斋日。TypeElement代表的是源代碼中的類型元素,例如類墓陈。然而恶守,TypeElement并不包含類本身的信息。你可以從TypeElement中獲取類的名字贡必,但是你獲取不到類的信息熬的,例如它的父類。類信息需要通過TypeMirror獲取赊级。你可以通過調(diào)用elements.asType()獲取元素的TypeMirror押框。

搜索@Factory注解
開始處理process()的邏輯,在其中添加這樣一段:

for (Element annotatedElement:roundEnv.getElementsAnnotatedWith(Factory.class))

目的是遍歷所有被注解了@Factory的元素理逊,而我們知道TypeElement可能是class橡伞,也可能是其他,那么我們在循環(huán)中需要加以判斷:

if (annotatedElement.getKind() != ElementKind.CLASS) {
    throw new Exception(
            "Only classes can be annotated with @%s");
}
// 確認(rèn)為ElementKind.CLASS晋被,強(qiáng)制轉(zhuǎn)換為TypeElement
TypeElement typeElement = (TypeElement) annotatedElement;

為什么我們使用.getKind()而不是.instanceof()兑徘?
首先,instanceof是檢查對象和類的關(guān)系的方法羡洛。其次挂脑,接口(interface)類型也是TypeElement。所以不采用.instanceof()的做法。

優(yōu)雅的錯誤處理
可能上面的代碼給你造成一種錯覺崭闲,就是我直接跑出了異常肋联。事實上不是的,上面的代碼我寫在try..catch..塊中刁俭,并且使用

messager.printMessage(Diagnostic.Kind.ERROR, String.format(msg, args), e);

來處理異常橄仍。這是一種優(yōu)雅的錯誤處理方式。

為什么說是一種優(yōu)雅的錯誤處理方式牍戚?
在傳統(tǒng)Java應(yīng)用中我們可能就拋出一個Exception侮繁。如果你在process()中拋出一個異常,那么運行注解處理器的JVM將會崩潰(就像其他Java應(yīng)用一樣)如孝。而使用我們注解處理器的第三方開發(fā)者將會從javac中得到非常難懂的出錯信息宪哩,因為它包含自定義注解處理器的堆棧跟蹤(Stacktace)信息。因此第晰,注解處理器就有一個Messager類斋射,它能夠打印非常優(yōu)美的錯誤信息。除此之外但荤,你還可以鏈接到出錯的元素罗岖。在像IntelliJ這種現(xiàn)代的IDE(集成開發(fā)環(huán)境)中,第三方開發(fā)者可以直接點擊錯誤信息腹躁,IDE將會直接跳轉(zhuǎn)到第三方開發(fā)者項目的出錯的源文件的相應(yīng)的行桑包。當(dāng)然,這里需要之前初始化好的Messager對象纺非。

接下來哑了,我們會希望有一個類能解析typeElement并保存信息。

public class FactoryAnnotatedClass {
    private TypeElement annotatedClassElement;
    private String qualifiedSuperClassName;
    private String simpleTypeName;
    private String id;

    public FactoryAnnotatedClass(TypeElement classElement) throws IllegalArgumentException {
        this.annotatedClassElement = classElement;
        Factory annotation = classElement.getAnnotation(Factory.class);
        id = annotation.id();

        if (id == null || "".equals(id)) {
            throw new IllegalArgumentException(
                    String.format(
                            "id() in @%s for class %s is null or empty! that's not allowed",
                            Factory.class.getSimpleName(), classElement
                                .getQualifiedName().toString()));
        }

        try {
            // 這個類已經(jīng)被編譯
            Class<?> clazz = annotation.type();
            qualifiedSuperClassName = clazz.getCanonicalName();
            simpleTypeName = clazz.getSimpleName();
        } catch (MirroredTypeException mte) {
            // 這個類還沒被編譯
            DeclaredType classTypeMirror = (DeclaredType) mte.getTypeMirror();
            TypeElement classTypeElement = (TypeElement) classTypeMirror.asElement();
            qualifiedSuperClassName = classTypeElement.getQualifiedName().toString();
            simpleTypeName = classTypeElement.getSimpleName().toString();
        }
    }

    /**
     * 獲取在{@link Factory#id()}中指定的id return the id
     */
    public String getId() {
        return id;
    }

    /**
     * 獲取在{@link Factory#type()}指定的類型合法全名
     *
     * @return qualified name
     */
    public String getQualifiedFactoryGroupName() {
        return qualifiedSuperClassName;
    }

    /**
     * 獲取在{@link Factory#type()}{@link Factory#type()}指定的類型的簡單名字
     *
     * @return qualified name
     */
    public String getSimpleFactoryGroupName() {
        return simpleTypeName;
    }

    /**
     * 獲取被@Factory注解的原始元素
     */
    public TypeElement getTypeElement() {
        return annotatedClassElement;
    }
}

解析并保存信息烧颖,接著需要有一個生成工廠類的類弱左。

public class FactoryGroupedClasses {
    /**
     * 將被添加到生成的工廠類的名字中
     */
    private static final String SUFFIX = "Factory";

    private String qualifiedClassName;

    private Map<String, FactoryAnnotatedClass> itemsMap = new LinkedHashMap<String, FactoryAnnotatedClass>();

    public FactoryGroupedClasses(String qualifiedClassName) {
        this.qualifiedClassName = qualifiedClassName;
    }

    public void add(FactoryAnnotatedClass toInsert) throws Exception {

        FactoryAnnotatedClass existing = itemsMap.get(toInsert.getId());
        if (existing != null) {
            throw new Exception(existing + " is existed.");
        }

        itemsMap.put(toInsert.getId(), toInsert);
    }

    public void generateCode(Elements elementUtils, Filer filer) throws IOException {
        TypeElement superClassName = elementUtils.getTypeElement(qualifiedClassName);
        String factoryClassName = superClassName.getSimpleName() + SUFFIX;

        // 通過filer生成.java文件
        JavaFileObject jfo = filer.createSourceFile(qualifiedClassName + SUFFIX);
        Writer writer = jfo.openWriter();
        JavaWriter jw = new JavaWriter(writer);

        // 寫包名
        PackageElement pkg = elementUtils.getPackageOf(superClassName);
        if (!pkg.isUnnamed()) {
            jw.emitPackage(pkg.getQualifiedName().toString());
            // 空一行
        jw.emitEmptyLine();
        } else {
            jw.emitPackage("");
        }

        // public class factoryClassName
        jw.beginType(factoryClassName, "class", EnumSet.of(Modifier.PUBLIC));
        jw.emitEmptyLine();
    
        // public qualifiedClassName create(String id)
        jw.beginMethod(qualifiedClassName, "create",EnumSet.of(Modifier.PUBLIC), "String", "id");

        jw.beginControlFlow("if (id == null)");
        jw.emitStatement("throw new IllegalArgumentException(\"id is null!\")");
        jw.endControlFlow();

        for (FactoryAnnotatedClass item : itemsMap.values()) {
            jw.beginControlFlow("if (\"%s\".equals(id))", item.getId());
            jw.emitStatement("return new %s()", item.getTypeElement().getQualifiedName().toString());
            jw.endControlFlow();
            jw.emitEmptyLine();
        }

        jw.emitStatement("throw new IllegalArgumentException(\"Unknown id = \" + id)");
        jw.endMethod();
        jw.endType();
        jw.close();
    }
}

基本的思路整理清晰后,我們回到process()方法的循環(huán)中炕淮。

FactoryAnnotatedClass annotatedClass = new FactoryAnnotatedClass(typeElement);
...
// 從map集合取數(shù)據(jù)
FactoryGroupedClasses factoryClass = factoryClasses.get(annotatedClass.getQualifiedFactoryGroupName());
if (factoryClass == null) {
    String qualifiedGroupName = annotatedClass.getQualifiedFactoryGroupName();
    //初始化生成工廠類的類
    factoryClass = new FactoryGroupedClasses(qualifiedGroupName);
    factoryClasses.put(qualifiedGroupName, factoryClass);
}

在循環(huán)結(jié)束后拆火,我們這么調(diào)用

// Generate code
for (FactoryGroupedClasses factoryClass : factoryClasses.values()) {
    // 生成工廠類
    factoryClass.generateCode(elementUtils, filer);
}
// 打個標(biāo)記  這里要注意
factoryClasses.clear();

官方j(luò)avadoc定義處理過程如下:
注解處理過程是一個有序的循環(huán)過程。在每次循環(huán)中涂圆,一個處理器可能被要求去處理那些在上一次循環(huán)中產(chǎn)生的源文件和類文件中的注解们镜。第一次循環(huán)的輸入是運行此工具的初始輸入。
就是說润歉,processor會在執(zhí)行完process()后模狭,生成工廠類,然后再執(zhí)行process()以檢查生成的文件是否包含@Factory標(biāo)記踩衩,第三次執(zhí)行發(fā)現(xiàn)為none嚼鹉,返回贩汉。因此,上面的邏輯是process()會被調(diào)用三次锚赤。
因此匹舞,如果不對數(shù)據(jù)進(jìn)行clear,會重復(fù)生成第一次生成的文件并且報錯宴树。

另外策菜,需要注意的是上面提到一個叫做JavaWriter的類晶疼,這是apache的工具類酒贬。只不過在工程中我把它復(fù)制出來了。很多處理器翠霍、庫锭吨、工具都依賴于JavaWriter。目前已經(jīng)被JavaPoet取代寒匙。有空可以看看零如。

使用自定義好的注解處理器


使用Maven編譯。把a(bǔ)nnotation.jar和processor.jar放到builpath中锄弱,javac會自動檢查和讀取javax.annotation.processing.Processor中的內(nèi)容考蕾,并且注冊MyProcessor作為注解處理器。

annotation_eclipse.png

為什么要分別打包会宪?
在開發(fā)過程中肖卧,第三方開放著僅僅需要processor產(chǎn)生需要的代碼,而并不希望它跟隨源代碼一起打包掸鹅。因此塞帐,一起打包不合適!
如果你是一個Android的開發(fā)者巍沙,你肯定聽說過65k個方法的限制(即在一個.dex文件中葵姥,只能尋址65000個方法)。如果你在FactoryProcessor中使用guava句携,并且把注解和處理器打包在一個包中榔幸,這樣的話,Android APK安裝包中不只是包含F(xiàn)actoryProcessor的代碼矮嫉,而也包含了整個guava的代碼牡辽。Guava有大約20000個方法。所以分開注解和處理器是非常有意義的敞临。

補(bǔ)充Animal的實現(xiàn)類:

@Factory(id = "bird", type = Animal.class)
public class Bird implements Animal {

    @Override
    public void doSomething() {
        System.out.println("fly");
    }
}

寫main()方法入口:

public static void main(String[] args) {
    new AnimalFactory().create("dog").doSomething();
}

打開AnimalFactory态辛,

annotation_factory.png

生成的代碼符合我們的預(yù)期要求。成功挺尿!

說說題外話


如果你是一個Android的開發(fā)者奏黑,你應(yīng)該非常熟悉一個叫做ButterKnife的注解處理器炊邦。在ButterKnife中,你使用@InjectView注解Android的View熟史。ButterKnifeProcessor生成一個MyActivity$$ViewInjector馁害,但是在ButterKnife你不需要手動調(diào)用new MyActivity$$ViewInjector()實例化一個ButterKnife注入的對象,而是使用Butterknife.inject(activity)蹂匹。ButterKnife內(nèi)部使用反射機(jī)制來實例化MyActivity$$ViewInjector()對象:

try {  
    Class<?> injector = Class.forName(clsName + "$$ViewInjector");
} catch (ClassNotFoundException e) { 
    ...
}

上一篇文章我們提到反射影響性能碘菜,使用注解處理來生成本地代碼,會不會導(dǎo)致很多的反射性能的問題限寞?
的確忍啸,反射機(jī)制的性能確實是一個問題。然而它并不需要手動去創(chuàng)建對象履植,確實提高了開發(fā)者的開發(fā)速度计雌。ButterKnife中有一個哈希表HashMap來緩存實例化過的對象。所以MyActivity$$ViewInjector只是使用反射機(jī)制實例化一次玫霎,第二次需要MyActivity$$ViewInjector的時候凿滤,就直接沖哈希表中獲得。
FragmentArgs非常類似于ButterKnife庶近。它使用反射機(jī)制來創(chuàng)建對象翁脆,而不需要開發(fā)者手動來做這些。FragmentArgs在處理注解的時候生成一個特別的查找表類(其實就是一種哈希表)鼻种,所以整個FragmentArgs庫只是在第一次使用的時候反番,執(zhí)行一次反射調(diào)用(一旦整個Class.forName()的Fragemnt的參數(shù)對象被創(chuàng)建),后面的都是本地代碼運行了普舆。
而如果你使用過Realm的話恬口,你也能發(fā)現(xiàn)類似的細(xì)節(jié)。

總結(jié)


注解處理器是一個強(qiáng)大的工具沼侣,為第三方開發(fā)者提供了巨大的便捷性祖能。我也想提醒的是,注解處理器可以做到比我上面提到例子復(fù)雜很多的事情蛾洛。
另外养铸,如果你決定在其他類使用ElementUtils, TypeUtils和Messager,你就必須把他們作為參數(shù)傳進(jìn)去轧膘〕可以使用Dagger(一個依賴注入庫)來解決這個問題。這在上面也有提到谎碍。
對了鳞滨,我也找到一個使用gradle構(gòu)建注解處理器的例子。需要的朋友也可以參考一下蟆淀。地址如下:
http://blog.csdn.net/ucxiii/article/details/52025005

那么拯啦,關(guān)于注解的模塊到此就講完了澡匪。如果有疑問的小伙伴可以在評論區(qū)下面留言。

(以上內(nèi)容參考總結(jié)自很多文章褒链,感謝互聯(lián)網(wǎng)給了我一個學(xué)習(xí)的平臺~)

項目地址:https://github.com/walidake/Annotation_Processor

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末唁情,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子甫匹,更是在濱河造成了極大的恐慌甸鸟,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,430評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件兵迅,死亡現(xiàn)場離奇詭異抢韭,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)喷兼,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,406評論 3 398
  • 文/潘曉璐 我一進(jìn)店門篮绰,熙熙樓的掌柜王于貴愁眉苦臉地迎上來后雷,“玉大人季惯,你說我怎么就攤上這事⊥瓮唬” “怎么了勉抓?”我有些...
    開封第一講書人閱讀 167,834評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長候学。 經(jīng)常有香客問我藕筋,道長,這世上最難降的妖魔是什么梳码? 我笑而不...
    開封第一講書人閱讀 59,543評論 1 296
  • 正文 為了忘掉前任隐圾,我火速辦了婚禮,結(jié)果婚禮上掰茶,老公的妹妹穿的比我還像新娘暇藏。我一直安慰自己,他們只是感情好濒蒋,可當(dāng)我...
    茶點故事閱讀 68,547評論 6 397
  • 文/花漫 我一把揭開白布盐碱。 她就那樣靜靜地躺著,像睡著了一般沪伙。 火紅的嫁衣襯著肌膚如雪瓮顽。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,196評論 1 308
  • 那天围橡,我揣著相機(jī)與錄音暖混,去河邊找鬼。 笑死翁授,一個胖子當(dāng)著我的面吹牛拣播,可吹牛的內(nèi)容都是我干的善绎。 我是一名探鬼主播,決...
    沈念sama閱讀 40,776評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼诫尽,長吁一口氣:“原來是場噩夢啊……” “哼禀酱!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起牧嫉,我...
    開封第一講書人閱讀 39,671評論 0 276
  • 序言:老撾萬榮一對情侶失蹤剂跟,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后酣藻,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體曹洽,經(jīng)...
    沈念sama閱讀 46,221評論 1 320
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,303評論 3 340
  • 正文 我和宋清朗相戀三年辽剧,在試婚紗的時候發(fā)現(xiàn)自己被綠了送淆。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,444評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡怕轿,死狀恐怖偷崩,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情撞羽,我是刑警寧澤阐斜,帶...
    沈念sama閱讀 36,134評論 5 350
  • 正文 年R本政府宣布,位于F島的核電站诀紊,受9級特大地震影響谒出,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜邻奠,卻給世界環(huán)境...
    茶點故事閱讀 41,810評論 3 333
  • 文/蒙蒙 一笤喳、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧碌宴,春花似錦杀狡、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,285評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至八孝,卻和暖如春董朝,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背干跛。 一陣腳步聲響...
    開封第一講書人閱讀 33,399評論 1 272
  • 我被黑心中介騙來泰國打工子姜, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人楼入。 一個月前我還...
    沈念sama閱讀 48,837評論 3 376
  • 正文 我出身青樓哥捕,卻偏偏與公主長得像牧抽,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子遥赚,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,455評論 2 359

推薦閱讀更多精彩內(nèi)容

  • 什么是注解注解分類注解作用分類 元注解 Java內(nèi)置注解 自定義注解自定義注解實現(xiàn)及使用編譯時注解注解處理器注解處...
    Mr槑閱讀 1,084評論 0 3
  • Java 中的注解(Annotation) 是一個很方便的特性在Spring當(dāng)中得到了大量的應(yīng)用 , 我們也可以開...
    _秋天閱讀 8,985評論 3 22
  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理扬舒,服務(wù)發(fā)現(xiàn),斷路器凫佛,智...
    卡卡羅2017閱讀 134,696評論 18 139
  • 我沒有辜負(fù)自己的期望讲坎,在我18歲這一年,我成為了我一直以來希望成為的人類品種之一---渣男愧薛。 我很擅長給自己找...
    第九天堂閱讀 216評論 0 0
  • 你只知道公眾號可以改名了毫炉,卻不知道如何給公眾號起個好名字瓮栗。 今天打開公眾號后臺,無意中發(fā)現(xiàn)公眾號昵稱可以修改了瞄勾,各...
    華一說閱讀 6,904評論 4 21