SpringBoot(六)外部化配置 - @ConfigurationProperties

3、外部化配置的核心

        接着上一章,《Spring Boot 外部化配置(一)》

3.2 @ConfigurationProperties

众所周知,当 Spring Boot 集成外部组件后,就可在 propertiesYAML 配置文件中定义组件需要的属性,如 Redis 组件:

spring.redis.url=redis://user:password@example.com:6379
spring.redis.host=localhost
spring.redis.password=123456
spring.redis.port=6379

其中都是以 spring.redis 为前缀。这其实是 Spring Boot 为每个组件提供了对应的 Properties 配置类,并将配置文件中的属性值給映射到配置类中,而且它们有个特点,都是以 Properties 结尾,如 Redis 对应的配置类是 RedisProperties

@ConfigurationProperties(prefix = "spring.redis")
public class RedisProperties {
    private String url;

    private String host = "localhost";

    private String password;

    private int port = 6379;
    
    ...
}

其中有个名为 @ConfigurationProperties 的注解,它的 prefix 参数就是约定好的前缀。该注解的功能就是将配置文件中的属性和 Properties 配置类中的属性进行映射,来达到自动配置的目的。这个过程分为两步,第一步是注册 Properties 配置类,第二步是绑定配置属性,过程中还涉及到一个注解,它就是 @EnableConfigurationProperties ,该注解是用来触发那两步操作的。我们以 Redis 为例来看它使用方式:

...
@EnableConfigurationProperties(RedisProperties.class)
public class RedisAutoConfiguration {
    ...
}

可以看到它的参数是 RedisProperties 配置类。通过前面的 《Spring Boot 自动装配(一)》 我们知道,该注解是属于 @Enable 模块注解,所以,该注解中必然有 @Import 导入的实现了 ImportSelectorImportBeanDefinitionRegistrar 接口的类,具体的功能都由导入的类来实现。我们进入该注解:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(EnableConfigurationPropertiesImportSelector.class)
public @interface EnableConfigurationProperties {

    /**
     * Convenient way to quickly register {@link ConfigurationProperties} annotated beans
     * with Spring. Standard Spring Beans will also be scanned regardless of this value.
     * @return {@link ConfigurationProperties} annotated beans to register
     */
    Class<?>[] value() default {};

}

果不其然,通过 @Import 导入了 EnableConfigurationPropertiesImportSelector 类,整个的处理流程都是在该类中进行处理:

class EnableConfigurationPropertiesImportSelector implements ImportSelector {

    private static final String[] IMPORTS = { ConfigurationPropertiesBeanRegistrar.class.getName(),
            ConfigurationPropertiesBindingPostProcessorRegistrar.class.getName() };

    @Override
    public String[] selectImports(AnnotationMetadata metadata) {
        return IMPORTS;
    }

    ...
}

该类实现了 ImportSelector 接口,并重写了 selectImports 方法,该方法返回的类会被 Spring 加载。可以看到这里返回了两个类,其中 ConfigurationPropertiesBeanRegistrar 就是用来注册 Properties 配置类的,而 ConfigurationPropertiesBindingPostProcessorRegistrar 则是用来绑定配置属性,且它们都实现了 ImportBeanDefinitionRegistrar 接口,会在重写的 registerBeanDefinitions 方法中进行直接注册 Bean 的操作。以上特性都在 《Spring Boot 自动装配(一)》的 3.1 小节介绍过,这里不在叙述。接下来,我们分别介绍这两个类。

3.2.1 注册 Properties 配置类

我们先来看看 ConfigurationPropertiesBeanRegistrar 是如何注册这些配置类的。我们直接进入该类的实现:

public static class ConfigurationPropertiesBeanRegistrar implements ImportBeanDefinitionRegistrar {

        // 1、第一步会先执行重写的 registerBeanDefinitions 方法,
        // 入参分别是 AnnotationMetadata 和 BeanDefinitionRegistry。
        // AnnotationMetadata 是获取类的元数据的,如注解信息、 classLoader 等,
        // BeanDefinitionRegistry 则是直接注册所需要的 Bean 
        public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
            
