第二章 裝配bean

裝配Bean

[TOC]


Spring裝配bean的可選方案

裝配:創建應用對象之間協作關系的行為通常稱為裝配,這這也是依賴注入的本質。

Spring裝配bean的三種方案:

  • 在XML中進行顯示配置
  • 在Java中進行顯示配置
  • 隱式的bean發現機制和自動裝配

自動化裝配bean

Spring有兩種角度來實現自動化裝配

  • 組件掃描:Spring會自動發現應用上下文中所創建的bean
  • 自動裝配:Spring自動滿足bean之間的依賴

創建可被發現的bean

CompactDisc.class - CD接口

package soundsystem.compactdisc;

public interface CompactDisc {
    void play();
}

SgtPeppers.class - CD接口實現類:一張專輯唱片

package soundsystem.compactdisc;

import org.springframework.stereotype.Component;

@Component
public class SgtPeppers implements CompactDisc {
    private String title = "Sgt. Pepper's Lonely Hearts Club Band";
    private String artist = "The Beatles";
    @Override
    public void play() {
        System.out.print("Playing " + title + " by " + artist);
    }
}

Component 注解:表明該類會作為組件類,并告知Spring要為這個類創建bean。

除此之外,組件掃描默認是不啟動的,我們需要顯示配置Spring,從而命令它去尋找Component類:

通過Java注解的方式開啟組件掃描

CDPlayerConfig.class - 定義Spring裝配規則

package soundsystem.config;
import org.springframework.context.annotation.ComponentScan;
@ComponentScan(basePackages = {"soundsystem"})
public class CDPlayerConfig {

}

在這個項目中,我的分包如下圖所示:

此處輸入圖片的描述
此處輸入圖片的描述

如果沒有其他配置的話,@ComponentScan 默認會掃描與配置類相同的包以及該包下所有的子包,查找帶有@Component 注解的類,這樣的話,Spring會自動為其創建bean。

CDPlayerTest.class - 測試文件

package soundsystem.test;

import org.junit.Rule;
import org.junit.Test;
import org.junit.contrib.java.lang.system.SystemOutRule;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import soundsystem.compactdisc.CompactDisc;
import soundsystem.config.CDPlayerConfig;
import soundsystem.mediaplayer.MediaPlayer;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = CDPlayerConfig.class)
public class CDPlayerTest {
    @Rule
    public final SystemOutRule log = new SystemOutRule().enableLog();
    @Autowired
    private MediaPlayer mediaPlayer;
    @Autowired
    private CompactDisc compactDisc;
    @Test
    public void cdShouldNotBeNull(){
        assertNotNull(compactDisc);
    }
    @Test
    public void play(){
        mediaPlayer.play();
        assertEquals("Playing Sgt. Pepper's Lonely Hearts Club Band by The Beatles",log.getLog());
    }
}

SpringJUnit4ClassRunner 可以在測試開始的時候自動創建Spring應用上下文,@ContextConfiguration 告訴測試類需要在CDPlayerConfig 中加載配置。

通過XML的方式開啟組件掃描

spring-config.xml

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
    <!-- 使用XML開啟組件掃描 -->
    <context:component-scan base-package="soundsystem"/>
</beans>

在這個項目中,我的分包如下圖所示:

此處輸入圖片的描述
此處輸入圖片的描述

CDPlayerTest.class - 測試文件

package soundsystem.test;

import org.junit.Rule;
import org.junit.Test;
import org.junit.contrib.java.lang.system.SystemOutRule;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import soundsystem.compactdisc.CompactDisc;
import soundsystem.mediaplayer.MediaPlayer;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"classpath:spring-config.xml"})
public class CDPlayerTest {
    @Rule
    public final SystemOutRule log = new SystemOutRule().enableLog();
    @Autowired
    private MediaPlayer mediaPlayer;
    @Autowired
    private CompactDisc compactDisc;
    @Test
    public void cdShouldNotBeNull(){
        assertNotNull(compactDisc);
    }
    @Test
    public void play(){
        mediaPlayer.play();
        assertEquals("Playing Sgt. Pepper's Lonely Hearts Club Band by The Beatles",log.getLog());
    }
}

