SpringBoot3

JDK19中有用的新特性

Java Record
// 声明Student Record
public record Student(Integer id, String name){
}

// 创建Record对象
Student student = new Student(001, "John");

Record和Class相比,有以下特点:
首先Recordpublic访问器,带有全部参数的构造方法,且自带toString()hashCode()equals()方法;
其次是隐式的final类,不能被继承,属性也是final,通过构造方法创建后不可修改;
最后,没有set()get()方法,不能声明实例属性,能声明static属性。

public record Student(Integer id, String name){
  // 紧凑构造方法
  public Student {}
  // 自定义构造方法
  public Student(String name) {
    this(null, name);
  }
}

除了全部参数的构造方法,还提供紧凑构造方法和自定义构造方法。

Switch

int week = 2;
String memo = "";
switch (week) {
  case 1,7 -> memo = "weekend";
  case 2,3,4,5,6 -> memo = "workday";
  default -> throw new IllegaArgumentExcepting("Ineffective date");
}

支持使用箭头->->:不能混用。

int week = 2;
String memo = switch (week) {
  case 1,7 : yield "weekend";
  case 2,3,4,5,6 : yield = "workday";
  default -> throw new IllegaArgumentExcepting("Ineffective date");
}

使用yield关键字返回结果。

Text Block

// 第一行必须是三个双引号
String colors = """
  red
  green
  blue
  """;
colors.indent(4);

文本块在使用时和普通字符串没有区别,但文本块中的缩进会被自动去除,要保留左侧的缩进,需要使用Tab键按需移动或者使用indent()方法。
另外还可以使用formatted()方法进行格式化;使用stripIndent()方法删除每行开头和结尾的空格;使用translateEscapes()转义字面量。

Var

var num = 0;
var name = "John";
var customer = getCuntomer();

var是一个保留字不是关键字,用于声明局部变量。必须有初值。
能够代替显示类型,让代码简洁,但也降低了程序的可读性。

sealed

// 声明sealed类
public sealed class Shape permits Circle, Square, Rectangle {
}

// 声明子类
// final 子类不能被继承,依然是密封的
public final class Circle extends Shape {
}

// sealed 子类也是密封类,仍需要指定子类实现
public sealed class Square extends Shape permits RoundSquare {
}

// non-sealed 子类非密封类,可以被任意类继承
public non-sealed class Rectangle extends Shape {
}
public class Line extends Rectangle {
}

sealed类主要特点是限制继承,拒绝无限的扩张。

Spring Boot的特性

1创建独立的Spring应用程序,并且内嵌Web容器;
2提供自动化配置starter功能,简化Maven配置;
3没有代码生成,无需XML配置就能尽可能地自动配置好Spring和第三方框架;
4开箱即用,使用脚手架创建项目后,所有基础配置就已经完成;
5约定优于配置,定义了常用类,包的位置和结构,代码不需做任何调整,项目就能按照预期运行。

与Spring,Spring Cloud的关系

Spring Boot和Spring都可以创建Spring应用程序,不同的是Spring Boot消除了设置Spring应用程序所需的XML配置,更快更高效更易用。
Spring Cloud是一些列框架的有序组合,提供了快速构建分布式系统中常用工具。而Spring Boot是每个独立的微服务的平台。

Spring Boot3中的变化

1.JDK支持版本17-20
2.更新Servlet6.0规范
3.支持Jackson2.14
4.SpringMVC默认使用PathpatternParser,删除过时的FreeMarker和JSP支持
5.对第三方库更新版本支持
6.底层依赖从Java EE迁移到Jakarta EE API。
7.支持GraalVM原生镜像,将Java应用编译为本机代码,提供显著的内存和气动性能改善。
8.支持Ahead Of Time(运行前编译)。
9.SpringHttp客户端提供基于Micrometer的可观察性跟踪服务,记录服务运行状态
10.其他

使用Spring Boot

从spring-boot-starter-parent开始

starter是一组依赖描述,在应用中加入starter依赖就可以获取Spring相关技术的一站式的依赖和版本。通过starter能够快速启动并运行项目。
starter包含:
依赖坐标和版本;
传递依赖的坐标和版本;
配置类和配置项

// pom.xml
// 在parent标签中指定,表示继承父项目
<parent>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-parent</artifactId>
  <version>3.0.1</version>
</parent>

