springboot学习2

第05课:Spring Boot中的MVC支持

Spring Boot 的 MVC 支持主要来介绍实际项目中最常用的几个注解,包括 @RestController@RequestMapping@PathVariable@RequestParam 以及 @RequestBody。主要介绍这几个注解常用的使用方式和特点。

1. @RestController

@RestController 是 Spring Boot 新增的一个注解,我们看一下该注解都包含了哪些东西。

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Controller
@ResponseBody
public @interface RestController {
    String value() default "";
}

可以看出, @RestController 注解包含了原来的 @Controller@ResponseBody 注解,使用过 Spring 的朋友对 @Controller 注解已经非常了解了,这里不再赘述, @ResponseBody 注解是将返回的数据结构转换为 Json 格式。所以 @RestController 可以看作是 @Controller@ResponseBody 的结合体,相当于偷个懒,我们使用 @RestController 之后就不用再使用 @Controller 了。但是需要注意一个问题:如果是前后端分离,不用模板渲染的话,比如 Thymeleaf,这种情况下是可以直接使用@RestController 将数据以 json 格式传给前端,前端拿到之后解析;但如果不是前后端分离,需要使用模板来渲染的话,一般 Controller 中都会返回到具体的页面,那么此时就不能使用@RestController了,比如:

public String getUser() {
    return "user";
}

其实是需要返回到 user.html 页面的,如果使用 @RestController 的话,会将 user 作为字符串返回的,所以这时候我们需要使用 @Controller 注解。这在下一节 Spring Boot 集成 Thymeleaf 模板引擎中会再说明。

2. @RequestMapping

@RequestMapping 是一个用来处理请求地址映射的注解,它可以用于类上,也可以用于方法上。在类的级别上的注解会将一个特定请求或者请求模式映射到一个控制器之上,表示类中的所有响应请求的方法都是以该地址作为父路径;在方法的级别表示进一步指定到处理方法的映射关系。

该注解有6个属性,一般在项目中比较常用的有三个属性:value、method 和 produces。

  • value 属性:指定请求的实际地址,value 可以省略不写
  • method 属性:指定请求的类型,主要有 GET、PUT、POST、DELETE,默认为 GET
  • produces属性:指定返回内容类型,如 produces = "application/json; charset=UTF-8"

@RequestMapping 注解比较简单,举个例子:

@RestController
@RequestMapping(value = "/test", produces = "application/json; charset=UTF-8")
public class TestController {

    @RequestMapping(value = "/get", method = RequestMethod.GET)
    public String testGet() {
        return "success";
    }
}

这个很简单,启动项目在浏览器中输入 localhost:8080/test/get 测试一下即可。

针对四种不同的请求方式,是有相应注解的,不用每次在 @RequestMapping 注解中加 method 属性来指定,上面的 GET 方式请求可以直接使用 @GetMapping("/get") 注解,效果一样。相应地,PUT 方式、POST 方式和 DELETE 方式对应的注解分别为 @PutMapping@PostMappingDeleteMapping

3. @PathVariable

@PathVariable 注解主要是用来获取 url 参数,Spring Boot 支持 restfull 风格的 url,比如一个 GET 请求携带一个参数 id 过来,我们将 id 作为参数接收,可以使用 @PathVariable 注解。如下:

@GetMapping("/user/{id}")
public String testPathVariable(@PathVariable Integer id) {
    System.out.println("获取到的id为:" + id);
    return "success";
}

这里需要注意一个问题,如果想要 url 中占位符中的 id 值直接赋值到参数 id 中,需要保证 url 中的参数和方法接收参数一致,否则就无法接收。如果不一致的话,其实也可以解决,需要用 @PathVariable 中的 value 属性来指定对应关系。如下:

@RequestMapping("/user/{idd}")
public String testPathVariable(@PathVariable(value = "idd") Integer id) {
    System.out.println("获取到的id为:" + id);
    return "success";
}

对于访问的 url,占位符的位置可以在任何位置,不一定非要在最后,比如这样也行:/xxx/{id}/user。另外,url 也支持多个占位符,方法参数使用同样数量的参数来接收,原理和一个参数是一样的,例如:

@GetMapping("/user/{idd}/{name}")
    public String testPathVariable(@PathVariable(value = "idd") Integer id, @PathVariable String name) {
        System.out.println("获取到的id为:" + id);
        System.out.println("获取到的name为:" + name);
        return "success";
    }

运行项目,在浏览器中请求 localhost:8080/test/user/2/zhangsan 可以看到控制台输出如下信息:

获取到的id为:2
获取到的name为:zhangsan

所以支持多个参数的接收。同样地,如果 url 中的参数和方法中的参数名称不同的话,也需要使用 value 属性来绑定两个参数。

4. @RequestParam

@RequestParam 注解顾名思义,也是获取请求参数的,上面我们介绍了 @PathValiable 注解也是获取请求参数的,那么 @RequestParam@PathVariable 有什么不同呢?主要区别在于: @PathValiable 是从 url 模板中获取参数值, 即这种风格的 url:http://localhost:8080/user/{id} ;而 @RequestParam 是从 request 里面获取参数值,即这种风格的 url:http://localhost:8080/user?id=1 。我们使用该 url 带上参数 id 来测试一下如下代码:

@GetMapping("/user")
public String testRequestParam(@RequestParam Integer id) {
    System.out.println("获取到的id为:" + id);
    return "success";
}

可以正常从控制台打印出 id 信息。同样地,url 上面的参数和方法的参数需要一致,如果不一致,也需要使用 value 属性来说明,比如 url 为:http://localhost:8080/user?idd=1

@RequestMapping("/user")
public String testRequestParam(@RequestParam(value = "idd", required = false) Integer id) {
    System.out.println("获取到的id为:" + id);
    return "success";
}

除了 value 属性外,还有个两个属性比较常用:

  • required 属性:true 表示该参数必须要传,否则就会报 404 错误,false 表示可有可无。
  • defaultValue 属性:默认值,表示如果请求中没有同名参数时的默认值。