給bean起一個不同的名稱

@Component(value = "lonelyHeartsClub")
public class SgtPeppers implements CompactDisc {
    // ...
}

@Component(value = "lonelyHeartsClub") : 給bean設置不同的ID

設置組件掃描的基礎包

@ComponentScan(basePackages = {"soundsystem","video"})
public class CDPlayerConfig {
    // ...
}

@ComponentScan(basePackages = {"soundsystem","video"}) 可以掃描多個基礎包;但是這樣是類型不安全的。

除了將包設置為String之外,@ComponentScan 還提供了另一種格式,那就是將其指定為包中的類或者接口:

@ComponentScan(basePackageClasses = {CompactDisc.class, CDPlayer.class})
public class CDPlayerConfig {
}

這些類所在包將會作為組件掃描的基礎包。

通過為bean添加注解實現自動裝配

自動裝配:自動裝配就是讓Spring自動滿足bean依賴的一種方法,在滿足依賴的過程中,會在Spring應用上下文中尋找匹配某個bean需求的其他bean 。為了聲明要進行自動匹配,我們可以借助Spring中的 @Autowired 注解 。

@Component
public class CDPlayer implements MediaPlayer {
    private CompactDisc compactDisc;
    @Autowired
    public CDPlayer(CompactDisc compactDisc){
        this.compactDisc = compactDisc;
    }
    @Override
    public void play() {
        compactDisc.play();
    }
}

上段代碼表明:當Spring創建CDPlayer bean的時候,會通過構造器來進行實例化并傳入一個可設置給CompactDisc 類型的bean

@Autowired 不僅可以用在構造器上,還可以用在屬性的Setter方法(或其他任何方法)上。

@Autowired
public void insertDisc(CompactDisc compactDisc){
    this.compactDisc = compactDisc;
}

假如有且只有一個bean匹配依賴需求的話,那么這個bean將會被匹配進來。

如果沒有匹配的bean,那么在應用上下文創建的時候,Spring會拋出一個異常,為了避免異常的出現,可以將@Autowired的required屬性設置為false

@Autowired(required = false)
public void insertDisc(CompactDisc compactDisc){
    this.compactDisc = compactDisc;
}

將@Autowired的required屬性設置為false時,Spring會嘗試自動匹配,如果匹配不到相應的bean,Spring會將這個bean處于未裝配的狀態。如果你的代碼中沒有進行null檢查的話,這個處于未裝配狀態的屬性可能會出現NullPointerException 。

如果有多個bean都能滿足依賴關系的話,Spring將會拋出一個異常,表示沒有明確指定要選擇哪一個bean進行自動裝配。

通過Java代碼裝配bean

當你需要將第三方庫組件裝配到你的應用中,是沒有辦法在它的類上加@Component和@Autowired注解的,這時候我們就需要顯示的裝配了。有兩種可選方案:Java和XML 。

創建配置類

@Configuration
public class CDPlayerConfig {

}

@Configuration 注解表明這個類是一個配置類,該類應該包含在Spring應用上下文如何創建bean的細節。

聲明簡單的bean

@Configuration
public class CDPlayerConfig {
    @Bean
    public CompactDisc sgtPeppers(){
        return new SgtPeppers();
    }
    @Bean
    public CDPlayer cdPlayer(CompactDisc compactDisc){
        return new CDPlayer(compactDisc);
    }
}

@Bean 注解會告訴Spring這個方法將會返回一個對象,該對象要注冊為Spring應用上下文中的bean 。

默認情況下,bean的ID與帶有@Bean注解的方法名是一樣的,但是,你也可以指定一個不同的名字:

@Bean(name = "lonelyHeartsClubBand")
public CompactDisc sgtPeppers(){
    return new SgtPeppers();
}

借助JavaConfig實現注入