// 在依赖管理中单独加入依赖
<dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-parent</artifactId>
      <version>3.0.1</version>
    </dependency>
  </dependencies>
<dependencyManagement>

可以使用两种方式加入Spring Boot提供的父项目,从spring-boot-starter-parent继承获得合理的默认值和完整的依赖树,从而快速建立一个Spring Boot项目。
父项目提供以下功能:
1.JDK的基准版本;
2.源码使用UTF-8格式编码;
3.公共依赖版本;
4.自动化的资源过滤,默认把src/main/resources目录下的文件进行打包;
5.maven的占位符是@
6.对maven插件完成默认设置
7.其他

application配置文件

Spring Boot同时支持properties和yml格式的配置文件,推荐使用yml。默认名称是application
YAML基本语法规则:
大小写敏感;
使用空格缩进表示层级关系;
#表示注释,只能单行注释。

// application.yml
spring:
  redis:
    host:localhost
    port:6379

// ReadConfigService.java
@Service
public class ReadConfigService {
  // 通过@Value注解获取
  @Value("${spring.redis.port:8080}")
  private String port;

  @Autowired
  private Environment environment;
  // 通过Environment对象获取
  public void printConfig() {
    this.port = environment.getProperty("server.port");
  }
}

通过@Value注解或者外部化配置的抽象对象Environment可以读取配置文件。

// db.yml
spring: 
  datasource:
    url: jdbc:mysql://...
    username: operation
    password: 123456

// redis.yml
spring:
  redis:
    host:localhost
    sort:6379

// application-test.yml
spring:
  config:
    activate:
    on-profile: test

// application-dev.yml
spring:
  config:
    activate:
    on-profile: dev
 
// application.yml
spring:
  config:
    import: conf/db.yml, conf/redis.yml
  profiles:
    active:dev

可以在application.yml中使用import导入多个配置;
使用on-profileactive激活某个环境配置。

绑定Bean

使用@Value每个成员变量只能绑定一个属性。当属性较多时需要需要重复写很多注解。此时可以使用绑定Bean。将多个配置项绑定到一个Bean,这个强类型的Bean就能访问到所有的配置数据了。

// application.yml
app:
  host: Lession07-yml
  port: 8002
  security:
    username: root
    password: 12345
// 第三方库属性
administrator:
 username: common
 password: 123

// ReadConfigBean.java
// com.example.config
@Configuration(proxyBeanMethods=false)
@ConfigurationProperties(prefix="app")
public class ReadConfigBean {
  private String host;
  private String port;
  private Security security;

  @ConfigurationProperties("administrator")
  @Bean
  public Security createAdministrator() {
    return new Administrator();
  }
}

// group-info.properties
group.name=testGroup
group.leader=John
group.member=10

// GroupConfigBean.java
// com.example.config
@Configuration
@ConfigurationProperties(prefix="group")
@PropertySource(Value="classpath:/group-info.properties")
public class GroupConfigBean {
  private String name;
  private String leader;
  private String member;
}

// Application.java
@ConfigurationPropertiesScan({"com.example.config"})
@SpringBootApplication
public class Application {
  ...
}

基本原则是定义标准的Java Bean,要有无参构造方法并且包含属性的访问器。配合@ConfigurationProperties注解一起使用。注解中的prefix属性可以写前缀。
@ConfigurationProperties注解还需要@EnableConfigurationProperties@ConfigurationPropertiesScan才能起作用,所以最后在启动类上使用扫描注解@ConfigurationPropertiesScan,声明所在的包就完成了。
除了读取简单的属性和集合提供的属性外,将嵌套对象声明为成员变量后,也可以读取到嵌套的属性。
有时候需要读取第三方库的属性,此时需要使用@Bean注解,将第三方库中的类注入到绑定Bean中,配合@ConfigurationProperties注解,就能读取了。
还可以读取指定的配置文件。具体做法除了使用@ConfigurationProperties注解外,还需要使用@Configuration@PropertySource注解,指定文件路径。

自动配置

自动配置的注解是@EnableAutoConfiguration。这个注解也被标注在@SpringBootApplication注解上,所以通常是由@SpringBootApplication注解带入。
@EnableAutoConfiguration注解源码上标注了@Import(AutoConfigruationImportSelector.class),这行代码意味着AutoConfigruationImportSelector这个对象也被导入进了IOC容器。
AutoConfigruationImportSelector的作用是导入自动配置类,从META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports读取到要加载的配置类列表。这个文件在2.7版本之前是spring.factories
另外框架根据pom.xml中配置的xxxstarter启动器,找到相关的xxxAutoConfiguration,然后通过@import注解找到要加载的配置类列表。
最终框架把以上所有的配置类加载到容器中,根据条件注解判定配置是否生效。