从 url 中可以看出,@RequestParam 注解用于 GET 请求上时,接收拼接在 url 中的参数。除此之外,该注解还可以用于 POST 请求,接收前端表单提交的参数,假如前端通过表单提交 username 和 password 两个参数,那我们可以使用 @RequestParam 来接收,用法和上面一样。

@PostMapping("/form1")
    public String testForm(@RequestParam String username, @RequestParam String password) {
        System.out.println("获取到的username为:" + username);
        System.out.println("获取到的password为:" + password);
        return "success";
    }

我们使用 postman 来模拟一下表单提交,测试一下接口:

使用postman测试表单提交

那么问题来了,如果表单数据很多,我们不可能在后台方法中写上很多参数,每个参数还要 @RequestParam 注解。针对这种情况,我们需要封装一个实体类来接收这些参数,实体中的属性名和表单中的参数名一致即可。

public class User {
    private String username;
    private String password;
    // set get
}

使用实体接收的话,我们不能在前面加 @RequestParam 注解了,直接使用即可。

@PostMapping("/form2")
    public String testForm(User user) {
        System.out.println("获取到的username为:" + user.getUsername());
        System.out.println("获取到的password为:" + user.getPassword());
        return "success";
    }

使用 postman 再次测试一下表单提交,观察一下返回值和控制台打印出的日志即可。在实际项目中,一般都是封装一个实体类来接收表单数据,因为实际项目中表单数据一般都很多。

5. @RequestBody

@RequestBody 注解用于接收前端传来的实体,接收参数也是对应的实体,比如前端通过 json 提交传来两个参数 username 和 password,此时我们需要在后端封装一个实体来接收。在传递的参数比较多的情况下,使用 @RequestBody 接收会非常方便。例如:

public class User {
    private String username;
    private String password;
    // set get
}
@PostMapping("/user")
public String testRequestBody(@RequestBody User user) {
    System.out.println("获取到的username为:" + user.getUsername());
    System.out.println("获取到的password为:" + user.getPassword());
    return "success";
}

我们使用 postman 工具来测试一下效果,打开 postman,然后输入请求地址和参数,参数我们用 json 来模拟,如下图所有,调用之后返回 success。

使用Postman测试requestBody

同时看一下后台控制台输出的日志:

获取到的username为:倪升武
获取到的password为:123456

可以看出,@RequestBody 注解用于 POST 请求上,接收 json 实体参数。它和上面我们介绍的表单提交有点类似,只不过参数的格式不同,一个是 json 实体,一个是表单提交。在实际项目中根据具体场景和需要使用对应的注解即可。

6. 总结

本节课主要讲解了 Spring Boot 中对 MVC 的支持,分析了 @RestController@RequestMapping@PathVariable@RequestParam@RequestBody 四个注解的使用方式,由于 @RestController 中集成了 @ResponseBody 所以对返回 json 的注解不再赘述。以上四个注解是使用频率很高的注解,在所有的实际项目中基本都会遇到,要熟练掌握。

课程源代码下载地址:戳我下载

第06课:Spring Boot集成 Swagger2 展现在线接口文档

1. Swagger 简介

1.1 解决的问题

随着互联网技术的发展,现在的网站架构基本都由原来的后端渲染,变成了前后端分离的形态,而且前端技术和后端技术在各自的道路上越走越远。前端和后端的唯一联系,变成了 API 接口,所以 API 文档变成了前后端开发人员联系的纽带,变得越来越重要。

那么问题来了,随着代码的不断更新,开发人员在开发新的接口或者更新旧的接口后,由于开发任务的繁重,往往文档很难持续跟着更新,Swagger 就是用来解决该问题的一款重要的工具,对使用接口的人来说,开发人员不需要给他们提供文档,只要告诉他们一个 Swagger 地址,即可展示在线的 API 接口文档,除此之外,调用接口的人员还可以在线测试接口数据,同样地,开发人员在开发接口时,同样也可以利用 Swagger 在线接口文档测试接口数据,这给开发人员提供了便利。

1.2 Swagger 官方

我们打开 Swagger 官网,官方对 Swagger 的定义为:

The Best APIs are Built with Swagger Tools

翻译成中文是:“最好的 API 是使用 Swagger 工具构建的”。由此可见,Swagger 官方对其功能和所处的地位非常自信,由于其非常好用,所以官方对其定位也合情合理。如下图所示:

官方对swagger的定位

本文主要讲解在 Spring Boot 中如何导入 Swagger2 工具来展现项目中的接口文档。本节课使用的 Swagger 版本为 2.2.2。下面开始进入 Swagger2 之旅。

2. Swagger2 的 maven 依赖

使用 Swagger2 工具,必须要导入 maven 依赖,当前官方最高版本是 2.8.0,我尝试了一下,个人感觉页面展示的效果不太好,而且不够紧凑,不利于操作。另外,最新版本并不一定是最稳定版本,当前我们实际项目中使用的是 2.2.2 版本,该版本稳定,界面友好,所以本节课主要围绕着 2.2.2 版本来展开,依赖如下:

<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger2</artifactId>
    <version>2.2.2</version>
</dependency>
<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger-ui</artifactId>
    <version>2.2.2</version>
</dependency>

3. Swagger2 的配置

使用 Swagger2 需要进行配置,Spring Boot 中对 Swagger2 的配置非常方便,新建一个配置类,Swagger2 的配置类上除了添加必要的 @Configuration 注解外,还需要添加 @EnableSwagger2 注解。

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;

/**
 * @author shengwu ni
 */
@Configuration
@EnableSwagger2
public class SwaggerConfig {

    @Bean
    public Docket createRestApi() {
        return new Docket(DocumentationType.SWAGGER_2)
                // 指定构建api文档的详细信息的方法:apiInfo()
                .apiInfo(apiInfo())
                .select()
                // 指定要生成api接口的包路径,这里把controller作为包路径,生成controller中的所有接口
                .apis(RequestHandlerSelectors.basePackage("com.itcodai.course06.controller"))
                .paths(PathSelectors.any())
                .build();
    }

