Springboot核心技术学习笔记二

  • 第 5 章 SpringBoot 与 Docker
    • 5.1 Docker 简介
    • 5.2 核心概念
    • 5.3 安装Docker
      • 安装linux虚拟机
      • 在linux虚拟机上安装docker
    • 5.4 Docker常用命令&操作
        1. 镜像操作
        1. 容器操作
        1. 安装 MySQL 示例
  • 第 6 章 SpringBoot 与数据访问
    • 6.1 数据源初始化与 JDBC
        1. 配置 MySQL
        1. 数据源自动配置原理
        1. 数据表自动初始化
        1. 使用 JdbcTemplate 查询数据
        1. 数据库自动初始化原理
    • 6.2 使用外部数据源
    • 6.3 自定义数据源原理
    • 6.4 配置 Druid 数据源
    • 6.5 整合 MyBatis
        1. 注解版
        1. Mybatis 常见配置
        1. xml 版
    • 6.6 整合 SpringData JPA
        1. Spring Data 简介
        1. 整合 SpringData JPA
  • 第 7 章 SpringBoot 启动配置原理
    • 7.1 启动流程
        1. 创建SpringApplication对象
        1. 运行run方法
    • 7.2 事件监听机制
  • 第 8 章 SpringBoot 自定义 starter
    • 8.1 starter 原理
    • 8.2 自定义 starter
    1. SpringBoot 与开发热部署
  • 进阶学习
  • 待补充
  • 推荐阅读
  • 参考文档

第 5 章 SpringBoot 与 Docker

5.1 Docker 简介

Docker是一个开源的应用容器引擎,轻量级容器技术。Docker支持将软件编译成一个镜像,然后在镜像中各种软件做好配置,将镜像发布出去,其他使用者可以直接使用这个镜像;运行中的这个镜像称为容器,容器启动是非常快速的。
参考 我的 Docker 入门笔记

[图片上传失败...(image-82059e-1606212213489)]

5.2 核心概念

  • 主机(Host):安装了Docker程序的机器(Docker直接安装在操作系统之上)
  • 客户端(Client):连接docker主机进行操作
  • 仓库(Registry):用来保存各种打包好的软件镜像
  • 镜像(Images):软件打包好的镜像;放在docker仓库中
  • 容器(Container):镜像启动后的实例称为一个容器,容器是独立运行的一个或一组应用

使用Docker的步骤:

  1. 安装Docker
  2. 去Docker仓库找到这个软件对应的镜像
  3. 使用Docker运行这个镜像,这个镜像就会生成一个Docker容器
  4. 对容器的启动停止就是对软件的启动停止

5.3 安装Docker

安装linux虚拟机

  1. VMWare、VirtualBox(安装)

  2. 导入虚拟机文件centos7-atguigu.ova

  3. 双击启动linux虚拟机;使用 root/ 123456登陆

  4. 使用客户端连接linux服务器进行命令操作

  5. 设置虚拟机网络;桥接网络===选好网卡====接入网线;

  6. 设置好网络以后使用命令重启虚拟机的网络

    service network restart
    
  7. 查看linux的ip地址

    ip addr
    
  8. 使用客户端连接linux

在linux虚拟机上安装docker

参考 Docker 安装与启动
步骤:

# 1、检查内核版本,必须是3.10及以上
uname -r
# 2、安装docker
yum install docker
# 3、输入y确认安装
# 4、启动docker
[root@localhost ~]# systemctl start docker
[root@localhost ~]# docker -v
Docker version 1.12.6, build 3e8e77d/1.12.6
# 5、开机启动docker
[root@localhost ~]# systemctl enable docker
Created symlink from /etc/systemd/system/multi-user.target.wants/docker.service to /usr/lib/systemd/system/docker.service.
# 6、停止docker
systemctl stop docker

5.4 Docker常用命令&操作

1. 镜像操作

操作 命令 说明
检索 docker search 关键字 eg:docker search redis 我们经常去docker hub上检索镜像的详细信息,如镜像的TAG。
拉取 docker pull 镜像名:tag :tag是可选的,tag表示标签,多为软件的版本,默认是latest
列表 docker images 查看所有本地镜像
删除 docker rmi image-id 删除指定的本地镜像

https://hub.docker.com/

2. 容器操作

软件镜像(QQ安装程序)----运行镜像----产生一个容器(正在运行的软件,运行的QQ);

步骤:

# 1、搜索镜像
[root@localhost ~]# docker search tomcat
# 2、拉取镜像
[root@localhost ~]# docker pull tomcat
# 3、根据镜像启动容器
docker run --name mytomcat -d tomcat:latest
# 4、查看运行中的容器
docker ps  
# 5、 停止运行中的容器
docker stop  容器的id
# 6、查看所有的容器
docker ps -a
# 7、启动容器
docker start 容器id
# 8、删除一个容器
docker rm 容器id
# 9、启动一个做了端口映射的tomcat
[root@localhost ~]# docker run -d -p 8888:8080 tomcat
-d:后台运行
-p: 将主机的端口映射到容器的一个端口    主机端口:容器内部的端口

# 10、为了演示简单关闭了linux的防火墙
service firewalld status ;查看防火墙状态
service firewalld stop:关闭防火墙
# 11、查看容器的日志
docker logs container-name/container-id

更多命令参考 Docker 官方文档

3. 安装 MySQL 示例

拉取 MySQL镜像:

docker pull mysql:5.6

$ docker images
REPOSITORY            TAG                 IMAGE ID            CREATED             SIZE
mysql                 5.6                 8de95e6026c3        4 weeks ago         302MB

启动 MySQL:

启动 MySQL的命令比较特殊,以后遇到不熟悉的镜像可以查看官方文档,如 启动MySQL 可以查看 MySQL 镜像文档,可以看到启动命令中需要设置 MySQL 的初始密码:

Start a mysql server instance
Starting a MySQL instance is simple:

$ docker run --name some-mysql -e MYSQL_ROOT_PASSWORD=my-secret-pw -d mysql:tag
[root@localhost ~]# docker run --name mysql01 -d mysql:5.6
42f09819908bb72dd99ae19e792e0a5d03c48638421fa64cce5f8ba0f40f5846

# 查看容器,发现mysql并没有启动成功
[root@localhost ~]# docker ps -a
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS                           PORTS               NAMES
42f09819908b        mysql               "docker-entrypoint.sh"   34 seconds ago      Exited (1) 33 seconds ago                            mysql01
538bde63e500        tomcat              "catalina.sh run"        About an hour ago   Exited (143) About an hour ago                       compassionate_
goldstine
c4f1ac60b3fc        tomcat              "catalina.sh run"        About an hour ago   Exited (143) About an hour ago                       lonely_fermi
81ec743a5271        tomcat              "catalina.sh run"        About an hour ago   Exited (143) About an hour ago                       sick_ramanujan


# 查看错误日志
[root@localhost ~]# docker logs 42f09819908b
error: database is uninitialized and password option is not specified 
  You need to specify one of MYSQL_ROOT_PASSWORD, MYSQL_ALLOW_EMPTY_PASSWORD and MYSQL_RANDOM_ROOT_PASSWORD;这个三个参数必须指定一个

# 正确的启动,设置MySQL密码
[root@localhost ~]# docker run --name mysql01 -e MYSQL_ROOT_PASSWORD=123456 -d mysql:5.6
b874c56bec49fb43024b3805ab51e9097da779f2f572c22c695305dedd684c5f
[root@localhost ~]# docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS               NAMES
b874c56bec49        mysql               "docker-entrypoint.sh"   4 seconds ago       Up 3 seconds        3306/tcp            mysql01

# 做端口映射
[root@localhost ~]# docker run -p 3306:3306 --name mysql02 -e MYSQL_ROOT_PASSWORD=123456 -d mysql:5.6
ad10e4bc5c6a0f61cbad43898de71d366117d120e39db651844c0e73863b9434

# 可以看到容器的端口映射 0.0.0.0:3306->3306/tcp
[root@localhost ~]# docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                    NAMES
ad10e4bc5c6a        mysql               "docker-entrypoint.sh"   4 seconds ago       Up 2 seconds        0.0.0.0:3306->3306/tcp   mysql02

操作 MySQL 命令行:

启动 MySQL 成功后,可以进入 MySQL 命令行操作 MySQL

# 启动MySQL
> docker run --name mysql02 -p 3316:3306 -e MYSQL_ROOT_PASSWORD=123456 -d mysql:5.6
6a356cfef74fd3c0c81b298bb04913ed1faa2c676cd30b4c57efa220d691d6b2

# 查看mysql容器信息
> docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                    NAMES
6a356cfef74f        mysql:5.6           "docker-entrypoint.s…"   5 seconds ago       Up 4 seconds        0.0.0.0:3316->3306/tcp   mysql02

# 进入mysql命令行,可以看到Mysql的版本号等信息
> docker exec -it mysql02 bash
root@6a356cfef74f:/# mysql -uroot -p123456
Warning: Using a password on the command line interface can be insecure.
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 2
Server version: 5.6.48 MySQL Community Server (GPL)

Copyright (c) 2000, 2020, Oracle and/or its affiliates. All rights reserved.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
# 显示所有数据库
mysql> show databases;
+--------------------+
| Database           |
+--------------------+
| information_schema |
| mysql              |
| performance_schema |
+--------------------+
3 rows in set (0.00 sec)

安装完成后,可以使用 Navicat 连接 Docker 中的 MySQL,连接端口为容器映射到主机的端口,但是发生错误:

Client does not support authentication protocol

经过查找资料,发现是 Navicat 不支持 MySQL 8.0 的原因,于是我关闭了 MySQL:8.0 容器,重新拉取启动了 MySQL:5.6,使用 Navicat 连接成功。

高级操作:

# 把主机的/conf/mysql文件夹挂载到 mysqldocker容器的/etc/mysql/conf.d文件夹里面
# 改mysql的配置文件就只需要把mysql配置文件放在自定义的文件夹下(/conf/mysql)
docker run --name mysql03 -v /conf/mysql:/etc/mysql/conf.d -e MYSQL_ROOT_PASSWORD=my-secret-pw -d mysql:tag

# 指定mysql的一些配置参数
docker run --name some-mysql -e MYSQL_ROOT_PASSWORD=my-secret-pw -d mysql:tag --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci

第 6 章 SpringBoot 与数据访问

对于数据访问层,无论是 SQL 还是 NOSQL,Spring Boot 默认采用整合 Spring Data 的方式进行统一处理,添加大量自动配置,屏蔽了很多设置。引入各种 xxxTemplate,xxxRepository 来简化我们对数据访问层的操作。对我们来说只需要进行简单的设置即可。我们将在数据访问章节测试使用SQL相关、NOSQL在缓存、消息、检索等章节测试。
– JDBC
– MyBatis
– JPA

6.1 数据源初始化与 JDBC

1. 配置 MySQL

  1. 引入 Mysql 驱动 和 jdbc-starter:

默认MySQL 驱动是 8.0

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

    <!--    mysql 驱动,这里默认是8.0.20版本,测试兼容 MySQL5.6    -->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <scope>runtime</scope>
    </dependency>

Mysql驱动scope设置为runtime?

  1. application.yml中配置数据源,新版驱动变为了 com.mysql.cj.jdbc.Driver