SpringBoot中的Spring MVC

控制器 Controller

@Controller注解被声明为@Component,控制器对象本质就是被Spring容器管理的Bean对象。
创建普通的类,定义public方法,类上注解@Controller或者@RestController,这个类就是一个控制器对象。
@RestController表示@Controller + @ResponseBody
@ResponseBody表示将返回值序列化输出。如果返回值是字符串,直接将字符串写到客户端;如果是一个对象就转化成json串返回。
类上或者方法上注解@RequestMapping和一个Uri,用于将web请求映射到控制器类的方法,表示这个Uri由这个类或者方法处理。
参数上常用@RequestParam@RequestBody这两个注解。
请求参数要与形参一一对应,@RequestParam用来接收前台传来的键值对参数,@RequestBody用来接收前台传来的对象,也就是json数据。这两个注解可以一起使用。
同时还可以使用字符流,@RequestHeader注解等接收参数。
使用Java Bean Validation对参数进行验证。
在Bean的属性上,加入JSR-303注解,实现验证规则的定义。通过BindingResult获取验证结果。还可以定义组接口,进行分组操作。
常用的JSR-303注解有@Null, @NotNull, @AssertTrue, @AssertFalse等。

模型 Model

简单来说就是传给浏览器的数据,Model对象,ModelAndView对象或者json字符串都是模型。

视图 View

视图用于展示数据,所有的视图类都实现了org.springframework.web.servlet.View接口。
常用的返回值可以是String对象,自定义Object对象,ResponseEntity对象(包含HttpStatus Code和应答数据的结合体)。

SpringMVC的请求流程

在SpringMVC框架中,DispatcherServlet是核心对象。作为中央调度器,负责接收对所有Controller的请求,调用Controller中的方法处理业务逻辑,最终将返回值经过视图处理响应给浏览器。
应用了门面设计模式,是SpringMVC对外的入口,解耦其他组件,使其他组件间彼此没有关联。实现了ApplicationContextAware接口,所有可以访问容器中的所有对象。
具体流程如下:
DispatcherServlet接收到客户端发送的请求后,调用HandlerMapping根据URI找到对应的handler拦截器,组装成HandlerExecutionChain对象并返回;
接着DispatcherServlet根据返回值调用HanderAdapter进行接口转换,调用具体的handler中的方法并返回ModelAndView对象;
DispatcherServlet调用HandlerExceptionResolver处理异常,如果有异常则返回包含异常的ModelAndView对象;
然后DispatcherServlet调用ViewResolver解析ModelAndView对象,返回View对象;
最终DispatcherServlet将得到的View对象进行渲染,填充Model中的数据到request域,返回给客户端响应结果。

SpringMVC中的自动配置

WebMvcAutoConfiguration是SpringMVC的自动配置类,根据类上的注解,有以下几个代表性的配置类。
1.@DispatcherServletAutoConfiguration自动配置DispatcherServlet:创建DispatcherServlet,并注册到容器中,设置load-on-startup = -1;创建MultipartResolver;它的绑定Bean是WebMvcProperties
2.@WebMvcConfigurationSupport:创建HandlerMapping, HandlerAdapter, HandlerExceptionResolver接口的多个实现对象;PathMatchConfigurar, ContentNegotiationManager, OptionalValidatorFactoryBean, HttpMessageConverter等实例。
在SpringMVC中,Handler的表现形式有多种,所以处理请求的方式也不同。通过HandlerMapping实现类包装Handler,再由HandlerAdapter接口的实现类调用handler
平时用的最多的@RequestMapping方式是通过RequestMappingHandlerMapping, RequestMappingHandlerAdapter, DefaultHandlerExceptionResolver, ExceptionHandlerExceptionResolver这几个组件实现的。
3.ValidationAutoConfiguration:配置JSR-303验证器
4.ServletWebServerFactoryAutoConfiguration:SpringBoot根据classpath上存在的类,判断当前应用使用的是Tomcat/Jetty/Undertow中的哪个服务器没从而决定定义相应的Web服务器。
绑定Bean是ServerProperties

使用Servlet

