SpringBoot开发单体应用

Web开发探究

SpringBoot的最大特点:自动装配
使用SpringBoot的步骤:
1、创建一个SpringBoot应用,选择模块、然后等待项目构建完成即可。
2、编写一些需要的配置文件。
3、专注于编写业务代码。其余东西不需要手动配置。
配置相关的类:

  • xxxAutoConfiguration:向容器中自动配置组件。
  • xxxProperties:自动配置类,封装配置文件的内容。

静态资源处理

简介

SpringBoot中,所有的配置文件都在application.yaml或application.properties文件中,并没有web.xml文件。我们需要想办法实现*.css , *.js等静态资源文件生效,我们就需要明白其中的规则。
学习方法:
1、从源码分析,然后得出结论。
2、尝试去测试,验证结论是否正确。
看源码之前,抛出一个问题:SpringBoot默认没有webapp目录,那我们的资源文件应该放在哪里呢?

尝试分析源码并进行探究

在SSM架构中,整个SSM都是基于SpringMVC的。因此我们第一步应该去研究SpringBoot中关于MVC的自动配置。
研究之后可以得到以下结论:
1、所有MVC相关的配置都在WebMvcAutoConfiguration类中,比如视图解析器、静态资源过滤等。
2、addResourceHandlers静态资源处理方法分析:

@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
    //禁用默认规则配置,若手动添加了资源映射路径的配置,这些配置将直接失效
    if (!this.resourceProperties.isAddMappings()) {
        logger.debug("Default resource handling disabled");
        return;
    }
    //缓存控制
    Duration cachePeriod = this.resourceProperties.getCache().getPeriod();
    CacheControl cacheControl = this.resourceProperties.getCache().getCachecontrol().toHttpCacheControl();
    //分析源代码,需要掌握看对象的方法调用
    // localhost:8080/webjars/jquery.js 
    // 判断是否存在一个映射路径 /webjars/**
    // addResourceHandler 处理逻辑 /webjars/a.js 
    // addResourceLocations 处理资源的地址 classpath:/META- INF/resources/webjars/a.js
    if (!registry.hasMappingForPattern("/webjars/**")) {
        customizeResourceHandlerRegistration(registry.addResourceHandler("/webjars/**")
                .addResourceLocations("classpath:/META-INF/resources/webjars/")
                .setCachePeriod(getSeconds(cachePeriod)).setCacheControl(cacheControl));
    }
    //获取静态资源路径
    String staticPathPattern = this.mvcProperties.getStaticPathPattern();
    // localhost:8080/ 
    // 如果访问映射的路径是 staticPathPattern = "/**"
    // this.resourceProperties.getStaticLocations())
    if (!registry.hasMappingForPattern(staticPathPattern)) {
        customizeResourceHandlerRegistration(registry.addResourceHandler(staticPathPattern)
                .addResourceLocations(getResourceLocations(this.resourceProperties.getStaticLocations()))
                .setCachePeriod(getSeconds(cachePeriod)).setCacheControl(cacheControl));
    }
}

什么是webjars

webjars,简单来说就是使用maven的方式导入前端静态资源,比如jQuery的依赖如下:

<dependency>            
  <groupId>org.webjars</groupId>
  <artifactId>jquery</artifactId>
  <version>3.4.1</version>
</dependency>

导入依赖后可以发现项目中导入了如下图所示jar包。

webjars jQuery jar包

测试访问一下,访问地址:http://localhost:8080/webjars/jquery/3.4.1/jquery.js,发现可以成功访问到jQuery源文件。
webjars jQuery源文件访问测试

第二种静态资源处理规则

SpringBoot默认的静态资源处理映射目录如下代码所示:

private static final String[] CLASSPATH_RESOURCE_LOCATIONS = 
{ 
  "classpath:/META-INF/resources/", //在starter中使用,如SWAGGER-UI
  "classpath:/resources/",//文件资源
  "classpath:/static/", //静态资源
"classpath:/public/"//公共的文件,比如图标...
};

classpath映射的是resources目录,这些静态资源处理映射目录的优先级由高到低跟其索引顺序保持一致。

自定义配置静态资源映射路径

我们可以配置自己的静态资源映射目录,只不过这样配置之后,一切原来的配置都会失效。

