SpringCloud 入门

SpringCloud入门

  • SpringCloud系统架构解析
  • SpringCloud全方位介绍
  • SpringCloud与Dubbo的技术选型
  • 对于SpringCloud版本号作业处理
  • 简单项目实现

知识转载出处:https://blog.csdn.net/qq_24313635/article/details/84347832
https://mp.weixin.qq.com/s/N507Cfb_mbkGvHtg_FIaVg

项目转载出处:http://blog.itpub.net/31379315/viewspace-2648511/

SpringCloud系统架构解析

1. Spring Cloud整体核心架构只有一点:Rest服务

7张图了解 Spring Cloud 的整体构架

在整个Spring Cloud配置过程之中,所有的配置处理都是围绕着Rest完成的,在这个Rest处理之中,一定要有两个端:服务的提供者(Provider)、服务的消费者(Consumer)

  • 为了可以及时的告诉用户哪些服务不可用,所以应该准备一个分布式的注册中心,Eureka的注册中心
  • 对于整个的WEB端的构架(SpringBoot实现)可以轻松方便的进行WEB程序的编写,而后利用Nginx或Apache实现负载均衡处理,但是你WEB端出现了负载均衡,那么业务端呢?应该也提供有多个业务端进行负载均衡,Feign 伪造接口实现负载均衡
  • 熔断处理机制,在一些设备出现了故障之后依然可以保护其他服务可以正常使用,Hystrix 熔断处理机制
  • 访问服务器要访问那么多端口吗?所有的微服务通过 zuul 进行代理之后也更加合理的进行名称隐藏,Zuul 的代理机制
  • 突然有一天你的主机要进行机房的变更,所有的服务的IP地址都可能发生改变?Spring Cloud Config
2. SpringCloud 组件详细介绍
  • Eureka : 微服务架构中的注册中心,专门负责服务的注册与发现
    各个服务启动时,Eureka Client都会将服务注册到Eureka Server,并且Eureka Client还可以反过来从Eureka Server拉取注册表,从而知道其他服务在哪里

  • Ribbon : 负载均衡默认使用的最经典的Round Robin轮询算法
    服务间发起请求的时候,基于Ribbon做负载均衡,从一个服务的多台机器中选择一台

Round Robin轮询算法: 如果订单服务对库存服务发起10次请求,那就先让你请求第1台机器、然后是第2台机器、第3台机器、第4台机器、第5台机器,接着再来—个循环,第1台机器、第2台机器。。。以此类推

  • 首先Ribbon会从 Eureka Client里获取到对应的服务注册表,也就知道了所有的服务都部署在了哪些机器上,在监听哪些端口号。
  • 然后Ribbon就可以使用默认的Round Robin算法,从中选择一台机器
  • Feign就会针对这台机器,构造并发起请求。
  • Feign : 关键机制就是使用了动态代理
    基于Feign的动态代理机制,根据注解和选择的机器,拼接请求URL地址,发起请求
  • 首先,如果你对某个接口定义了@FeignClient注解,Feign就会针对这个接口创建一个动态代理
  • 接着你要是调用那个接口,本质就是会调用 Feign创建的动态代理,这是核心中的核心
    Feign的动态代理会根据你在接口上的@RequestMapping等注解,来动态构造出你要请求的服务的地址
  • 最后针对这个地址,发起请求、解析响应
  • Hystrix : 是隔离、熔断以及降级的一个框架
    发起请求是通过Hystrix的线程池来走的,不同的服务走不同的线程池,实现了不同服务调用的隔离,避免了服务雪崩的问题

如果积分服务都挂了,每次调用都要去卡住几秒钟干啥呢?有意义吗?当然没有!所以我们直接对积分服务熔断不就得了,比如在5分钟内请求积分服务直接就返回了,不要去走网络请求卡住几秒钟,这个过程,就是所谓的熔断!
那人家又说,兄弟,积分服务挂了你就熔断,好歹你干点儿什么啊!别啥都不干就直接返回啊?没问题,咱们就来个降级:每次调用积分服务,你就在数据库里记录一条消息,说给某某用户增加了多少积分,因为积分服务挂了,导致没增加成功!这样等积分服务恢复了,你可以根据这些记录手工加一下积分。这个过程,就是所谓的降级。

  • Zuul : 微服务网关。这个组件是负责网络路由的
    如果前端、移动端要调用后端系统,统一从Zuul网关进入,由Zuul网关转发请求给对应的服务