            // 2、调用 getTypes 方法,返回 Properties 配置类集合。进入 2.1 详细查看
            // 3、调用 register 方法,把 Properties 配置类注册到 Spring 容器中。进入 3.1 详细查看
            getTypes(metadata).forEach((type) -> register(registry, (ConfigurableListableBeanFactory) registry, type));
        }

        // 2.1 
        private List<Class<?>> getTypes(AnnotationMetadata metadata) {
            
            // 获取指定注解的所有属性值,key是属性名称,Value是值
            MultiValueMap<String, Object> attributes = metadata
                    .getAllAnnotationAttributes(EnableConfigurationProperties.class.getName(), false);
            
            // 返回 key 名称为 value 的值,这里返回的就是 Properties 配置类
            return collectClasses((attributes != null) ? attributes.get("value") : Collections.emptyList());
        }

        // 3.1
        private void register(BeanDefinitionRegistry registry, ConfigurableListableBeanFactory beanFactory,
                Class<?> type) {
            // getName 返回的是 Bean 的名称。进入 3.2 详细查看
            String name = getName(type);
            
            // 判断有没有注册过这个 Bean
            if (!containsBeanDefinition(beanFactory, name)) {
            
                // 没有则注册该 Bean。入参是注册器、Bean 的名称、需注册的 Bean。进入 4 详细查看
                registerBeanDefinition(registry, name, type);
            }
        }

        // 3.2
        private String getName(Class<?> type) {
            
            // 获取 Properties 配置类上标注的 ConfigurationProperties 注解信息
            ConfigurationProperties annotation = AnnotationUtils.findAnnotation(type, ConfigurationProperties.class);
            
            // 获取该注解中 prefix 的属性值
            String prefix = (annotation != null) ? annotation.prefix() : "";
            
            // 最后返回的是名称格式是 属性前缀-配置类全路径名,如:
            // spring.redis-org.springframework.boot.autoconfigure.data.redis.RedisProperties
            return (StringUtils.hasText(prefix) ? prefix + "-" + type.getName() : type.getName());
        }

        // 4、
        private void registerBeanDefinition(BeanDefinitionRegistry registry, String name, Class<?> type) {
            assertHasAnnotation(type);
            GenericBeanDefinition definition = new GenericBeanDefinition();
            definition.setBeanClass(type);
            
            // 通过 registerBeanDefinition 方法,注册 Bean 。
            // 后期会有 Spring 系列的文章详细介绍该过程,到时候大家再一起讨论。
            registry.registerBeanDefinition(name, definition);
        }
    }

执行完后,我们所有的 Properties 配置类就被注册到了 Spring 容器中。接下来,我们来看看配置文件中的数据是如何与 Properties 配置类中的属性进行绑定的。

3.2.2 绑定配置属性

我们直接进入 ConfigurationPropertiesBindingPostProcessorRegistrar 类中进行查看:

public class ConfigurationPropertiesBindingPostProcessorRegistrar implements ImportBeanDefinitionRegistrar {

    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
        if (!registry.containsBeanDefinition(ConfigurationPropertiesBindingPostProcessor.BEAN_NAME)) {
            registerConfigurationPropertiesBindingPostProcessor(registry);
            registerConfigurationBeanFactoryMetadata(registry);
        }
    }

    ...
}

这里也是在重写的 registerBeanDefinitions 方法中注册了两个 Bean,一个是 ConfigurationBeanFactoryMetadata,这个是用来存储元数据的,我们不做过多关注;另一个是 ConfigurationPropertiesBindingPostProcessor ,该类就是用来绑定属性的,我们主要对该类进行讨论:

public class ConfigurationPropertiesBindingPostProcessor
        implements BeanPostProcessor, PriorityOrdered, ApplicationContextAware, InitializingBean {

    ...
}

可以看到,该类实现了几个接口,且都是 Spring 提供的扩展接口。这里我们简要介绍一下:

1、BeanPostProcessor:这是 Bean 的后置处理器。该类有两个方法,一个是 postProcessBeforeInitialization ,Bean 初始化前该方法会被调用;
另一个是 postProcessAfterInitialization ,Bean 初始化后该方法会被调用;需注意的是,Spring 上下文中所有 Bean 的初始化都会触发这两个方法。

2、ApplicationContextAware:这是 SpringAware系列接口之一。该类有一个 setApplicationContext 方法,主要是用来获取 ApplicationContext 上下文对象;同理,如果是其它前缀的 Aware,则获取相应前缀名的对象。

