Spring 基础(二)

上一个章节主要介绍了Spring 的基本概念之一 IOC,后面的章节将继续窥探Spring的其他核心内容。

6.Resources

6.1介绍

仅仅使用 java 标准 java.net.URL 和针对不同 URL 前缀的标准处理器并不能满足我们对各种底层资源的访问,比如:我们就不能通过 URL 的标准实现来访问相对类路径或者相对 ServletContext 的各种资源。虽然我们可以针对特定的 url 前缀来注册一个新的 URLStreamHandler(和现有的针对各种特定前缀的处理器类似,比如 http:),然而这往往会是一件比较麻烦的事情(要求了解 url 的实现机制等),而且 url 接口也缺少了部分基本的方法,如检查当前资源是否存在的方法。
相对标准 url 访问机制,spring 的 Resource 接口对抽象底层资源的访问提供了一套更好的机制。

public interface Resource extends InputStreamSource {

    boolean exists();

    boolean isOpen();

    URL getURL() throws IOException;

    File getFile() throws IOException;

    Resource createRelative(String relativePath) throws IOException;

    String getFilename();

    String getDescription();

}
  • getInputStream(): 定位并且打开当前资源,返回当前资源的 InputStream。预计每一次调用都会返回一个新的 InputStream,因此关闭当前输出流就成为了调用者的责任。
  • exists(): 返回一个 boolean,表示当前资源是否真的存在。
  • isOpen(): 返回一个 boolean,表示当前资源是否一个已打开的输入流。如果结果为 true,返回的 InputStream 不能多次读取,只能是一次性读取之后,就关闭 InputStream,以防止内存泄漏。除了 InputStreamResource,其他常用 Resource 实现都会返回 false。
  • getDescription(): 返回当前资源的描述,当处理资源出错时,资源的描述会用于错误信息的输出。一般来说,资源的描述是一个完全限定的文件名称,或者是当前资源的真实 url。

6.3 内置的 Resource 实现

6.3.1 UrlResource

UrlResource 封装了一个 java.net.URL 对象,用来访问 URL 可以正常访问的任意对象,比如文件、an HTTP target, an FTP target, 等等。所有的 URL 都可以用一个标准化的字符串来表示。如通过正确的标准化前缀,可以用来表示当前 URL 的类型,当中就包括用于访问文件系统路径的 file:,通过 http 协议访问资源的 http:,通过 ftp 协议访问资源的 ftp:,还有很多……

可以显式化地使用 UrlResource 构造函数来创建一个 UrlResource,不过通常我们可以在调用一个 api 方法是,使用一个代表路径的 String 参数来隐式创建一个 UrlResource。对于后一种情况,会由一个 javabean PropertyEditor 来决定创建哪一种 Resource。如果路径里包含某一个通用的前缀(如 classpath:),PropertyEditor 会根据这个通用的前缀来创建恰当的 Resource;反之,如果 PropertyEditor 无法识别这个前缀,会把这个路径作为一个标准的 URL 来创建一个 UrlResource。

6.3.2 ClassPathResource

可以使用 ClassPathResource 来获取类路径上的资源。ClassPathResource 可以使用线程上下文的加载器、调用者提供的加载器或指定的类中的任意一个来加载资源。

ClassPathResource 可以从类路径上加载资源,其可以使用线程上下文加载器、指定加载器或指定的 class 类型中的任意一个来加载资源。

当类路径上资源存于文件系统中,ClassPathResource 支持以 java.io.File 的形式访问,可当类路径上的资源存于尚未解压(没有 被Servlet 引擎或其他可解压的环境解压)的 jar 包中,ClassPathResource 就不再支持以 java.io.File 的形式访问。鉴于上面所说这个问题,spring 中各式 Resource 实现都支持以 jave.net.URL 的形式访问。

可以显式使用 ClassPathResource 构造函数来创建一个 ClassPathResource ,不过通常我们可以在调用一个 api 方法时,使用一个代表路径的 String 参数来隐式创建一个 ClassPathResource。对于后一种情况,会由一个 javabean PropertyEditor 来识别路径中 classpath: 前缀,从而创建一个 ClassPathResource。

6.3.3 FileSystemResource

