SpringBoot入门及原理

SpringBoot的诞生

什么是Spring?

现代化的java开发主要就是面向Spring开发。
Spring是一个开源框架,它由Rod Johnson在2003年创建。它是为了简化企业级开发而创建的。
Spring是十分优雅的,这是因为它的创始人是一位音乐学博士,某种意义上说,它是一个含有艺术基因的框架。

Spring是如何简化开发的?

1、JavaBean代表着一个个要new的对象。
2、Ioc控制反转思想,所有的对象都去容器中去getBean即可。
3、AOP面向切面编程,降低入侵性。
4、提供了许多的模板类简化开发,很多功能无需手动编写步骤。

什么是SpringBoot?

传统的JavaWeb开发:Tomcat+Servlet+JSP,web.xml中需要自己配置大量的Servlet,需要自己解决管理版本依赖问题。
SSH+SSM:框架,简化了一些固有的开发,但需要配置大量的xml配置文件。
于是人们开始思考:有一天要是能够实现自动配置就好了!
于是SpringBoot应运而生。SpringBoot的主要特点有:

  • 自动配置。
  • 它不是一个新的事物,它是Spring的增强版。
  • 约定大于配置。
  • 集成了市场上所有常用的依赖,并且自动管理版本依赖。
  • 内置了Web容器。

从Hello World开始

Spring官方提供了初识项目生成器,供开发者自动生成一个SpringBoot项目。


Spring Initializr

上述网站被IDEA集成了,因此我们平时开发直接使用IDEA即可。

构建SpringBoot项目的步骤

1、使用IDEA创建一个SpringBoot应用。


创建SpringBoot项目

2、打开启动类,启动项目进行测试。
3、编写一个controller,然后再次启动测试。

@RestController
public class HelloController {

    @RequestMapping("/hello")
    public String hello(){
        return "Hello World";
    }
}

和传统开发进行对比

SSM框架构建项目步骤:
1、配置Tomcat。
2、在Web.xml中注册Spring。
3、添加或者扫描我们需要的所有依赖是否存在,不存在就需要配置依赖。
4、编写Spring、SpringMVC、MyBatis等其他所有框架的配置。
5、配置完毕,需要编写无关的业务代码。
6、整体框架搭建完毕,才开始编写业务代码。
7、启动测试。
SpringBoot构建项目步骤:
1、构建项目,一键生成。
2、编写业务代码。
3、启动测试。
对比之后可以发现,使用了SpringBoot之后一切都变得十分简单了,正如官网所说:you can "just run"。

项目如何发布到服务器运行?

单体应用直接打包成jar包就可以了,命令:mvn package
通过java -jar或者 java -jar nohup(后台运行)执行即可。

初探原理

通过分析源码,我们可以窥探出SpringBoot的一部分原理。

pom.xml

只要是Maven项目都先分析pom.xml文件。

pom.xml

1、发现一个父依赖。

<parent>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-parent</artifactId>
  <version>2.2.5.RELEASE</version>
  <relativePath/> <!-- lookup parent from repository -->
</parent>

2、点进父依赖,查看代码分析父依赖作用:资源过滤,插件管理

<!--发现未知文件 yml--> 
<includes>
  <include>**/application*.yml</include> 
  <include>**/application*.yaml</include> 
  <include>**/application*.properties</include> 
</includes>

3、仔细查看代码后发现它还有一个父依赖。

<parent>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-dependencies</artifactId>
  <version>2.2.5.RELEASE</version>
  <relativePath>../../spring-boot-dependencies</relativePath>
</parent>

点进去可以发现它定义了许多依赖的版本号,说明它才是真正的版本控制中心
这表明我们以后导入的依赖都不需要编写版本号,如果自动依赖配置中不存在,才需要手动导入依赖版本。

启动器

回到最外层的pom文件中,发现有一个spring-boot-starter-web依赖,spring-boot-starter是SpringBoot的启动器,它的作用是:导入所有的依赖。spring-boot-starter-xxx,后面的xxx就是对应的场景依赖。本例中的spring-boot-starter-web就是导入了Web模块运行所需要的全部场景依赖。

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-web</artifactId>
</dependency>