    /**
     * 构建api文档的详细信息
     * @return
     */
    private ApiInfo apiInfo() {
        return new ApiInfoBuilder()
                // 设置页面标题
                .title("Spring Boot集成Swagger2接口总览")
                // 设置接口描述
                .description("跟武哥一起学Spring Boot第06课")
                // 设置联系方式
                .contact("倪升武," + "CSDN:http://blog.csdn.net/eson_15")
                // 设置版本
                .version("1.0")
                // 构建
                .build();
    }
}

在该配置类中,已经使用注释详细解释了每个方法的作用了,在此不再赘述。到此为止,我们已经配置好了 Swagger2 了。现在我们可以测试一下配置有没有生效,启动项目,在浏览器中输入 localhost:8080/swagger-ui.html,即可看到 swagger2 的接口页面,如下图所示,说明Swagger2 集成成功。

swagger2页面

结合该图,对照上面的 Swagger2 配置文件中的配置,可以很明确的知道配置类中每个方法的作用。这样就很容易理解和掌握 Swagger2 中的配置了,也可以看出,其实 Swagger2 配置很简单。

【友情提示】可能有很多朋友在配置 Swagger 的时候会遇到下面的情况,而且还关不掉的,这是因为浏览器缓存引起的,清空一下浏览器缓存即可解决问题。

[图片上传失败...(image-2d541a-1595915746075)]

4. Swagger2 的使用

上面我们已经配置好了 Swagger2,并且也启动测试了一下,功能正常,下面我们开始使用 Swagger2,主要来介绍 Swagger2 中的几个常用的注解,分别在实体类上、 Controller 类上以及 Controller 中的方法上,最后我们看一下 Swagger2 是如何在页面上呈现在线接口文档的,并且结合 Controller 中的方法在接口中测试一下数据。

4.1 实体类注解

本节我们建一个 User 实体类,主要介绍一下 Swagger2 中的 @ApiModel@ApiModelProperty 注解,同时为后面的测试做准备。

import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;

@ApiModel(value = "用户实体类")
public class User {

    @ApiModelProperty(value = "用户唯一标识")
    private Long id;

    @ApiModelProperty(value = "用户姓名")
    private String username;

    @ApiModelProperty(value = "用户密码")
    private String password;

    // 省略set和get方法
}

解释下 @ApiModel@ApiModelProperty 注解:

@ApiModel 注解用于实体类,表示对类进行说明,用于参数用实体类接收。
@ApiModelProperty 注解用于类中属性,表示对 model 属性的说明或者数据操作更改。

该注解在在线 API 文档中的具体效果在下文说明。

4.2 Controller 类中相关注解

我们写一个 TestController,再写几个接口,然后学习一下 Controller 中和 Swagger2 相关的注解。

import com.itcodai.course06.entiy.JsonResult;
import com.itcodai.course06.entiy.User;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/swagger")
@Api(value = "Swagger2 在线接口文档")
public class TestController {

    @GetMapping("/get/{id}")
    @ApiOperation(value = "根据用户唯一标识获取用户信息")
    public JsonResult<User> getUserInfo(@PathVariable @ApiParam(value = "用户唯一标识") Long id) {
        // 模拟数据库中根据id获取User信息
        User user = new User(id, "倪升武", "123456");
        return new JsonResult(user);
    }
}

我们来学习一下 @Api@ApiOperation@ApiParam 注解。

@Api 注解用于类上,表示标识这个类是 swagger 的资源。
@ApiOperation 注解用于方法,表示一个 http 请求的操作。
@ApiParam 注解用于参数上,用来标明参数信息。

这里返回的是 JsonResult,是第02课中学习返回 json 数据时封装的实体。以上是 Swagger 中最常用的 5 个注解,接下来运行一下项目工程,在浏览器中输入 localhost:8080/swagger-ui.html 看一下 Swagger 页面的接口状态。

swagger接口展示

可以看出,Swagger 页面对该接口的信息展示的非常全面,每个注解的作用以及展示的地方在上图中已经标明,通过页面即可知道该接口的所有信息,那么我们直接在线测试一下该接口返回的信息,输入id为1,看一下返回数据:

返回数据测试

可以看出,直接在页面返回了 json 格式的数据,开发人员可以直接使用该在线接口来测试数据的正确与否,非常方便。上面是对于单个参数的输入,如果输入参数为某个对象这种情况,Swagger 是什么样子呢?我们再写一个接口。

@PostMapping("/insert")
    @ApiOperation(value = "添加用户信息")
    public JsonResult<Void> insertUser(@RequestBody @ApiParam(value = "用户信息") User user) {
        // 处理添加逻辑
        return new JsonResult<>();
    }

重启项目,在浏览器中输入 localhost:8080/swagger-ui.html 看一下效果:

swagger接口展示

5. 总结

OK,本节课详细分析了 Swagger 的优点,以及 Spring Boot 如何集成 Swagger2,包括配置,相关注解的讲解,涉及到了实体类和接口类,以及如何使用。最后通过页面测试,体验了 Swagger 的强大之处,基本上是每个项目组中必备的工具之一,所以要掌握该工具的使用,也不难。

课程源代码下载地址:戳我下载

第07课:Spring Boot集成Thymeleaf模板引擎

1. Thymeleaf 介绍

Thymeleaf 是适用于 Web 和独立环境的现代服务器端 Java 模板引擎。
Thymeleaf 的主要目标是为您的开发工作流程带来优雅的自然模板 - 可以在浏览器中正确显示的HTML,也可以用作静态原型,从而在开发团队中实现更强大的协作。

以上翻译自 Thymeleaf 官方网站。传统的 JSP+JSTL 组合是已经过去了,Thymeleaf 是现代服务端的模板引擎,与传统的 JSP 不同,Thymeleaf 可以使用浏览器直接打开,因为可以忽略掉拓展属性,相当于打开原生页面,给前端人员也带来一定的便利。

