Java 注解(Annotation)

深入理解Java注解

依赖注入是一种较流行的设计模式,在 Android开发中也有很多实用的依赖注入框架,可以帮助我们少写一些样板代码,达到各个类之间解耦的目的

一 注解

        从 JDK 5 开始, Java 增加了注解,注解是代码里的特殊标记,这些标记可以在编译、类加载、运行时被读取,并执行相应的处理。通过使用注解,开发人员可以在不改变原有逻辑的情况下,在源文件中嵌入一些补充的信息。代码分析工具、 开发工具和部署工具可以通过这些补充信息进行验证、处理或者进行部署。

1.1 注解分类

       注解分为标准注解和元注解,下面分别介绍它们。

1. 标准注解,标准注解有以下4种。

      •@Override:对覆盖超类中的方法进行标记,如果被标记的方法并没有实际覆盖超类中的方法,则编

译器会发出错误警告。

       •@Deprecated:对不鼓励使用或者已过时的方法添加注解,当编程人员使用这些方法时,将会在编译

时显示提示信息。

       •@SuppressWarnings:指示编译器去忽略注解中声明的警告。

       •@SafeVarargs: JDK 7 新增,用来声明使用了可变长度参数的方法,其在与泛型类一起使用时不会出现

类型安全问题。

2. 元注解,除了标准注解,还有元注解,它用来注解其他注解,从而创建新的注解。元注解有以下几种。

       •@Targe:注解所修饰的对象范围。

       •@Inherited:标记这个注解是继承于哪个注解类

       •@Documented:表示这个注解应该被JavaDoc工具记录(即该注解包含在用户文档中)。

       •@Retention:用来声明注解的保留策略。

•@Repeatable:JDK 8 新增,允许一个注解在同一声明类型(类、属性或方法)上多次使用。


       其中@Targe注解取值是一个ElementType 类型的数组,其中有以下几种取值,对应不同的对象范围。

       • ElementType.TYPE:能修饰类、接口或枚举类型。

       • ElementType.FIELD:能修饰成员变量。

       • ElementType.METHOD:能修饰方法。

       • ElementType.PARAMETER:能修饰参数。

       • ElementType.CONSTRUCTOR:能修饰构造方法。

       • ElementType.LOCAL_VARIABLE:能修饰局部变量。

       • ElementType.ANNOTATION_TYPE:能修饰注解。

       • ElementType.PACKAGE:能修饰包。

       • ElementType.TYPE_PARAMETER:类型参数声明。

       • ElementType.TYPE_USE:使用类型。

       其中 @Retention 注解有 3 种类型,分别表示不同级别的保留策略。

       • RetentionPolicy.SOURCE:源码级注解。注解信息只会保留在.java源码中,源码在编译后,注解信息

被丢弃,不会保留在.class中。

       • RetentionPolicy.CLASS:编译时注解。注解信息会保留在.java 源码以及.class 中。当运行 Java 程序时,

JVM 会丢弃该注解信息,不会保留在 JVM 中。

       • RetentionPolicy.RUNTIME:运行时注解。当运行 Java 程序时, JVM 也会保留该注解信息,可以通过反 射获取该注解信息。

1.2 定义注解

       1.基本定义

       定义新的注解类型使用@interface关键字,这与定义一个接口很像,如下所示:

public@interfaceAnnotationDemo{

    ......

}

       定义完注解后,就可以在程序中使用该注解:

@AnnotationDemo

publicclassAnnotationTest{

    ......

}

       2. 定义成员变量

       注解只有成员变量,没有方法。注解的成员变量在注解定义中以“无形参的方法”形式来声明,其“方法名”定义了该成员变量的名字,其返回值定义了该成员变量的类型:

public@interfaceAnnotationDemo{

Stringname();

intlength();

}

      上面的代码定义了两个成员变量,定义了成员变量后,使用该注解时就应该为该注解的成员变量指定值:

publicclassAnnotationTest{

@AnnotationDemo(naem = "我是注解名字",length = 6)

publicvoidtest(){

        ......

    }

}

      也可以在定义注解的成员变量时,使用 default 关键字为其指定默认值,如下所示:

public@interfaceAnnotationDemo{

Stringname()default"我是注解名字";

intlength()default6;

}

       因为注解定义了默认值,所以使用时可以不为这些成员变量指定值,而是直接使用默认值:

publicclassAnnotationTest{

@AnnotationDemo

publicvoidtest(){

        ......

    }

}

       3. 定义运行时注解

       可以用 @Retention 来设定注解的保留策略,这 3 个策略的生命周期长度为 SOURCE < CLASS < RUNTIME。生命周期短的能起作用的地方生命周期长的一定也能起作用。一般如果需要在运行时去动态 获取注解信息,那只能用RetentionPolicy.RUNTIME;如果要在编译时进行一些预处理操作,比如生成一些辅助代码,就用 RetentionPolicy.CLASS;如果只是做一些检查性的操作,比如@Override 和 @SuppressWarnings,则可选用 RetentionPolicy.SOURCE。当设定为 RetentionPolicy.RUNTIME时,这个注解

就是运行时注解,如下所示:

@Retention(RetentionPolicy.RUNTIME)

public@interfaceAnnotationDemo {

Stringname()default"我是注解名字";

intlength()default6;

}

       4. 定义编译时注解

       同样地,如果将 @Retention 的保留策略设定为 RetentionPolicy.CLASS,这个注解就是编译时注解,如下 所示:

@Retention(RetentionPolicy.CLASS)

public@interfaceAnnotationDemo {

Stringname()default"我是注解名字";

intlength()default6;

}

       5. 定义源码时注解 

       同样地,如果将@Retention的保留策略设定为RetentionPolicy.SOURCE,这个注解就是编译时注解,如下 所示:

@Retention(RetentionPolicy.SOURCE)

public@interfaceAnnotationDemo {

Stringname()default"我是注解名字";

intlength()default6;

}

1.3 注解处理器

如果没有处理注解的工具,那么注解也不会有什么大的作用。对于不同的注解有不同的注解处理器。 虽然注解处理器的编写会千变万化,但是其也有处理标准,比如:针对运行时注解会采用反射机制处理, 针对编译时注解会采用 AbstractProcessor 来处理。本篇就针对前面讲到的运行时注解和编译时注解来编写注解处理器。


       1. 运行时注解处理器

       处理运行时注解需要用到反射机制。首先我们要定义运行时注解,如下所示:

@Documented

@Target(value = ElementType.METHOD)

@Retention(RetentionPolicy.RUNTIME)

public@interfaceGet {

String value()default"";

}

上面的代码定了@Get注解,使用了@Target(ElementType.METHOD),意味着Get注解应用于方法。接下来应用该注解,如下所示:

publicclassAnnotationTest{

@Get(value ="欢迎学习Java注解")

publicString getMessage() {

return"";

    }

}

      上面的代码为 @Get 的成员变量赋值。接下来写一个简单的注解处理器,如下所示:

publicclassAnnotationProcessor{

publicstaticvoidprocessor(){

        Method[] methods = AnnotationTest.class.getDeclaredMethods();

for(Method method:methods) {

Getget= method.getAnnotation(Get.class);

if(get!=null){

System.out.println("get.value() = " +get.value());

            }

        }

    }

}

      上面的代码用到了两个反射方法: getDeclaredMethods 和 getAnnotation, 调用 getAnnotation 方法返回指定类型的注解对象,也就是Get 。最后调用 Get 的 value 方法返回从 Get对象中提取元素的值。在MainActivity中调用AnnotationProcessor 的processor()方法,代码如下:

publicclassMainActivityextendsAppCompatActivity{

@Override

protectedvoid onCreate(BundlesavedInstanceState) {

super.onCreate(savedInstanceState);

setContentView(R.layout.activity_main);

AnnotationProcessor.processor();

    }

}

 输出结果如下:


 2. 编译时注解处理器

   ( 1 )定义注解 这里首先在项目中新建一个Java Library 来专门存放注解,这个 Library 名为 annotations 。接下来定义注

解,如下所示:

@Documented

@Target(value = ElementType.FIELD)

@Retention(value = RetentionPolicy.CLASS)