spring.resources.static-locations=classpath:/wunian

首页和图标处理

分析源码

欢迎页(首页)处理器映射器方法位于WebMvcAutoConfiguration类中,其源代码如下:

@Bean //欢迎页(首页)会被映射到这个处理器下
public WelcomePageHandlerMapping welcomePageHandlerMapping(ApplicationContext applicationContext,
        FormattingConversionService mvcConversionService, ResourceUrlProvider mvcResourceUrlProvider) {
    WelcomePageHandlerMapping welcomePageHandlerMapping = new WelcomePageHandlerMapping(
            new TemplateAvailabilityProviders(applicationContext), applicationContext, getWelcomePage(),
            this.mvcProperties.getStaticPathPattern());
    welcomePageHandlerMapping.setInterceptors(getInterceptors(mvcConversionService, mvcResourceUrlProvider));
    return welcomePageHandlerMapping;
}
//获得欢迎页
private Optional<Resource> getWelcomePage() {
    String[] locations = getResourceLocations(this.resourceProperties.getStaticLocations());
    // stream 流 
    // map 映射 
    // filter 过滤 
    // findFirst 找第一个符合条件的 
    // 调用对象方法的语法糖! this 对象 :: getIndexHtml 方法名字 
    // this::getIndexHtml
    return Arrays.stream(locations).map(this::getIndexHtml).filter(this::isReadable).findFirst();
}

private Resource getIndexHtml(String location) {
    return this.resourceLoader.getResource(location + "index.html");
}

只要在上面的静态资源映射目录下存在的index.html就会被自动映射为首页(欢迎页)。

网站图标

在StaticResourceLocation类中定义了网站图标的映射规则。

/**
 * The {@code "favicon.ico"} resource.
*/
FAVICON("/**/favicon.ico");

由源码可知,要设置自定义的网站图标,只需要将一个favicon.ico放入静态资源目录即可。一般把它放在public目录下。

Thymeleaf模板引擎

什么是模板引擎?

模板引擎是为了使用户界面与业务数据(内容)分离而产生的,它可以生成特定格式的文档,用于网站的模板引擎就会生成一个标准的html文档。
前端给后端的页面一般都是一些静态资源或者html等文件,在传统开发中,后端开发人员需要把它们转换为jsp页面,通过嵌入Java变量或代码来实现一些功能,jsp无疑是非常强大的。
但问题在于:在SpringBoot项目中无法编写jsp页面,因为SpringBoot项目是以jar包方式运行而不是war包方式运行。运行jar包命令:java -jar xxx.jar
这就涉及到页面处理和前后端交互的问题,这时就需要使用模板引擎了。

模板引擎原理示意图

现今较为知名的模板引擎有:jsp、freemarker、Thymeleaf等。
SpringBoot推荐我们使用Thymeleaf模板引擎,该引擎使用的就是html页面。
我们只需要记住一句话:所有模板引擎原理都是一样的,唯一区别就是每个模板的语法有些不一样而已。

Thymeleaf使用

官网:https://www.thymeleaf.org/documentation.html
github:https://github.com/thymeleaf/thymeleaf/blob/3.0-master
Thymeleaf的依赖如下:

<dependency>
  <groupId>org.thymeleaf</groupId> 
  <artifactId>thymeleaf</artifactId> 
  <version>3.0.12-SNAPSHOT</version>
</dependency>

1、SpringBoot提供了Thymeleaf的启动器,导入以下依赖即可:

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

2、配置Thymeleaf,参考配置类ThymeleafProperties。

@ConfigurationProperties(prefix = "spring.thymeleaf") 
public class ThymeleafProperties { 
  private static final Charset DEFAULT_ENCODING = StandardCharsets.UTF_8; 

  public static final String DEFAULT_PREFIX = "classpath:/templates/"; 
  
  public static final String DEFAULT_SUFFIX = ".html";
}

3、使用Thymeleaf并测试。
Controller代码如下:

@Controller // 可以被视图解析器解析
public class HelloController {

    @GetMapping("/test")
    public String test(Model model){
        //classpath:/templates/test.html
        model.addAttribute("msg","Hello Thymeleaf");
        model.addAttribute("msg2","<h2>Hello Thymeleaf<h2>");
        model.addAttribute("users", Arrays.asList("coding","qinjiang"));
        return "test";
    }
}