这是针对 java.io.File 提供的 Resource 实现。显然,我们可以使用 FileSystemResource 的 getFile() 函数获取 File 对象,使用 getURL() 获取 URL 对象。

6.3.4 ServletContextResource

这是为了获取 web 根路径的 ServletContext 资源而提供的 Resource 实现。

ServletContextResource 完全支持以流和 URL 的方式访问,可只有当 web 项目是已解压的(不是以 war 等压缩包形式存在)且该 ServletContext 资源存于文件系统里,ServletContextResource 才支持以 java.io.File 的方式访问。至于说到,我们的 web 项目是否已解压和相关的 ServletContext 资源是否会存于文件系统里,这个取决于我们所使用的 Servlet 容器。若 Servlet 容器没有解压 web 项目,我们可以直接以 JAR 的形式的访问,或者其他可以想到的方式(如访问数据库)等。

6.3.5 InputStreamResource

这是针对 InputStream 提供的 Resource 实现。建议,在确实没有找到其他合适的 Resource 实现时,才使用 InputSteamResource。如果可以,尽量选择 ByteArrayResource 或其他基于文件的 Resource 实现来代替。

与其他 Resource 实现已比较,InputStreamRsource 倒像一个已打开资源的描述符,因此,调用 isOpen() 方法会返回 true。除了在需要获取资源的描述符或需要从输入流多次读取时,都不要使用 InputStreamResource 来读取资源。

6.3.6 ByteArrayResource

这是针对字节数组提供的 Resource 实现。可以通过一个字节数组来创建 ByteArrayResource。

当需要从字节数组加载内容时,ByteArrayResource 是一个不错的选择,使用 ByteArrayResource 可以不用求助于 InputStreamResource。

6.4 ResourceLoader 接口

ResourceLoader 接口是用来加载 Resource 对象的,换句话说,就是当一个对象需要获取 Resource 实例时,可以选择实现 ResourceLoader 接口。
spring 里所有的应用上下文都是实现了 ResourceLoader 接口,因此,所有应用上下文都可以通过 getResource() 方法获取 Resource 实例。

6.5 ResourceLoaderAware 接口

ResourceLoaderAware 是一个特殊的标记接口,用来标记提供 ResourceLoader 引用的对象。

public interface ResourceLoaderAware {

    void setResourceLoader(ResourceLoader resourceLoader);
}

当将一个 ResourceLoaderAware 接口的实现类部署到应用上下文时(此类会作为一个 spring 管理的 bean), 应用上下文会识别出此为一个 ResourceLoaderAware 对象,并将自身作为一个参数来调用 setResourceLoader() 函数,如此,该实现类便可使用 ResourceLoader 获取 Resource 实例来加载你所需要的资源。(附:为什么能将应用上下文作为一个参数来调用 setResourceLoader() 函数呢?不要忘了,在前文有谈过,spring 的所有上下文都实现了 ResourceLoader 接口)。

当然了,一个 bean 若想加载指定路径下的资源,除了刚才提到的实现 ResourcesLoaderAware 接口之外(将 ApplicationContext 作为一个 ResourceLoader 对象注入),bean 也可以实现 ApplicationContextAware 接口,这样可以直接使用应用上下文来加载资源。但总的来说,在需求满足都满足的情况下,最好是使用的专用 ResourceLoader 接口,因为这样代码只会与接口耦合,而不会与整个 spring ApplicationContext 耦合。与 ResourceLoader 接口耦合,抛开 spring 来看,就是提供了一个加载资源的工具类接口。

7. 数据校验、数据绑定和类型转换

Validator这个模块其实是之前接触比较少的,只是依稀在别人的代码中看到过类似的使用,这次趁机会详细看看这一块的内容~

在业务逻辑中考虑数据校验利弊参半,Spring 提供的校验(和数据绑定)方案也未能解决这个问题。 能明确的是数据校验不应该被限定在web层使用,它应该能很方便的执行本地化,并且能在任何需要 数据校验的场合以插件的形式提供服务。基于以上考虑,Spring 设计了一个既基本又方便使用且能 在所有层使用的Validator接口。