3、InitializingBean:这是 Bean 的生命周期相关接口。该类有一个 afterPropertiesSet 方法,当 Bean 的所有属性初始化后,该方法会被调用。

其中, BeanPostProcessorInitializingBean 的功能都是在 Bean 的生命周期中执行额外的操作。

这里我们简单的了解就行,后面会在 Spring 系列的文章中详细讨论。

接着,我们介绍该类中的方法:

public class ConfigurationPropertiesBindingPostProcessor
        implements BeanPostProcessor, PriorityOrdered, ApplicationContextAware, InitializingBean {

    ...

    public static final String VALIDATOR_BEAN_NAME = "configurationPropertiesValidator";

    private ConfigurationBeanFactoryMetadata beanFactoryMetadata;

    private ApplicationContext applicationContext;

    private ConfigurationPropertiesBinder configurationPropertiesBinder;

    // 1、这是重写的 ApplicationContextAware 接口中的方法,用来获取 ApplicationContext 上下文对象
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }

    // 2、这是重写的 InitializingBean 接口中的方法,当 Bean 的属性初始化后会被调用。
    // 该方法主要对 ConfigurationBeanFactoryMetadata 和 ConfigurationPropertiesBinder 进行实例化
    @Override
    public void afterPropertiesSet() throws Exception {
        this.beanFactoryMetadata = this.applicationContext.getBean(ConfigurationBeanFactoryMetadata.BEAN_NAME,
                ConfigurationBeanFactoryMetadata.class);
        this.configurationPropertiesBinder = new ConfigurationPropertiesBinder(this.applicationContext,
                VALIDATOR_BEAN_NAME);
    }

    // 3、这是重写的 BeanPostProcessor 接口中的方法,在 Bean 初始化前会被调用,绑定属性的操作就是从这里开始。
    // 入参 bean 就是待初始化的 Bean,beanName 就是 Bean 的名称
    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        ConfigurationProperties annotation = getAnnotation(bean, beanName, ConfigurationProperties.class);
        if (annotation != null) {
            bind(bean, beanName, annotation);
        }
        return bean;
    }

    ...
}

我们先来看第二步的 afterPropertiesSet 方法,该方法中实例化了两个类,一个是从 ApplicationContext 中获取的 ConfigurationBeanFactoryMetadata 类,是用来操作元数据的,不做过多关注;另一个是通过带参构造器初始化的 ConfigurationPropertiesBinder 类,参数是 ApplicationContext 对象和 configurationPropertiesValidator 字符串。我们进入该类的构造器中:

class ConfigurationPropertiesBinder {

    private final ApplicationContext applicationContext;

    private final PropertySources propertySources;

    private final Validator configurationPropertiesValidator;

    private final boolean jsr303Present;

    ...

    ConfigurationPropertiesBinder(ApplicationContext applicationContext, String validatorBeanName) {
        this.applicationContext = applicationContext;
        this.propertySources = new PropertySourcesDeducer(applicationContext).getPropertySources();
        this.configurationPropertiesValidator = getConfigurationPropertiesValidator(applicationContext,
                validatorBeanName);
        this.jsr303Present = ConfigurationPropertiesJsr303Validator.isJsr303Present(applicationContext);
    }
    ...
}

该类中又实例化了四个类,我们重点关注 PropertySources 的实例化过程,具体是通过 PropertySourcesDeducer 类的 getPropertySources 方法,我们进入该类:

class PropertySourcesDeducer {
    
    ...
    private final ApplicationContext applicationContext;

    PropertySourcesDeducer(ApplicationContext applicationContext) {
        this.applicationContext = applicationContext;
    }

    // 1、通过 extractEnvironmentPropertySources 方法,返回 MutablePropertySources 对象,
    // MutablePropertySources 是 PropertySources 的实现类
    public PropertySources getPropertySources() {
        
        ...
        
        MutablePropertySources sources = extractEnvironmentPropertySources();
        if (sources != null) {
            return sources;
        }
        throw new IllegalStateException(
                "Unable to obtain PropertySources from " + "PropertySourcesPlaceholderConfigurer or Environment");
    }
    