什么意思呢?就是说在本地环境或者有网络的环境下,Thymeleaf 均可运行。由于 thymeleaf 支持 html 原型,也支持在 html 标签里增加额外的属性来达到 “模板+数据” 的展示方式,所以美工可以直接在浏览器中查看页面效果,当服务启动后,也可以让后台开发人员查看带数据的动态页面效果。比如:

<div class="ui right aligned basic segment">
      <div class="ui orange basic label" th:text="${blog.flag}">静态原创信息</div>
</div>
<h2 class="ui center aligned header" th:text="${blog.title}">这是静态标题</h2>

类似与上面这样,在静态页面时,会展示静态信息,当服务启动后,动态获取数据库中的数据后,就可以展示动态数据,th:text 标签是用来动态替换文本的,这会在下文说明。该例子说明浏览器解释 html 时会忽略 html 中未定义的标签属性(比如 th:text),所以 thymeleaf 的模板可以静态地运行;当有数据返回到页面时,Thymeleaf 标签会动态地替换掉静态内容,使页面动态显示数据。

2. 依赖导入

在 Spring Boot 中使用 thymeleaf 模板需要引入依赖,可以在创建项目工程时勾选 Thymeleaf,也可以创建之后再手动导入,如下:

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

另外,在 html 页面上如果要使用 thymeleaf 模板,需要在页面标签中引入:

<html xmlns:th="http://www.thymeleaf.org">

3. Thymeleaf相关配置

因为 Thymeleaf 中已经有默认的配置了,我们不需要再对其做过多的配置,有一个需要注意一下,Thymeleaf 默认是开启页面缓存的,所以在开发的时候,需要关闭这个页面缓存,配置如下。

spring:
  thymeleaf:
    cache: false #关闭缓存

否则会有缓存,导致页面没法及时看到更新后的效果。 比如你修改了一个文件,已经 update 到 tomcat 了,但刷新页面还是之前的页面,就是因为缓存引起的。

4. Thymeleaf 的使用

4.1 访问静态页面

这个和 Thymeleaf 没啥关系,应该说是通用的,我把它一并写到这里的原因是一般我们做网站的时候,都会做一个 404 页面和 500 页面,为了出错时给用户一个友好的展示,而不至于一堆异常信息抛出来。Spring Boot 中会自动识别模板目录(templates/)下的 404.html 和 500.html 文件。我们在 templates/ 目录下新建一个 error 文件夹,专门放置错误的 html 页面,然后分别打印些信息。以 404.html 为例:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    这是404页面
</body>
</html>

我们再写一个 controller 来测试一下 404 和 500 页面:

@Controller
@RequestMapping("/thymeleaf")
public class ThymeleafController {

    @RequestMapping("/test404")
    public String test404() {
        return "index";
    }

    @RequestMapping("/test500")
    public String test500() {
        int i = 1 / 0;
        return "index";
    }
}

当我们在浏览器中输入 localhost:8080/thymeleaf/test400 时,故意输入错误,找不到对应的方法,就会跳转到 404.html 显示。
当我们在浏览器中输入 localhost:8088/thymeleaf/test505 时,会抛出异常,然后会自动跳转到 500.html 显示。

【注】这里有个问题需要注意一下,前面的课程中我们说了微服务中会走向前后端分离,我们在 Controller 层上都是使用的 @RestController 注解,自动会把返回的数据转成 json 格式。但是在使用模板引擎时,Controller 层就不能用 @RestController 注解了,因为在使用 thymeleaf 模板时,返回的是视图文件名,比如上面的 Controller 中是返回到 index.html 页面,如果使用 @RestController 的话,会把 index 当作 String 解析了,直接返回到页面了,而不是去找 index.html 页面,大家可以试一下。所以在使用模板时要用 @Controller 注解。

4.2 Thymeleaf 中处理对象

我们来看一下 thymeleaf 模板中如何处理对象信息,假如我们在做个人博客的时候,需要给前端传博主相关信息来展示,那么我们会封装成一个博主对象,比如:

public class Blogger {
    private Long id;
    private String name;
    private String pass;
    // 省去set和get
}

然后在controller层中初始化一下:

@GetMapping("/getBlogger")
public String getBlogger(Model model) {
    Blogger blogger = new Blogger(1L, "倪升武", "123456");
    model.addAttribute("blogger", blogger);
    return "blogger";
}

我们先初始化一个 Blogger 对象,然后将该对象放到 Model 中,然后返回到 blogger.html 页面去渲染。接下来我们再写一个 blogger.html 来渲染 blogger 信息:

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>博主信息</title>
</head>
<body>
<form action="" th:object="${blogger}" >
    用户编号:<input name="id" th:value="${blogger.id}"/><br>
    用户姓名:<input type="text" name="username" th:value="${blogger.getName()}" /><br>
    登陆密码:<input type="text" name="password" th:value="*{pass}" />
</form>
</body>
</html>

可以看出,在 thymeleaf 模板中,使用 th:object="${}" 来获取对象信息,然后在表单里面可以有三种方式来获取对象属性。如下:

使用 th:value="*{属性名}"
使用 th:value="${对象.属性名}",对象指的是上面使用 th:object 获取的对象
使用 th:value="${对象.get方法}",对象指的是上面使用 th:object 获取的对象

可以看出,在 Thymeleaf 中可以像写 java 一样写代码,很方便。我们在浏览器中输入 localhost:8080/thymeleaf/getBlogger 来测试一下数据:

thymeleaf中处理对象

4.3 Thymeleaf 中处理 List

处理 List 的话,和处理上面介绍的对象差不多,但是需要在 thymeleaf 中进行遍历。我们先在 Controller 中模拟一个 List。

@GetMapping("/getList")
public String getList(Model model) {
    Blogger blogger1 = new Blogger(1L, "倪升武", "123456");
    Blogger blogger2 = new Blogger(2L, "达人课", "123456");
    List<Blogger> list = new ArrayList<>();
    list.add(blogger1);
    list.add(blogger2);
    model.addAttribute("list", list);
    return "list";
}