CDPlayerConfig.class - JavaConfig 類

@Configuration
public class CDPlayerConfig {
    @Bean(name = "lonelyHeartsClubBand")
    public CompactDisc sgtPeppers(){
        return new SgtPeppers();
    }
    @Bean
    public CDPlayer cdPlayer(){
        return new CDPlayer(sgtPeppers());
    }
}

看起來,CompactDisc是通過調用 sgtPeppers() 得到的,但情況并非完全如此,因為 sgtPeppers() 方法上添加了@Bean注解,Spring 將會攔截所有對它的調用,并確保直接返回該方法所創建的bean , 而不是每一次都對其進行實際的調用。

假設你引入了其他的CDPlayer的bean:

@Bean
public CDPlayer cdPlayer(CompactDisc compactDisc){
    return new CDPlayer(sgtPeppers());
}
@Bean
public CDPlayer anthorCDPlayer(CompactDisc compactDisc){
    return new CDPlayer(sgtPeppers());
}

默認情況下,SPirng中的bean都是單例的,所以,Spring會攔截對sgtPeppers()的調用并確保返回的是Sprring所創建的bean , 在這里,bean就是Spirng在調用sgtPeppers()時所創建的CompactDisc bean 。

還有另一種寫法:

@Configuration
public class CDPlayerConfig {
    @Bean(name = "lonelyHeartsClubBand")
    public CompactDisc sgtPeppers(){
        return new SgtPeppers();
    }
    @Bean
    public CDPlayer cdPlayer(CompactDisc compactDisc){
        return new CDPlayer(compactDisc);
    }
}

在Spring調用cdPlayer() 創建 CDPlayer bean 的時候,會自動裝配一個CompactDisc 到配置方法中。不管CompactDisc是通過什么方式創建出來的,SPring都會將其傳入到配置方法中,并用來創建CDPlyaer bean

通過XML裝配bean

spring-config.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="sgtPeppers" class="soundsystem.compactdisc.SgtPeppers"/>

</beans>

當spring發現<bean...>元素時,會調用SgtPeppers的默認構造器來創建bean 。

借助構造器初始化bean

有兩種基本方案可供選擇:

  • <constructor-arg> 元素
  • 使用Spring3.0 引入的 c-命名空間

有些事情<constructor-arg>可以做到,但是使用c-命名空間卻無法實現

構造器注入bean的引用

使用<constructor-arg>元素:

<bean id="cdPlayer" class="soundsystem.mediaplayer.CDPlayer">
    <constructor-arg ref="sgtPeppers"/>
</bean>

使用c-命名空間的方案:

<bean id="cdPlayer" class="soundsystem.mediaplayer.CDPlayer" c:compactDisc-ref="sgtPeppers"/>
此處輸入圖片的描述
此處輸入圖片的描述

屬性名以c: 開頭,也就是命名空間的前綴,表示是構造器初始化。 接下來是要裝配的構造器參數名稱,-ref表正在裝配的是一個bean的引用,這個bean的名稱是compactDisc(以圖為例)

代替方案如下:

<bean id="cdPlayer" class="soundsystem.mediaplayer.CDPlayer" c:_0-ref="sgtPeppers"/>

c:_0-ref 中的 _0 表示參數的索引 , 使用索引來識別會比使用名稱識別構造器參數更好一些。

將字面量(字符串)注入到構造器中

BlankDisc.class

public class BlankDisc implements CompactDisc {
    private String title;
    private String artist;
    public BlankDisc(String title, String artist) {
        this.title = title;
        this.artist = artist;
    }
    @Override
    public void play() {
        System.out.print("Playing " + title + " by " + artist);
    }
}

spring-config.xml

<bean id="blankDisc" class="soundsystem.compactdisc.BlankDisc">
    <constructor-arg value="Sgt. Pepper's Lonely Hearts Club Band"/>
    <constructor-arg value="The Beatles"/>
</bean>

<constructor-arg> 標簽中的ref屬性表示引用了其他的bean,而value屬性表明給定的值要以字符串的形式注入到構造器中

