springboot3第一章jdk新特性

动力节点springboot3视频笔记第一章

1.JDK关注的新特性

1.1 搭建学习环境

JDK:JDK19OpenJDK: https://jdk.java.net/19/Liberica JDK: https://bell-sw.com/pages/downloads/, 是一个OpenJDK发行版,为云原生,容器特别优化。

Maven:构建和依赖管理,版本选择3.6以上配置本地仓库和阿里云镜像

IDEA2022.3.1Ultimate:主要的开发工具,我是30天试用版本

数据库:MySQL 5以上版本

火狐浏览器:版本用比较新的,中文版本。

文本工具:EditPlus, Sublime任意。

1.2 有用的新特性

JDK8-19新增了不少新特性,这里我们把实际常用的新特性,给大家介绍一下。包括以下几个方面:

[if !supportLists]1. [endif]Java Record

[if !supportLists]2. [endif]Swich开关表达式

[if !supportLists]3. [endif]Text Block文本块

[if !supportLists]4. [endif]var 声明局部变量

[if !supportLists]5. [endif]sealed 密封类

1.2.1 Java Record

Java14中预览的新特性叫做Record,在Java中,Record是一种特殊类型的Java类。可用来创建不可变类,语法简短。参考JEP 395. Jackson 2.12支持Record类。

任何时候创建Java类,都会创建大量的样板代码,我们可能做如下:

[if !supportLists]· [endif]每个字段的set,get方法

[if !supportLists]· [endif]公共的构造方法

[if !supportLists]· [endif]重写hashCode, toString(), equals()方法

Java Record避免上述的样板代码,如下特点:

[if !supportLists]· [endif]带有全部参数的构造方法

[if !supportLists]· [endif]public访问器

[if !supportLists]· [endif]toString(),hashCode(),equals()

[if !supportLists]· [endif]无set,get方法。没有遵循Bean的命名规范

[if !supportLists]· [endif]final类,不能继承Record,Record为隐士的final类。除此之外与普通类一样

[if !supportLists]· [endif]不可变类,通过构造创建Record

[if !supportLists]· [endif]final属性,不可修改

[if !supportLists]· [endif]不能声明实例属性,能声明static成员

IDEA创建新的Maven工程 Lession01-feature

1.2.1.1 看看Record怎么用

IDEA新建Class,选择类Record

step1: 创建Student Recordpublic record Student(Integer id,String name,String email,Integer age) {}

step2:创建Record对象

public static void main(String[] args) {Student lisi = new Student(1001, "lisi","lisi@qq.com",20);    System.out.println("lisi = " + lisi.toString());Student zhangsan = new Student(1002, "zhangsan","lisi@qq.com",20);    System.out.println("zhangsan = " + zhangsan.toString());    System.out.println("lisi.equals(zhangsan) = " + lisi.equals(zhangsan));    System.out.println("lisi.name() = " + lisi.name());    System.out.println("zhangsan.name() = " + zhangsan.name());}

现在能查看控制台输出:lisi = Student[id=1001, name=lisi,email=lisi@qq.com, age=20]zhangsan = Student[id=1002, name=zhangsan, email=lisi@qq.com, age=20]lisi.equals(zhangsan) = falselisi.name() = lisizhangsan.name() = zhangsan

Record通过构造方法创建了只读的对象,能够读取每个属性,不能设置新的属性值。 Record用于创建不可变的对象,同时减少了样板代码。Record对每个属性提供了public访问器,例如lisi.name()

1.2.1.2 Instance Methods

Record是Java类,和普通Java类一样定义方法。下面定义方法concat,将姓名和年龄一起打印输出。我们创建普通的方法concat,将name和age连接为一个字符串输出。

step1:创建实例方法

public record Student(Integer id,String name,String email,Integer age) {public String concat(){return String.format("姓名:%s,年龄是:%d", this.name,this.age);}}

step2: 调用实例方法

public static void main(String[] args) {Student lisi = new Student(1001, "lisi","lisi@qq.com",20);String nameAndAge = lisi.concat();System.out.println( nameAndAge);}

最后控制台输出:姓名:lisi,年龄是:20

1.2.1.3 静态方法 Static Method

Record类定义静态方法,试用静态方法与普通类一样。step1: 创建静态方法