接下来我们写一个 list.html 来获取该 list 信息,然后在 list.html 中遍历这个list。如下:

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>博主信息</title>
</head>
<body>
<form action="" th:each="blogger : ${list}" >
    用户编号:<input name="id" th:value="${blogger.id}"/><br>
    用户姓名:<input type="text" name="password" th:value="${blogger.name}"/><br>
    登录密码:<input type="text" name="username" th:value="${blogger.getPass()}"/>
</form>
</body>
</html>

可以看出,其实和处理单个对象信息差不多,Thymeleaf 使用 th:each 进行遍历,${} 取 model 中传过来的参数,然后自定义 list 中取出来的每个对象,这里定义为 blogger。表单里面可以直接使用 ${对象.属性名} 来获取 list 中对象的属性值,也可以使用 ${对象.get方法} 来获取,这点和上面处理对象信息是一样的,但是不能使用 *{属性名} 来获取对象中的属性,thymeleaf 模板获取不到。

4.4 其他常用 thymeleaf 操作

我们来总结一下 thymeleaf 中的一些常用的标签操作,如下:

标签 功能 例子
th:value 给属性赋值 <input th:value="${blog.name}" />
th:style 设置样式 th:style="'display:'+@{(${sitrue}?'none':'inline-block')} + ''"
th:onclick 点击事件 th:onclick="'getInfo()'"
th:if 条件判断 <a th:if="${userId == collect.userId}" >
th:href 超链接 <a th:href="@{/blogger/login}">Login</a> />
th:unless 条件判断和th:if相反 <a th:href="@{/blogger/login}" th:unless=${session.user != null}>Login</a>
th:switch 配合th:case <div th:switch="${user.role}">
th:case 配合th:switch <p th:case="'admin'">administator</p>
th:src 地址引入 <img alt="csdn logo" th:src="@{/img/logo.png}" />
th:action 表单提交的地址 <form th:action="@{/blogger/update}">

Thymeleaf 还有很多其他用法,这里就不总结了,具体的可以参考Thymeleaf的官方文档(v3.0)。主要要学会如何在 Spring Boot 中去使用 thymeleaf,遇到对应的标签或者方法,查阅官方文档即可。

5. 总结

Thymeleaf 在 Spring Boot 中使用非常广泛,本节课主要分析了 thymeleaf 的优点,以及如何在 Spring Boot 中集成并使用 thymeleaf 模板,包括依赖、配置,相关数据的获取、以及一些注意事项等等。最后列举了一些 thymeleaf 中常用的标签,在实际项目中多使用,多查阅就能熟练掌握,thymeleaf 中的一些标签或者方法不用死记硬背,用到什么去查阅什么,关键是要会在 Spring Boot 中集成,用的多了就熟能生巧。

课程源代码下载地址:戳我下载

第08课:Spring Boot中的全局异常处理

在项目开发过程中,不管是对底层数据库的操作过程,还是业务层的处理过程,还是控制层的处理过程,都不可避免会遇到各种可预知的、不可预知的异常需要处理。如果对每个过程都单独作异常处理,那系统的代码耦合度会变得很高,此外,开发工作量也会加大而且不好统一,这也增加了代码的维护成本。
针对这种实际情况,我们需要将所有类型的异常处理从各处理过程解耦出来,这样既保证了相关处理过程的功能单一,也实现了异常信息的统一处理和维护。同时,我们也不希望直接把异常抛给用户,应该对异常进行处理,对错误信息进行封装,然后返回一个友好的信息给用户。这节主要总结一下项目中如何使用 Spring Boot 如何拦截并处理全局的异常。

1. 定义返回的统一 json 结构

前端或者其他服务请求本服务的接口时,该接口需要返回对应的 json 数据,一般该服务只需要返回请求着需要的参数即可,但是在实际项目中,我们需要封装更多的信息,比如状态码 code、相关信息 msg 等等,这一方面是在项目中可以有个统一的返回结构,整个项目组都适用,另一方面是方便结合全局异常处理信息,因为异常处理信息中一般我们需要把状态码和异常内容反馈给调用方。
这个统一的 json 结构这可以参考第02课:Spring Boot 返回 JSON 数据及数据封装中封装的统一 json 结构,本节内容我们简化一下,只保留状态码 code 和异常信息 msg即可。如下:

public class JsonResult {
    /**
     * 异常码
     */
    protected String code;

    /**
     * 异常信息
     */
    protected String msg;
    
    public JsonResult() {
        this.code = "200";
        this.msg = "操作成功";
    }
    
    public JsonResult(String code, String msg) {
        this.code = code;
        this.msg = msg;
    }
    // get set
}

2. 处理系统异常

新建一个 GlobalExceptionHandler 全局异常处理类,然后加上 @ControllerAdvice 注解即可拦截项目中抛出的异常,如下:

@ControllerAdvice
@ResponseBody
public class GlobalExceptionHandler {
    // 打印log
    private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
    // ……
}

我们点开 @ControllerAdvice 注解可以看到,@ControllerAdvice 注解包含了 @Component 注解,说明在 Spring Boot 启动时,也会把该类作为组件交给 Spring 来管理。除此之外,该注解还有个 basePackages 属性,该属性是用来拦截哪个包中的异常信息,一般我们不指定这个属性,我们拦截项目工程中的所有异常。@ResponseBody 注解是为了异常处理完之后给调用方输出一个 json 格式的封装数据。
在项目中如何使用呢?Spring Boot 中很简单,在方法上通过 @ExceptionHandler 注解来指定具体的异常,然后在方法中处理该异常信息,最后将结果通过统一的 json 结构体返回给调用者。下面我们举几个例子来说明如何来使用。

2.1 处理参数缺失异常

在前后端分离的架构中,前端请求后台的接口都是通过 rest 风格来调用,有时候,比如 POST 请求 需要携带一些参数,但是往往有时候参数会漏掉。另外,在微服务架构中,涉及到多个微服务之间的接口调用时,也可能出现这种情况,此时我们需要定义一个处理参数缺失异常的方法,来给前端或者调用方提示一个友好信息。