更為簡單的寫法:

spring-config.xml ———— 通過索引

<bean id="blankDisc" class="soundsystem.compactdisc.BlankDisc" c:_0="Sgt. Pepper's Lonely Hearts Club Band" c:_1="The Beatles"/>

裝配集合

在裝配bean引用或者字符串的方面,<constructor-arg> 和 c-命名空間的功能是相同的,但有一種情況是 <constructor-arg> 能夠實現,但是c-命名空間不能實現的 : 那就是 集合的裝配

BlankDisc.class ———— 引入磁道的概念

public class BlankDisc implements CompactDisc {

    private String title;
    private String artist;
    /**
     *  磁道
     */
    private List<String> tracks;

    public BlankDisc(String title, String artist, List<String> tracks) {
        this.title = title;
        this.artist = artist;
        this.tracks = tracks;
    }

    @Override
    public void play() {
        System.out.print("Playing " + title + " by " + artist);
        for(String track:tracks){
            System.out.println("-Track : " +track);
        }
    }
}

spring-config.xml

<bean id="blankDisc" class="soundsystem.compactdisc.BlankDisc">
        <constructor-arg value="Sgt. Pepper's Lonely Hearts Club Band"/>
        <constructor-arg value="The Beatles"/>
        <constructor-arg>
            <list>
                <value>track1</value>
                <value>track2</value>
                <value>track3</value>
                <value>track4</value>
            </list>
        </constructor-arg>
    </bean>

與之類似的,我們也可以使用<ref> 代替 <value> , 來實現bean引用列表的裝配

當磁道為set 類型的時候,我們可以使用<set> 而非 <list> :

<bean id="blankDisc" class="soundsystem.compactdisc.BlankDisc">
        <constructor-arg value="Sgt. Pepper's Lonely Hearts Club Band"/>
        <constructor-arg value="The Beatles"/>
        <constructor-arg>
            <set>
                <value>track1</value>
                <value>track2</value>
                <value>track3</value>
                <value>track4</value>
            </set>
        </constructor-arg>
    </bean>

借助setter初始化bean

CDPlayer.class

public class CDPlayer implements MediaPlayer {
    private CompactDisc compactDisc;
    public void setCompactDisc(CompactDisc compactDisc){
        this.compactDisc = compactDisc;
    }
    @Override
    public void play() {
        compactDisc.play();
    }
}

spring-config.xml

<bean id="cdPlayer" class="soundsystem.mediaplayer.CDPlayer">
        <property name="compactDisc" ref="sgtPeppers"/>
    </bean>

    <bean id="sgtPeppers" class="soundsystem.compactdisc.SgtPeppers">

    </bean>

Spring中 <constructor-arg> 元素提供了 c-命名空間作為替代方案,與之類似,Spring提供了更加簡潔的p-命名空間,作為 <property>元素的替代方案。

spring-config.xml

<bean id="cdPlayer" class="soundsystem.mediaplayer.CDPlayer" p:compactDisc-ref="sgtPeppers"/>

將字面值注入到屬性中

BlankDisc.class

public class BlankDisc implements CompactDisc {

    private String title;
    private String artist;
    private List<String> tracks;

    public void setTitle(String title) {
        this.title = title;
    }

    public void setArtist(String artist) {
        this.artist = artist;
    }

    public void setTracks(List<String> tracks) {
        this.tracks = tracks;
    }

    @Override
    public void play() {
        System.out.print("Playing " + title + " by " + artist);
        for (String track:tracks){
            System.out.println("- Track : " + track);
        }
    }
}

spring-config.xml

<bean id="sgtPeppers" class="soundsystem.compactdisc.BlankDisc">
        <property name="title" value="Sgt. Pepper's Lonely Hearts Club Band"/>
        <property name="artist" value="The Beatles"/>
        <property name="tracks">
            <list>
                <value>track1</value>
                <value>track2</value>
                <value>track3</value>
            </list>
        </property>
    </bean>