public record Student(Integer id,String name,String email,Integer age) {public String concat(){return String.format("姓名:%s,年龄是:%d", this.name,this.age);}/**  静态方法  */public static String emailUpperCase(String email){return Optional.ofNullable(email).orElse("no email").toUpperCase();}}

step2:测试静态方法

public static void main(String[] args) {    String emailUpperCase = Student.emailUpperCase("lisi@163.com");        System.out.println("emailUpperCase = " + emailUpperCase);    }

1.2.1.4 Record的构造方法

我们可以在Record中添加构造方法, 有三种类型的构造方法分别:是紧凑的,规范的和定制构造方法v紧凑型构造方法没有任何参数,甚至没有括号。v规范构造方法是以所有成员作为参数v定制构造方法是自定义参数个数

step1: 紧凑和定制构造方法

public record Student(Integer id,String name,String email,Integer age) {/紧凑构造方法/public Student {System.out.println("id"+ id );if( id < 1 ){throw new RuntimeException("ok");}}/自定义构造方法/public Student(Integer id, String name) {this(id, name, null, null);}}

step2:编译Student.java -> Student.class

public record Student(Integer id, String name, String email, Integer age) {/** 紧凑构造方法和规范构造方法合并了 */public Student(Integer id, String name, String email, Integer age) {System.out.println("id" + id);if (id < 1) {throw new RuntimeException("ok");    }else {this.id = id;this.name = name;this.email = email;this.age = age;}}public Student(Integer id, String name) {this(id, name, (String)null, (Integer)null);}}

1.2.1.5 Record与Lombok

Java Record是创建不可变类且减少样板代码的好方法。Lombok是一种减少样板代码的工具。两者有表面上的重叠部分。可能有人会说Java Record会代替Lombok. 两者是有不同用途的工具。Lombok提供语法的便利性,通常预装一些代码模板,根据您加入到类中的注解自动执行代码模板。这样的库纯粹是为了方便实现POJO类。通过预编译代码。将代码的模板加入到class中。Java Record是语言级别的,一种语义特性,为了建模而用,数据聚合。简单说就是提供了通用的数据类,充当“数据载体",用于在类和应用程序之间进行数据传输。

1.2.1.6 Record实现接口

Java Record可以与普通类一样实现接口,重写接口的方法。

step1: 创建新的接口,定义一个规范方法。

public interface PrintInterface {/** 输出自定义描述信息 */void print();}

step2: 创建新的Record实现接口,重写接口的方法,实现当前Record有关的业务逻辑

public record ProductRecord(String id,String name,Integer qty)implements PrintInterface {@Override  public void print() {String productDesc = String.join("-", id, name, qty.toString());     System.out.println("商品信息 = " + productDesc);}}ProductRecord实现print()方法,打印商品详情。

step3:测试print方法

public static void main(String[] args) {ProductRecord product = new ProductRecord("P001", "手机", 100);product.print();}

1.2.1.7 Local Record

Record可以作为局部对象使用。在代码块中定义并使用Record,下面定义一个SaleRecord

step1:定义Local Record