具体存在哪些spring-boot-starter呢?可以参考官网:https://docs.spring.io/spring-boot/docs/2.2.5.RELEASE/reference/html/using-spring-boot.html#using-boot-starter

插件

插件都配置在build标签下的plugins标签中。

<!--插件-->
<build>
    <plugins>
        <!--打包插件,可以将项目打成jar包或者war包-->
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>
    </plugins>
</build>

启动类

默认的启动类都被@SpringBootApplication注解所标注。

@SpringBootApplication //一个注解
public class SpringBoot01Application {

    public static void main(String[] args) {
        //一个方法
        SpringApplication.run(SpringBoot01Application.class, args);
    }
}

@SpringBootApplication

标注了此注解,就表示这个类是我们的一个启动类。点进去我们看到类上有三个注解。

@SpringBootConfiguration 
@EnableAutoConfiguration 
@ComponentScan() // 对应xml 中的 ComponentScan 标签; 
public @interface SpringBootApplication {
 }

JavaConfig回顾

Spring4之后官方推荐我们使用 JavaConfig。
传统的applicationContext.xml配置:

<bean id="" class="">
<import > 
<mvc:component-scan base-package="">

注解配置类applicationContext:

@Configuration 
@import 
@ComponentScan() 
class applicationContext{ 
  // 方法名就是 xml 中对应的id,返回值类型就是对应的 class 
  @Bean 
  public User uesr(){ } 
}

@ComponentScan

此注解就是扫描包,将扫描包中的bean注入到Spring中。

@SpringBootConfiguration

点进这个注解可以发现,该类上标注了@Configuration注解,再点进 Configuration类,该类上标注了@Componet注解。

@Configuration // 这就是个配置类 
public @interface SpringBootConfiguration { }
@Component // 这是一个组件

说明:
1、主启动类也是Spring的一个组件。
2、主启动类也是一个配置类。

@EnableAutoConfiguration(重点)

@EnableAutoConfiguration : 开启自动配置功能。
@AutoConfiurationPackae : 自动配置和注册包。

static class Registrar implements ImportBeanDefinitionRegistrar, DeterminableImports { 
  @Override 
  public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) { 
    register(registry, new PackageImport(metadata).getPackageName()); 
  } 
}

AutoConfigurationImportSelector

点进@EnableAutoConfiguration,在此类上我们找到了@Import(AutoConfigurationImportSelector.class)注解,点进AutoConfigurationImportSelector.类,来分析下这个类。
首先谈谈分析一个类的方法:
1、分析构造器。
2、分析初始化常用方法。
3、如果有继承,重点分析一下被重写的方法。


AutoConfigurationImportSelector类源码

通过分析源码我们可以知道,项目在加载的时候会读取配置,返回具体的结果,但是有没有生效,我们并不清楚。
整个源码分析过程如下图所示。


源码分析过程示意图

SpringApplication

构造器

回到启动类,点进SpringApplication类,先来看一下它的构造器。

public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) { 
  // 1、推断当前引用的类型,是否是Web应用 
  this.webApplicationType = WebApplicationType.deduceFromClasspath(); 
  // 2、加载初始化器 
  setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class)); 
  // 3、设置监听器 
  setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class)); 
  // 4、推断main方法 
  this.mainApplicationClass = deduceMainApplicationClass(); 
}

SpringApplication的作用:
1、推断当前引用的类型,是否是Web应用。
2、加载初始化器。
3、设置监听器。
4、推断main方法。

run方法

回到启动类,点进run方法,一直点进去,直到进入到如下方法。