一般微服务架构中都必然会设计一个网关在里面,像android、ios、pc前端、微信小程序、H5等等,不用去关心后端有几百个服务,就知道有一个网关,所有请求都往网关走,网关会根据请求中的一些特征,将请求转发给后端的各个服务。
有一个网关之后,还有很多好处,比如可以做统一的降级、限流、认证授权、安全,等等。

SpringCloud全方位介绍

服务化的核心就是将传统的一站式应用根据业务拆分成一个一个的服务,而微服务在这个基础上要更彻底地去耦合(不再共享DB、KV,去掉重量级ESB),并且强调DevOps和快速演化。

1、为什么微服务架构需要Spring Cloud

1.1 从使用nginx说起

最初的服务化解决方案是给提供相同服务提供一个统一的域名,然后服务调用者向这个域名发送HTTP请求,由Nginx负责请求的分发和跳转。


image.png

这种架构存在很多问题:

  • Nginx作为中间层,在配置文件中耦合了服务调用的逻辑,这削弱了微服务的完整性,也使得Nginx在一定程度上变成了一个重量级的ESB。
  • 服务的信息分散在各个系统,无法统一管理和维护。每一次的服务调用都是一次尝试,服务消费者并不知道有哪些实例在给他们提供服务。这不符合DevOps的理念。
  • 无法直观的看到服务提供者和服务消费者当前的运行状况和通信频率。这也不符合DevOps的理念。
  • 消费者的失败重发,负载均衡等都没有统一策略,这加大了开发每个服务的难度,不利于快速演化。

为了解决上面的问题,我们需要一个现成的中心组件对服务进行整合,将每个服务的信息汇总,包括服务的组件名称、地址、数量等。服务的调用方在请求某项服务时首先通过中心组件获取提供这项服务的实例的信息(IP、端口等),再通过默认或自定义的策略选择该服务的某一提供者直接进行访问。所以,我们引入了Dubbo。

1.2 基于Dubbo实现微服务

Dubbo是阿里开源的一个SOA服务治理解决方案,文档丰富,在国内的使用度非常高。
使用Dubbo构建的微服务,已经可以比较好地解决上面提到的问题:

image
  • 调用中间层变成了可选组件,消费者可以直接访问服务提供者。
  • 服务信息被集中到Registry中,形成了服务治理的中心组件。
  • 通过Monitor监控系统,可以直观地展示服务调用的统计信息。
  • Consumer可以进行负载均衡、服务降级的选择。

但是对于微服务架构而言,Dubbo也并不是十全十美的:

  • Registry 严重依赖第三方组件(zookeeper或者redis),当这些组件出现问题时,服务调用很快就会中断。
  • Dubbo 只支持RPC调用。使得服务提供方与调用方在代码上产生了强依赖,服务提供者需要不断将包含公共代码的jar包打包出来供消费者使用。一旦打包出现问题,就会导致服务调用出错。
  • 最为重要的是,Dubbo 现在已经重新维护了,对于技术发展的新需求,需要由开发者自行拓展升级。这对于很多想要采用微服务架构的中小软件组织,显然是相当合适的。

目前Github社区上有一个 Dubbo 的升级版,叫 DubboX,提供了更高效的RPC序列化方式和REST调用方式。但是该项目也基本停止维护了。

1.3 新的选择——Spring Cloud

作为新一代的服务框架,Spring Cloud提出的口号是开发“面向云环境的应用程序”,它为微服务架构提供了更加全面的技术支持。点击这里查看Spring系列教程集合。

结合我们一开始提到的微服务的诉求,我们把Spring Cloud与Dubbo 进行一番对比:

微服务需要的功能 Dubbo Spring Cloud
服务注册和发现 Zookeeper Eureka
服务调用方式 RPC RESTful API
断路器
负载均衡
服务路由和过滤
分布式配置
分布式锁 计划开发
集群选主
分布式消息