参数缺失的时候,会抛出 HttpMessageNotReadableException,我们可以拦截该异常,做一个友好处理,如下:

/**
* 缺少请求参数异常
* @param ex HttpMessageNotReadableException
* @return
*/
@ExceptionHandler(MissingServletRequestParameterException.class)
@ResponseStatus(value = HttpStatus.BAD_REQUEST)
public JsonResult handleHttpMessageNotReadableException(
    MissingServletRequestParameterException ex) {
    logger.error("缺少请求参数,{}", ex.getMessage());
    return new JsonResult("400", "缺少必要的请求参数");
}

我们来写个简单的 Controller 测试一下该异常,通过 POST 请求方式接收两个参数:姓名和密码。

@RestController
@RequestMapping("/exception")
public class ExceptionController {

    private static final Logger logger = LoggerFactory.getLogger(ExceptionController.class);

    @PostMapping("/test")
    public JsonResult test(@RequestParam("name") String name,
                           @RequestParam("pass") String pass) {
        logger.info("name:{}", name);
        logger.info("pass:{}", pass);
        return new JsonResult();
    }
}

然后使用 Postman 来调用一下该接口,调用的时候,只传姓名,不传密码,就会抛缺少参数异常,该异常被捕获之后,就会进入我们写好的逻辑,给调用方返回一个友好信息,如下:

缺失参数异常

2.2 处理空指针异常

空指针异常是开发中司空见惯的东西了,一般发生的地方有哪些呢?
先来聊一聊一些注意的地方,比如在微服务中,经常会调用其他服务获取数据,这个数据主要是 json 格式的,但是在解析 json 的过程中,可能会有空出现,所以我们在获取某个 jsonObject 时,再通过该 jsonObject 去获取相关信息时,应该要先做非空判断。
还有一个很常见的地方就是从数据库中查询的数据,不管是查询一条记录封装在某个对象中,还是查询多条记录封装在一个 List 中,我们接下来都要去处理数据,那么就有可能出现空指针异常,因为谁也不能保证从数据库中查出来的东西就一定不为空,所以在使用数据时一定要先做非空判断。
对空指针异常的处理很简单,和上面的逻辑一样,将异常信息换掉即可。如下:

@ControllerAdvice
@ResponseBody
public class GlobalExceptionHandler {

    private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);

    /**
     * 空指针异常
     * @param ex NullPointerException
     * @return
     */
    @ExceptionHandler(NullPointerException.class)
    @ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR)
    public JsonResult handleTypeMismatchException(NullPointerException ex) {
        logger.error("空指针异常,{}", ex.getMessage());
        return new JsonResult("500", "空指针异常了");
    }
}

这个我就不测试了,代码中 ExceptionController 有个 testNullPointException 方法,模拟了一个空指针异常,我们在浏览器中请求一下对应的 url 即可看到返回的信息:

{"code":"500","msg":"空指针异常了"}

2.3 一劳永逸?

当然了,异常很多,比如还有 RuntimeException,数据库还有一些查询或者操作异常等等。由于 Exception 异常是父类,所有异常都会继承该异常,所以我们可以直接拦截 Exception 异常,一劳永逸:

@ControllerAdvice
@ResponseBody
public class GlobalExceptionHandler {

    private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
    /**
     * 系统异常 预期以外异常
     * @param ex
     * @return
     */
    @ExceptionHandler(Exception.class)
    @ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR)
    public JsonResult handleUnexpectedServer(Exception ex) {
        logger.error("系统异常:", ex);
        return new JsonResult("500", "系统发生异常,请联系管理员");
    }
}

但是项目中,我们一般都会比较详细的去拦截一些常见异常,拦截 Exception 虽然可以一劳永逸,但是不利于我们去排查或者定位问题。实际项目中,可以把拦截 Exception 异常写在 GlobalExceptionHandler 最下面,如果都没有找到,最后再拦截一下 Exception 异常,保证输出信息友好。

3. 拦截自定义异常

在实际项目中,除了拦截一些系统异常外,在某些业务上,我们需要自定义一些业务异常,比如在微服务中,服务之间的相互调用很平凡,很常见。要处理一个服务的调用时,那么可能会调用失败或者调用超时等等,此时我们需要自定义一个异常,当调用失败时抛出该异常,给 GlobalExceptionHandler 去捕获。

3.1 定义异常信息

由于在业务中,有很多异常,针对不同的业务,可能给出的提示信息不同,所以为了方便项目异常信息管理,我们一般会定义一个异常信息枚举类。比如:

/**
 * 业务异常提示信息枚举类
 * @author shengwu ni
 */
public enum BusinessMsgEnum {
    /** 参数异常 */
    PARMETER_EXCEPTION("102", "参数异常!"),
    /** 等待超时 */
    SERVICE_TIME_OUT("103", "服务调用超时!"),
    /** 参数过大 */
    PARMETER_BIG_EXCEPTION("102", "输入的图片数量不能超过50张!"),
    /** 500 : 一劳永逸的提示也可以在这定义 */
    UNEXPECTED_EXCEPTION("500", "系统发生异常,请联系管理员!");
    // 还可以定义更多的业务异常

    /**
     * 消息码
     */
    private String code;
    /**
     * 消息内容
     */
    private String msg;

    private BusinessMsgEnum(String code, String msg) {
        this.code = code;
        this.msg = msg;
    }
    // set get方法
}

3.2 拦截自定义异常

然后我们可以定义一个业务异常,当出现业务异常时,我们就抛这个自定义的业务异常即可。比如我们定义一个 BusinessErrorException 异常,如下:

/**
 * 自定义业务异常
 * @author shengwu ni
 */
public class BusinessErrorException extends RuntimeException {
    
    private static final long serialVersionUID = -7480022450501760611L;

    /**
     * 异常码
     */
    private String code;
    /**
     * 异常提示信息
     */
    private String message;

    public BusinessErrorException(BusinessMsgEnum businessMsgEnum) {
        this.code = businessMsgEnum.code();
        this.message = businessMsgEnum.msg();
    }
    // get set方法
}