Spring 提供了我们称作DataBinder的对象来处理数据绑定,所谓的数据绑定就是将用户的输入 自动的绑定到我们的领域模型(或者说任意用来处理用户输入的对象)。Spring 的Validator和 DataBinder构成了validation包,这个包主要被Spring MVC框架使用,但绝不限于只能在该 框架使用。

在Spring中BeanWrapper是一个很基本的概念,在很多地方都有使用到它。但是,你可能从来都没有 直接使用到它。鉴于这是一份参考文档,我们认为很有必要对BeanWrapper进行必要的解释。在这一 章中我们将解释BeanWrapper,在你尝试将数据绑定到对象时一定会使用到它。

7.2

Spring 提供了Validator接口用来进行对象的数据校验。Validator接口在进行数据校验的时候 会要求传入一个Errors对象,当有错误产生时会将错误信息放入该Errors对象。

public class Person {

    private String name;
    private int age;
}
public class PersonValidator implements Validator {

    /**
     * 这个校验器*仅仅*只校验Person实例
     */
    public boolean supports(Class clazz) {
        return Person.class.equals(clazz);
    }

    public void validate(Object obj, Errors e) {
        ValidationUtils.rejectIfEmpty(e, "name", "name.empty");
        Person p = (Person) obj;
        if (p.getAge() < 0) {
            e.rejectValue("age", "negativevalue");
        } else if (p.getAge() > 110) {
            e.rejectValue("age", "too.darn.old");
        }
    }
}

ValidationUtils中的静态方法rejectIfEmpty(..)用来拒绝'name'这个属性当它为null或空字符串时。当校验一个复杂的对象时,自定义一个校验器类(封装嵌套对象的校验器类)比把校验逻辑分散到各个嵌套对象会更方便管理。

public class CustomerValidator implements Validator {

    private final Validator addressValidator;

    public CustomerValidator(Validator addressValidator) {
        if (addressValidator == null) {
            throw new IllegalArgumentException("The supplied [Validator] is " +
                "required and must not be null.");
        }
        if (!addressValidator.supports(Address.class)) {
            throw new IllegalArgumentException("The supplied [Validator] must " +
                support the validation of [Address] instances.");
        }
        this.addressValidator = addressValidator;
    }

    /**
     * 这个校验器校验Customer实例,同时也会校验Customer的子类实例
     */
    public boolean supports(Class clazz) {
        return Customer.class.isAssignableFrom(clazz);
    }

    public void validate(Object target, Errors errors) {
        ValidationUtils.rejectIfEmptyOrWhitespace(errors, "firstName", "field.required");
        ValidationUtils.rejectIfEmptyOrWhitespace(errors, "surname", "field.required");
        Customer customer = (Customer) target;
        try {
            errors.pushNestedPath("address");
            ValidationUtils.invokeValidator(this.addressValidator, customer.getAddress(), errors);
        } finally {
            errors.popNestedPath();
        }
    }
}

7.4 Bean的操作和BeanWrapper

在beans包中相当重要的是BeanWrapper接口和它的实现类(BeanWrapperImpl)。引用其javadocs中的说明,BeanWrapper提供了设置和获取属性值, 获取属性描述符以及遍历属性来确定它们是可读的还是可写的功能。BeanWrapper也支持嵌套属性,允许不限嵌套级数的子属性设置。BeanWrapper还支持 在不需要目标类中加入额外的代码就能添加标准的JavaBeansPropertyChangeListeners和VetoableChangeListeners。值得一提的是BeanWrapper还支持 索引的属性。通常我们一般不会在应用代码中直接用到BeanWrapper,除了DataBinder和BeanFactory。

BeanWrapper基本上是通过它的名字来进行工作的:它包裹一个bean来代替它执行某些动作,如设置以及获取属性。
BeanWrapper主要用于解藕操作,将任何规范的Bean都进行统一的包装,然后进行统一的管理。(核心思想)

7.4.1 Setting和getting基本及嵌套属性

Setting和getting属性是通过一组变形的重载方法setPropertyValue(s)和getPropertyValue(s)来完成的。您可以通过Spring的javadoc来获得更多的信息。 你必须知道的是描述对象的属性有一些约定俗成的规则。

7.5 PropertyEditor

PropertyEditor是用来在String和特定类型之间进行转换的工具类,我们在xml中配置的属性,例如<bean id="aa" class="a.b.c.DemoClass />那么Spring如何将我们配置的字符串转换为特定的类呢,这其中就是PropertyEditor在起到作用。
Spring已经内置了很多PropertyEditor,如果我们想自定义的话,可以这么做:

  1. 定义自己的PropertyEditor
  2. 将自己定义的PropertyEditor注册到Spring容器中(xml配置或者注解)
  3. 使用自己的PropertyEditor中涉及到的转换类

7.6 Converter

实际上PropertyEditor就是一种特殊的Convertor,只不过前者只是在字符串和特定类型之间进行转换,而后者是在所有类型之间进行转换,PropertyEditor可以理解为一个Converter的一个子集。

8. Spring 表达式语言 (SpEL)

Spring的EL表达式语言主要是用于与Spring框架交互的一种自定义的语言(可以理解为一种有规则的微型语言),通过Spring EL语言我们可以方便的处理Spring框架中用到的一些数据。
因为在日常使用的时候我们直接用el表达式的时候并不多,看了一下官网的文档主要是介绍了el表达式的语法。如果有兴趣的可以直接到这里阅读:SpEL表达式

9. AOP

Spring的AOP在Spring体系中也占用重要的地位,作为Spring的重要功能,需要注意的是IOC并不依赖AOP,所以我们在不需要引用AOP的时候可以不引入。 Spring2.0对于AOP的支持更加广泛,可以支持schema配置或者@AspectJ注解。
AOP中有很多基础的概念性的内容,下面我们要先熟悉一下这些基本的名词:

  • JoinPoint:Spring提供的JoinPoint都是在方法级别的,比如方法前,方法后或者方法前后。Spring为了方便我们理解可以在哪些地方插入我们想要的逻辑,提供了JoinPoint这个概念,意思是“可以插入AOP逻辑的点”。
  • Advice:Advice的含义跟JoinPoint比较接近,为了简单理解可以把它跟JoinPoint的使用思路看成一样。主要分为Before Advice(方法前),After Advice(方法后),Around Advice(方法前后)。
  • Pointcut:Pointcut是具体的横切逻辑生效的点,比如我们想要在A方法前生效而不是B方法前,这时候就要定义特殊的Pointcut。可以理解为特殊位置的JoinPoint。
  • Aspect:当上面的逻辑都定义好之后实际上就形成了一个完整的切面就完成了,所以切面的通俗理解就是:在需要关注的逻辑前后加入自定义的逻辑,但是又不会耦合原来的代码。
  • introduction:允许我们像现有的类中添加新的方法
  • Target object:就是我们将切面逻辑作用的对象
  • AOP proxy:切面逻辑生效主要是靠的代理,一般有JDK代理或者CGLIB代理,两者的区别就是JDK的代理主要是通过实现接口来实现的,如果一个代理对象有具体的接口的话Spring会默认使用JDK代理来作用(当然也可以指定Cglib生效);但是Cglib则是通过继承来实现代理的,这样的话即使一个类没有具体的实现接口也不会影响其代理功能。如果Spring发现一个目标对象没有实现具体的接口的话则会使用Cglib来进行代理。
  • Weaving:中文翻译就是织入,是个动词。通俗讲就是让切面逻辑生效。

9.2 @AspectJ 支持

9.2.1 开启 @AspectJ 支持并声明切面

@AspectJ 是一种使用普通Java类注解来声明AOP切面的方式。要在Spring中使用@AspectJ切面,需要开启Spring对@AspectJ的支持以及使用autoproxying bean类。 自动代理是指如果Spring判定一个bean被一个或多个切面通知,它将自动为一个bean生产一个代理,来拦截方法调用并保证通知按需执行。
要使@AspectJ生效,我们需要开启Spring对于@AspectJ 的扫描,我们可以使用以下两种方式来时@AspectJ被Spring扫描到:

方案1:
@Configuration
@EnableAspectJAutoProxy
public class AppConfig {

}
========================
方案2:
<aop:aspectj-autoproxy/>

在我们生命完@AspectJ之后,我们可以需要让Spring来知道这个特殊的Bean,我们可以在xml中生命这个bean,或者利用Component-scan来扫描这个类,但是有一点需要注意的是:包扫描的功能并不会主动识别,需要添加另外一个注解@Component来帮助Spring识别。

9.2.2 定义Pointcut

一个完整的Pointcut主要包含两部分:一个返回值为void的方法,一个含有@Pointcut的注解。void方法讲道理就是为了结构而床在的@Pointcut的载体,重点是@Pointcut标示了在那些地方生效,下面给一个简单的例子:

@Pointcut("execution(* transfer(..))")// the pointcut expression
private void anyOldTransfer() {}// the pointcut signature

Spring AOP对于Pointcut的语法支持有限,虽然没有完整的AspectJ语言支持的丰富,但也基本能满足绝大多数的需求了。主要有以下的内容:

  • execution:这是我们用得最多的pointcut提示符,用于匹配方法执行时候的joinpointr。

  • within:用于匹配指定类型内的方法执行,比execution更加简单,但是适用范围没有那么广泛。

  • this:用于匹配当前AOP代理对象类型的执行方法。 this(com.xyz.service.AccountService) 匹配任何实现了AccountService的代理中的方法

  • target:用于匹配当前目标对象类型的执行方法;注意是目标对象的类型匹配,这样就不包括引入接口也类型匹配。 target(com.xyz.service.AccountService) 匹配任何实现了AccountService的接口实现中的方法

  • args:当参数是指定类型的实例时候进行匹配, args(java.io.Serializable) 匹配任何方法中只有一个Serializable参数的方法

  • @target:当执行的类有特定类的注解的时候进行匹配, @target(org.springframework.transaction.annotation.Transactional) 匹配任何有@Transactional注解的类

  • @args:当实际运行的方法参数有特定的注解时候进行匹配, @args(com.xyz.security.Classified)匹配任何参数中有@Classified注解的方法

  • @within:用于匹配所以持有指定注解类型内的方法

  • @annotation:当实际执行的方法有也定注解的时候进行匹配, @annotation(org.springframework.transaction.annotation.Transactional)匹配任何有@Transactional注解的方法

  • bean:匹配符合条件的beanName。bean(tradeService) 匹配beanName是tradeService的类中所有方法

我们在使用pointcut提示符的时候还可以结合&&, || 和 !来使用,分别标示与,或,非。下面给出简单的例子:

//匹配所有的方法
@Pointcut("execution(public * *(..))")
private void anyPublicOperation() {}

//匹配特定包下的方法
@Pointcut("within(com.xyz.someapp.trading..*)")
private void inTrading() {}

//相当于上面两者的匹配条件取交集
@Pointcut("anyPublicOperation() && inTrading()")
private void tradingOperation() {}

当我们在日常使用Spring AOP的时候,通常都会定义一个自己的aspect包用来专门存放我们的切面逻辑,然后可以在需要的地方引用这些切面。大多数的切面使用逻辑都会用到execution,它的主要语法如下:(?代表可有可无)

execution(修饰符? 返回值类型修饰符 声明值修饰符? 方法名称修饰符(参数修饰符)
            异常修饰符?)

大多数的这些匹配项,如果想用通配符的话可以使用作为替代,代表匹配所有。但是对于方法参数的匹配要更加复杂一些,()可以匹配空参数,(..)可以匹配所有参数,即使是空也可以匹配,而只会匹配一个任意参数,(*,String)这种可以匹配第二个参数是String类型的方法。 下面给出一些简答的实例:

execution(public * *(..)) 匹配任何public方法
execution(* set*(..)) 匹配任何set开头的方法
execution(* com.xyz.service.AccountService.*(..)) 匹配AccountService下的任何方法
execution(* com.xyz.service.*.*(..)) 匹配在com.xyz.service包下的所有方法

9.2.3 定义Advice

我们在之前的篇章中说明了如果定义个pointcut,但是只简单定一个pointcout并不能让切面逻辑生效,因为一个切面逻辑是有了,但是具体的切入点还没有明确,因此我们需要Advice,来决定具体的方法切入点是方法前,方法后还是方法前后。
如果我们的pointcout逻辑在之前已经定义好的话,我们可以这么定义Advice:


@Aspect
public class BeforeExample {