spring:
  datasource:
    username: root
    password: 123456
    url: jdbc:mysql://localhost:3316/sp-jdbc
    driver-class-name: com.mysql.cj.jdbc.Driver
  1. 单元测试 JDBC 配置
    @Autowired
    private DataSource dataSource;

    @Test
    void testJdbc() throws SQLException {
        System.out.println("数据源:" + dataSource.getClass());
        Connection connection = dataSource.getConnection();
        System.out.println("Connection: " + connection);
    }

经过测试,说明配置 MySQL 成功,SpringBoot 2.x 默认使用 Hikari 数据源

数据源:class com.zaxxer.hikari.HikariDataSource
Connection: HikariProxyConnection@356519935 wrapping com.mysql.cj.jdbc.ConnectionImpl@18d910b3

什么是数据源?

数据源是对数据库操作的抽象,封装了目标源的位置信息,验证信息和建立与关闭连接的操作。不同数据库可以实现接口提供不同策略。常见数据源包括:DriverManagerDataSource(不提供连接池),C3P0,Dbcp2,Hikari等

数据源更多的属性配置参考:DataSourceProperties

  • @ConfigurationProperties 从配置文件根据prefix读取属性绑定
@ConfigurationProperties(prefix = "spring.datasource")
public class DataSourceProperties {
    private String username;
    private String password;
    private String url;
    private String driverClassName;
    private Class<? extends DataSource> type;

}

2. 数据源自动配置原理

数据源自动配置原理: DataSourceConfiguration

  • SpringBoot 2.x 默认数据源为 hikari
  • 可以使用spring.datasource.type修改数据源类型
  • @Configuration 底层是 @Component,标注这个类会被扫描
  • @ConfigurationProperties 从配置文件根据prefix读取属性绑定
  • @Bean 将方法返回的对象加入到 Spring 容器,该方法只会被调用一次
  • @ConditionalOnProperty 表示配置中name=havingValue时则生效,matchIfMissing 表示没有这项配置时也生效
DataSourceConfiguration:
    @Configuration(proxyBeanMethods = false)
    @ConditionalOnClass(HikariDataSource.class)
    @ConditionalOnMissingBean(DataSource.class)
    @ConditionalOnProperty(name = "spring.datasource.type", havingValue = "com.zaxxer.hikari.HikariDataSource",
            matchIfMissing = true)
    static class Hikari {

        @Bean
        @ConfigurationProperties(prefix = "spring.datasource.hikari")
        HikariDataSource dataSource(DataSourceProperties properties) {
            HikariDataSource dataSource = createDataSource(properties, HikariDataSource.class);
            if (StringUtils.hasText(properties.getName())) {
                dataSource.setPoolName(properties.getName());
            }
            return dataSource;
        }
    }

3. 数据表自动初始化

  1. 创建建表 SQL schema-*.sql,插入数据 SQL data-*.sql,放在 /resource 目录下
  2. 配置中加入spring.datasource.initialization-mode=always表示启动自动建表的操作 (SpringBoot 2.x)
  3. 启动项目就可以自动运行建表,插入数据了

注意:每次项目启动都会执行 SQL,即重新创建表和插入数据

默认建表 SQL名称为schema-*.sql,插入数据 SQL 名称为:data-*.sql。也可以在配置application.yml 中指定建表 SQL 文件:spring.datasource.schema: - classpath:department.sql

spring:
  datasource:
    username: root
    password: 123456
    url: jdbc:mysql://localhost:3316/sp-jdbc
    driver-class-name: com.mysql.cj.jdbc.Driver
    # 2.x 开启自动初始化数据库
    initialization-mode: always
    # 指定自动执行的建表SQL,List需要用 - 表示
    schema:
      - classpath:department.sql   # - 后面有空格,classpath:后无空格
      - classpath:employee.sql
    data: 
      - classpath:init-dept.sql    # 插入数据SQL

4. 使用 JdbcTemplate 查询数据

JdbcTemplateConfiguration 会创建 JdbcTemplate Bean 并加入到容器,使用 @Autowired 自动装配即可查询数据

@RestController
public class HelloController {

    // SpringBoot自动配置了 JdbcTemplate组件,并加入到容器
    @Autowired
    JdbcTemplate jdbcTemplate;

    @GetMapping("/query")
    public Map<String, Object> getDept() {
        List<Map<String, Object>> depts = jdbcTemplate.queryForList("SELECT * FROM department");

        return depts.get(0);
    }
}

5. 数据库自动初始化原理

数据库自动初始化原理: DataSourceInitializerInvoker,DataSourceInitializer
- 数据源初始化完成(被创建)时,帮我们运行 schema-.sql 建表
- 监听建表完成事件,完成后执行 data-
.sql 插入数据

DataSourceInitializerInvoker:
class DataSourceInitializerInvoker implements ApplicationListener<DataSourceSchemaCreatedEvent>, InitializingBean {

    // 数据源 DataSource Bean初始化完成后调用
    @Override
    public void afterPropertiesSet() {
        DataSourceInitializer initializer = getDataSourceInitializer();
        if (initializer != null) {
            // 获取建表 schema-*.sql 并执行,见下文
            boolean schemaCreated = this.dataSourceInitializer.createSchema();
            if (schemaCreated) {

                // 表已经创建完成,触发DataSourceSchemaCreatedEvent事件
                initialize(initializer);
            }
        }
    }

    // 监听 DataSourceSchemaCreatedEvent事件,执行插入数据SQL
    @Override
    public void onApplicationEvent(DataSourceSchemaCreatedEvent event) {
 
        DataSourceInitializer initializer = getDataSourceInitializer();
        if (!this.initialized && initializer != null) {

            // 获取data-*.sql 并执行,见下文
            initializer.initSchema();
            this.initialized = true;
        }
    }

上面执行建表SQL 与 插入数据 SQL 方法内容如下:

DataSourceInitializer:

    // 执行建表SQL schema-*.sql
    boolean createSchema() {
        // 获取需要执行的建表schema-*.sql
        List<Resource> scripts = getScripts("spring.datasource.schema", this.properties.getSchema(), "schema");
        if (!scripts.isEmpty()) {
            if (!isEnabled()) {
                logger.debug("Initialization disabled (not running DDL scripts)");
                return false;
            }
            String username = this.properties.getSchemaUsername();
            String password = this.properties.getSchemaPassword();
            // 运行sql
            runScripts(scripts, username, password);
        }
        return !scripts.isEmpty();
    }

    // 执行插入数据SQL data-*.sql
    void initSchema() {
        // 获取插入数据SQL data-*.sql
        List<Resource> scripts = getScripts("spring.datasource.data", this.properties.getData(), "data");
        if (!scripts.isEmpty()) {
            if (!isEnabled()) {
                logger.debug("Initialization disabled (not running data scripts)");
                return;
            }
            String username = this.properties.getDataUsername();
            String password = this.properties.getDataPassword();
            // 运行SQL
            runScripts(scripts, username, password);
        }
    }

6.2 使用外部数据源

前面提到可以使用spring.datasource.type来指定数据源,SpringBoot 支持的数据源包括

  • Hikari
  • Tomcat
  • Dbcp2

自定义数据源 Druid 步骤:

  1. 引入 Druid 依赖

     <!-- 引入自定义数据源 druid-->
     <dependency>
         <groupId>com.alibaba</groupId>
         <artifactId>druid</artifactId>
         <version>1.1.8</version>
     </dependency>
    
  2. 配置自定义数据源 application.yml

    spring:
    datasource:
        # 自定义数据源
        type: com.alibaba.druid.pool.DruidDataSource
    
  3. 测试数据源 Druid 配置

        @Test
        void testJdbc() throws SQLException {
            System.out.println("数据源:" + dataSource.getClass());
            Connection connection = dataSource.getConnection();
            System.out.println("Connection: " + connection);
        }
    

    输出:

    数据源:class com.alibaba.druid.pool.DruidDataSource
    Connection: com.mysql.cj.jdbc.ConnectionImpl@5c080ef3
    

6.3 自定义数据源原理

SpringBoot 中自定义数据源自动配置功能,是通过配置属性 spring.datasource.type获取数据源的全类名,然后通过反射创建该类的实例对象,然后绑定数据源的相关属性配置,最后返回实例对象,加入到 Spring 容器。源码如下:

DataSourceConfiguration:

    @Configuration(proxyBeanMethods = false)
    // 如果容器中不存在DataSource Bean,才执行该方法将自定义数据源Bean加入容器
    @ConditionalOnMissingBean(DataSource.class)
    @ConditionalOnProperty(name = "spring.datasource.type")
    static class Generic {

        // 将创建的数据源加入到容器,使用时使用 @AutoWired
        @Bean
        DataSource dataSource(DataSourceProperties properties) {
            // 使用 DataSourceBuilder 创建数据源,build下下文
            return properties.initializeDataSourceBuilder().build();
        }

    }

DataSourceBuilder:

    public T build() {
        // 获取 spring.datasource.type 指定的数据源类名称
        Class<? extends DataSource> type = getType();
        // 根据数据源类名称,反射创建数据源示例
        DataSource result = BeanUtils.instantiateClass(type);
        maybeGetDriverClassName();
        // 绑定数据源的属性配置,如url, username, password
        bind(result);
        return (T) result;
    }

这种方式无法配置自定义数据源的特有属性,所以一般使用 《6.4 配置 Druid 数据源》 自定义配置类的方式引入外部数据源

6.4 配置 Druid 数据源

上一节中使用了外部数据源 Druid,但是不支持配置 Druid 的相关属性,所以我们来自定义 Druid 配置类来创建数据源Bean DataSource 加入到 Spring 容器。

  1. 配置 Druid 相关属性
spring:
  datasource:
    username: root
    password: 123456
    url: jdbc:mysql://localhost:3316/sp-jdbc
    driver-class-name: com.mysql.cj.jdbc.Driver
    # 2.x 开启自动初始化数据库
    initialization-mode: always
    # 自定义数据源
    type: com.alibaba.druid.pool.DruidDataSource
    # 自动执行的建表SQL,默认为schema-*.sql data-*.sql
#    schema:
#      - classpath:department.sql   # - 后面有空格,classpath:后无空格
#      - classpath:employee.sql

    #  druid 数据源其他配置
    # 初始化连接个数
    initialSize: 5
    minIdle: 5
    # 连接池的最大数据库连接数。设为0表示无限制
    maxActive: 20
    maxWait: 60000
    timeBetweenEvictionRunsMillis: 60000
    minEvictableIdleTimeMillis: 300000
    validationQuery: SELECT 1 FROM DUAL
    testWhileIdle: true
    testOnBorrow: false
    testOnReturn: false
    poolPreparedStatements: true
    #   配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙
    filters: stat,wall
    maxPoolPreparedStatementPerConnectionSize: 20
    useGlobalDataSourceStat: true
    connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500
  1. 将 Druid 数据源加入 Spring 容器

将上面配置的 Druid 数据源的属性与 DruidDataSource 进行绑定,并将其加入到 Spring 容器