public ConfigurableApplicationContext run(String... args) {
        StopWatch stopWatch = new StopWatch();
        stopWatch.start();
        ConfigurableApplicationContext context = null;
        Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
        configureHeadlessProperty();
        // 1、监听器启动
        SpringApplicationRunListeners listeners = getRunListeners(args);
        listeners.starting();
        try {
            ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
            // 初始化环境配置
            ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
            configureIgnoreBeanInfo(environment);
            // 打印 Banner
            Banner printedBanner = printBanner(environment);
            // 创建上下文
            context = createApplicationContext();
            exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class,
                    new Class[] { ConfigurableApplicationContext.class }, context);
            prepareContext(context, environment, listeners, applicationArguments, printedBanner);
            refreshContext(context);
            afterRefresh(context, applicationArguments);
            stopWatch.stop();
            if (this.logStartupInfo) {
                new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
            }
            listeners.started(context);
            callRunners(context, applicationArguments);
        }
        catch (Throwable ex) {
            handleRunFailure(context, ex, exceptionReporters, listeners);
            throw new IllegalStateException(ex);
        }

        try {
            listeners.running(context);
        }
        catch (Throwable ex) {
            handleRunFailure(context, ex, exceptionReporters, null);
            throw new IllegalStateException(ex);
        }
        return context;
}

run方法思路思考图如下图所示。


run方法思路参考图

Yaml学习

配置文件

SpringBoot默认使用一个全局的配置文件, application*. 这个配置文件的名称是固定的。配置文件有properties和yaml两种文件格式:

  • application.properties,语法:key = value
  • application.yml,语法: key: value

配置文件的作用是修改SpringBoot自动配置的默认值,但实际上很多配置SpringBoot都已经给我们自动配置好了,我们只需要偶尔修改一些内容就可以通过这个配置文件。

yaml

yaml是一个可读性高,用来表达数据序列化的语言。这种语言以数据为中心。
数据一般包括:对象、map、list、key/value
传统的properties配置使用key=value格式,十分单一,而xml配置又比较繁琐,比如配置服务器端口用xml方式可以这样表示:

<server> 
  <port>8080</port> 
</servet>

yaml配置的好处:数据类型支持较多,符合Java程序员的审美。SpringBoot官方推荐我们使用yaml配置。

基本语法

注意事项:
1、空格要求很严格(key和value之间必须有一个空格)。
2、缩进严格要求。
3、大小写严格要求。

key/value

key:value

字符串需要注意""和''的区别:
使用""转义字符会生效,如:

name: "coding\nlove" 
结果: 
coding 
love

使用''转义字符不会生效,如:

name: "coding\nlove" 
结果:
coding\nlove

对象、map

key:
  v1: 1 
  v2: 2

比如:

student: 
  name: wunian
  age: 18 
  happy: true

上述例子也可以缩减为行内写法,这种写法和JSON配置一样。

student: {name: wunian,age: 18,happy:true}

list、数组类型

key:
  - v1
  - v2
  - v3

比如:

color:
  - green
  - red
  - blue

上述例子也可以缩减为行内写法,这种写法和JSON配置一样。

color: [green,red,blue]

需要注意的是,在SpringBoot中,yaml配置和properties配置可以同时配置,简单的配置可以写在properties文件中,复杂的写在yaml文件中,这样就可以互补了。
properties配置的优先级大于yaml配置。因为yaml配置先加载,properties配置后加载,如果有相同的配置项的话properties中的配置会生效。

注入配置文件

编写配置类的步骤:
1、编写配置文件。

person:
  name: wunian
  age: 18
  happy: true
  birth: 2020/3/20
  maps: {k1:v1,k2:v2}
  lists:
    - coder
    - boy
    - game
  dog:
      name: 大黄   
      age: 5

2、编写实体类。

@Data
@ConfigurationProperties(prefix = "person")
//@PropertySource(value="classpath:person.properties")
@Validated //数据校验
public class Person {

    @NotNull
    private String name;
    @NotNull
    private Integer age;
    @NotNull
    private Boolean happy;
    @NotNull
    private Date birth;
    @Size(1,10)
    private Map<String, Object> maps;
    @NotEmpty
    private List<Object> lists;
    private Dog dog;
}

3、测试。

@SpringBootTest
class SpringBoot01ApplicationTests {

    @Autowired
    Person person;

    @Test
    void contextLoads() {
        System.out.println(person);
    }
}