Spring Cloud抛弃了Dubbo的RPC通信,采用的是基于HTTP的REST方式。严格来说,这两种方式各有优劣。虽然从一定程度上来说,后者牺牲了服务调用的性能,但也避免了上面提到的原生RPC带来的问题。而且REST相比RPC更为灵活,服务提供方和调用方的依赖只依靠一纸契约,不存在代码级别的强依赖,这在强调快速演化的微服务环境下,显得更加合适。


Eureka相比于zookeeper,更加适合于服务发现的场景,这点会在下一篇会详细展开。

很明显,Spring Cloud的功能比Dubbo更加强大,涵盖面更广,而且作为Spring的拳头项目,它也能够与Spring Framework、Spring Boot、Spring Data、Spring Batch等其他Spring项目完美融合,这些对于微服务而言是至关重要的。前面提到,微服务背后一个重要的理念就是持续集成、快速交付,而在服务内部使用一个统一的技术框架,显然比把分散的技术组合到一起更有效率。更重要的是,相比于Dubbo,它是一个正在持续维护的、社区更加火热的开源项目,这就保证使用它构建的系统,可以持续地得到开源力量的支持。

2、Spring Cloud技术概览

下图展示了Spring Cloud的完整技术组成:

image

服务治理:这是Spring Cloud的核心。目前Spring Cloud主要通过整合Netflix的相关产品来实现这方面的功能(Spring Cloud Netflix),包括用于服务注册和发现的Eureka,调用断路器Hystrix,调用端负载均衡Ribbon,Rest客户端Feign,智能服务路由Zuul,用于监控数据收集和展示的Spectator、Servo、Atlas,用于配置读取的Archaius和提供Controller层Reactive封装的RxJava。除此之外,针对

Feign和RxJava并不是Netiflix的产品,但是被整合到了Spring Cloud Netflix中。


对于服务的注册和发现,除了Eureka,Spring Cloud也整合了Consul和Zookeeper作为备选,但是因为这两个方案在CAP理论上都遵循CP而不是AP(下一篇会详细介绍这点),所以官方并没有推荐使用。

分布式链路监控:Spring Cloud Sleuth提供了全自动、可配置的数据埋点,以收集微服务调用链路上的性能数据,并发送给Zipkin进行存储、统计和展示。
消息组件:Spring Cloud Stream对于分布式消息的各种需求进行了抽象,包括发布订阅、分组消费、消息分片等功能,实现了微服务之间的异步通信。Spring Cloud Stream也集成了第三方的 RabbitMQ 和 Apache Kafka 作为消息队列的实现。而Spring Cloud Bus基于Spring Cloud Stream,主要提供了服务间的事件通信(比如刷新配置)。
配置中心:基于Spring Cloud Netflix和Spring Cloud Bus,Spring又提供了Spring Cloud Config,实现了配置集中管理、动态刷新的配置中心概念。配置通过Git或者简单文件来存储,支持加解密。
安全控制:Spring Cloud Security基于OAUTH2这个开放网络的安全标准,提供了微服务环境下的单点登录、资源授权、令牌管理等功能。
命令行工具:Spring Cloud Cli提供了以命令行和脚本的方式来管理微服务及Spring Cloud组件的方式。
集群工具:Spring Cloud Cluster提供了集群选主、分布式锁(暂未实现)、一次性令牌(暂未实现)等分布式集群需要的技术组件。

SpringCloud与Dubbo的技术选型

1. 架构完整度

如果将spring cloud比作品牌机,那dubbo就是组装机。spring cloud到现在为止,已经提供了服务注册中心,服务治理等24个模块,并且还在增加中。

2. 社区活跃度

spring cloud 社区活跃度很高

3. 通讯协议

dubbo服务间通讯采用rpc,而spring cloud采用的时http的rest。相比rpc,rest更加轻量化,服务调用者和提供者间的依赖仅仅是一纸契约,一段文本,不存在代码层面的强依赖,服务提供者和调用者之间还可以通过不同的语言来实现,只需要提供rest接口就可以通讯。

4. 技术改造和微服务开发