在构造方法中,传入我们上面自定义的异常枚举类,所以在项目中,如果有新的异常信息需要添加,我们直接在枚举类中添加即可,很方便,做到统一维护,然后再拦截该异常时获取即可。

@ControllerAdvice
@ResponseBody
public class GlobalExceptionHandler {

    private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
    /**
     * 拦截业务异常,返回业务异常信息
     * @param ex
     * @return
     */
    @ExceptionHandler(BusinessErrorException.class)
    @ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR)
    public JsonResult handleBusinessError(BusinessErrorException ex) {
        String code = ex.getCode();
        String message = ex.getMessage();
        return new JsonResult(code, message);
    }
}

在业务代码中,我们可以直接模拟一下抛出业务异常,测试一下:

@RestController
@RequestMapping("/exception")
public class ExceptionController {

    private static final Logger logger = LoggerFactory.getLogger(ExceptionController.class);

    @GetMapping("/business")
    public JsonResult testException() {
        try {
            int i = 1 / 0;
        } catch (Exception e) {
            throw new BusinessErrorException(BusinessMsgEnum.UNEXPECTED_EXCEPTION);
        }
        return new JsonResult();
    }
}

运行一下项目,测试一下,返回 json 如下,说明我们自定义的业务异常捕获成功:

{"code":"500","msg":"系统发生异常,请联系管理员!"}

4. 总结

本节课程主要讲解了Spring Boot 的全局异常处理,包括异常信息的封装、异常信息的捕获和处理,以及在实际项目中,我们用到的自定义异常枚举类和业务异常的捕获与处理,在项目中运用的非常广泛,基本上每个项目中都需要做全局异常处理。

课程源代码下载地址:戳我下载

第09课:Spring Boot中的切面AOP处理

1. 什么是AOP

AOP:Aspect Oriented Programming 的缩写,意为:面向切面编程。面向切面编程的目标就是分离关注点。什么是关注点呢?就是关注点,就是你要做的事情。假如你是一位公子哥,没啥人生目标,每天衣来伸手,饭来张口,整天只知道一件事:玩(这就是你的关注点,你只要做这一件事)!但是有个问题,你在玩之前,你还需要起床、穿衣服、穿鞋子、叠被子、做早饭等等等等,但是这些事情你不想关注,也不用关注,你只想想玩,那么怎么办呢?

对!这些事情通通交给下人去干。你有一个专门的仆人 A 帮你穿衣服,仆人 B 帮你穿鞋子,仆人 C 帮你叠好被子,仆人 D 帮你做饭,然后你就开始吃饭、去玩(这就是你一天的正事),你干完你的正事之后,回来,然后一系列仆人又开始帮你干这个干那个,然后一天就结束了!

这就是 AOP。AOP 的好处就是你只需要干你的正事,其它事情别人帮你干。也许有一天,你想裸奔,不想穿衣服,那么你把仆人 A 解雇就是了!也许有一天,出门之前你还想带点钱,那么你再雇一个仆人 E 专门帮你干取钱的活!这就是AOP。每个人各司其职,灵活组合,达到一种可配置的、可插拔的程序结构。

2. Spring Boot 中的 AOP 处理

2.1 AOP 依赖

使用AOP,首先需要引入AOP的依赖。

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

2.2 实现 AOP 切面

Spring Boot 中使用 AOP 非常简单,假如我们要在项目中打印一些 log,在引入了上面的依赖之后,我们新建一个类 LogAspectHandler,用来定义切面和处理方法。只要在类上加个@Aspect注解即可。@Aspect 注解用来描述一个切面类,定义切面类的时候需要打上这个注解。@Component 注解让该类交给 Spring 来管理。

@Aspect
@Component
public class LogAspectHandler {

}

这里主要介绍几个常用的注解及使用:

1.@Pointcut:定义一个切面,即上面所描述的关注的某件事入口。
2.@Before:在做某件事之前做的事。
3.@After:在做某件事之后做的事。
4.@AfterReturning:在做某件事之后,对其返回值做增强处理。
5.@AfterThrowing:在做某件事抛出异常时,处理。

2.2.1 @Pointcut 注解

@Pointcut 注解:用来定义一个切面(切入点),即上文中所关注的某件事情的入口。切入点决定了连接点关注的内容,使得我们可以控制通知什么时候执行。

@Aspect
@Component
public class LogAspectHandler {

    /**
     * 定义一个切面,拦截com.itcodai.course09.controller包和子包下的所有方法
     */
    @Pointcut("execution(* com.itcodai.course09.controller..*.*(..))")
    public void pointCut() {}
}

@Pointcut 注解指定一个切面,定义需要拦截的东西,这里介绍两个常用的表达式:一个是使用 execution(),另一个是使用 annotation()
execution(* com.itcodai.course09.controller..*.*(..))) 表达式为例,语法如下:

execution() 为表达式主体
第一个 * 号的位置:表示返回值类型,* 表示所有类型
包名:表示需要拦截的包名,后面的两个句点表示当前包和当前包的所有子包,com.itcodai.course09.controller 包、子包下所有类的方法
第二个 * 号的位置:表示类名,* 表示所有类
*(..) :这个星号表示方法名,* 表示所有的方法,后面括弧里面表示方法的参数,两个句点表示任何参数

annotation() 方式是针对某个注解来定义切面,比如我们对具有@GetMapping注解的方法做切面,可以如下定义切面:

@Pointcut("@annotation(org.springframework.web.bind.annotation.GetMapping)")
public void annotationCut() {}

然后使用该切面的话,就会切入注解是 @GetMapping 的方法。因为在实际项目中,可能对于不同的注解有不同的逻辑处理,比如 @GetMapping@PostMapping@DeleteMapping 等。所以这种按照注解的切入方式在实际项目中也很常用。

2.2.2 @Before 注解

@Before 注解指定的方法在切面切入目标方法之前执行,可以做一些 log 处理,也可以做一些信息的统计,比如获取用户的请求 url 以及用户的 ip 地址等等,这个在做个人站点的时候都能用得到,都是常用的方法。例如:

@Aspect
@Component
public class LogAspectHandler {

    private final Logger logger = LoggerFactory.getLogger(this.getClass());

    /**
     * 在上面定义的切面方法之前执行该方法
     * @param joinPoint jointPoint
     */
    @Before("pointCut()")
    public void doBefore(JoinPoint joinPoint) {
        logger.info("====doBefore方法进入了====");

        // 获取签名
        Signature signature = joinPoint.getSignature();
        // 获取切入的包名
        String declaringTypeName = signature.getDeclaringTypeName();
        // 获取即将执行的方法名
        String funcName = signature.getName();
        logger.info("即将执行方法为: {},属于{}包", funcName, declaringTypeName);
        
        // 也可以用来记录一些信息,比如获取请求的url和ip
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        // 获取请求url
        String url = request.getRequestURL().toString();
        // 获取请求ip
        String ip = request.getRemoteAddr();
        logger.info("用户请求的url为:{},ip地址为:{}", url, ip);
    }
}

JointPoint 对象很有用,可以用它来获取一个签名,然后利用签名可以获取请求的包名、方法名,包括参数(通过 joinPoint.getArgs() 获取)等等。

2.2.3 @After 注解

@After 注解和 @Before 注解相对应,指定的方法在切面切入目标方法之后执行,也可以做一些完成某方法之后的 log 处理。

@Aspect
@Component
public class LogAspectHandler {

    private final Logger logger = LoggerFactory.getLogger(this.getClass());

    /**
     * 定义一个切面,拦截com.itcodai.course09.controller包下的所有方法
     */
    @Pointcut("execution(* com.itcodai.course09.controller..*.*(..))")
    public void pointCut() {}

    /**
     * 在上面定义的切面方法之后执行该方法
     * @param joinPoint jointPoint
     */
    @After("pointCut()")
    public void doAfter(JoinPoint joinPoint) {

        logger.info("====doAfter方法进入了====");
        Signature signature = joinPoint.getSignature();
        String method = signature.getName();
        logger.info("方法{}已经执行完", method);
    }
}

到这里,我们来写一个 Controller 来测试一下执行结果,新建一个 AopController 如下:

@RestController
@RequestMapping("/aop")
public class AopController {

    @GetMapping("/{name}")
    public String testAop(@PathVariable String name) {
        return "Hello " + name;
    }
}

启动项目,在浏览器中输入 localhost:8080/aop/CSDN,观察一下控制台的输出信息:

====doBefore方法进入了====  
即将执行方法为: testAop,属于com.itcodai.course09.controller.AopController包  
用户请求的url为:http://localhost:8080/aop/name,ip地址为:0:0:0:0:0:0:0:1  
====doAfter方法进入了====  
方法testAop已经执行完

从打印出来的 log 中可以看出程序执行的逻辑与顺序,可以很直观的掌握 @Before@After 两个注解的实际作用。

2.2.4 @AfterReturning 注解

@AfterReturning 注解和 @After 有些类似,区别在于 @AfterReturning 注解可以用来捕获切入方法执行完之后的返回值,对返回值进行业务逻辑上的增强处理,例如:

@Aspect
@Component
public class LogAspectHandler {

    private final Logger logger = LoggerFactory.getLogger(this.getClass());

    /**
     * 在上面定义的切面方法返回后执行该方法,可以捕获返回对象或者对返回对象进行增强
     * @param joinPoint joinPoint
     * @param result result
     */
    @AfterReturning(pointcut = "pointCut()", returning = "result")
    public void doAfterReturning(JoinPoint joinPoint, Object result) {

        Signature signature = joinPoint.getSignature();
        String classMethod = signature.getName();
        logger.info("方法{}执行完毕,返回参数为:{}", classMethod, result);
        // 实际项目中可以根据业务做具体的返回值增强
        logger.info("对返回参数进行业务上的增强:{}", result + "增强版");
    }
}

需要注意的是:在 @AfterReturning注解 中,属性 returning 的值必须要和参数保持一致,否则会检测不到。该方法中的第二个入参就是被切方法的返回值,在 doAfterReturning 方法中可以对返回值进行增强,可以根据业务需要做相应的封装。我们重启一下服务,再测试一下(多余的 log 我不贴出来了):

方法testAop执行完毕,返回参数为:Hello CSDN  
对返回参数进行业务上的增强:Hello CSDN增强版

2.2.5 @AfterThrowing 注解

顾名思义,@AfterThrowing 注解是当被切方法执行时抛出异常时,会进入 @AfterThrowing 注解的方法中执行,在该方法中可以做一些异常的处理逻辑。要注意的是 throwing 属性的值必须要和参数一致,否则会报错。该方法中的第二个入参即为抛出的异常。

/**
 * 使用AOP处理log
 * @author shengwu ni
 * @date 2018/05/04 20:24
 */
@Aspect
@Component
public class LogAspectHandler {

    private final Logger logger = LoggerFactory.getLogger(this.getClass());

    /**
     * 在上面定义的切面方法执行抛异常时,执行该方法
     * @param joinPoint jointPoint
     * @param ex ex
     */
    @AfterThrowing(pointcut = "pointCut()", throwing = "ex")
    public void afterThrowing(JoinPoint joinPoint, Throwable ex) {
        Signature signature = joinPoint.getSignature();
        String method = signature.getName();
        // 处理异常的逻辑
        logger.info("执行方法{}出错,异常为:{}", method, ex);
    }
}

该方法我就不测试了,大家可以自行测试一下。

3. 总结

本节课针对 Spring Boot 中的切面 AOP 做了详细的讲解,主要介绍了 Spring Boot 中 AOP 的引入,常用注解的使用,参数的使用,以及常用 api 的介绍。AOP 在实际项目中很有用,对切面方法执行前后都可以根据具体的业务,做相应的预处理或者增强处理,同时也可以用作异常捕获处理,可以根据具体业务场景,合理去使用 AOP。

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