同樣的,也可以使用p-命名空間的方式:

<bean id="blankDisc" class="soundsystem.compactdisc.BlankDisc" p:title="Sgt. Pepper's Lonely Hearts Club Band" p:artist="The Beatles">
        <property name="tracks">
            <list>
                <value>track1</value>
                <value>track2</value>
                <value>track3</value>
            </list>
        </property>
    </bean>

我們可以使用util-命名空間的方式來簡化bean:
我們可以把磁道列表轉移到blankDisc bean 之外,并將其聲明到單獨的bean 之中,如下所示:

<util:list id="tracks">
        <value>track1</value>
        <value>track2</value>
    </util:list>

這樣,我們就可以簡化blankDisc bean的定義了:

<bean id="sgtPeppers" class="soundsystem.compactdisc.BlankDisc" p:title="Sgt. Pepper's Lonely Hearts Club Band" p:artist="The Beatles" p:tracks-ref="tracks">
    </bean>
util-命名空間中的元素
util-命名空間中的元素

導入和混合配置

在JavaConfig中引用XML配置

我們假設CDPlayerConfig有一些笨重,需要將blankDiscCDPlayerConfig 中拆分出來。定義到它自己的CDConfig類中:

CDConfig.class

@Configuration
public class CDConfig {

    @Bean
    public CompactDisc compactDisc(){
        return new SgtPeppers();
    }
}

CDPlayerConfig.class

@Configuration
public class CDPlayerConfig {
    @Bean
    public CDPlayer cdPlayer(CompactDisc compactDisc){
        return new CDPlayer(compactDisc);
    }
}

我們需要將這兩個config類組合到一起,第一種方法是直接在CDPlayerConfig.class使用@Import注解導入CDConfgi.class

@Configuration
@Import(CDConfig.class)
public class CDPlayerConfig{
    // ...
}

更高級的辦法是創建一個新的Config,將兩個配置類組合到一起:

@Configuration
@Import({CDConfig.class,CDPlayerConfig.class})
public class SoundSystemConfig {

}

假設我們通過XML的方式配置BlankDisc:

spring-config.xml

<?xml version="1.0" encoding="utf-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
    <bean id="blankDisc" class="soundsystem.compactdisc.BlankDisc" p:title="Sgt. Pepper's Lonely Hearts Club Band" p:artist="The Beatles">
        <property name="tracks">
            <list>
                <value>track1</value>
                <value>track2</value>
                <value>track3</value>
            </list>
        </property>
    </bean>
</beans>

使用@ImportResource注解,加載XML文件

@Configuration
@Import({CDConfig.class,CDPlayerConfig.class})
@ImportResource({"classpath:spring-context.xml"})
public class SoundSystemConfig {

}

在XML配置中引用JavaConfig

cd-config.xml

<bean id="compactDisc" class="soundsystem.compactdisc.BlankDisc" p:title="Sgt. Pepper's Lonely Hearts Club Band" p:artist="The Beatles">
        <property name="tracks">
            <list>
                <value>track1</value>
                <value>track2</value>
                <value>track3</value>
            </list>
        </property>
    </bean>

spring-context.xml

<import resource="cd-config.xml"/>

    <bean id="cdPlayer" class="soundsystem.mediaplayer.CDPlayer" c:_0-ref="compactDisc">

    </bean>

<import> 元素可以導入其他的XML配置文件,但是不可以導入JavaConfig類

所以,我們可以使用下面的方法引用:

<bean class="soundsystem.config.CDConfig"/>

    <bean id="cdPlayer" class="soundsystem.mediaplayer.CDPlayer" c:_0-ref="compactDisc"/>

小結

Spring中裝配bean的三種主要方式:自動化配置,基于Java的顯示配置,以及基于XML的顯示配置

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,505評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,556評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,463評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,009評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,778評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,218評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,281評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,436評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,969評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,795評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,993評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,537評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,229評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,659評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,917評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,687評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,990評論 2 374

推薦閱讀更多精彩內容