public@interfaceBindView {

intvalue()default1;

}

      上面代码中定义的注解类似于 ButterKnife 的 @BindView 注解。


    ( 2 )编写注解处理器

      我们在项目中再新建一个Java Library 来存放注解处理器,这个 Library 名为 processor 。我们来配置 processor库的 build.gradle:

apply plugin:'java-library'

dependencies{

implementation fileTree(dir:'libs',include: ['*.jar'])

implementationproject(path:':annotaions')

}

sourceCompatibility="1.7"

targetCompatibility ="1.7"

      接下来编写注解处理器 ClassProcessor ,它继承 AbstractProcessor ,如下所示:

publicclassClassProcessorextendsAbstractProcessor{

@Override

publicsynchronizedvoidinit(ProcessingEnvironment processingEnvironment){

super.init(processingEnvironment);

    }

@Override

publicbooleanprocess(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment){

        ...

returnfalse;

    }

@Override

publicSetgetSupportedAnnotationTypes(){

Set annotations =newLinkedHashSet<>();

        annotations.add(BindView.class.getCanonicalName());

returnannotations;

    }

@Override

publicSourceVersiongetSupportedSourceVersion(){

returnSourceVersion.latestSupported();

    }

}

       process 方法的实现会在后文讲到,这里首先分别介绍这 4 个方法的作用。

       • init:被注解处理工具调用,并输入 ProcessingEnviroment 参数。 ProcessingEnviroment 提供很多有用的 工具类,比如Elements 、 Types 、 Filer 和 Messager 等。

       • process:相当于每个处理器的主函数 main (),在这里写你的扫描、评估和处理注解的代码,以及生成Java 文件。输入参数 RoundEnviroment ,可以让你查询出包含特定注解的被注解元素。

       • getSupportedAnnotationTypes:这是必须指定的方法,指定这个注解处理器是注册给哪个注解的。注 意,它的返回值是一个字符串的集合,包含本处理器想要处理的注解类型的合法全称。

• getSupportedSourceVersion:用来指定你使用的 Java 版本,通常这里返回 SourceVersion.latestSupported()。


       在 Java 7 以后,也可以使用注解来代替 getSupportedAnnotationTypes 方法和 getSupportedSourceVersion 方 法,如下所示:

@SupportedAnnotationTypes(value ="com.lx.annotaions.BindView")

@SupportedSourceVersion(SourceVersion.RELEASE_8)

publicclassClassProcessorextendsAbstractProcessor{

@Override

public synchronized void init(ProcessingEnvironmentprocessingEnvironment) {

super.init(processingEnvironment);

    }

@Override

public boolean process(Set set,RoundEnvironmentroundEnvironment) {

        ...

returnfalse;

    }

}

但是考虑到 Android 兼容性的问题,这里不建议采用这种注解的方式。接下来编写还未实现的 process 方法,如下所示:

@Override

publicboolean process(Setset, RoundEnvironment roundEnvironment) {

    Messager messager = processingEnv.getMessager();

for(Element element : roundEnvironment.getElementsAnnotatedWith(BindView.class)) {

if(element.getKind() == ElementKind.FIELD) {

messager.printMessage(Diagnostic.Kind.NOTE,"printMessage:"

                    + element.toString());

        }

    }

returnfalse;

}

       这里用到 Messager 的 printMessage 方法来打印出注解修饰的成员变量的名称,这个名称会在 Android Studio的 Gradle Console 窗口中打印出来。


     ( 3 )注册注解处理器

       为了能使用注解处理器,需要用一个服务文件来注册它,我们 可以使用 Google 开源的 AutoService ,它用来帮助开发者注册注解处理器。首先我们添加该开源库,在processor 的 build.gradle中 直接添加如下代码:

dependencies{

    ......

implementation('com.google.auto.service:auto-service:1.0-rc2')

}

       最后在注解处理器 ClassProcessor 中添加 @AutoService ( Processor.class )就可以了:

@AutoService(Processor.class)

publicclassClassProcessorextendsAbstractProcessor{

    ......

}

     ( 4 )应用注解

       接下来在我们的主工程项目( app )中引用注解。首先要在主工程项目的 build.graldle中引用 annotations 和processor 这两个库:

dependencies{

    ......

implementationproject(path:':processor')

implementationproject(path:':annotaions')

}