  • @ConfigurationProperties 将配置文件中的相关属性绑定到 DruidDataSource 对象
  • @Bean 将方法返回的 DataSource 对象加入到 Spring 容器,使用 @Autowired 自动装配
  • 不会与默认的数据源 Hikari 冲突,因为 Hikari 数据源加入容器的条件是容器中没有 DataSource Bean
  • Hikari Bean 上的注解 @ConditionalOnMissingBean(DataSource.class) 表示没有 DataSource Bean 时才加入容器
    `
@Configuration
public class DruidConfig {

    /**
     * 读取配置文件中的 spring.datasource 属性绑定到DruidDataSource,返回一个 DataSource 数据源对象
     * 并将其加入容器,替换了默认的 DataSource Bean(Hikari)
     * {@link org.springframework.boot.autoconfigure.jdbc.DataSourceConfiguration.Hikari.dataSource}
     */
    @Bean
    @ConfigurationProperties(prefix = "spring.datasource")
    public DataSource druid() {
        return new DruidDataSource();
    }
}

  1. 使用 Druid 数据源

    测试 Druid 数据源的属性配置,输出相关属性,与配置文件中一致,说明配置成功

        // 这里装配的数据源已经从默认的 Hrki 变为了 Druid
        @Autowired
        private DataSource dataSource;
    
        @Test
        void testDruid() throws SQLException {
            System.out.println("数据源:" + dataSource.getClass());
    
            // 获取Druid 数据源的initialSize属性,配置文件中为5
            int initialSize = ((DruidDataSource)dataSource).getInitialSize();
            System.out.println("initialSize: " + initialSize);
    
            // 获取Druid 数据源的maxActive属性,配置文件中为20
            int maxActive = ((DruidDataSource)dataSource).getMaxActive();
            System.out.println("maxActive: " + maxActive);
        }
    
  2. 配置 Druid Web 监控

    @Configuration
    public class DruidConfig {
    
        /**
         * 读取配置文件中的 spring.datasource 属性绑定到DruidDataSource,返回一个 DataSource 数据源对象
         * 并将其加入容器,替换了默认的 DataSource Bean(Hikari)
         * {@link org.springframework.boot.autoconfigure.jdbc.DataSourceConfiguration.Hikari.dataSource}
         */
        @Bean
        @ConfigurationProperties(prefix = "spring.datasource")
        public DataSource druid() {
            return new DruidDataSource();
        }
    
        // 配置Druid的监控
        // 1. 注册一个Druid管理后台的Servlet
        @Bean
        public ServletRegistrationBean statViewServlet() {
            // 注册Servlet到容器,处理/druid/*下的所有请求
            // 这里的 urlMappings 类似Controller,访问时前面需要加上项目路径 context-path
            ServletRegistrationBean bean = new ServletRegistrationBean(new StatViewServlet(), "/druid/*");
    
            Map<String, String> initParams = new HashMap<>();
            initParams.put("loginUsername", "admin");
            initParams.put("loginPassword", "admin");
            initParams.put("allow", "");     // 允许所有人访问
            initParams.put("deny", "192.168.1.1");    // 拒绝这个IP访问
    
            bean.setInitParameters(initParams);
            return bean;
        }
    
        // 2. 注册一个 Web监控的filter
        @Bean
        public FilterRegistrationBean webStatFilter() {
            FilterRegistrationBean<Filter> bean = new FilterRegistrationBean<>();
            bean.setFilter(new WebStatFilter());
    
            Map<String, String> initParams = new HashMap<>();
            initParams.put("exclusions", "*.js, *.css, /druid/*");
            bean.setInitParameters(initParams);
    
            bean.setUrlPatterns(Arrays.asList("/*"));
            return bean;
        }
    }
    

访问localhost:8080/context-path/druid ,就可以查看 Druid 的 Web 监控,可以查看数据源的配置,调用过的SQL,访问过URI等监控信息。

Druid 监控控制台

6.5 整合 MyBatis

1. 注解版

整合 Mybatis 的前提预备步骤:

  1. 已经配置好了 MySQL驱动 和数据源
  2. 数据库建表
  3. 创建了和表对应的实体类
  4. 可以使用 JDBC 单元测试一下前三步是否准备完成

做好预备步骤后,接下来整合 SprintBoot 与 Mybatis:

  1. 引入 MyBatis-starter
        <!-- 引入 mybatis-starter,不是Spring官方出点  -->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.3</version>
        </dependency>
    

[图片上传失败...(image-2717e4-1606212213489)]

  1. 创建注解版Mapper,实现增删改查 SQL
// 指定这是一个操作数据库的mapper,将接口扫描装配到容器中,接口名不需要与表名一致
@Mapper
public interface DepartmentMapper {

    // 注解版,直接写sql语句,不需要xml
    @Select("select * from department where id=#{id}")
    public Department getDeptById(Integer id);

    @Delete("delete from department where id=#{id}")
    public int deleteDeptById(Integer id);

    // 标注使用自增主键,并且将生成的主键绑定到入参 dept.id 属性上
    @Options(useGeneratedKeys = true, keyProperty = "id")
    @Insert("insert into department(departmentName) values(#{departmentName})")
    public int insertDept(Department dept);

    @Update("update dapartment set departmentName=#{departmentName} where id=#{id}")
    public int updateDept(Department dept);
}

  1. 使用 Mapper
@RestController
public class DeptController {

    @Autowired
    private DepartmentMapper departmentMapper;

    @GetMapping("/dept/{id}")
    public Department getDepartment(@PathVariable("id") Integer id) {
        return departmentMapper.getDeptById(id);
    }

    @GetMapping("/dept")
    public Department getDepartment(Department dept) {
        int i = departmentMapper.insertDept(dept);
        
        // 这里返回的dept已经设置了自动生成的id
        return dept;
    }
}

2. Mybatis 常见配置

  1. 获取自增主键:插入数据后,通常需要将数据返回,而自增主键一般在数据库中生成。我们可以使用 @Options 注解,Mybatis 会自己将主键绑定到插入对象的属性上,useGeneratedKeys 表示使用了自增逐渐,keyProperty 指定主键需要绑定的对象属性

     @Options(useGeneratedKeys = true, keyProperty = "id")
    
  2. 扫描配置:在SpringBoot 主类上加@MappserScan 指定扫描的包,不需要每个Mapper接口上标注Mapper注解

    // 扫描指定包下的 mapper,不需要每个Mapper类上标注Mapper注解
     @MapperScan(value = "com.atguigu.springboot.mapper")
     @SpringBootApplication
     public class SpringBoot06DataMybatisApplication {
    

    @Mapper 注解针对的是一个个的接口,相当于是一个个 Mapper.xml 文件,可以将 SQL 写到接口中,Mybatis 会生成代理类来执行SQL,代理类会加入到Spring 容器。@ComponentScan 扫描的是所有 @Component注解,而 @MapperScan 扫描的是指定包下的所有接口。

  3. 松散绑定:查询到的结果需要自动绑定到对象上返回,但是默认是不支持松散绑定的,如数据库dept_name字段无法绑定到对象的 deptName属性上,需要开启松散绑定。

    mybatis:
     configuration:
         map-underscore-to-camel-case: true
    

    Mybatis 自动配置类为 MybatisAutoConfiguration,根据@EnableConfigurationProperties 指定的配置类找到 MybatisProperties,其属性 configuration 拥有属性 mapUnderscoreToCamelCase,正是用来设置松散绑定的。

  4. 自定义配置:设置 Mybatis 属性,除了配置文件,还可以使用自定义 Mybatis 配置类,下面就使用自定义 Mybatis 配置类设置松散绑定:

    @org.springframework.context.annotation.Configuration
    public class MybatisConfig {
    
        // 这里的ConfigurationCustomizer是mybatis的自定义配置
        @Bean
        public ConfigurationCustomizer configurationCustomizer() {
            return new ConfigurationCustomizer() {
                @Override
                public void customize(Configuration configuration) {
                    // 设置松散绑定
                    configuration.setMapUnderscoreToCamelCase(true);
                }
            };
        }
    }
    
  5. 开启 SQL 打印:

    mybatis
       configuration:
           log-impl: org.apache.ibatis.logging.stdout.StdOutImpl 
    
    <settings>
       <setting name="logImpl" value="STDOUT_LOGGING" />
    </settings>
    
  6. 关闭缓存:Mybatis 中一级缓存默认开启,查询操作会优先从缓存中查询,增删改操作会使缓存失效。
    但是在调试过程中,如果手动修改了数据库数据,Mybatis 仍会使用一级缓存中的数据,导致需要重启项目才能生效,所以开发过程中,建议关闭一级缓存

    <settings>
        <setting name="cacheEnabled" value="false"/>
    </settings>
    

3. xml 版

  1. 指定 Mybatis 的全局配置文件与所有 Mapper 文件的路径
mybatis:
  config-location: classpath:mybatis/mybatis-config.xml     # mybatis全局配置文件
  mapper-locations: classpath:mybatis/mapper/*.xml          # 需要扫描的mapper文件
  1. 创建 Mybatis的全局配置文件

根据 Mybatis 官方文档 创建全局配置文件 mybatis-config.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <settings>
        <!-- 设置松散绑定,下划线转驼峰 -->
        <setting name="mapUnderscoreToCamelCase" value="true"/>
    </settings>
</configuration>
  1. 创建Mapper 接口类与 xml 配置文件
public interface EmployeeMapper {
    // 查询
    public Employee getEmpById(Integer id);

    // 插入
    public void insertEmp(Employee employee);
}
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
        
<!-- 与指定的Mapper接口类绑定 -->
<mapper namespace="com.atguigu.springboot.mapper.EmployeeMapper">

    <!-- id对应Mapper方法名,resultType是返回类型 -->
    <select id="getEmpById" resultType="com.atguigu.springboot.bean.Employee">
        select * from employee where id=#{id}
    </select>

    <!--  自增ID绑定  -->
    <insert id="insertEmp"  useGeneratedKeys="true" keyProperty="id" >
        insert into employee(lastName, email, gender, d_id) values(
            #{lastName},
            #{email},
            #{gender},
            #{dId}
        )
    </insert>
</mapper>

// 补充:对于Mybatis的xml写法还是不熟,如果以后用到了可以专门学一下MyBatis实战教程 - 10小时

6.6 整合 SpringData JPA

1. Spring Data 简介

Spring Data 是为了简化数据访问,提供统一 API 对数据访问层进行操作,包括关系型数据库,非关系型数据库,Map-Reduce框架,云数据服务等,让我们在使用多种数据访问技术时,都使用 Spring 提供的统一标准,包括 增删改查,排序,分页等操作。

Spring Data 包含多个子项目:

  • Spring Data Commons
  • Spring Data JPA
  • Spring Data MongoDB
  • Spring Data Redis
  • Spring Data Elasticsearch

2. 整合 SpringData JPA

// 补充:这里只是复制了笔记,等学懂了JPA,再来补充理解

  1. 编写一个实体类(bean)和数据表进行映射,并且配置好映射关系;
//使用JPA注解配置映射关系
@Entity //告诉JPA这是一个实体类(和数据表映射的类)
@Table(name = "tbl_user") //@Table来指定和哪个数据表对应;如果省略默认表名就是user;
public class User {

    @Id //这是一个主键
    @GeneratedValue(strategy = GenerationType.IDENTITY)//自增主键
    private Integer id;

    @Column(name = "last_name",length = 50) //这是和数据表对应的一个列
    private String lastName;
    @Column //省略默认列名就是属性名
    private String email;
  1. 编写一个Dao接口来操作实体类对应的数据表(Repository)
//继承JpaRepository来完成对数据库的操作
public interface UserRepository extends JpaRepository<User,Integer> {
}

  1. 基本的配置JpaProperties
spring:  
 jpa:
    hibernate:
#     更新或者创建数据表结构
      ddl-auto: update
#    控制台显示SQL
    show-sql: true

第 7 章 SpringBoot 启动配置原理

// 补充:这一章其实并没有听懂,只是复制了笔记,后面进行补充吧

下面的启动配置原理源码解析使用的是 SpringBoot 1.5.10 版本,

几个重要的事件回调机制

配置在META-INF/spring.factories

ApplicationContextInitializer

SpringApplicationRunListener

只需要放在ioc容器中

ApplicationRunner

CommandLineRunner

7.1 启动流程

1. 创建SpringApplication对象

进入 SpringBoot 主类的主方法,是调用 run()方法,其中创建了 SpringApplication 对象,该类的构造方法调用了initialize(),如下所示:

initialize(sources);
private void initialize(Object[] sources) {
    //保存主配置类,即启动类
    if (sources != null && sources.length > 0) {
        this.sources.addAll(Arrays.asList(sources));
    }
    //判断当前是否一个web应用,通过查看是否存在javax.servlet.Servlet类
    this.webEnvironment = deduceWebEnvironment();
    //从类路径下找到META-INF/spring.factories配置的所有ApplicationContextInitializer;然后保存起来
    setInitializers((Collection) getSpringFactoriesInstances(
        ApplicationContextInitializer.class));
    //从类路径下找到ETA-INF/spring.factories配置的所有ApplicationListener
    setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
    //从多个配置类中找到有main方法的主配置类
    this.mainApplicationClass = deduceMainApplicationClass();
}

2. 运行run方法

SpringApplication 初始化完成后,调用其run()方法:

public ConfigurableApplicationContext run(String... args) {
   StopWatch stopWatch = new StopWatch();
   stopWatch.start();
   ConfigurableApplicationContext context = null;
   FailureAnalyzers analyzers = null;
   configureHeadlessProperty();
    
   //获取SpringApplicationRunListeners;从类路径下META-INF/spring.factories
   SpringApplicationRunListeners listeners = getRunListeners(args);
    //回调所有的获取SpringApplicationRunListener.starting()方法
   listeners.starting();
   try {
       //封装命令行参数
      ApplicationArguments applicationArguments = new DefaultApplicationArguments(
            args);
      //准备环境
      ConfigurableEnvironment environment = prepareEnvironment(listeners,
            applicationArguments);
            //创建环境完成后回调SpringApplicationRunListener.environmentPrepared();表示环境准备完成

      // 控制台输出Spring Logo 
      Banner printedBanner = printBanner(environment);
       
       //创建ApplicationContext;决定创建web的ioc还是普通的ioc
      context = createApplicationContext();
       
      analyzers = new FailureAnalyzers(context);
       //准备上下文环境;将environment保存到ioc中;而且applyInitializers();
       //applyInitializers():回调之前保存的所有的ApplicationContextInitializer的initialize方法
       //回调所有的SpringApplicationRunListener的contextPrepared();
       //
      prepareContext(context, environment, listeners, applicationArguments,
            printedBanner);
       //prepareContext运行完成以后回调所有的SpringApplicationRunListener的contextLoaded();
       
       // 这个方法进入到了Spring中,创建所有 bean 对象,
       // 刷新容器;ioc容器初始化(如果是web应用还会创建嵌入式的Tomcat);Spring注解版
       // 扫描,创建,加载所有组件的地方;(配置类,组件,自动配置)
      refreshContext(context);

       //从ioc容器中获取所有的ApplicationRunner和CommandLineRunner进行回调
       //ApplicationRunner先回调,CommandLineRunner再回调
      afterRefresh(context, applicationArguments);
       //所有的SpringApplicationRunListener回调finished方法
      listeners.finished(context, null);
      stopWatch.stop();
      if (this.logStartupInfo) {
         new StartupInfoLogger(this.mainApplicationClass)
               .logStarted(getApplicationLog(), stopWatch);
      }
       //整个SpringBoot应用启动完成以后返回启动的ioc容器;
      return context;
   }
   catch (Throwable ex) {
      handleRunFailure(context, listeners, analyzers, ex);
      throw new IllegalStateException(ex);
   }
}
AbstractApplicationContext: spring 容器启动初始化Bean工厂

    public void refresh() throws BeansException, IllegalStateException {
        synchronized (this.startupShutdownMonitor) {
            // Prepare this context for refreshing.
            prepareRefresh();

            // Tell the subclass to refresh the internal bean factory.
            ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();

            // Prepare the bean factory for use in this context.
            prepareBeanFactory(beanFactory);

            try {
                // Allows post-processing of the bean factory in context subclasses.
                postProcessBeanFactory(beanFactory);

                // Invoke factory processors registered as beans in the context.
                invokeBeanFactoryPostProcessors(beanFactory);

                // Register bean processors that intercept bean creation.
                registerBeanPostProcessors(beanFactory);

                // Initialize message source for this context.
                initMessageSource();

                // Initialize event multicaster for this context.
                initApplicationEventMulticaster();

                // Initialize other special beans in specific context subclasses.
                onRefresh();

                // Check for listener beans and register them.
                registerListeners();

                // 对所有Bean进行初始化后保存到beanFactory,Bean是单例
                finishBeanFactoryInitialization(beanFactory);

                // Last step: publish corresponding event.
                finishRefresh();
            }
            catch (BeansException ex) { }
            finally {  }
        }
    }

7.2 事件监听机制

// 补充

第 8 章 SpringBoot 自定义 starter

8.1 starter 原理

starter:

1、这个场景需要使用到的依赖是什么?

2、如何编写自动配置类
@Configuration          //指定这个类是一个配置类
@ConditionalOnXXX       //在指定条件成立的情况下自动配置类生效
@AutoConfigureAfter     //指定自动配置类的顺序
@Bean                   //给容器中添加组件

@ConfigurationPropertie           // 指定相关xxxProperties类来绑定相关的配置
@EnableConfigurationProperties    // 让指定的xxxProperties生效加入到容器中


参考 DataSourceAutoConfiguration,WebMvcAutoConfiguration 等自动配置类

而自动配置类要能加载,将需要启动就加载的自动配置类,配置在META-INF/spring.factories

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\

8.2 自定义 starter

// 补充:很简单,但是心静不下来,后面再补充,23分钟的视频 p71

9. SpringBoot 与开发热部署

在实际开发中我们修改一个文件后想看到效果必须重启应用,这导致大量时间
被浪费,我们希望不重启应用的情况下,程序可以自动部署(热部署)。有以下四
种方式可以实现热部署:

  1. 模板引擎

    • 在 Spring Boot 中开发情况下禁用模板引擎的缓存 spring.thymeleaf.cache=false
    • 页面模板改变ctrl+F9可以重新编译当前页面并生效
  2. Spring Loaded

    • Spring官方提供的热部署程序,实现修改类文件的热部署
    • 下载Spring Loaded(项目地址https://github.com/spring�projects/spring-loaded)
    • 添加运行时参数 -javaagent:C:/springloaded-1.2.5.RELEASE.jar -noverify
  3. Spring Boot Devtools

    1. 引入依赖
      <!--    引入热部署依赖    -->
      <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-devtools</artifactId>
      </dependency>
      
    2. ctrl + f9 重新构建一次项目即可生效
  4. JRebel (推荐)

Spring Boot Devtools 热部署的本质还是重启,效率很低。IDEA 中自带插件 HotSwap,例如在修改 Controller 层的代码后ctrl + f9能够立即生效,但是修改 Controller 引用的其他模块代码,并不会立即生效,而 JRebel 能够实现动态类加载,所以推荐使用 JRebel 进行热加载。

进阶学习

  1. SpringBoot 整合教程 - 尚硅谷

    7小时,主要讲了 SpringBoot 与常用技术的整合,包括 Redis,RabbitMQ,ElasticSearch,定时任务,SpringSecurity,Zookeeper/dubbo,SpringCloud等,等熟悉了这些技术后再学习与 SpringBoot 整合

  2. Spring注解驱动教程 - 尚硅谷

    11 小时,学习当前这门课,许多源码部分都涉及到了 Spring 原生注解,很多实际开发用不到的注解,在看源码时很有用,需要查缺补漏

  3. SpringCloud 教程 - 尚硅谷

    25 小时,分布式微服务必备,必须具有 SpringBoot 基础

  4. 图解+仿写深入解析SpringBoot源码课 - 慕课网

    13小时,当前笔记中许多 SpringBoot 源码和原理的地方没有完全搞清楚,借助这门课搞清

  5. Spring Boot企业微信点餐系统 - 慕课网

    学了SpringBoot,需要使用项目巩固一下,否则很容易忘记,也无法检测学习成果。这门课并不算全面,缺少密码加密,购物车,图片上传等功能,参考从0开始Java电商网站开发

  6. SSM 教程

待补充

  1. 补充笔记中写了// 补充的部分,这些部分大多都是重难点。包括自动配置原理,@Conditional,SpringMVC自动配置原理,嵌入式 Servlet 容器自动配置原理,SpringBoot启动配置原理,监听事件机制,自定义 starter
  2. 分析SpringMVC工作流程,根据LoginController debug和3y mvc笔记,核心控制器,映射器,适配器,视图解析器等等,非常简单(反射调用方法,传参)
    SpringMVC - 运行流程图及原理分析
    https://blog.csdn.net/j080624/article/details/58041191
    https://blog.csdn.net/j080624/article/details/57411870

[图片上传失败...(image-fbd23b-1606212213489)]

[图片上传失败...(image-e93db3-1606212213489)]

Spring MVC工作流程图
  1. Bean 的生命周期,Spring IOC 的初始化,参考文章Bean的生命周期 - Guide 推荐

    https://blog.csdn.net/fageweiketang/article/details/80994433
    https://mp.weixin.qq.com/s/IKrWnD_O4_L7tbhmTFG_pg
    https://mp.weixin.qq.com/s/0tWgaYxavixiDCppvOfd-w

在讲解该控制器之前,首先我们要明白SpringMVC控制器一个与Struts2不同的地方:SpringMVC的控制器是单例的,Struts2的控制器是多例的!

也就是说:Struts2收集变量是定义成员变量来进行接收,而SpringMVC作为单例的,是不可能使用成员变量来进行接收的【因为会有多个用户访问,就会出现数据不合理性】!

那么SpringMVC作为单例的,他只能通过方法的参数来进行接收对应的参数!只有方法才能保证不同的用户对应不同的数据!

  1. 看一下过滤器和监听器,搞明白CharacterEncodingFilter,HiddenHttpMethodFilter,ContextLoaderListener的原理

推荐阅读

  1. @RequestParam注解详解
  2. 15个经典的Spring面试题 - Guide
  3. Spring 面试题及解析 - 程序员诸葛
  4. Spring Boot热部署 - 慕课网
  5. Spring MVC中的Controller是Serlvet吗?
  6. 关于 @EnableConfigurationProperties 注解
  7. Spring Boot启动流程源码详解
  8. Spring/SpringBoot常用注解总结 - Guide
  9. 15个经典的Spring面试常见问题解析 - Guide
  10. Spring Boot 热部署入门 - 芋道源码

参考文档

  1. SpringBoot 核心技术 - 尚硅谷

  2. Spring 注解驱动教程 - 尚硅谷

  3. Spring 和 SpringBoot 比较

  4. SpringBoot 2.3.1 官方文档

  5. SpringBoot 66个常用Demo - Github

  6. SpringBoot 整合示例

  7. Srping 易混淆注解对比 - Hollis

  8. thymeleaf3.0 官方文档

  9. Docker MySQL 镜像 官方文档

  10. Docker 入门笔记

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

推荐阅读更多精彩内容