通过注解@WebServlet定义Servlet对象

@WebServlet(urlPatterns="/helloServlet", name="HelloServlet")
public class HelloServlet extends HttpServlet {
    ...
}

@ServletComponentScan(basePackages="com.example.servlet")
@SpringBootApplication
public calss Application {
    ...
}

通过ServletRegistrationBean注册Servlet对象

@Configuration
public class WebAppConfig {
    @Bean
    public ServletRegistrationBean addServlet() {
        ServletRegistrationBean registrationBean = new ServletRegistrationBean();
        registrationBean.setServlet(new LoginServlet());
        registrationBean.addUrlMappings("/user/login");
        registrationBean.setLoadOnStartup(1);
        return registrationBean;
    }
}
使用Filter

通过@WebFilter定义Filter对象

@WebFilter(urlPatterns = "/*")
public class LogFilter implements Filter {
    ...
}

@ServletComponentScan(basePackages="com.example.filter")
@SpringBootApplication
public calss Application {
    ...
}

通过FilterRegistrationBean注册Filter对象

@Bean
public FilterRegistrationBean addFilter() {
    FilterRegistrationBean filterRegistration = new FilterRegistrationBean();
    filterRegistration.setFilter(new Filter());
    filterRegistration.addUrlPatterns("/*");
    filterRegistration.setOrder(1);
    return filterRegistration;
}

使用框架中的Filter

@Bean
public FilterRegistrationBean addOtherFilter() {
    FilterRegistrationBean filterRegistration = new FilterRegistrationBean();
    CommonsRequestLoggingFilter filter = new CommonsRequestLoggingFilter();
    ...
    filterRegistration.setFilter(filter);
    ...
    return filterRegistration;
}
使用Listener

使用@WebListener定义Listener

@WebListener
public class MySessionListener implements HttpSessionListener {
    ...
}

@ServletComponentScan(basePackages="com.example.listener")
@SpringBootApplication
public calss Application {
    ...
}

通过ServletListenerRegistrationBean注册Filter对象

@Bean
public ServletListenerRegistrationBean addListener() {
    ServletListenerRegistrationBean registration = new ServletListenerRegistrationBean();
    registration.setListener(new Filter());
    ...
    return registration;
}
使用WebMvcConfigurar定制SpringMVC

定义一个配置类实现接口,并@Override需要的方法
常用的功能有页面跳转控制器,数据格式化,拦截器等

// 实现类
public class DeviceFormatter implements Formatter<DeviceInfo> {
    ...
}

public class AuthInterceptor implements HandlerInterceptor [
    ...
}

@Configuration
public class MvcSettings implements WebMvcConfigurar {
    // 页面跳转控制器
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/welcome").setViewName("index");
    }
    
    // 数据格式化
    @Override
    public void addFormatters(FormatterRegistry registry) {
        registry.addFormatter(new DeviceFormatter());
    }

    // 拦截器
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new AuthInterceptor());
                .order(1)
                .addPathPatterns("/**");
                .excludePathPatterns("/article/query");
    }
}
使用MultipartResolver上传文件

MultipartResolver的内部实现类是StandardServletMultipartResolver,封装了Part接口,实现基于Servlet3.0规范的文件上传。

@Controller
public class UploadFileController {
    // 使用MultipartResolver读取POST请求数据
    @PostMapping("/uploadByMultipartResolver")
    public String uploadM(@RequestParam("uploadFile") MultipartFile multipartFile) {
        ...
    }

    // 使用Part接口实现文件上传
    @PostMapping("/uploadByPartInterface")
    public String uploadP(HttpServletRequest request) {
        Part part = request.getParts();
        ...
    }
}
使用HandlerExceptionResolver处理全局异常

常用的注解:
@ControllerAdvice, @RestControllerAdvice, @ExceptionHandler

@ControllerAdvice
public class GlobalExceptionHandler {
    // 处理全局异常
    @ExceptionHandler({Exception.class})
    public String handlerException(Exception e, Model model) {
        ...
    }

    // 校验JSR-303规范验证参数的结果
    @ExceptionHandler({BindException.class})
    public Map<String, Object> handleJSR303Exception(BindException e) {
        BindingResult result = e.getBindingResult();
        ...
    }
}
SpringBoot3中支持使用ProblemDetail增强异常处理