@ConfigurationProperties注解的作用:

  • 绑定配置文件中的对象。
  • 将对象的属性值配置文件的值一一对应,然后注入。
  • 这是一种批量注入的方式,更加快捷和方便,推荐使用。

配置的思想:配置文件如果存在值,就使用配置文件中的值,如果不存在,就设置为默认值或者null。

加载指定的配置文件

@PropertySource注解的作用:加载指定的配置文件,value表示要填写指定配置文件中的值,如:

@PropertySource(value="classpath:person.properties")

在配置中使用随机数、占位符

yaml 配置文件中可以使用一些随机数、占位符,如:

person:
  name: wunian${random.uuid}
  age: ${random.int}
  happy: true
  birth: 2020/3/20
  maps: {k1:v1,k2:v2}
  lists:
    - coder
    - boy
    - game
  dog:
      #如果person.age存在值就设置值为person.age+大黄,否则设置值为默认名是大黄
      name: ${person.age:默认名是}大黄   
      age: 5

回顾传统的Properties配置

1、properties文件一定要注意乱码问题,可以打开File-->Settings-->File Encodings中设置编码格式。

设置properties文件编码格式

2、测试properties配置。

@Data
@Component
@PropertySource("classpath:user.properties")
public class User {

    @Value("${user1.name}") //从配置文件读取
    private String name;
    @Value("#{2*9}") //SPEL表达式
    private Integer age;
    @Value("男")  //普通字符串
    private String sex;
}

@Value和@ConfigurationProperties的对比

@Value和@ConfigurationProperties分别属于properties和yaml的配置方式,它们之间的区别如下表所示:

@Value @ConfigurationProperties
功能 一个个指定 批量注入
松散绑定 不支持 支持
SPEL 支持 不支持,有自己的占位符
JSR303数据校验 不支持 支持
复杂类型封装 不支持 支持

JSR303数据校验

JSR是Java Specification Requests的缩写,意为Java 规范提案。是指向Java Community Process提出新增一个标准化技术规范的正式请求。任何人都可以提交JSR,以向Java平台增添新的API和服务,它可以验证实体类中的变量值的输入格式是否正确。
使用JSR303数据校验必须开启@Validated注解支持。

@Validated //数据校验 
public class Person { }

常见参数如下:

@NotNull(message="名字不能为空") 

private String userName; 

@Max(value=120,message="年龄最大不能查过120") 

private int age; 

@Email(message="邮箱格式错误") 

private String email; 

空检查 

@Null 验证对象是否为null 

@NotNull 验证对象是否不为null, 无法查检长度为0的字符串 

@NotBlank 检查约束字符串是不是Null还有被Trim的长度是否大于0,只对字符串,且会去掉前后空格. 

@NotEmpty 检查约束元素是否为NULL或者是EMPTY. 

Booelan检查 

@AssertTrue 验证 Boolean 对象是否为 true 

@AssertFalse 验证 Boolean 对象是否为 false 

长度检查 

@Size(min=, max=) 验证对象(Array,Collection,Map,String)长度是否在给定的范围之内 

@Length(min=, max=) string is between min and max included. 

日期检查 

@Past 验证 Date 和 Calendar 对象是否在当前时间之前 

@Future 验证 Date 和 Calendar 对象是否在当前时间之后 

@Pattern 验证 String 对象是否符合正则表达式的规则 

.......等等 

除此以外,我们还可以自定义一些数据校验规则

在SpringBoot底层所有配置类都使用的是@ConfigurationProperties注解来绑定配置文件。

最佳实践

1、如果配置类十分简单,使用@Value,在只需要获取个别值的情况下,@Value依旧最高效。
2、如果配置比较繁琐,使用@ConfigurationProperties。

多环境配置

什么是多环境配置?

真实开发的时候,配置文件一定不止一个,我们可能在开发、测试、生产环境下都有一套配置,配置文件的命名固定格式为:application-xxx,比如:

application-dev.properties
application-test.properties
application-prod.properties

以上三个配置文件分别表示开发、测试、生产环境下的配置。