接下来在MainActivity中应用注解,如下所示:

publicclassMainActivityextendsAppCompatActivity{

@BindView(value =R.id.tv_text)

TextViewtv_text;

@Override

protectedvoid onCreate(BundlesavedInstanceState) {

super.onCreate(savedInstanceState);

setContentView(R.layout.activity_main);

    }

}

      最后,我们先 Clean Project 再 Make Project ,在 Gradle Console 窗口中打印的结果如图 所示。

       可以发现编译时会打印出 @BindView 注解修饰的成员变量名: tv_text 。


二 注解搭配Javapoet在Android的应用

      这里通过ButterKinfe的@BindView的使用做讲解,

     1. 使用运行时注解的方式

    (1)定义@BindView注解,代码如下:

@Target(ElementType.FIELD)

@Retention(RetentionPolicy.RUNTIME)

public@interfaceBindView {

intvalue();

}

  (2)应用注解,在MainActivity中使用注解,代码如下

publicclassMainActivityextendsAppCompatActivity{

@BindView(value =R.id.tv_text)

TextViewtv_text;

@Override

protectedvoid onCreate(BundlesavedInstanceState) {

super.onCreate(savedInstanceState);

setContentView(R.layout.activity_main);

    }

}

    (3)定义注解处理器,代码如下

publicclassButterKnife{

publicstaticvoidbind(@Nullable Activity activity){

        Field[] fields = activity.getClass().getDeclaredFields();

for(inti =0; i < fields.length; i++) {

          Field field = fields[i];

if(field.isAnnotationPresent(BindView.class)){

BindView bindView = field.getAnnotation(BindView.class);

intresId = bindView.value();

              View view = activity.findViewById(resId);

field.setAccessible(true);

Class targetType = field.getType();

Class viewType = view.getClass();

if(!targetType.isAssignableFrom(viewType)) {

continue;

              }

try{

                  field.set(activity, view);

}catch(IllegalAccessException e) {

                  e.printStackTrace();

              }

          }

        }

    }

}

      (4)使用,在MainActivity中调用注解处理器完成View的绑定,代码如下

@Override

protectedvoidonCreate(Bundle savedInstanceState){

super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_main);

ButterKnife.bind(this);


tv_text.setText("欢迎学习Java注解");

    }

        运行程序之后,屏幕显示“欢迎学习Java注解”,并没有报tv_text的空指针异常,说明绑定成功。

        由于是运行时处理注解,而且需要用到反射,导致效率不高,因此建议使用编译时注解,下面讲解如果使用编译时注解完成View的绑定。

        2. 使用编译时注解的方式,

此处代码接着第一节中第三小节中2.编译时注解处理器”的代码接着往下研究

          (1)在annotations  module 中添加ViewBinder接口,代码如下

publicinterfaceViewBinder {

voidbind(T target);

}

         (2)在processor module 中修改ClassProcessor类中的process方法,代码如下

@Override