public static void main(String[] args) {//定义Java Recordrecord SaleRecord(String saleId,String productName,Double money){};//创建Local RecordSaleRecord saleRecord = new SaleRecord("S22020301", "手机", 3000.0);//使用SaleRecordSystem.out.println("销售记录 = " + saleRecord.toString());}

控制台输出:销售记录= SaleRecord[saleId=S22020301, productName=手机, money=3000.0]

1.2.1.8 嵌套Record

多个Record可以组合定义, 一个Record能够包含其他的Record。我们定义Record为Customer,存储客户信息,包含了Address和PhoneNumber两个Record

step1:定义Record

public record Address(String city,String address,String zipcode) {    }public record PhoneNumber(String areaCode,String number) {    }public record Customer(String id,  String name,  PhoneNumber phoneNumber,                       Address address) {    }

step2: 创建Customer对象

public static void main(String[] args) {Address address = new Address("北京", "大兴区凉水河二街-8号10栋三层", "100176");    PhoneNumber phoneNumber = new PhoneNumber("010", "400-8080-105");Customer customer = new Customer("C1001", "李项", phoneNumber, address);    System.out.println("客户 = " + customer.toString());}

控制台输出:客户= Customer[id=C1001, name=李项, phoneNumber=PhoneNumber[areaCode=010, number=400-8080-105], address=Address[city=北京, address=大兴区凉水河二街9号10栋三层, zipcode=100176]]

1.2.1.9 instanceof 判断Record类型

instanceof 能够与 Java Record一起使用。编译器知道记录组件的确切数量和类型。

step1:声明Person Record,拥有两个属性name和agepublic record Person(String name,Integer age) {}

step2: 在一个业务方法判断当是Record类型时,继续判断age年龄是否满足18岁。

public class SomeService {public boolean isEligible(Object obj){// 判断obj为Person 记录类型if( obj instanceof Person(String name, Integer age)){return age >= 18;}return false;}}instanceof 还可以下面的方式if( obj instanceof Person(String name, Integer age) person){return person.age() >= 18;}或者if( obj instanceof Person p){return p.age() >= 18;}public static void main(String[] args) {SomeService service = new SomeService();boolean flag = service.isEligible(new Person("李四", 20));  System.out.println("年龄符合吗?" + flag);}

step3: 测试代码

控制台输出:控制台输出flag为true

处理判断中Record为nullJava Record能够自动处理null。step1:record为nullpublic static void main(String[] args) {SomeService service = new SomeService();boolean eligible = service.isEligible(null);System.out.println("年龄符合吗?" + eligible);}控制台输出eligible为false ,Debug调试代码,发现if语句判断为false,不执行

*总结*1. abstract类java.lang.Record是所有Record的父类。2. 有对于equals(),hashCode(),toString()方法的定义说明3. Record类能够实现 java.io.Serializable序列化或反序列化4. Record支持泛型,例如 record Gif( T t ) { }5. java.lang.Class类与Record类有关的两个方法:boolean isRecord() : 判断一个类是否是Record类型RecordComponent[] getRecordComponents():Record的数组,表示此记录类的所有记录组件

Customer customer = new Customer(....);RecordComponent[] recordComponents = customer.getClass().getRecordComponents();   

for (RecordComponent recordComponent : recordComponents) {System.out.println("recordComponent = " + recordComponent);}boolean record = customer.getClass().isRecord();System.out.println("record = " + record);

1.2.2 Switch

Switch的三个方面,参考:JEP 361

[if !supportLists]· [endif]支持箭头表达式

[if !supportLists]· [endif]支持yied返回值

[if !supportLists]· [endif]支持Java Record

1.2.2.1 箭头表达式,新的case标签

Switch新的语法,case label -> 表达式|throw 语句|blockcase label_1, label_2, ..., label_n -> expression;|throw-statement;|block

step1:新的case 标签week:表示周日(1)到周六(7),1和7是休息日,其他是工作日。如果1-7以外为无需日期

public static void main(String[] args) {int week = 7;String memo = "";switch (week){case 1 -> memo = "星期日,休息";case 2,3,4,5,6-> memo="工作日";case 7 -> memo="星期六,休息";default ->  throw new IllegalArgumentException("无效的日期:");}System.out.println("week = " + memo);}

1.2.2.2 yeild返回值

yeild让switch作为表达式,能够返回值

语法变量= switch(value) { case v1: yield 结果值; case v2: yield 结果值;case v3,v4,v5.. yield 结果值 }

示例:yield返回值,跳出switch块public static void main(String[] args) {int week = 2;//yield是switch的返回值, yield跳出当前switch块String memo = switch (week){case 1: yield "星期日,休息";case 2,3,4,5,6: yield "工作日";case 7: yield "星期六,休息";default: yield "无效日期";};

System.out.println("week = " + memo);

}

无需中间变量,switch作为表达式计算,可以得到结果。yield是表达式的返回值

示例:多表达式,case 与yield 结合使用

public static void main(String[] args) {int week = 1;//yield是switch的返回值, yield跳出当前switch块String memo  = switch (week){case 1 ->{System.out.println("week=1的 表达式部分");yield "星期日,休息";}case 2,3,4,5,6 ->{System.out.println("week=2,3,4,5,6的 表达式部分");yield "工作日";}case 7 -> {System.out.println("week=7的 表达式部分");yield "星期六,休息";}default -> {System.out.println("其他语句");yield "无效日期";}};System.out.println("week = " + memo);}

提示:case 标签->  与 case 标签:不能混用。    一个switch语句块中使用一种语法格式。switch作为表达式,赋值给变量,需要yield或者case 标签-> 表达式。->右侧表达式为case返回值。

示例:

public static void main(String[] args) {int week = 1;//yield是switch的返回值, yield跳出当前switch块String memo  = switch (week){case 1 ->{System.out.println("week=1的 表达式部分");yield "星期日,休息";}case 2,3,4,5,6 ->{System.out.println("week=2,3,4,5,6的 表达式部分");yield "工作日";}case 7 -> "星期六,休息";default -> "无效日期";};System.out.println("week = " + memo);}

1.2.2.3 Java Record

switch表达式中使用record,结合 case 标签-> 表达式,yield实现复杂的计算step1: 准备三个Record

public record Line(int x,int y) {}public record Rectangle(int width,int height) {}public record Shape(int width,int height) {}

step2: switch record

public static void main(String[] args) {Line line = new Line(10,100);Rectangle rectangle = new Rectangle(100,200);Shape shape = new Shape(200,200);Object obj = rectangle;int result = switch (obj){case Line(int x,int y) -> {System.out.println("图形是线, X:"+x+",Y:"+y);yield x+y;}case Rectangle(int w,int h) -> w * h;case Shape(int w,int h) ->{System.out.println("这是图形,要计算周长");yield 2* (w + h);}default -> throw new IllegalStateException("无效的对象:" + obj);};System.out.println("result = " + result);}

case Line , Rectangle,Shape 在代码块执行多条语句,或者箭头->表达式。

1.2.3 Text Block

Text Block处理多行文本十分方便,省时省力。无需连接 "+",单引号,换行符等。Java 15 ,参考JEP 378.

1.2.3.1 认识文本块

语法:使用三个双引号字符括起来的字符串."""内容"""

例如:String name = """lisi"""; //Error 不能将文本块放在单行上String name= """lisi20""";  //Error 文本块的内容不能在没有中间行结束符的情况下跟随三个开头双引号

String myname= """zhangsan20"""; //正确

文本块定义要求:v文本块以三个双引号字符开始,后跟一个行结束符。v不能将文本块放在单行上v文本块的内容也不能在没有中间行结束符的情况下跟随三个开头双引号

三个双引号字符""" 与两个双引号""的字符串处理是一样的。与普通字符串一样使用。例如equals() , "==" , 连接字符串(”+“), 作为方法的参数等。

1.2.3.2 文本块与普通的双引号字符串一样

Text Block使用方式与普通字符串一样,==,equals比较,调用String类的方法。

step1:字符串比较与方法public void fun1() {String s1= """lisi""";String s2 = """lisi""";

//比较字符串boolean b1 = s1.equals(s2);System.out.println("b1 = " + b1);

//使用 == 的比较boolean b2 = s1 == s2;System.out.println("b2 = " + b2);

String msg = """hello world""";//字符串方法substringString sub = msg.substring(0, 5);System.out.println("sub = " + sub);}

step2:输出结果b1 = trueb2 = truesub = hello

1.2.3.3 空白

[if !supportLists]1. [endif]JEP 378中包含空格处理的详细算法说明。

[if !supportLists]2. [endif]Text Block中的缩进会自动去除,左侧和右侧的。

[if !supportLists]3. [endif]要保留左侧的缩进,空格。将文本块的内容向左移动(tab键)

示例:

public void fun2(){//按tab向右移动,保留左侧空格String html= """动力节点,Java黄埔军校""";System.out.println( html);}

示例2:indent()方法

public void fun3(){String colors= """redgreenblue""";System.out.println( colors);//indent(int space)包含缩进,space空格的数量String indent = colors.indent(5);System.out.println( indent);}

输出:redgreenblue

red green blue

1.2.3.4 文本块的方法Text Block的格式方法formatted()

public void fun4(){String info= """Name:%sPhone:%sAge:%d""".formatted("张三","13800000000",20);System.out.println("info = " + info);}

String stripIndent():删除每行开头和结尾的空白String translateEscapes() :转义序列转换为字符串字面量1.2.3.5 转义字符新的转义字符"",表示隐士换行符,这个转义字符被Text Block转义为空格。通常用于是拆分非常长的字符串文本 ,串联多个较小子字符串,包装为多行生成字符串。

新的转义字符,组合非常长的字符串。

示例

public void fun5(){String str= """Spring Boot是一个快速开发框架 基于"Spring"框架,创建Spring应用内嵌Web服务器,以jar或war方式运行""";System.out.println("str = " + str);}

输出:Spring Boot是一个快速开发框架 基于Spring框架,创建Spring应用 内嵌Web服务器,以jar或war方式运行

总结:1.多行字符串,应该使用Text Block2.当Text Block可以提高代码的清晰度时,推荐使用。比如代码中嵌入SQL语句3.避免不必要的缩进,开头和结尾部分。4.使用空格或仅使用制表符 文本块的缩进。混合空白将导致不规则的缩进。5.对于大多数多行字符串, 分隔符位于上一行的右端,并将结束分隔符位于文本块单独行上。例如:String colors= """redgreenblue""";

1.2.4 var

在JDK 10及更高版本中,您可以使用var标识符声明具有非空初始化式的局部变量,这可以帮助您编写简洁的代码,消除冗余信息使代码更具可读性,谨慎使用.

1.2.4.1 var 声明局部变量

var特点

[if !supportLists]1. [endif]var是一个保留字,不是关键字(可以声明var为变量名)

[if !supportLists]2. [endif]方法内声明的局部变量,必须有初值

[if !supportLists]3. [endif]每次声明一个变量,不可复合声明多个变量。var s1="Hello", age=20; //Error

[if !supportLists]4. [endif]var动态类型是编译器根据变量所赋的值来推断类型

[if !supportLists]5. [endif]var代替显示类型,代码简洁,减少不必要的排版,混乱。

var优缺点

[if !supportLists]· [endif]代码简洁和整齐。

[if !supportLists]· [endif]降低了程序的可读性(无强类型声明)

示例:

//通常try (Stream result = dbconn.executeQuery(query)) {//...//推荐try (var customers = dbconn.executeQuery(query)) {//...}比较Stream result 与  var customers

1.2.4.2 使用时候使用var

[if !supportLists]· [endif]简单的临时变量

[if !supportLists]· [endif]复杂,多步骤逻辑,嵌套的表达式等,简短的变量有助理解代码

[if !supportLists]· [endif]能够确定变量初始值

[if !supportLists]· [endif]变量类型比较长时

示例

public void fun1(){var s1="lisi";var age = 20;for(var i=0;i<10;i++){System.out.println("i = " + i);}List strings = Arrays.asList("a", "b", "c");for (var str: strings){System.out.println("str = " + str);}}

1.2.5 sealed

sealed 翻译为密封,密封类(Sealed Classes)的首次提出是在 Java15 的 JEP 360 中,并在 Java 16 的 JEP 397 再次预览,而在 Java 17 的 JEP 409 成为正式的功能。

Sealed Classes主要特点是限制继承

Sealed Classes主要特点是限制继承,Java中通过继承增强,扩展了类的能力,复用某些功能。当这种能力不受控。与原有类的设计相违背,导致不预见的异常逻辑。

Sealed Classes限制无限的扩张。

Java中已有sealed 的设计

[if !supportLists]· [endif]final关键字,修饰类不能被继承

[if !supportLists]· [endif]private限制私有类

sealed 作为关键字可在class和interface上使用,结合permits 关键字。定义限制继承的密封类

1.2.5.1 Sealed Classes

sealed class 类名 permits 子类1,子类N列表 {}

step1::声明sealed Class

public sealed class Shape permits Circle, Square, Rectangle {private Integer width;private Integer height;public void draw(){System.out.println("=Shape图形");}}

permits表示允许的子类,一个或多个

step2::声明子类

子类声明有三种

[if !supportLists]1. [endif]final 终结,依然是密封的

[if !supportLists]2. [endif]sealed 子类是密封类,需要子类实现

[if !supportLists]3. [endif]non-sealed 非密封类,扩展使用,不受限

示例:

//第一种 finalpublic final class Circle extends Shape {}//第二种 sealed classpublic sealed class Square extends Shape permits RoundSquare {@Override  public void draw() {System.out.println("=Square图形");}}//密封类的子类的子类public final class RoundSquare extends Square{}//非密封类 , 可以被扩展。放弃密封public non-sealed class Rectangle extends Shape {}//继承非密封类public class Line extends  Rectangle{}

密封类不支持匿名类与函数式接口

1.2.5.2 Sealed Interface

密封接口同密封类

step1:声明密封接口

public sealed interface SomeService permits SomeServiceImpl {void doThing();}

step2:实现接口

public final class SomeServiceImpl implements SomeService {@Override  public void doThing() {}}

以上类和接口要在同一包可访问范围内。

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

推荐阅读更多精彩内容