html代码如下:

<!DOCTYPE html>
<!--
    xmlns xml namespace 命名空间,加上了才可以支持thymeleaf
-->
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<!--
    模板引擎:后端可以进行跳转,给出相应数据时前端可以接收一些数据
-->
  <body>
    <h1>测试页面</h1>
    <p th:text="${msg}"></p>
    <!--转义和不转义-->
    <p th:text="${msg2}"></p>
    <p th:utext="${msg2}"></p>
    <!--遍历-->
    <h3 th:each="user:${users}" th:text="${user}"></h3>
  </body>
</html>

Thymeleaf语法

查阅官方文档,Thymeleaf常用标签如下图所示。


Thymeleaf常用标签

除此之外,还有很多表达式也可以使用。

  • Variable Expressions: ${...} 获取一些基本的变量值。使用场景:
    1、获取对象的属性、调用方法。
    2、使用内置的基本对象,如下代码所示:
${#ctx.locale} 
${param.foo} 
${session.foo} 
${application.foo} 
${#request.getAttribute('foo')} 
${#servletContext.contextPath}

3、工具对象,如下代码所示:

${#messages.msg('msgKey')} 
${#uris.escapePath(uri)} 
${#conversions.convert(object, 'java.util.TimeZone')} 
${#dates.format(date, 'dd/MMM/yyyy HH:mm')} 
${#calendars.format(cal)} 
${#numbers.formatInteger(num,3)} 
${#strings.toString(obj)} 
${#arrays.toArray(object)}
......
  • Selection Variable Expressions:*{...} 选择表达式,和 ${} 是一样的;
  • Message Expressions:#{...} 国际化内容获取!
  • Link URL Expressions:@{...} URL表达式; th:href=“@{/login}”
  • Fragment Expressions: ~{...} 组件化表达式;
  • Literals (字面量)
  • Text literals: 'one text' , 'Another one!' ,… (字符串)
  • Number literals: 0 , 34 , 3.0 , 12.3 ,…
  • Boolean literals: true , false
  • Null literal: null
  • Literal tokens: one , sometext , main ,…
  • Text operations: (文本操作)
  • String concatenation: +
  • Literal substitutions: |The name is ${name}|
  • Arithmetic operations: (数学运算)
  • Binary operators: + , - , * , / , %
  • Minus sign (unary operator): -
  • Boolean operations: (布尔运算)
  • Binary operators: and , or
  • Boolean negation (unary operator): ! , not
  • Comparisons and equality: (比较运算)
  • Comparators: > , < , >= , <= ( gt , lt , ge , le )
  • Equality operators: == , != ( eq , ne )
  • Conditional operators: (条件运算符)
  • If-then: (if) ? (then)
  • If-then-else: (if) ? (then) : (else)
  • Default: (value) ?: (defaultvalue)
  • Special tokens:

MVC自动配置原理

阅读官方文档

在进行项目开发的学习之前,我们必须知道的最后一个东西就是MVC自动配置原理,我们的学习方式依然是源码+官方文档。
官方文档地址:https://docs.spring.io/spring-boot/docs/2.2.5.RELEASE/reference/htmlsingle/#boot-features-spring-mvc-auto-configuration
下面是一段关于Spring MVC自动配置的描述,详细介绍了Spring MVC自动配置的功能:

// Spring MVC 自动配置
Spring MVC Auto-configuration 
// SpringBoot为SpringMVC 提供提供了自动配置,他可以很多好的工作于大多数的应用! 
Spring Boot provides auto-configuration for Spring MVC that works well with most applications. 
// 自动配置在Spring默认配置的基础上添加了以下功能: 
The auto-configuration adds the following features on top of Spring’s defaults: 
// 包含视图解析器 
Inclusion of ContentNegotiatingViewResolver and BeanNameViewResolver beans. 
// 支持静态资源文件的路径吗,包含webjar的支持 
Support for serving static resources, including support for WebJars (covered later in this document)). 
// 自动注册了转换器 
// 转换器 网页提交的前端对象,到后台自动封装为具体的对象;"1" 自动转换为 数字 1; 
// 格式化器Formatter 【2020-03-18 后台可以自动封装为Date】 
Automatic registration of Converter, GenericConverter, and Formatter beans. 
// 支持消息转换 
// request、response,对象自动转换为 json对象 S
upport for HttpMessageConverters (covered later in this document). 
// 定错代码生成规则 
Automatic registration of MessageCodesResolver (covered later in this document). 
// 支持首页定制 
Static index.html support. 
// 支持自定义图标 
Custom Favicon support (covered later in this document). 
//配置web数据绑定 
Automatic use of a ConfigurableWebBindingInitializer bean (covered later in this document).

内容协商视图解析器ContentNegotiatingViewResolver

这是SpringBoot自动配置的视图解析器,所有的视图解析器都要经过它来进行协商,最终返回一个最好的视图。其源码如下所示:

@Bean
@ConditionalOnBean(ViewResolver.class)
@ConditionalOnMissingBean(name = "viewResolver", value = ContentNegotiatingViewResolver.class)
public ContentNegotiatingViewResolver viewResolver(BeanFactory beanFactory) {
    ContentNegotiatingViewResolver resolver = new ContentNegotiatingViewResolver();
    resolver.setContentNegotiationManager(beanFactory.getBean(ContentNegotiationManager.class));
    // ContentNegotiatingViewResolver uses all the other view resolvers to locate
    // a view so it should have a high precedence 
    // ContentNegotiatingViewResolver 
    //使用其他所有的视图解析器定位视图,因此它应该具有一个高的优先级!
    resolver.setOrder(Ordered.HIGHEST_PRECEDENCE);
    return resolver;
}

//ContentNegotiatingViewResolver 解析视图名字
@Override
@Nullable  // 参数可以为空
public View resolveViewName(String viewName, Locale locale) throws Exception {
    RequestAttributes attrs = RequestContextHolder.getRequestAttributes();
    Assert.state(attrs instanceof ServletRequestAttributes, "No current ServletRequestAttributes");
    List<MediaType> requestedMediaTypes = getMediaTypes(((ServletRequestAttributes) attrs).getRequest());
    if (requestedMediaTypes != null) {
        //获取所有候选的视图
        List<View> candidateViews = getCandidateViews(viewName, locale, requestedMediaTypes);
        //获取最好的视图
        View bestView = getBestView(candidateViews, requestedMediaTypes, attrs);
        //返回最好的视图
        if (bestView != null) {
            return bestView;
        }
    }

    String mediaTypeInfo = logger.isDebugEnabled() && requestedMediaTypes != null ?
            " given " + requestedMediaTypes.toString() : "";

    if (this.useNotAcceptableStatusCode) {
        if (logger.isDebugEnabled()) {
            logger.debug("Using 406 NOT_ACCEPTABLE" + mediaTypeInfo);
        }
        return NOT_ACCEPTABLE_VIEW;
    }
    else {
        logger.debug("View remains unresolved" + mediaTypeInfo);
        return null;
    }
}

分析源码可知,它是从容器中加载所有的视图解析器,那么我们可以猜想,我们自定义一个视图解析器,也可以被扫描并加载。
编写自定义视图解析器。

//自己写一个视图解析器
@Bean
public ViewResolver myViewResolver(){
    return new MyViewResolver();
}

private static class MyViewResolver implements ViewResolver{

    @Override
    public View resolveViewName(String s, Locale locale) throws Exception {
        return null;
    }
}

在DispatcherServlet类下的doDispatch方法的第一行代码打上断点,启动项目以debug模式运行,当程序运行到断点处,可以看DispatcherServlet对象的viewResolvers数组中已经包含了我们的自定义视图解析器。


自定义视图解析器

由此我们可以得出一个结论:在SpringBoot中,如果我们想使用自己定制化的组件,只需要往容器中添加这个组件即可。因为剩下的事情SpringBoot已经自动帮我们处理了。

格式化转换器Formatter

Formatter用于对日期类型进行格式化操作,在SpringBoot中是作为服务注册的。注册Formatter的源码如下:

@Bean 
@Override // 服务
public FormattingConversionService mvcConversionService() {
    // 默认的时间 Formatting 的格式:
    WebConversionService conversionService = new WebConversionService(this.mvcProperties.getDateFormat());
    addFormatters(conversionService);
    return conversionService;
}
// 源码中默认的格式是通过 /来分割的
//Date format to use. For instance, `dd/MM/yyyy`. 
private String dateFormat; 
// 只要在 mvcProperties 中的,我们都可以进行手动的配置!

修改SpringBoot默认配置

官方文档中有如下一段描述:

// 如果你希望保持 Spring Boot MVC 一些功能,并且希望添加一些其他的
//MVC配置(拦截器、格式化 器、视图控制器、或其他的配置),你可以
//添加自己的配置类 (类型为WebMvcConfigurer) 需要添加注解
//@Configuration ,一定不能拥有注解@EnableWebMvc.
If you want to keep those Spring Boot MVC customizations and make more MVC 
customizations (interceptors, formatters, view controllers, and other features), 
you can add your own @Configuration class of type WebMvcConfigurer but without @EnableWebMvc. 
If you want to provide custom instances of RequestMappingHandlerMapping, RequestMappingHandlerAdapter, 
or ExceptionHandlerExceptionResolver, and still keep the Spring Boot MVC customizations, you can declare 
a bean of type WebMvcRegistrations and use it to provide custom instances of those components. 
//全面接管Spring MVC,自己配置配置类的时候加上 @EnableWebMvc即可! 
If you want to take complete control of Spring MVC, you can add your own @Configuration annotated with 
@EnableWebMvc, or alternatively add your own @Configuration-annotated DelegatingWebMvcConfiguration as described 
in the Javadoc of @EnableWebMvc.

扩展MVC方法步骤

1、编写一个自己的config配置类。
2、实现WebMvcConfigurer接口。
3、重写该接口下的方法即可。


WebMvcConfigurer接口重写方法

@Configuration
//@EnableWebMvc 不要加这个注解,会使WebMvcAutoConfiguration配置失效
public class MyMvcConfig implements WebMvcConfigurer {

    //自己编写一个视图解析路由
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        //视图跳转的控制
        registry.addViewController("/test").setViewName("test");
    }
}

注意:@EnableWebMvc这个注解不能加,会使WebMvcAutoConfiguration配置失效。因为在WebMvcAutoConfiguration类中有如下注解:

// 如果这个bean不存在,这个类才生效!~ 
@ConditionalOnMissingBean(WebMvcConfigurationSupport.class) 
// @EnableWebMvc 源码 
@Import(DelegatingWebMvcConfiguration.class) public @interface EnableWebMvc 
// 点进DelegatingWebMvcConfiguration继承了WebMvcConfigurationSupport 
public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport

实际上@EnableWebMvc注解就是导入了WebMvcConfigurationSupport类但是源码中有@ConditionalOnMissingBean这个注解判断是否导入了这个类,一旦导入了这个类,我们的自动配置类就会全部失效。

建议

如果你要扩展MVC配置,但是又需要保留原来的配置,可以直接实现WebMvcConfigurer接口,重写方法进行扩展即可,但是不要添加@EnableWebMvc注解。

@Configuration 
public class MyMvcConfig implements WebMvcConfigurer { }

全面接管Spring MVC

只需要增加一个@EnableWebMvc注解即可,此时WebMvcAutoConfiguration只提供基本的MVC功能,其余功能需要自己开发编写。在SpringBoot中有非常多的这种扩展配置,我们只要看见了这种配置,就应该多留心注意。

配置项目环境及首页

准备工作

1、将我们学习MyBatis的文件代码拷贝过来。
2、导入maven依赖。
3、导入实体类。
4、导入mapper及mapper.xml文件。
5、配置资源问题、druid配置。
6、导入前端素材。


前端资源目录

首页实现

1、添加视图控制。

@Override
public void addViewControllers(ViewControllerRegistry registry) {
    //视图跳转的控制
    registry.addViewController("/index").setViewName("login");
    registry.addViewController("/").setViewName("login");
    registry.addViewController("/index.html").setViewName("login");
}

2、添加控制器。

@Controller
public class IndexController {

    @GetMapping({"/","/index.html","index"})
    public String index(){
        return "login";
    }
}

修改前端页面链接

1、配置页面支持 Thymeleaf。
2、正式开发项目之前,需要将所有页面的中链接的路径改为Thymeleaf的路径配置。所有页面的链接改为@{}进行支持适配。

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

推荐阅读更多精彩内容