Spring原理分析-BeanFactory后處理器

前置文章:
一鹅龄、Spring原理分析-BeanFactory與ApplicationContext
二有巧、Spring原理分析-Bean生命周期
三、Spring原理分析-Bean后處理器

零政供、本文綱要

  • 一、基礎準備
  • 二、ConfigurationClassPostProcessor后處理器
  • 三、MapperScannerConfigurer后處理器
  • 四蜕提、深入ConfigurationClassPostProcessor后處理器-解析@ComponentScan的實現(xiàn)
  • 五、深入ConfigurationClassPostProcessor后處理器-解析@Bean的實現(xiàn)
  • 六靶端、深入Mapper接口掃描添加后處理器

一谎势、基礎準備

0、基礎依賴

<!--base-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter</artifactId>
</dependency>
<!--web-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--test-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>
<!--mysql-->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>
<!--mybatis_plus-->
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.4.3</version>
</dependency>
<!--druid-->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid-spring-boot-starter</artifactId>
    <version>1.2.8</version>
</dependency>

1躲查、待注入類

  • ① com.stone.demo05.Bean1
public class Bean1 {
    private static final Logger log = LoggerFactory.getLogger(Bean1.class);
    public Bean1() {log.debug("Bean1被Spring管理啦!");}
}
  • ② com.stone.demo05.component.Bean2
@Component
public class Bean2 {
    private static final Logger log = LoggerFactory.getLogger(Bean2.class);
    public Bean2(){log.debug("Bean2被Spring容器管理了它浅!");}
}
  • ③ com.stone.demo05.Config
@Configuration
@ComponentScan("com.stone.demo05.component")
public class Config {
    @Bean
    public Bean1 bean1(){return new Bean1();};

    @Bean
    public SqlSessionFactoryBean sqlSessionFactoryBean(DataSource dataSource){
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(dataSource);
        return sqlSessionFactoryBean;
    }

    @Bean(initMethod = "init")
    public DruidDataSource dataSource(){
        DruidDataSource dataSource = new DruidDataSource();
        dataSource.setUsername("root");
        dataSource.setPassword("root");
        dataSource.setUrl("jdbc:mysql://localhost:3306/test");
        return dataSource;
    }
}

2译柏、測試類

public class Demo05 {
    public static void main(String[] args) {
        GenericApplicationContext context = new GenericApplicationContext();
        context.registerBean("config", Config.class);

        context.refresh();

        for (String definitionName : context.getBeanDefinitionNames()) {
            System.out.println(definitionName);
        }

        context.close();
    }
}
基礎準備測試.png

可以發(fā)現(xiàn)镣煮,僅我們手動注冊的bean被添加到Spring容器了。

二鄙麦、ConfigurationClassPostProcessor后處理器

1典唇、作用

解析@ComponentScan镊折、@Bean、@Import介衔、@ImportResource 注解恨胚。

2、添加ConfigurationClassPostProcessor后處理器

// 解析@ComponentScan炎咖、@Bean赃泡、@Import、@ImportResource 注解
context.registerBean(ConfigurationClassPostProcessor.class);
ConfigurationClassPostProcessor后處理器.png

三乘盼、MapperScannerConfigurer后處理器

1升熊、作用

類似于@MapperScanner注解,將@Mapper注解指定的Bean放入Spring容器绸栅。

2级野、添加MapperScannerConfigurer后處理器

// @MapperScanner
context.registerBean(MapperScannerConfigurer.class, bd -> {
    bd.getPropertyValues().add("basePackage", "com/stone/demo05/mapper");
});
MapperScannerConfigurer后處理器.png

使用MapperScannerConfigurer后,同時我們也看到其他后處理器也被添加進來了粹胯,如上日志后面的Processor們蓖柔。

四、深入ConfigurationClassPostProcessor后處理器-解析@ComponentScan的實現(xiàn)

1风纠、獲取類上的@ComponentScan注解及其屬性

這里使用AnnotationUtils工具類來查找指定類上的@ComponentScan注解况鸣,并從注解中取出basePackages屬性內(nèi)容,如下:

ComponentScan componentScan = AnnotationUtils.findAnnotation(Config.class, ComponentScan.class);
if (componentScan != null){
    for (String basePackage : componentScan.basePackages()) {
        System.out.println(basePackage);
    }
}
獲取類上的@ComponentScan注解中的屬性.png