publicbooleanprocess(Set<?extendsTypeElement> set, RoundEnvironment roundEnvironment) {

// 输出打印工具

        Messager messager = processingEnv.getMessager();

// 拿到整个模块中(app)用到BindView的注解的节点

Set<?extendsElement> elementsAnnotatedWith = roundEnvironment.getElementsAnnotatedWith(BindView.class);

        // key -- activity名字

Map> map =newHashMap<>();

for(Element element : elementsAnnotatedWith) {

if(element.getKind() == ElementKind.FIELD) {

                // 获取到成员变量的节点,也就是控件

                VariableElement variableElement = (VariableElement) element;

                // 获取到activity名字

StringactivityName = variableElement.getEnclosingElement().getSimpleName().toString();

messager.printMessage(Diagnostic.Kind.NOTE,"printMessage:"+ activityName);

List variableElements = map.get(activityName);

if(variableElements ==null) {

variableElements =newArrayList<>();

                    map.put(activityName, variableElements);

                }

                variableElements.add(variableElement);

            }

        }

if(map.size() >0) {

Writer writer =null;

Iteratoriterator= map.keySet().iterator();

while(iterator.hasNext()) {

StringactivityName =iterator.next();

// 得到的是activity对应的控件

List variableElements = map.get(activityName);

// 通过空间的成员变量节点,获取到他的上一个节点,也就是类节点

Element enclosingElement = variableElements.get(0).getEnclosingElement();

// 通过类节点获取到包名

StringpackageName = processingEnv.getElementUtils().getPackageOf(enclosingElement).toString();

try{

// 生成文件的对象

                    Filer filer = processingEnv.getFiler();

// 生成java文件

JavaFileObject sourceFile = filer.createSourceFile(packageName +"."

+ activityName +"_ViewBinding");

                    writer = sourceFile.openWriter();

writer.write("package "+ packageName +";\n");

writer.write("import "+ packageName +"."+ activityName +";\n");

writer.write("import "+ ViewBinder.class.getName() +";\n");

writer.write("public class "+ activityName +"_ViewBinding<T extends "+ activityName +

"> implements ViewBinder<"+ activityName +">{\n");

writer.write("    public void bind("+ activityName +" target){\n");

// 遍历所有成员变量 添加代码

for(VariableElement variableElement : variableElements) {

// 获取到控件的名字

StringvariableName = variableElement.getSimpleName().toString();

// 获取到控件的id

intresId = variableElement.getAnnotation(BindView.class).value();

                        // 获取到这个控件的类型

                        TypeMirror typeMirror = variableElement.asType();

writer.write("        target."+ variableName +"=("+ typeMirror +")target.findViewById("+ resId +");\n");

                    }

writer.write("    }\n}\n");

}catch(IOException e) {

                    e.printStackTrace();

}finally{

if(writer !=null) {

try{

                            writer.flush();

                            writer.close();

}catch(IOException e) {

                            e.printStackTrace();

                        }

                    }

                }

            }

        }

returnfalse;

    }

       然后,我们先Clean Project再Make Project,会生成MainAcitivty_ViewBinding.java文件,内容如下图

      (3)在app module中创建ButterKnife.java 文件,调用我们生成的MainActivity_ViewBinding中的bind方法,完成View的绑定,代码如下

publicclassButterKnife{

publicstaticvoidbind(@NullableActivity activity){

String name = activity.getClass().getName() +"_ViewBinding";

try{

            Class<?> clazz = Class.forName(name);

            ViewBinder binder = (ViewBinder)clazz.newInstance();

            binder.bind(activity);

}catch(ClassNotFoundException e) {

            e.printStackTrace();

}catch(IllegalAccessException e) {

            e.printStackTrace();

}catch(InstantiationException e) {

            e.printStackTrace();

        }

    }

}

    (4)最后在MainActivity中使用@BindView注解,然后调用ButterKnife的bind方法,代码如下

publicclassMainActivityextendsAppCompatActivity{

@BindView(value =R.id.tv_text)

TextViewtv_text;

@Override

protectedvoid onCreate(BundlesavedInstanceState) {

super.onCreate(savedInstanceState);

setContentView(R.layout.activity_main);

ButterKnife.bind(this);

tv_text.setText("欢迎学习Java注解");

    }

}

       运行程序之后,屏幕显示“欢迎学习Java注解”,并没有报tv_text的空指针异常,说明绑定成功。

       在这个过程中,我们使用的Filer通过拼接的形式创建的MainActivity_ViewBinding.java文件,这里很容易出错,所以我们接下来使用Javapoet来完成MainActivity_ViewBinding.java文件的生成。

        3. 使用Javapoet完成代码生成

   JavaPoet是一个动态生成代码的开源项目,在某些情况下具有特殊用处。Github地址:https://github.com/square/javapoet

在Github上有JavaPoet的官方教程,权威且全面,因为太好了

    Javapoet的优点

JavaPoet是一款可以自动生成Java文件的第三方依赖

 简洁易懂的API,上手快

 让繁杂、重复的Java文件,自动化生成,提高工作效率,简化流程,不容易出错

    (1) Javapoet的使用,在processor module的build.gradle中添加依赖,代码如下

dependencies{

implementation fileTree(dir:'libs',include: ['*.jar'])

implementationproject(path:':annotaions')

implementation('com.google.auto.service:auto-service:1.0-rc2')

    // 引入生成代码的库

implementation'com.squareup:javapoet:1.11.1'

}

    (2)在processor module 中修改ClassProcessor类中的process方法,代码如下