    // 2、调用 Environment 的 getPropertySources 方法,返回 MutablePropertySources
    private MutablePropertySources extractEnvironmentPropertySources() {
        Environment environment = this.applicationContext.getEnvironment();
        if (environment instanceof ConfigurableEnvironment) {
            return ((ConfigurableEnvironment) environment).getPropertySources();
        }
        return null;
    }
    ...
    
}

看到这,大家应该比较熟悉了,Environment 就是我们在 《Spring Boot 外部化配置(一)》中 3.1 小节讲过的应用运行时的环境,通过该类可获取所有的外部化配置数据,而 MutablePropertySources 则是底层真正存储外部化配置对象的。

到这里,第二步的 afterPropertiesSet 方法就执行完了,主要是实例化了 ConfigurationPropertiesBinder 对象,而该对象中存储了所有的外部化配置。接着看第三步的 postProcessBeforeInitialization 方法:

public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
    ConfigurationProperties annotation = getAnnotation(bean, beanName, ConfigurationProperties.class);
    if (annotation != null) {
        bind(bean, beanName, annotation);
    }
    return bean;
}

上面说过,所有 Bean 初始化都会调用这个方法,所以先判断当前 Bean 有没有标注 @ConfigurationProperties 注解,有则表示当前 BeanProperties 配置类,并调用 bind 方法对该类进行绑定属性的操作,我们进入该方法:

private void bind(Object bean, String beanName, ConfigurationProperties annotation) {
    
    ...
    
    try {
        this.configurationPropertiesBinder.bind(target);
    }
    catch (Exception ex) {
        throw new ConfigurationPropertiesBindException(beanName, bean, annotation, ex);
    }
}

这里调用了在第二步实例化的 ConfigurationPropertiesBinder 对象中的 bind 方法:

class ConfigurationPropertiesBinder {
    
    ...
    
    public void bind(Bindable<?> target) {
        
        ...
        
        getBinder().bind(annotation.prefix(), target, bindHandler);
    }
    
    ...
    
    private Binder getBinder() {
        if (this.binder == null) {
            this.binder = new Binder(getConfigurationPropertySources(), getPropertySourcesPlaceholdersResolver(),
                    getConversionService(), getPropertyEditorInitializer());
        }
        return this.binder;
    }
    
    private Iterable<ConfigurationPropertySource> getConfigurationPropertySources() {
        return ConfigurationPropertySources.from(this.propertySources);
    }
    
    ...
}

里面先通过 getBinder() 返回 Binder 对象。在 getBinder 方法中是通过 Binder 带参构造器创建的该对象,我们主要关注 getConfigurationPropertySources 方法返回的第一个参数:

class ConfigurationPropertiesBinder {
    
    ...
    
    private final PropertySources propertySources;
    
    ...
    
    private Iterable<ConfigurationPropertySource> getConfigurationPropertySources() {
        return ConfigurationPropertySources.from(this.propertySources);
    }
    
    ...
}

具体的是通过 ConfigurationPropertySources 中的 from 方法返回,入参 propertySources 是在第二步实例化 ConfigurationPropertiesBinder 对象时初始化好的值,里面存储的是外部化配置的源对象 PropertySource ,我们进入该方法:

public final class ConfigurationPropertySources {
    
    ...

    public static Iterable<ConfigurationPropertySource> from(Iterable<PropertySource<?>> sources) {
        return new SpringConfigurationPropertySources(sources);
    }
    
    ...
}

最终返回的就是 SpringConfigurationPropertySources 配置源对象,在 《Spring Boot 外部化配置(一)》中讲过,该类主要是做一个适配器的工作,将 MutablePropertySources 转换为 ConfigurationPropertySource

之后,该对象传入了 Binder 的构造器中,用于创建该对象:

public class Binder {
    
    ...
    
    private final Iterable<ConfigurationPropertySource> sources;
    
    ...
    
    public Binder(Iterable<ConfigurationPropertySource> sources,
            PlaceholdersResolver placeholdersResolver,
            ConversionService conversionService,
            Consumer<PropertyEditorRegistry> propertyEditorInitializer) {
        
        this.sources = sources;
        
        ...
    }
    
    ...
}

至此, Binder 对象中就存有一份外部化配置的数据,且后续所有的绑定操作都在该类中进行。因后续中间过程实在太过庞杂,且不易理解,这里我们直接进入最后一步,对详细过程感兴趣的同学请自行研究,这里不再赘述。

进入最后阶段的 bind 方法:

// 这里着重介绍一下 BeanProperty 类,该类存储了 properties 配置类中的字段及字段的set、get方法,存储的是反射中的类。
// 如 RedisProperties 中的 url 字段,则 BeanProperty 对象中存储的是
// url 的 Field 类、setUrl 的 Method 类、getUrl 的 Method 类。
private <T> boolean bind(BeanSupplier<T> beanSupplier,
            BeanPropertyBinder propertyBinder, BeanProperty property) {
    
    // 这里获取的是字段名
    String propertyName = property.getName();
    
    // 这里获取的是字段类型
    ResolvableType type = property.getType();
    Supplier<Object> value = property.getValue(beanSupplier);
    Annotation[] annotations = property.getAnnotations();
    
    // 这里获取到了配置文件中的值,该值来源于 SpringConfigurationPropertySources 对象
    Object bound = propertyBinder.bindProperty(propertyName,
            Bindable.of(type).withSuppliedValue(value).withAnnotations(annotations));
    if (bound == null) {
        return false;
    }
    if (property.isSettable()) {
        
        // 最后则是通过 set Method 的 invoke 方法,也就是反射的形式进行赋值。
        property.setValue(beanSupplier, bound);
    }
    else if (value == null || !bound.equals(value.get())) {
        throw new IllegalStateException(
                "No setter found for property: " + property.getName());
    }
    return true;
}

至此,整个绑定配置属性的流程结束。可以看到,最终获取的外部化配置数据来源于前文加载的 Environment 对象。

最后来简单回顾一下 @ConfigurationProperties 注解实现配置文件中属性值和配置类属性映射的过程:

1、首先将 @ConfigurationProperties 标注在 Properties 配置类中,参数是约定好的属性前缀。

2、然后通过 @EnableConfigurationProperties 来触发整个流程,参数是 Properties 配置类。

3、在 @EnableConfigurationProperties 中通过 @import 导入了 EnableConfigurationPropertiesImportSelector 类,该类中又加载了两个类,一个用来注册 Properties 配置类,另一个用来绑定配置属性。

4、最后,是通过反射的方式进行属性绑定,且属性值来源于 Environment

3.1.3 ConfigurationPropertiesAutoConfiguration

其实,当我们使用 @ConfigurationProperties 时,无需标注 @EnableConfigurationProperties 注解,因为 Spring Boot 在自动装配的过程中会帮我们加载一个名为 ConfigurationPropertiesAutoConfiguration 的类,该类是在 spring.factories 中定义好的:

# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.context.ConfigurationPropertiesAutoConfiguration

具体的自动装配过程在 《Spring Boot 自动装配(二)》 这篇文章中讨论过,这里不再赘述。我们来看看 ConfigurationPropertiesAutoConfiguration 实现:

@Configuration
@EnableConfigurationProperties
public class ConfigurationPropertiesAutoConfiguration {

}

很简单,直接通过标注 @EnableConfigurationProperties 注解来开启自动配置的流程。那这样怎么注册 Properties 配置类呢?因为上面说过,Properties 配置类是通过该注解的参数传递进来的。其实,只需在配置类上标注 @Component 注解就行了,之后会被 Spring 扫描到,然后注册。

4、总结

        最后,来对 Spring Boot 外部化配置做一个整体的总结:

1、首先,外部化配置是 Spring Boot 的一个特性,主要是通过外部的配置资源实现与代码的相互配合,来避免硬编码,提供应用数据或行为变化的灵活性。

2、然后介绍了几种外部化配置的资源类型,如 propertiesYAML 配置文件类型,并介绍了获取外部化配置资源的几种方式。

3、其次,介绍了 Environment 类的加载流程,以及所有外部化配置加载到 Environment 中的底层是实现。EnvironmentSpring Boot 外部化配置的核心类,该类存储了所有的外部化配置资源,且其它获取外部化配置资源的方式也都依赖于该类。

4、最后,介绍了 Spring Boot 框架中核心的 @ConfigurationProperties 注解,该注解是将 application 配置文件中的属性值和 Properties 配置类中的属性进行映射,来达到自动配置的目的,并带大家探讨了这一过程的底层实现。

以上就是本章的内容,如过文章中有错误或者需要补充的请及时提出,本人感激不尽。

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

推荐阅读更多精彩内容