RFC7807规范中,对错误信息做了更规范的定义,使异常信息更完善。ProblemDetail类封装了标准字段和扩展字段,作为返回值输出RFC7807规范的字段。
除了ProblemDetail类,还可以使用ErrorResponse处理异常。包含异常信息和完整的REF7807格式的ProblemDetail正文。ErrorResponseExceptionErrorResponse接口的一个实现。通过继承ErrorResponseException类,那么这个异常会交给框架处理。

public class BookNotFoundException extends RuntimeException {
    ...
}

public class caculErrorException extends RuntimeException {
    ...
}

@RestControllerAdvice
public class GlobalExceptionHandler {
    // 使用ProblemDetail处理异常
    @ExceptionHandler({BookNotFoundException.class})
    public ProblemDetail handleBookNotFoundException(BookNotFoundException e) {
        ProblemDetail detail = ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, e.getMessage());
        //  设置标识错误类型的URI
        detail.setType()
        ...
        // 可以设置自定义字段
        detail.setProperty("timestamp", Instant.now());
        return detail;
    }

    // 直接使用ErrorResponse作为异常处理方法的返回值
    @ExceptionHandler({caculErrorException.class})
    public ErrorResponse handleCaculErrorException  (CaculErrorException e) {
        ErrorResponse error = new ErrorResponseException(HttpStatus.INTERNAL_SERVER_ERROR, e);
       return error; 
}

// 通过继承ErrorResponseException类,将异常交给框架处理
public class BookNotFoundException extends ErrorResponseException {
    ...
}

远程访问

SpringBoot3提供了多种远程访问技术。其中基于HTTP协议的远程访问应用最广泛。首先定义一个接口,使用@HttpExchange提供HTTP服务;通过生成HTTP客户端WebClient的代理对象,实现远程访问。WebClient是基于Reactor的非阻塞异步请求客户端。

声明式
// POJO
public class Schedule {
    ...
}

// 声明服务接口
public interface ScheduleService {
    @GetExchange("/schedules/{id}")
    Schedule getScheduleById(@PathVariable Integer id);
}

// 创建代理类
@Coufiguration(proxyBeanMethods = false)
public class HttpConfiguration {
    @Bean
    public ScheduleService requestService() {
        WebClient webClient = WebClient.builder().baseUrl("https://jsonplaceholder.typicode.com/").build();
        return HttpServiceProxyFactory.builder(WebClientAdapter.forClient(webClient)).build().createClient(ScheduleService.class);
}

// 调用
public class CallHttpClient {
    @Resource
    private ScheduleService service;
    public Schedule getSchedule(Integer id) {
        return service.getScheduleById(id);
    }
}
使用接口组合
// POJO
public class Schedule {
    ...
}

// 声明服务接口
@HttpExchange(url = "https://jsonplaceholder.typicode.com/")
public interface ScheduleService {
    @GetExchange("/schedules/{id}")
    Schedule getScheduleById(@PathVariable Integer id);
}

// 创建代理类
@Coufiguration(proxyBeanMethods = false)
public class HttpConfiguration {
    @Bean
    public ScheduleService requestService() {
        WebClient webClient = WebClient.create();
        return HttpServiceProxyFactory.builder(WebClientAdapter.forClient(webClient)).build().createClient(ScheduleService.class);
    }
    // 自定义配置
    @Bean
    public ScheduleService selfDefiningService() {
        HttpClient client = HttpClient.create()
                                      .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 30000);
        WebClient webClient = WebClient.builder()
                                      .clientConnector(new ReactorClientHttpConnector(client ));
        return HttpServiceProxyFactory.builder(WebClientAdapter.forClient(client)).build().createClient(ScheduleService.class);
}

// 调用
public class CallHttpClient {
    @Resource
    private ScheduleService service;
    public Schedule getSchedule(Integer id) {
        return service.getScheduleById(id);
    }
}

AOT和GraalVM

JVM提高执行速度的技术有JIT实时编译技术,和在Java9中作为实验性功能的AOT预编译技术。
JIT会发现一些运行频繁的热点代码,并将热点代码编译成本地代码。这个过程是在程序执行中进行的,所以完成优化需要时间。
AOT是静态的,通过Native Image Builder这个工具,预先将代码编译为独立可执行文件Native Image本机镜像。这个编译过程会在项目构建期间完成。
目前,SpringBoot3支持使用GraalVm中的AOT技术编译,并执行镜像文件。

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

推荐阅读更多精彩内容