国内的开发团队选择dubbo的一个很重要的原因就是官方文档,dubbo提供了高质量的官方文档,而spring cloud都是英文版的,并且文档内容要比dubbo多的多,文档内容更偏向模块整合,要对每个模块的进行更深入的了解,需要查看更为详细的文档

对于SpringCloud版本号作业处理

1. 命名规则

Spring Cloud 采用了英国伦敦地铁站的名称来命名,并由地铁站名称字母A-Z依次类推的形式来发布迭代版本。Spring Cloud 的第一个版本是 "Angel" ,接着 "Brixton" 就是第二个版本。当一个项目到达发布临界点或者解决了一个严重的BUG后就会发布一个 "service Release" 版本, 简称 SR(X)版本,x 代表一个递增数字。

2. Spring Cloud & Spring Boot 依赖关系
  • Spring Cloud Greenwich.SR4 是基于 Spring Boot 2.1.10.RELEASE 构建的
    Spring Cloud Openfeign 的版本升级到了 OpenFeign 10.4.0
    Spring Cloud Vault 的依赖和文档变更和更新
    Spring Cloud Gateway 增加了对 Spring Cloud LoadBalancer 的支持
  • Spring Cloud Edgware SR4 => Spring Cloud Finchley.RELEASE
    Finchley 是基于 Spring Boot 2.0.x 构建的,不支持 Spring Boot 1.5.x
    Dalston 和 Edgware 是基于 Spring Boot 1.5.x 构建的,不支持 Spring Boot 2.0.x
    Camden 构建于 Spring Boot 1.4.x,但依然能支持 Spring Boot 1.5.x
3. 版本怎么选择

Spring Cloud 项目简单配置

new project -> new module -> Spring Assistant ->
项目架构图

1. 发现服务端配置

discovery-service: Cloud Discovery -> Eureka Discovery
服务注册需要30 s 的时间才能显示在 Eureka 服务中,因为 Eureka 需要从服务接收3次连续心跳包 ping,每次心跳包 ping 间隔10 s,然后才能使用这个服务。

server:
  port: 8761
# Eureka 服务器将要监听的端口
eureka:
  client:
    registerWithEureka: false #不要使用 Eureka 服务进行注册
    fetchRegistry: false #不要在本地缓存注册表信息

使用一个新的注解 @EnableEurekaServer ,就可以让我们的服务成为一个 Eureka 服务

2. 服务发现客户端配置

config-service: Cloud Config ->Config Server
以config-service为例
需要做2件事情

  1. 成为服务发现的客户端
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> 
</dependency>
  1. 配置application.yml(对应config-server来说我们只需要配置如下)
    http://localhost:8761/eureka/apps/config-service
spring:
  cloud:
    config:
      discovery:
        enabled: true
## 应用作为服务发现的客户端设置
// http://localhost:8761/eureka/apps/business_service
spring:
  application:
    name: business_service
eureka:
  instance:
    preferIpAddress: true
    #注册服务的 IP,而不是服务器名称
  client:
    registerWithEureka: true #向 Eureka 注册服务
    fetchRegistry: true
    serviceUrl:  #拉取注册表的本地副本
      defaultZone: http://localhost:8761/eureka/  #Eureka 服务的位置

zuul-rout : Cloud Routing -> Zuul

3. 用户认证中心

oAuth-core: Cloud Core -> Cloud OAth2

  • OAuth2协议说明:

整体OAuth协议包括两方面:
1、 访问授权:用户必须通过授权获取令牌
2、 资源权限:通过授权的用户访问受保护的资源,根据定义访问权限来决定是否可以访问资源

  • 配置说明:
    启用OAuth授权服务
    增加 @EnableAuthorizationServer 用于告诉 Spring Cloud,该服务将作为 OAuth2 服务
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
@SpringBootApplication
@EnableResourceServer
@EnableAuthorizationServer
public class DemoApplication {
   public static void main(String[] args) {
      SpringApplication.run(DemoApplication.class, args);
   }
}

OAuth访问授权配置,配置注册的客户端应用程序