properties配置

在application.properties文件中配置需要激活的环境即可,比如激活测试环境配置如下:

# application-xxx 的命名格式!
 # 选择激活那个配置文件 
spring.profiles.active=test

yaml配置

yaml不需要多个配置文件,只需要多文档块,使用---分割文档块。

server:
  port: 8080
spring:
  profiles:
    active: dev
#使用---分割文档块
---
server:
  port: 8081
spring:
  profiles: dev #dev环境

---
server:
  port: 8082
spring:
  profiles: test #test环境

---
server:
  port: 8083
spring:
  profiles: prod #prod环境

自动配置原理

我们可以配置的文件内容非常多,想查看所有的配置项可以参考官方文档:https://docs.spring.io/spring-boot/docs/2.1.9.RELEASE/reference/htmlsingle/#common-application-properties

@ConditionalOnXX

@ConditionalOnXX,条件判断注解,即通过条件来判断这个类是否生效。所有的@ConditionalOnXX注解的作用如下图所示:


@ConditionalOnXX注解列表

原理

接下来又是分析源码的时候了。
HttpEncodingAutoConfiguration类源代码分析:

// Configuration 表示这是一个配置类,和以前编写的配置文件一样! 
@Configuration(proxyBeanMethods = false) 
// 指定配置文件,HttpProperties 对应我们编写的配置文件,假设配置文件中有就有配置文件的,没有就用默认值 
@EnableConfigurationProperties(HttpProperties.class) 
// @ConditionalOnXX Spring底层注解; 
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET) 
// 如果 CharacterEncodingFilter 这个类,这个配置类才生效!
@ConditionalOnClass(CharacterEncodingFilter.class) 
@ConditionalOnProperty(prefix = "spring.http.encoding", value = "enabled", matchIfMissing = true) 
public class HttpEncodingAutoConfiguration { 
  // 注入绑定 HttpProperties 配置文件 
  private final HttpProperties.Encoding properties; 

  public HttpEncodingAutoConfiguration(HttpProperties properties) { 
    this.properties = properties.getEncoding(); 
  }

  // 注入对应的bean 
  // 如果自己配置了配置文件,就会注入到SpringBoot自动帮我们配置的bean中! 
  // 注册bean的时候,SpringBoot自动帮我们关联了 HttpProperties 
  // 我们编写的配置就可以直接生效! 
  @Bean 
  @ConditionalOnMissingBean 
  public CharacterEncodingFilter characterEncodingFilter() { 
    CharacterEncodingFilter filter = new OrderedCharacterEncodingFilter(); 
    filter.setEncoding(this.properties.getCharset().name()); 
    filter.setForceRequestEncoding(this.properties.shouldForce(Type.REQUEST)); 
    filter.setForceResponseEncoding(this.properties.shouldForce(Type.RESPONSE)); 
    return filter; 
  }

  @Bean 
  public LocaleCharsetMappingsCustomizer localeCharsetMappingsCustomizer() { 
    return new LocaleCharsetMappingsCustomizer(this.properties); 
  }

  private static class LocaleCharsetMappingsCustomizer implements WebServerFactoryCustomizer<ConfigurableServletWebServerFactory>, Ordered { 

    private final HttpProperties.Encoding properties; 

    LocaleCharsetMappingsCustomizer(HttpProperties.Encoding properties) { 
      this.properties = properties; 
    }

    @Override 
    public void customize(ConfigurableServletWebServerFactory factory) { 
      if (this.properties.getMapping() != null) { 
        factory.setLocaleCharsetMappings(this.properties.getMapping()); 
      } 
    }

    @Override 
    public int getOrder() { 
      return 0; 
    } 
   } 
 }

总结

1、SpringBoot启动会加载大量的自动配置类,自动配置类都是在spring.factories.properties文件中注册的。
2、我们只需要判断我们的类是否存在在这里面,如果不存在我们需要手动导入,如果存在导入启动器即可。
3、我们的配置文件可以自动配置生效,依赖于两个类:

  • xxxAutoConfiguation : 自动配置类,根据条件 @ConditionalOnXX 判断是否生效,如果生效则成功注入bean。
  • xxxProperties:封装配置文件中的相关属性。