    @Before("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")
    public void doAccessCheck() {
        // @Before和After的逻辑基本一样
    }

}

但是如果想把pointcut和Advice写到一起的话可以直接这么做:

@Aspect
public class BeforeExample {
    @Before("execution(* com.xyz.myapp.dao.*.*(..))")
    public void doAccessCheck() {
        // ...
    }

}

一般的话对于复用性不强的切面逻辑,可以直接将两者写到一起,这样更加方便些。
有时候我们在切面逻辑中用到实际的方法返回值信息,我们可以这么做:


@Aspect
public class AfterReturningExample {

    @AfterReturning(
        pointcut="com.xyz.myapp.SystemArchitecture.dataAccessOperation()",
        returning="retVal")
    public void doAccessCheck(Object retVal) {
        // 其中retVal就是实际业务执行逻辑的返回值
    }

}

在上面的逻辑中,returning的值一定要和实际的参数名称匹配.
Spring AOP支持的Advice还包括异常处理AfterThrowing。而且AfterThrowing还可以像上面的逻辑一样帮我们取到特定的异常信息.

@Aspect
public class AfterThrowingExample {

    @AfterThrowing(
        pointcut="com.xyz.myapp.SystemArchitecture.dataAccessOperation()",
        throwing="ex")
    public void doRecoveryActions(DataAccessException ex) {
        // ...
    }

}

像上面的这种情况,只有在抛出DataAccessException的时候才会进行匹配,如果我们希望进行异常信息的全局处理,可以使用Throwable作为参数。
值得一提的是Around Advice可能特殊些,这个切面可以选择在具体业务执行逻辑的前后执行自己的切面逻辑,虽然Around的功能很强大,但是如果可以用Before或者After解决的话还是希望用简单的逻辑处理简单的事情。Around的第一个参数必须是ProceedingJoinPoint,代表当前AOP逻辑执行的上下文,可以调用proceed()方法主动调用目标方法,proceed()方法可以接受参数,参数可以通过ProceedingJoinPoint拿到。@Around方法的返回值将会作为目标对象的方法返回值,这一点很重要,如果我们忘记执行preceed的话,可要谨慎检查一下噢。
我们在之前的逻辑中已经看到了如果将方法的返回值或者异常信息绑定到切面逻辑的参数中,其实我们也可以将目标方法的参数直接绑定到切面逻辑的参数中,具体的做法就像下面这样子:

@Before("com.xyz.myapp.SystemArchitecture.dataAccessOperation() && args(account,..)")
public void validateAccount(Account account) {
    // ...
}

当然我们不仅仅可以通过args来绑定参数,我们仍然可以通过this,target或者@annotation等来绑定参数。
Advice对于范性的支持有限,只能支持简答的范性,如果我们使用Collection<T>这种形式的范性的话将不会支持,原因是不知道如果正确处理null这种值。
我们可以在切面方法的第一个参数中指定为JoinPoint或者ProceedingJoinPoint。这种情况下是不需要做参数绑定的,Spring内部已经帮我们特殊处理过这中参数来。
有一种情况是需要我们注意的,那就是当有多个切面逻辑同时生效的时候,先后顺序是怎么决定的?要根据Spring提供的Orderd接口的返回值来确定切面执行的先后顺序,值越小的切面越先执行。如果所有的生效切面都没有实现这个接口的话,那么执行的顺序就看Spring 的加载顺序喽(随机)。

基本上的切面使用逻辑在上面都有涉及,但是上面的用法都是基于Annotation的,其实是因为我个人觉得这个方便很多,使用xml的当然也可以,但是我觉得要复杂一些,具体的语法也就不多介绍了。
在使用Spring AOP的时候有一点需要注意,就是像下面这种内部调用出现的情况:

public class Demo {
    void methodA(){
        methodB();
    }
    
    void methodB(){}
}

如果切面的覆盖范围包含methodA的话,在内部调用methodB并不会执行切面逻辑,原因是内部调用执行的this.methodB(),并不走代理。不走代理的话也就没办法执行到具体的切面逻辑,如果我们就是想在内部调用的时候依然使用切面逻辑的话,可以这么做:
强行把this替换为代理对象,使用AopContext.currentProxy()或者ApplicationContext.getBean()等方式取到代理对象,然后在方法内部调用methodB()。

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

推荐阅读更多精彩内容