2议忽、獲取指定路徑下的資源

注意:此處內(nèi)容跟我們Spring原理分析-BeanFactory與ApplicationContext中:一懒闷、容器接口 → 3、ApplicationContext接口 → ② 實現(xiàn)ResourcePatternResolver接口 的內(nèi)容串聯(lián)栈幸。

在資源路徑下添加一個沒有@Component注解的類愤估,如下:

public class Bean3 {
    private static final Logger log = LoggerFactory.getLogger(Bean3.class);
    public Bean3(){log.debug("Bean2被Spring容器管理了!");}
}

從指定路徑下獲取資源速址,如下:

// com.stone.demo05.component → classpath*:com/stone/demo05/component/**/*.class
String path = PATH_PREFIX
        + basePackage.trim().replace(".", "/") + PATH_SUFFIX;
System.out.println(path);
Resource[] resources = context.getResources(path);
for (Resource resource : resources) {
    System.out.println(resource);
}

常量類玩焰,如下:

public class DemoConstants {
    public static final String PATH_PREFIX = "classpath*:";
    public static final String PATH_SUFFIX = "/**/*.class";
}
掃描指定路徑下的資源文件.png

3、獲取資源內(nèi)部元信息

通過CachingMetadataReaderFactory類獲取MetadataReader對象芍锚,來獲取元信息數(shù)據(jù)昔园,如下:

// 用戶獲取元信息
CachingMetadataReaderFactory factory = new CachingMetadataReaderFactory();
Resource[] resources = context.getResources(path);
for (Resource resource : resources) {
    //System.out.println(resource);
    // 加載元信息的 reader
    MetadataReader metadataReader = factory.getMetadataReader(resource);
    // 讀取 類名 和 注解 信息
    System.out.println("CLASSNAME IS: " + metadataReader.getClassMetadata().getClassName());
    System.out.println("HAS @Component ? " +
            metadataReader.getAnnotationMetadata().hasAnnotation(Component.class.getName()));
    System.out.println("-------------------------");
}
獲取元信息數(shù)據(jù).png

由上可以知道hasAnnotation方法可以幫助我們判斷是否存在指定注解,下面我們演示判斷派生注解的hasMetaAnnotation方法并炮,如下:

System.out.println("HAS MATE @Component ? " +
        metadataReader.getAnnotationMetadata().hasMetaAnnotation(Component.class.getName()));

給Bean3添加@Controller注解或者@Configuration注解默刚,測試如下:

判斷派生注解的hasMetaAnnotation方法.png

4、將包含指定元信息的資源轉(zhuǎn)換成BeanDefinition并注冊

獲取BeanName生成器逃魄,如下:

// 設置 BeanName 的 generator
AnnotationBeanNameGenerator generator = new AnnotationBeanNameGenerator();

通過采用如上的元信息獲取形式荤西,進行判斷,然后注冊,如下:

if (metadataReader.getAnnotationMetadata().hasMetaAnnotation(Component.class.getName())
|| metadataReader.getAnnotationMetadata().hasMetaAnnotation(Component.class.getName())){
    // 將帶有指定 @Component 注解及其派生注解的資源轉(zhuǎn)換成 BeanDefinition
    AbstractBeanDefinition bd = BeanDefinitionBuilder
            .genericBeanDefinition(metadataReader.getClassMetadata().getClassName())
            .getBeanDefinition();
    // 通過 beanFactory 將 BeanDefinition 注冊到 Spring
    DefaultListableBeanFactory beanFactory = context.getDefaultListableBeanFactory();
    String beanName = generator.generateBeanName(bd, beanFactory);
    beanFactory.registerBeanDefinition(beanName, bd);
}
將包含指定元信息的資源轉(zhuǎn)換成BeanDefinition并注冊.png

可以看到邪锌,此時我們帶有@Component注解的Bean2已經(jīng)被Spring管理了勉躺。

5、封裝自定義解析@ComponentScan注解BeanFactory后處理器

實現(xiàn)BeanFactoryPostProcessor接口觅丰,重寫postProcessBeanFactory方法饵溅,該方法在 context.refresh() 時被調(diào)用,如下:

public class ComponentScanBothMetaPostProcessor implements BeanFactoryPostProcessor {
    @Override // 該方法在 context.refresh() 時被調(diào)用
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
        try {
            ComponentScan componentScan = AnnotationUtils.findAnnotation(Config.class, ComponentScan.class);
            if (componentScan != null){
                for (String basePackage : componentScan.basePackages()) {
                    System.out.println(basePackage);
                    // com.stone.demo05.component → classpath*:com/stone/demo05/component/**/*.class
                    String path = PATH_PREFIX
                            + basePackage.trim().replace(".", "/") + PATH_SUFFIX;
                    System.out.println(path);
                    // 用戶獲取元信息
                    CachingMetadataReaderFactory factory = new CachingMetadataReaderFactory();
                    Resource[] resources = new PathMatchingResourcePatternResolver().getResources(path);
                    // 設置 BeanName 的 generator
                    AnnotationBeanNameGenerator generator = new AnnotationBeanNameGenerator();
                    for (Resource resource : resources) {
                        // 加載元信息的 reader
                        MetadataReader metadataReader = factory.getMetadataReader(resource);
                        // 讀取元信息妇萄,判斷是否帶有 @Component 注解及其派生注解
                        if (metadataReader.getAnnotationMetadata().hasMetaAnnotation(Component.class.getName())
                                || metadataReader.getAnnotationMetadata().hasMetaAnnotation(Component.class.getName())){
                            // 將帶有指定 @Component 注解及其派生注解的資源轉(zhuǎn)換成 BeanDefinition
                            AbstractBeanDefinition bd = BeanDefinitionBuilder
                                    .genericBeanDefinition(metadataReader.getClassMetadata().getClassName())
                                    .getBeanDefinition();
                            // 通過 DefaultListableBeanFactory 將 BeanDefinition 注冊到 Spring
                            if (beanFactory instanceof DefaultListableBeanFactory defaultListableBeanFactory){
                                String beanName = generator.generateBeanName(bd, defaultListableBeanFactory);
                                defaultListableBeanFactory.registerBeanDefinition(beanName, bd);
                            }
                        }
                    }
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

測試類中注冊該后處理器蜕企,測試:

封裝自定義的BeanFactory后處理器測試.png

至此我們就大致清楚整個解析@Component注解實現(xiàn)的過程:
獲取資源路徑 → 通過路徑獲取資源 → 讀取資源元信息 → 判斷符合指定元信息需求的的資源 → 注冊篩選出的資源

五、深入ConfigurationClassPostProcessor后處理器-解析@Bean的實現(xiàn)

1冠句、讀取指定路徑資源元信息

// 讀取指定資源的元信息數(shù)據(jù)
CachingMetadataReaderFactory factory = new CachingMetadataReaderFactory();
MetadataReader metadataReader
        = factory.getMetadataReader(new ClassPathResource("com/stone/demo05/Config.class"));

2糖赔、獲取被@Bean注解標注的方法集合

// 獲取被 @Bean 注解標注的方法集合
Set<MethodMetadata> annotatedMethods
        = metadataReader.getAnnotationMetadata().getAnnotatedMethods(Bean.class.getName());

3、遍歷方法集合并注冊BeanDefinition

遍歷方法集合并注冊BeanDefinition報錯.png

因為sqlSessionFactoryBean需要注入dataSource轩端,而默認是關閉自動注入的放典,我們需要配置開啟,如下:

// 遍歷被注解的方法
for (MethodMetadata annotatedMethod : annotatedMethods) {
    String methodName = annotatedMethod.getMethodName();
    // 設置工廠方法基茵,創(chuàng)建 BeanDefinition 并注冊
    BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition();
    builder.setFactoryMethodOnBean(methodName, "config");
    // 開啟 工廠方法 和 構(gòu)造方法 的自動裝配
    builder.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_CONSTRUCTOR);
    AbstractBeanDefinition bd = builder.getBeanDefinition();
    context.getDefaultListableBeanFactory().registerBeanDefinition(methodName, bd);
}

builder.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_CONSTRUCTOR);通過該方法奋构,開啟工廠方法的自動裝配。

開啟工廠方法的自動裝配.png

4拱层、獲取@Bean注解中的屬性值

// 獲取 @Bean 注解中 initMethod 屬性值
String initMethod
        = annotatedMethod.getAnnotationAttributes(Bean.class.getName()).get("initMethod").toString();

if (initMethod.length() > 0){
    // 如果包含 initMethod 則設置
    builder.setInitMethodName(initMethod);
}
獲取@Bean注解中的屬性值.png

5弥臼、封裝自定義解析@Bean注解BeanFactory后處理器

public class AtBeanPostProcessor implements BeanFactoryPostProcessor {
    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
        try {
            // 讀取指定資源的元信息數(shù)據(jù)
            CachingMetadataReaderFactory factory = new CachingMetadataReaderFactory();
            MetadataReader metadataReader
                    = factory.getMetadataReader(new ClassPathResource("com/stone/demo05/Config.class"));
            // 獲取被 @Bean 注解標注的方法集合
            Set<MethodMetadata> annotatedMethods
                    = metadataReader.getAnnotationMetadata().getAnnotatedMethods(Bean.class.getName());
            // 遍歷被注解的方法
            for (MethodMetadata annotatedMethod : annotatedMethods) {
                String methodName = annotatedMethod.getMethodName();
                // 獲取 @Bean 注解中 initMethod 屬性值
                String initMethod
                        = annotatedMethod.getAnnotationAttributes(Bean.class.getName()).get("initMethod").toString();
                // 設置工廠方法,創(chuàng)建 BeanDefinition 并注冊
                BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition();
                builder.setFactoryMethodOnBean(methodName, "config");
                // 開啟 工廠方法 和 構(gòu)造方法 的自動裝配
                builder.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_CONSTRUCTOR);
                if (initMethod.length() > 0){
                    // 如果包含 initMethod 則設置
                    builder.setInitMethodName(initMethod);
                }
                AbstractBeanDefinition bd = builder.getBeanDefinition();
                if (beanFactory instanceof DefaultListableBeanFactory defaultListableBeanFactory){
                    defaultListableBeanFactory.registerBeanDefinition(methodName, bd);
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

六根灯、深入Mapper接口掃描添加后處理器

1径缅、原生添加Mapper接口方法

@Bean
public MapperFactoryBean<Mapper1> mapper1(SqlSessionFactory sqlSessionFactory){
    MapperFactoryBean<Mapper1> factory = new MapperFactoryBean<>(Mapper1.class);
    factory.setSqlSessionFactory(sqlSessionFactory);
    return factory;
}
原生添加Mapper接口方法.png

2、自定義批量掃描添加Mapper接口后處理器

這次我們實現(xiàn)BeanDefinitionRegistryPostProcessor接口烙肺,它是BeanFactoryPostProcessor的子接口纳猪。

public class MapperPostProcessor implements BeanDefinitionRegistryPostProcessor {
    @Override
    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry beanFactory) throws BeansException {
        try {
            // 找到指定類的 @MapperScan 注解
            MapperScan mapperScan = AnnotationUtils.findAnnotation(Config.class, MapperScan.class);
            if (mapperScan != null){
                String[] basePackages = mapperScan.basePackages();
                for (String basePackage : basePackages) {
                    // 通過資源絕對路徑,解析資源
                    String path = PATH_PREFIX
                            + basePackage.trim().replace(".", "/") + PATH_SUFFIX;
                    PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
                    Resource[] resources = resolver.getResources(path);
                    // 通過 CachingMetadataReaderFactory 對象桃笙,從資源 resources 中獲取元信息
                    CachingMetadataReaderFactory factory = new CachingMetadataReaderFactory();
                    AnnotationBeanNameGenerator generator = new AnnotationBeanNameGenerator();
                    for (Resource resource : resources) {
                        // 獲取 resource 資源的元信息 MetadataReader
                        MetadataReader reader = factory.getMetadataReader(resource);
                        // 獲取類元信息 classMetadata氏堤,生成并注冊 BeanDefinition
                        ClassMetadata classMetadata = reader.getClassMetadata();
                        if (classMetadata.isInterface()) {
                            // MapperFactoryBean<?> 的 BeanDefinition
                            AbstractBeanDefinition bd1 = BeanDefinitionBuilder
                                    .genericBeanDefinition(MapperFactoryBean.class)
                                    .addConstructorArgValue(classMetadata.getClassName())
                                    .setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE)
                                    .getBeanDefinition();
                            // Mapper 接口的 BeanDefinition
                            AbstractBeanDefinition bd2 = BeanDefinitionBuilder
                                    .genericBeanDefinition(classMetadata.getClassName()).getBeanDefinition();
                            // 對應 Mapper 來生成 beanName
                            String beanName = generator.generateBeanName(bd2, beanFactory);
                            // 對應 MapperFactoryBean 來注冊 BeanDefinition
                            beanFactory.registerBeanDefinition(beanName, bd1);
                        }
                    }
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

    }

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {

    }
}

值得注意的地方,如下:

// MapperFactoryBean<?> 的 BeanDefinition
AbstractBeanDefinition bd1 = BeanDefinitionBuilder
        .genericBeanDefinition(MapperFactoryBean.class)
        .addConstructorArgValue(classMetadata.getClassName())
        .setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE)
        .getBeanDefinition();
// Mapper 接口的 BeanDefinition
AbstractBeanDefinition bd2 = BeanDefinitionBuilder
        .genericBeanDefinition(classMetadata.getClassName()).getBeanDefinition();
// 對應 Mapper 來生成 beanName
String beanName = generator.generateBeanName(bd2, beanFactory);
// 對應 MapperFactoryBean 來注冊 BeanDefinition
beanFactory.registerBeanDefinition(beanName, bd1);

由于Mapper是接口搏明,我們是通過MapperFactoryBean工廠Bean對象來獲取的鼠锈,所以其BeanDefinition的內(nèi)容是MapperFactoryBean。而每個Bean需要有不同的BeanName星著,不然會先后覆蓋购笆。所以此處兩次獲取BeanDefinition,第二次是為了通過它獲取Mapper的自己的BeanName命名虚循。最后再將beanName與bd1注冊到Spring同欠。

七为黎、結(jié)尾

以上即為BeanFactory后處理器的全部內(nèi)容,感謝閱讀行您。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市剪廉,隨后出現(xiàn)的幾起案子娃循,更是在濱河造成了極大的恐慌,老刑警劉巖斗蒋,帶你破解...
    沈念sama閱讀 218,122評論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件捌斧,死亡現(xiàn)場離奇詭異,居然都是意外死亡泉沾,警方通過查閱死者的電腦和手機捞蚂,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,070評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來跷究,“玉大人姓迅,你說我怎么就攤上這事】÷恚” “怎么了丁存?”我有些...
    開封第一講書人閱讀 164,491評論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長柴我。 經(jīng)常有香客問我解寝,道長,這世上最難降的妖魔是什么艘儒? 我笑而不...
    開封第一講書人閱讀 58,636評論 1 293
  • 正文 為了忘掉前任聋伦,我火速辦了婚禮,結(jié)果婚禮上界睁,老公的妹妹穿的比我還像新娘觉增。我一直安慰自己,他們只是感情好翻斟,可當我...
    茶點故事閱讀 67,676評論 6 392
  • 文/花漫 我一把揭開白布抑片。 她就那樣靜靜地躺著,像睡著了一般杨赤。 火紅的嫁衣襯著肌膚如雪敞斋。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,541評論 1 305
  • 那天疾牲,我揣著相機與錄音植捎,去河邊找鬼。 笑死阳柔,一個胖子當著我的面吹牛焰枢,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 40,292評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼济锄,長吁一口氣:“原來是場噩夢啊……” “哼暑椰!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起荐绝,我...
    開封第一講書人閱讀 39,211評論 0 276
  • 序言:老撾萬榮一對情侶失蹤一汽,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后低滩,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體召夹,經(jīng)...
    沈念sama閱讀 45,655評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,846評論 3 336
  • 正文 我和宋清朗相戀三年恕沫,在試婚紗的時候發(fā)現(xiàn)自己被綠了监憎。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,965評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡婶溯,死狀恐怖鲸阔,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情迄委,我是刑警寧澤隶债,帶...
    沈念sama閱讀 35,684評論 5 347
  • 正文 年R本政府宣布,位于F島的核電站跑筝,受9級特大地震影響死讹,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜曲梗,卻給世界環(huán)境...
    茶點故事閱讀 41,295評論 3 329
  • 文/蒙蒙 一赞警、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧虏两,春花似錦愧旦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,894評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至祖凫,卻和暖如春琼蚯,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背惠况。 一陣腳步聲響...
    開封第一講書人閱讀 33,012評論 1 269
  • 我被黑心中介騙來泰國打工遭庶, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人稠屠。 一個月前我還...
    沈念sama閱讀 48,126評論 3 370
  • 正文 我出身青樓峦睡,卻偏偏與公主長得像翎苫,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子榨了,可洞房花燭夜當晚...
    茶點故事閱讀 44,914評論 2 355

推薦閱讀更多精彩內(nèi)容