@Configuration
public class Auth2Config extends AuthorizationServerConfigurerAdapter {
    @Autowired
    private AuthenticationManager authenticationManager;
    @Autowired
    private UserDetailsService userDetailsService;
//    覆盖 configure()方法。这定义了哪些客户端将注册到服务
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory()
                .withClient("yuaoq")
                .secret("{noop}secret")
                .authorizedGrantTypes(
                         "refresh_token",
                         "password",
                          "client_credentials")
                .scopes("webclient","mobileclient");
    }
//    该方法定义了 AuthenticationServerConfigurer 中使用的不同组件。这段代码告诉 Spring 使用 Spring 提供的默认验证管理器和用户详细信息服务
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints)
     throws Exception {
        endpoints
                .authenticationManager(authenticationManager)
                .userDetailsService(userDetailsService);
    }
}

配置用户权限

@Configuration
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
    @Override
    @Bean
//    AuthenticationManagerBean 被 Spring Security 用来处理验证
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
    //      Security 使用 UserDetailsService 处理返回的用户信息,这些用户信息将由 Spring Security 返回
    @Override
    @Bean
    public UserDetailsService userDetailsServiceBean() throws Exception {
        return super.userDetailsServiceBean();
    }
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//             configure()方法是定义用户、密码和角色的地方
        auth.inMemoryAuthentication()
                .withUser("admin")
                .password("{noop}password")
                .roles("ADMIN","USER")
                .and()
                .withUser("anyone")
                .password("{noop}password")
                .roles("USER");
    }
}

获取用户信息(提供给其他服务获取用户信息使用)

@GetMapping(value = "/auth/user")
public Map<String, Object> user(OAuth2Authentication user) {
    Map<String, Object> userInfo = new HashMap<>();
    userInfo.put("user", user.getUserAuthentication().getPrincipal());
    userInfo.put("authorities", AuthorityUtils.authorityListToSet(user.getUserAuthentication().getAuthorities()));
    return userInfo;
}

通过postman测试
image.png
4. 业务模块微服务

1、 对外提供restful Api
@RestController :由spring web提供的居于restful 的接口标签生成一个restful api

@PostMapping("/list")
public ResponseEntity<List<String>> getBusiness() throws Exception {
    List<String> list = new ArrayList<String>();
    list.add("a");
    list.add("b");
   return Optional.of(list)
            .map(a -> new ResponseEntity<List<String>>(a, HttpStatus.OK))
            .orElseThrow(() -> new Exception("error"));
}

使用postman调用接口

在线制图 springCloud设计

从postman返回的结果可以看到401,未授权。
因为business_service服务引入了spring-cloud-starter-security 那么默认是会对所有访问做安全控制。

2、 服务的授权保护
现在business/list 是未授权,那怎么配置一个受保护的oauth2.0资源,通过如下步骤
设置服务是一个受oauth保护的资源


在线制图 springCloud设计

定义应用的OAuth属性定义回调 URL

security:
  oauth2:
    resource:
      user-info-uri: http://localhost:8282/auth/user

定义授权用户可以访问

@Configuration
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
    //     antMatchers()允许开发人员限制对受保护的 URL 和 HTTP DELETE 动词的调用
//     hasRole()方法是一个允许访问的角色列表,该列表由逗号分隔
    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers(HttpMethod.POST, "/api/v1/business/**")
                .hasRole("ADMIN")
                .anyRequest()
                .authenticated();
    }  
}

该段代码说明具有ADMIN角色的用户可以访问/api/v1/business/ 下的所有的POST 请求
验证如下:


在线制图 springCloud设计

至此通过OAuth2.0保护微服务的基本做法已经完成。

5. . 服务路由网关

服务网关:服务客户端不再直接调用服务。取而代之的是,服务网关作为单个策略执行点(Policy Enforcement Point,PEP),所有调用都通过服务网关进行路由,然后被路由到最终目的地。
@EnabeZuulServer 使用此注解将创建一个 Zuul 服务器,它不会加载任何 Zuul 反向代理过滤器,也不会使用 Netflix Eureka 进行服务发现.

成为一个服务网关步骤:
1、 添加 @EnableZuulProxy

在线制图 springCloud设计

2、 在application.yml添加route 规则

zuul:
  sensitive-headers: set-cookies
  routes:
    business_service: /busi/**

通过postman测试如下:

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