@Override

publicbooleanprocess(Set<?extendsTypeElement> set, RoundEnvironment roundEnvironment) {

// 输出打印工具

        Messager messager = processingEnv.getMessager();

// 拿到整个模块中(app)用到BindView的注解的节点

Set<?extendsElement> elementsAnnotatedWith = roundEnvironment.getElementsAnnotatedWith(BindView.class);

        // key -- activity名字

Map> map =newHashMap<>();

for(Element element : elementsAnnotatedWith) {

if(element.getKind() == ElementKind.FIELD) {

                // 获取到成员变量的节点,也就是控件

                VariableElement variableElement = (VariableElement) element;

                // 获取到activity名字

StringactivityName = variableElement.getEnclosingElement().getSimpleName().toString();

messager.printMessage(Diagnostic.Kind.NOTE,"printMessage:"+ activityName);

List variableElements = map.get(activityName);

if(variableElements ==null) {

variableElements =newArrayList<>();

                    map.put(activityName, variableElements);

                }

                variableElements.add(variableElement);

            }

        }

if(map.size() >0) {

Writer writer =null;

Iteratoriterator= map.keySet().iterator();

while(iterator.hasNext()) {

StringactivityName =iterator.next();

// 得到的是activity对应的控件

List variableElements = map.get(activityName);

// 通过空间的成员变量节点,获取到他的上一个节点,也就是类节点

Element enclosingElement = variableElements.get(0).getEnclosingElement();

// 通过类节点获取到包名

StringpackageName = processingEnv.getElementUtils().getPackageOf(enclosingElement).toString();

                ClassName activityClassName = ClassName.bestGuess(activityName);

ClassName viewBuild = ClassName.get(ViewBinder.class.getPackage().getName(), ViewBinder.class.getSimpleName());

                //构造一个方法

MethodSpec.Builder method = MethodSpec.methodBuilder("bind")//名称

.addModifiers(Modifier.PUBLIC)//修饰

.returns(void.class)                                  //返回

.addAnnotation(Override.class)

.addParameter(activityClassName,"target", Modifier.FINAL);//参数

// 遍历所有成员变量 添加方法体

for(VariableElement variableElement : variableElements) {

// 获取到控件的名字

StringvariableName = variableElement.getSimpleName().toString();

// 获取到控件的id

intresId = variableElement.getAnnotation(BindView.class).value();

                    // 获取到这个控件的类型

                    TypeMirror typeMirror = variableElement.asType();

method.addStatement("target.$L=($L)target.findViewById($L)",variableName,typeMirror,resId);

                }

//构造一个类

TypeSpec Class_ViewBinding = TypeSpec.classBuilder(activityName +"_ViewBinding")//名称

.addModifiers(Modifier.PUBLIC)//修饰

.addMethod(method.build())//方法

.addTypeVariable(TypeVariableName.get("T",activityClassName))

                        .addSuperinterface(ParameterizedTypeName.get(viewBuild,activityClassName))

                        .build();

//生成一个Java文件

                JavaFile javaFile = JavaFile.builder(packageName, Class_ViewBinding)

                        .build();

try{

//将java写到当前项目中

javaFile.writeTo(System.out);//打印到命令行中

                    javaFile.writeTo(processingEnv.getFiler());

}catch(IOException e) {

                    e.printStackTrace();

                }

            }

        }

returnfalse;

    }

       然后,我们先Clean Project再Make Project,会生成MainAcitivty_ViewBinding.java文件,内容如下图

后续操作跟用Filer生成MainAcitivty_ViewBinding.java文件一样了,这里就不在说明了。


参考

链接:https://blog.csdn.net/lixiong0713/article/details/106549704?utm_medium=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-2.control&depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-2.control

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 200,961评论 5 473
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,444评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,009评论 0 333
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,082评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,101评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,271评论 1 278
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,738评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,395评论 0 255
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,539评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,434评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,481评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,160评论 3 317
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,749评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,816评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,038评论 1 256
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,548评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,140评论 2 341

推荐阅读更多精彩内容