4、给容器中自动配置类配置属性的时候,会通过 xxxProperties 类来获取某用户配置文件中的属性,如果没有则使用默认的,如果有则使用自己配置的。

如何查看框架加载了哪些类?

只需要在配置文件中将debug设置为true即可:

debug=true

重新启动项目,我们可以看到控制台打印出了三种情况下的类:

  • Positive matches(匹配成功的类)
  • Negative matches(没有匹配成功的类)
  • Unconditional classes(没有条件的类)

注意:虽然在启动时加载了大量的类,但是有些类并不一定生效,因为条件不满足。

自定义starter

启动器其实就是jar包。

命名规则

官方命名:

  • spring-boot-starter-xxx
  • spring-boot-starter-web

自己命名:

  • xxx-spring-boot-starter,如:mybatis-spring-boot-starter

自己开发一个启动器

1、启动器其实是两个相互依赖的项目,一个控制版本(xxx-spring-boot-starter),一个提供服务(xxx-spring-boot-starter-autoconfigure)。
2、新建一个空项目。
3、在空项目中构建两个module,一个是starter(Maven项目),一个是autoconfigure(SpringBoot项目)。

编写启动器

1、autoconfigure中pom中导入springboot启动器。

<!--所有启动器的基础模块-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter</artifactId>
</dependency>

2、starter中导入autoconfigure的依赖,后面会把autoconfigure打包,之后starter中什么都不用干,只要编写autoconfigure模块即可。

<dependency>
    <groupId>com.wunian</groupId>
    <artifactId>wunian-spring-boot-starter-autoconfigure</artifactId>
    <version>0.0.1-SNAPSHOT</version>
</dependency>

3、xxxService(具体业务)。

public class HelloService {

    //注入配置类 HelloProperties

    HelloProperties helloProperties;

    public HelloProperties getHelloProperties() {
        return helloProperties;
    }

    public void setHelloProperties(HelloProperties helloProperties) {
        this.helloProperties = helloProperties;
    }

    //具体业务
    public String sayHello(String  name){
        return helloProperties.getPrefix()+name+helloProperties.getSuffix();
    }
}

4、xxxProperties

@ConfigurationProperties(prefix = "wunian.hello")
public class HelloProperties {

    private String prefix;
    private String suffix;

    public String getPrefix() {
        return prefix;
    }

    public void setPrefix(String prefix) {
        this.prefix = prefix;
    }

    public String getSuffix() {
        return suffix;
    }

    public void setSuffix(String suffix) {
        this.suffix = suffix;
    }
}

5、xxxAutoConfiguration

@Configuration
@ConditionalOnWebApplication //只有在web项目下才会生效
@EnableConfigurationProperties(HelloProperties.class)
public class HelloServiceAutoConfiguration {

    @Autowired
    HelloProperties helloProperties;

    @Bean
    public HelloService helloService(){
        HelloService helloService=new HelloService();
        helloService.setHelloProperties(helloProperties);
        return helloService;
    }
}

6、将启动类放到resources/META-INF/spring.factories

# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.wunian.HelloServiceAutoConfiguration

7、将自己编写的starter和autoconfigure安装到自己本地的Maven仓库即可!命令:maven install

新建一个项目测试自己的启动器

1、新建一个SpringBoot Web项目(导入Spring Web依赖)。
2、导入自己的启动器。

<!--导入自己的启动器-->
<dependency>
    <groupId>com.wunian</groupId>
    <artifactId>wunian-spring-boot-starter</artifactId>
    <version>0.0.1-SNAPSHOT</version>
</dependency>

3、测试启动器功能。
HelloController

//测试自定义启动器
@RestController
public class HelloController {

    @Autowired
    HelloService helloService;

    @RequestMapping("/hello")
    public String hello(){
        return helloService.sayHello("==wunian==");
    }
}

application.properties

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

推荐阅读更多精彩内容